Dashboard Sign-in Setup
The dashboard signs users in itself and keeps the session in an httpOnly cookie. Browser JavaScript never sees a token.
- Sign in with GitHub: OAuth web flow with PKCE (S256). Only GitHub logins listed in
ALLOWED_GITHUB_USERSget a session. - Username and password (fallback): the dashboard server forwards the form to the Observatory's
POST /auth/login(CHAT_USERNAME/CHAT_PASSWORD). - Connect OpenRouter (optional, per user): OpenRouter's PKCE flow; the Observatory exchanges the code and stores the user's key.
Both sign-in methods end in the same session: an HS256 JWT signed with JWT_SECRET, with sub (username), iss and aud (github-backup), iat and exp. The Observatory accepts it when sub is CHAT_USERNAME or one of ALLOWED_GITHUB_USERS, so both services need the same JWT_SECRET and the same allowlist.
1. Pick the Public URL and Callback URLs
Set APP_URL on the dashboard to the origin users open, for example https://github.mishrashardendu22.is-a.dev. It is required in production. The dashboard builds its callback URLs from it, and whether its cookies are Secure. When it is unset, both come from the request's Host, X-Forwarded-Host and X-Forwarded-Proto headers. That breaks behind proxies that do not send them, and behind a proxy that passes client-supplied values the client's headers decide them. Leave it unset only for local development.
| Provider | Callback URL | Where it is registered |
|---|---|---|
| GitHub | <APP_URL>/api/auth/github/callback | In the GitHub App or OAuth App (next section) |
| OpenRouter | <APP_URL>/api/auth/openrouter/callback | Nowhere: the dashboard sends it with each authorization request |
For local development the GitHub callback is http://localhost:3000/api/auth/github/callback.
Cookies are marked Secure on every origin except plain-HTTP loopback (http://localhost, http://127.0.0.1, http://[::1]). Serving the dashboard over plain HTTP on any other host (for example http://192.168.1.20:3000) therefore cannot keep a session; use HTTPS or localhost.
2. Create the GitHub OAuth Credentials
Either a GitHub App or a classic OAuth App works; the dashboard only needs a client ID and a client secret, requests no scopes and throws the GitHub access token away after reading the login.
Option A: GitHub App
- GitHub, Settings > Developer settings > GitHub Apps > New GitHub App.
- Homepage URL:
APP_URL. - Callback URL:
<APP_URL>/api/auth/github/callback. A GitHub App can list several callback URLs, so the production and the localhost callback can share one app. - Leave Request user authorization (OAuth) during installation and Enable Device Flow off, and untick Webhook > Active.
- Permissions: none. Reading the signed-in user's login needs no permission.
- Where can this GitHub App be installed?: Only on this account. The app does not have to be installed for sign-in.
- Create the app, copy the Client ID and generate a client secret.
Option B: OAuth App
- GitHub, Settings > Developer settings > OAuth Apps > New OAuth App.
- Homepage URL:
APP_URL. - Authorization callback URL:
<APP_URL>/api/auth/github/callback. An OAuth App has a single callback URL, so use a second OAuth App for localhost. - Register the app, copy the Client ID and generate a client secret.
3. Set the Environment Variables
All dashboard variables are read by the dashboard server at request time and never sent to the browser.
Dashboard (src/apps/github-backup-dashboard)
| Variable | Required | Purpose |
|---|---|---|
GITHUB_OAUTH_CLIENT_ID | For GitHub sign-in | Client ID from step 2. When set, the sign-in panels show Sign in with GitHub. |
GITHUB_OAUTH_CLIENT_SECRET | For GitHub sign-in | Client secret from step 2. |
JWT_SECRET | For GitHub sign-in; recommended always | Same value as the Observatory's. Signs GitHub sessions and verifies every session locally. Without it, each session check (and each gated request) asks the Observatory's GET /auth/me. |
ALLOWED_GITHUB_USERS | For GitHub sign-in | Comma-separated GitHub logins, compared case-insensitively. Empty refuses every GitHub login. |
APP_URL | Required in production | Public origin for callback URLs and the cookies' Secure flag, for example https://dash.example.com. Unset (local development only) means the request's origin. |
JWT_EXPIRES_MINUTES | No (default 60) | Lifetime of sessions created by GitHub sign-in; the cookie expires with the JWT. Password sessions last as long as the Observatory's token. |
DASHBOARD_REQUIRE_LOGIN | No (default false) | true requires a session for every page and /api/proxy/* (see section 5). Unrecognised values count as true. |
AGENT_API_URL | Already required | The Observatory; the password login, session checks and the OpenRouter exchange call it. |
INTERNAL_SECRET | Already required | Same value as the API's and the Observatory's. The password login sends it with the browser's address (X-Client-Address), so the Observatory limits failed logins per browser instead of per dashboard. |
GITHUB_OAUTH_BASE_URL / GITHUB_API_BASE_URL | No | Default https://github.com / https://api.github.com. Only for GitHub Enterprise Server or a local test stub. |
Generate a secret with openssl rand -base64 48 and use the same value on both services.
Observatory (src/services/github-backup-observatory)
| Variable | Purpose |
|---|---|
JWT_SECRET | Same value as the dashboard's. |
ALLOWED_GITHUB_USERS | Same list as the dashboard's; the Observatory accepts a JWT whose sub is CHAT_USERNAME or one of these logins. |
CHAT_USERNAME / CHAT_PASSWORD | Credentials of the password fallback. |
JWT_EXPIRES_MINUTES | Lifetime of password sessions. |
Settings the Observatory needs for storing per-user OpenRouter keys are described in its own README.
Local Development
Put the dashboard values in a gitignored src/apps/github-backup-dashboard/.env.local:
AGENT_API_URL=http://localhost:8000
BACKUP_API_URL=http://localhost:8080
APP_URL=http://localhost:3000
GITHUB_OAUTH_CLIENT_ID=<client id>
GITHUB_OAUTH_CLIENT_SECRET=<client secret>
JWT_SECRET=<same value as the Observatory>
ALLOWED_GITHUB_USERS=<your-github-login>
4. Routes and Cookies
| Route | Method | Purpose |
|---|---|---|
/api/auth/github/start?returnTo=/path | GET | Starts GitHub sign-in; returnTo must be a relative path on the dashboard (default /ai). |
/api/auth/github/callback | GET | GitHub redirects here; sets the session cookie and returns to returnTo. |
/api/auth/password | POST | Form fields username, password; the Observatory login, done by the server. |
/api/auth/session | GET | {authenticated, username, method, expiresAt, providers: {github}}. |
/api/auth/logout | POST | Deletes the session cookie (same-origin requests only). |
/api/auth/openrouter/start | GET | Starts "Connect OpenRouter" (requires a session). |
/api/auth/openrouter/callback | GET | OpenRouter redirects here; the Observatory exchanges the code; lands on /settings. |
/api/health | GET | Liveness check, public even with the gate on. |
| Cookie | Path | Lifetime | Content |
|---|---|---|---|
ghb_session | / | The JWT's lifetime | Session JWT |
ghb_github_oauth | /api/auth/github | 10 minutes | OAuth state, PKCE verifier, return path |
ghb_openrouter_pkce | /api/auth/openrouter | 10 minutes | PKCE verifier |
All three are HttpOnly and SameSite=Lax, and Secure except on plain-HTTP loopback origins.
The Observatory proxy (/api/proxy/agent/*) sends the session JWT as Authorization: Bearer <jwt>, ignores any Authorization header from the browser, and refuses POST/PATCH/DELETE requests that do not come from the dashboard's own pages (Sec-Fetch-Site / Origin check). The Connections page (/settings) shows the signed-in account and the OpenRouter status, and connects or disconnects the key.
5. Optional Login Gate
By default the dashboard stays viewable without signing in; only the AI features need a session. With DASHBOARD_REQUIRE_LOGIN=true:
- Pages redirect signed-out visitors to
/signin?returnTo=<page>. /api/proxy/*answers401JSON (AUTH_REQUIRED), or503when the Observatory cannot confirm a session.- The landing page
/,/api/auth/*,/api/health,/signinand static assets (/_next/*and files with an extension underpublic/) stay public. The overview at/dashboardand every other page are gated.
Set JWT_SECRET on the dashboard when the gate is on; otherwise every gated request costs one call to the Observatory. Point container health checks at /api/health rather than a page. After sign-in with no valid returnTo, users land on /dashboard.
6. Troubleshooting
| Symptom | Cause |
|---|---|
| "GitHub sign-in is not configured" (503) | GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_CLIENT_SECRET or JWT_SECRET is missing on the dashboard; the page names the missing ones. |
| "GitHub sign-in is disabled" (403) | ALLOWED_GITHUB_USERS is empty on the dashboard. |
| "This GitHub account is not allowed" (403) | The login is not in ALLOWED_GITHUB_USERS. |
GitHub reports that the redirect_uri is not associated with the application | The registered callback URL differs from <APP_URL>/api/auth/github/callback, or APP_URL is unset behind a proxy. |
| "Sign-in could not be verified" (400) | The sign-in took longer than 10 minutes, was started in another browser, or cookies are blocked. |
| "The Observatory issued a session this dashboard cannot verify" | JWT_SECRET differs between the services, or the Observatory issues tokens without iss/aud. |
| Signed in, but the first AI request ends the session ("Your session expired or was not accepted by the Observatory") | The Observatory rejects the JWT: different JWT_SECRET, the GitHub login missing from the Observatory's ALLOWED_GITHUB_USERS, or an Observatory build without GitHub-user support. |
| Sign-in works on localhost but not on a LAN address | Session cookies are Secure; use HTTPS. |
7. Migrating From the Shared-Password Login
- Earlier builds kept the JWT in
localStorage(agent_token,agent_username). The dashboard deletes those keys on the first page load, so every existing user is signed out once and signs in again. /api/proxy/agent/auth/loginand/api/proxy/agent/auth/meare no longer forwarded; sign-in goes through/api/auth/*.- Deploy the Observatory with the matching
JWT_SECRETandALLOWED_GITHUB_USERSbefore enabling GitHub sign-in, or GitHub sessions are rejected by the Observatory.