forge-dashboard

module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: GPL-3.0

README

forge-dashboard

CI release licence

A single-page dashboard of open pull requests, issues, and CI status across every repository you have write access to on GitHub and a Forgejo instance — one page instead of two forge UIs. See #1 for the v1 acceptance criteria this was built against, and #3 for the passwordless login, per-user tokens, sharing, and an admin role this document now describes the first slice of.

Design

  • Passkey login, no passwords anywhere. Registration and sign-in are real WebAuthn ceremonies — works with Proton Pass, 1Password, iCloud Keychain, a platform authenticator, or a hardware key, anything that speaks passkeys. Nothing password-shaped is ever stored or transmitted.
  • Credentials never reach the browser. The Go backend holds both forges' tokens server-side and only ever hands the frontend its own already-filtered JSON. Confirm this yourself: open the browser's network tab and watch it call nothing but /api/dashboard, /api/auth/* and /healthz.
  • One Go binary, no frontend build step. Plain HTML/CSS/JS, embedded into the binary with //go:embed. go build is the whole pipeline — no Node toolchain needed to run the service, only to run its own lint/test tooling (see CONTRIBUTING.md).
  • Read-only against both forges. Nothing here writes back to GitHub or Forgejo. It aggregates and displays; every row links out to the real thing.
Where this stands against #3

This ships passkey registration and login gating the dashboard, and an admin flag established at startup rather than by whoever registers first. It does not yet ship #3's other two pieces: per-user GitHub/Forgejo tokens (every signed-in user currently sees the same dashboard, still configured by the environment variables below) and dashboard sharing between users. Those land as their own follow-up work on top of this.

Requirements

  • Go 1.27 or newer, to build.
  • A passkey-capable browser to sign in at all — any current Chrome, Safari, Firefox or Edge; a phone counts too. Nothing else to install.
  • Somewhere writable for the auth database (DB_PATH, default /data/forge-dashboard.db) that survives a restart — registered passkeys and sessions live there. In Docker that means a mounted volume at /data; see Docker below.
  • Credentials for at least one forge — see Credentials below for exactly which permissions to grant. GITHUB_* and FORGEJO_* are entirely independent and each optional on its own: run with just one forge configured, or neither (an empty dashboard), if that's ever useful for a smoke test.

Authentication

The first visit registers a passkey; every visit after that signs in with it — no password, no separate account system. POST /api/auth/register/begin and .../finish run the WebAuthn registration ceremony, .../login/begin/.../finish run the login ceremony, and a signed session cookie (HttpOnly, SameSite=Lax, Secure whenever the request arrived over HTTPS) is what actually gates / and /api/dashboard afterward — see internal/auth and api/openapi.yaml's auth tag.

  • ADMIN_USERNAME names the one username that becomes an admin the moment it registers — decided here, at startup, rather than by whoever happens to register first. Leave it unset and nobody registers as an admin (fine for now: nothing in this slice is admin-gated yet — that's part of #3's remaining scope).
  • RP_ID / RP_ORIGIN configure the WebAuthn relying party. They default to localhost / http://localhost:8080 for a local run; a real deployment must set both to its real domain, or every passkey registered against the default refuses to work there — a passkey is cryptographically bound to the origin it was created for, not something this service can paper over after the fact.

Credentials

Each forge takes either a token (sees private repos too) or a bare username (public repos only, no credential at all) — never both purposes at once. A token always wins over a username if you set both.

GitHub
  • Token (GITHUB_TOKEN): a personal access token with read access to the repositories you want tracked.
    • Classic token: the repo scope.
    • Fine-grained token: Contents, Issues, Pull requests (Read-only), plus Metadata (Read-only, mandatory on every fine-grained token regardless).
  • Username only (GITHUB_USERNAME, GITHUB_TOKEN unset): shows that account's public repositories, fully unauthenticated — the same data anyone gets landing on github.com/<username>?tab=repositories. Verified live against a real 105-public-repo account. Unauthenticated GitHub API calls are capped at 60 requests/hour, so this mode only sees everything on an account small enough to fit that budget — a token (5,000/hour) is what an account this size actually needs.
Forgejo
  • Token (FORGEJO_TOKEN): Settings → Applications → Generate New Token, with read:repository, read:issue, and read:user all checked. read:user is easy to miss — it isn't obviously related to repos, but GET /user/repos (how repository discovery works) refuses a token without it: confirmed live against a real instance, 403 token does not have at least one of required scope(s): [read:user], before read:user was added to the token.
  • Username only (FORGEJO_USERNAME, FORGEJO_TOKEN unset): shows that account's public repositories on the configured instance, unauthenticated — same trade-off as GitHub's username mode. Verified against a real Forgejo instance's GET /users/<username>/repos.

Configuration

Everything is environment variables — no config file:

Variable Required Default Meaning
ADDR no :8080 Listen address.
DB_PATH no /data/forge-dashboard.db Where the auth database (passkeys, sessions) lives. Needs to persist across restarts.
RP_ID no localhost The WebAuthn relying party ID — set to your real domain in any real deployment.
RP_ORIGIN no http://localhost:8080 The WebAuthn relying party origin — set to the real https:// origin users reach this at.
ADMIN_USERNAME no — The one username that becomes an admin on registration. Unset means nobody does.
GITHUB_TOKEN no* — A GitHub personal access token. Every repo it can push to is tracked, private included.
GITHUB_USERNAME no* — Used only when GITHUB_TOKEN is unset — shows that account's public repos, unauthenticated.
FORGEJO_URL no* — Base URL of the Forgejo instance, for example https://git.example.com.
FORGEJO_TOKEN no* — A Forgejo API token. Every repo it can push to is tracked, private included.
FORGEJO_USERNAME no* — Used only when FORGEJO_URL is set but FORGEJO_TOKEN isn't — that account's public repos only.
REFRESH_INTERVAL no 5m How often the backend re-polls both forges, as a Go duration (2m30s, 10m).

* Each forge is entirely optional, and GitHub and Forgejo don't depend on each other — configure one, both, or neither (the process still starts and serves an empty snapshot either way, rather than refusing to boot). Within a forge, its token wins over its username when both are set.

Repository discovery is automatic. With a token, the dashboard lists every repository it has push access to (GET /user/repos on both APIs); with only a username, it lists that account's public repositories (GET /users/<username>/repos, also both APIs) with no credential in play at all. Either way there's no per-repo allowlist to maintain.

Running it

go build -o forge-dashboard ./cmd/forge-dashboard
mkdir -p data
DB_PATH=./data/forge-dashboard.db \
  GITHUB_TOKEN=ghp_xxx FORGEJO_URL=https://git.example.com FORGEJO_TOKEN=xxx \
  ./forge-dashboard

Then open http://localhost:8080 and register a passkey — RP_ID/ RP_ORIGIN default to localhost/http://localhost:8080, which matches this local run with nothing extra to set.

Docker

The /data directory the image ships is where the auth database goes — mount a volume there, or every passkey registered is gone the moment the container is recreated:

docker build -t forge-dashboard .
docker run --rm -p 8080:8080 \
  --cap-drop=ALL --security-opt=no-new-privileges --read-only \
  --memory=64m --cpus=0.5 \
  -v forge-dashboard-data:/data \
  -e RP_ID=localhost -e RP_ORIGIN=http://localhost:8080 -e ADMIN_USERNAME=you \
  -e GITHUB_TOKEN=ghp_xxx -e FORGEJO_URL=https://git.example.com -e FORGEJO_TOKEN=xxx \
  forge-dashboard

The published image is ghcr.io/alrayyes/forge-dashboard, built and pushed on every release by .goreleaser.yml's dockers: block — tagged by version and :latest, linux/amd64 and linux/arm64.

Deployment

Deployed on a homelab, reachable only over Tailscale. Passkey login is now the actual access control — the tailnet is defence in depth on top of it, not the only thing standing between a visitor and the dashboard the way v1 originally had it. The image is pulled into that homelab's existing vps-docker compose setup as its own service directory, routed through the Traefik instance already running there; there's no per-service Tailscale sidecar, since every service on that host reaches the tailnet the same way. That deployment config — including the /data volume this now needs, and RP_ID/RP_ORIGIN set to the real deployed domain rather than the localhost defaults — lives in vps-docker, not here.

API

api/openapi.yaml is the contract: GET /healthz for liveness, and GET /api/dashboard for the aggregated snapshot the frontend renders. redocly lint validates it; nothing yet asserts the handlers still match it (see CONTRIBUTING.md).

Contributing

See CONTRIBUTING.md for the toolchain, the hooks, and how a change gets reviewed and released.

Licence

GPL-3.0.

Directories

Path Synopsis
cmd
forge-dashboard command
Command forge-dashboard is the composition root: it builds a Source per configured forge, starts the aggregator's background refresh, and serves the API and static frontend against whatever the aggregator most recently assembled.
Command forge-dashboard is the composition root: it builds a Source per configured forge, starts the aggregator's background refresh, and serves the API and static frontend against whatever the aggregator most recently assembled.
internal
api
Package api is the composition root's one dependency: an http.Handler satisfying api/openapi.yaml, serving the aggregated dashboard snapshot and the static frontend.
Package api is the composition root's one dependency: an http.Handler satisfying api/openapi.yaml, serving the aggregated dashboard snapshot and the static frontend.
auth
Package auth is passkey (WebAuthn) registration and login: no password ever stored or transmitted, a session cookie once a ceremony succeeds, and the store both ceremonies and sessions persist to (SQLite).
Package auth is passkey (WebAuthn) registration and login: no password ever stored or transmitted, a session cookie once a ceremony succeeds, and the store both ceremonies and sessions persist to (SQLite).
dashboard
Package dashboard holds the domain model both forge clients produce and the aggregator that merges them into one snapshot.
Package dashboard holds the domain model both forge clients produce and the aggregator that merges them into one snapshot.
forgejo
Package forgejo is a thin client for the pieces of the Forgejo API (Gitea-compatible) forge-dashboard needs — the same shape as internal/github, against a different API under a self-hosted host.
Package forgejo is a thin client for the pieces of the Forgejo API (Gitea-compatible) forge-dashboard needs — the same shape as internal/github, against a different API under a self-hosted host.
github
Package github is a thin client for the pieces of the GitHub REST API forge-dashboard needs: which repositories the token can write to, their open pull requests and issues, and combined CI status per pull request.
Package github is a thin client for the pieces of the GitHub REST API forge-dashboard needs: which repositories the token can write to, their open pull requests and issues, and combined CI status per pull request.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL