forge-dashboard

module
v0.63.0 Latest Latest
Warning

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

Go to latest
Published: Sep 17, 2026 License: AGPL-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.

forge-dashboard, light mode forge-dashboard, dark mode

Fixture data, not a real account's actual repositories — regenerated automatically by the release workflow on every release (see scripts/capture-screenshots.js), so it's never more than one release stale.

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.
  • Every user configures their own forges. Each signed-in user has their own GitHub/Forgejo tokens, set from the Settings page, and sees only their own dashboard — nobody else's tokens, nobody else's repos.
  • Credentials never reach the browser. The Go backend holds every user's tokens server-side, encrypted at rest, 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/settings, /api/auth/* and /healthz — and that /api/settings never echoes a token back, only whether one is set.
  • 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, with one caveat. Nothing here writes back to GitHub or Forgejo — it aggregates and displays, and every row links out to the real thing. The caveat: checking whether a repo's webhook is already pointed at this dashboard (the Webhooks card, and the dashboard's own coverage summary) needs a token scope that includes write access on Forgejo, since its hooks API has no separate read-only scope — this app still only reads that list, never creates or edits a hook itself.
Where this stands against #3

This ships every piece of #3: passkey registration and login gating the dashboard, per-user GitHub/Forgejo tokens (every signed-in user configures their own forges from the Settings page and sees only their own dashboard by default), an admin area for whoever registers first (list every registered user, revoke a user's passkeys and sessions without deleting their account, or remove one outright), and dashboard sharing (grant another registered user read-only access to your own dashboard, from Settings).

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 database (DB_PATH, default /data/forge-dashboard.db) that survives a restart — registered passkeys, sessions, and every user's encrypted forge credentials live there. In Docker that means a mounted volume at /data; see Docker below.
  • ENCRYPTION_KEY, a base64-encoded 32-byte key the process refuses to start without — see Configuration below for how to generate one. It's what encrypts every user's forge tokens at rest; there's no sensible default for a secret like this one.
  • Each signed-in user configures their own forge credentials from the Settings page after registering — see Credentials below for exactly which permissions to grant. Nothing forge-related needs setting before the process starts.

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.

  • Whoever registers first becomes admin. No environment variable to set or get wrong — the deployment's own network boundary (Tailscale-only, see Deployment below) is what actually keeps a stranger from racing to register before you do, the same protection an explicit ADMIN_USERNAME variable would only duplicate. See Admin area below for what an admin can do.
  • 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.

Admin area

Whoever registers first gets an Admin link in the dashboard header, leading to /admin.html: a list of every registered user, with two actions per row.

  • Revoke signs a user out everywhere and clears every passkey they've registered, without touching their account or saved forge credentials — they have to register a new passkey from scratch to get back in. Useful for a lost device or a compromised passkey manager, short of removing the person entirely.
  • Remove deletes the account outright — passkeys, sessions, and saved forge credentials all go with it, and the username becomes available for a fresh registration. Irreversible.

Neither action works on the admin's own account (the backend refuses it with a 400, and the frontend disables both buttons on that row) — there's no recovery path for locking yourself out this way.

Sharing

From Settings, share your own dashboard with another registered user, read-only — they need no GitHub/Forgejo credentials of their own configured. Once shared, a selector appears in their dashboard header so they can switch between their own dashboard and yours; Settings also lists everyone who's shared their dashboard with you, and everyone you've shared yours with, with a one-click way to stop.

Enforced server-side (GET /api/dashboard?owner=<username> answers 403 without an active share, not just a hidden frontend control) — see internal/sharing and api/openapi.yaml's sharing tag.

Credentials

Set from the Settings page (/settings.html, linked from the dashboard header) once you're signed in — nothing forge-related is configured by the process itself. 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, and leaving a token field blank on save keeps whatever was saved before, so updating your username doesn't mean re-pasting the token too.

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 (token left blank): 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: 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 (token left blank, instance URL still set): 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.

Webhooks

Optional. Without one, the dashboard still refreshes on its own schedule and the browser tab polls it every 30 seconds; a webhook just means a new pull request, a closed issue, or a CI status change on a tracked repo shows up within seconds instead of at the next poll. Set up from the Settings page, one forge at a time — full copy-pasteable steps, exactly which events each part of the dashboard needs (CI status specifically needs its own, easy-to-miss event), and how delivery verification works: docs/webhooks.md.

A delivery refreshes just the repository it names, not the account's whole tracked-repo set — a much smaller GraphQL query on GitHub than a full refresh runs. A payload with no repository (the initial "ping" delivery a new webhook sends, say) falls back to a full refresh instead.

Configuration

Everything the process itself needs is environment variables — no config file, and nothing forge-related, since that's per-user now (see Credentials above):

Variable Required Default Meaning
ADDR no :8080 Listen address.
DB_PATH no /data/forge-dashboard.db Where passkeys, sessions, and every user's encrypted forge credentials live.
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.
ENCRYPTION_KEY yes — Base64-encoded 32-byte key for encrypting forge tokens at rest. Generate with openssl rand -base64 32.
REFRESH_INTERVAL no 5m How often the backend re-polls a signed-in user's forges, as a Go duration (2m30s, 10m).
LOG_LEVEL no info debug, info, warn, or error. debug logs every outbound request to GitHub/Forgejo (method, URL) — turn it on to diagnose a request-volume spike from the process's own logs instead of reasoning about the code from the outside.

Repository discovery is automatic per user. With a token, the dashboard lists every repository it has push access to — on GitHub, one GraphQL query returns that whole list along with every repo's open pull requests, open issues, and CI status in a single request (drawing from GraphQL's own separate rate-limit pool, not the REST budget everything else on the account shares); on Forgejo, GET /user/repos. With only a username, it lists that account's public repositories (GET /users/<username>/repos on both) with no credential in play at all — GitHub's GraphQL API allows no anonymous access, so this mode stays REST-based there too. Either way there's no per-repo allowlist to maintain — archived and forked repositories are excluded automatically, on both forges, in either mode. On Forgejo, a mirrored repository is excluded too — a pull mirror has no pull requests or issues of its own to poll, and its canonical home is whichever forge it's mirrored from.

Running it

go build -o forge-dashboard ./cmd/forge-dashboard
mkdir -p data
DB_PATH=./data/forge-dashboard.db \
  ENCRYPTION_KEY="$(openssl rand -base64 32)" \
  ./forge-dashboard

Then open http://localhost:8080, register a passkey — RP_ID/ RP_ORIGIN default to localhost/http://localhost:8080, which matches this local run with nothing extra to set — and add your GitHub/Forgejo credentials from the Settings page.

Docker

The /data directory the image ships is where the database goes — mount a volume there, or every passkey and every saved credential 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 ENCRYPTION_KEY="$(openssl rand -base64 32)" \
  forge-dashboard

Generate ENCRYPTION_KEY once and keep it — losing it makes every saved credential unrecoverable, and every user has to re-enter theirs from the Settings page.

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, GET /api/dashboard for the signed-in user's aggregated snapshot (or, with ?owner=<username>, one shared with them), GET/PUT /api/settings for that user's own GitHub/Forgejo configuration — the PUT response never echoes a token back, only whether one is now set — GET/PUT/DELETE /api/sharing(/{username}) for managing who can see your dashboard, and GET /api/admin/users plus the revoke/delete endpoints under admin, every one of them refusing anyone but the designated admin. 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

AGPL-3.0. Chosen over the plain GPL-3.0 this project started under because AGPL's network-use clause also covers running a modified copy as a hosted service, which GPL alone doesn't.

Directories

Path Synopsis
cmd
forge-dashboard command
Command forge-dashboard is the composition root: it wires the SQLite database, the passkey auth service, and the per-user dashboard.Manager, then serves the API and static frontend.
Command forge-dashboard is the composition root: it wires the SQLite database, the passkey auth service, and the per-user dashboard.Manager, then serves the API and static frontend.
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 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 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 client for the pieces of the GitHub API forge-dashboard needs: which repositories the credential can write to, their open pull requests and issues, and combined CI status per pull request.
Package github is a client for the pieces of the GitHub API forge-dashboard needs: which repositories the credential can write to, their open pull requests and issues, and combined CI status per pull request.
settings
Package settings is each user's own GitHub/Forgejo credentials — saved through the Settings page, encrypted at rest, and what dashboard.Manager builds that user's forge sources from.
Package settings is each user's own GitHub/Forgejo credentials — saved through the Settings page, encrypted at rest, and what dashboard.Manager builds that user's forge sources from.
sharing
Package sharing tracks which users have granted which other users read-only access to their aggregated dashboard.
Package sharing tracks which users have granted which other users read-only access to their aggregated dashboard.

Jump to

Keyboard shortcuts

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