A security-focused, read-only dashboard for an existing Wei-Shaw/sub2api deployment. It adds a Vue SPA and a Fastify BFF without modifying, forking, or patching upstream. The compatibility baseline is fixed to upstream commit c2c19a7cbe8486ebb5b56834d1a6e07b3f12cffc (version 0.1.159).
- Authenticates users through the existing sub2api login and optional 2FA endpoints.
- Intersects upstream user authorization with a deployment-owned group allowlist.
- Lists group-filtered accounts and exposes only anonymous account references and safe status/quota DTOs.
- Uses passive usage only for supported Anthropic credentials. OpenAI OAuth quota is read from the account list's normalized Codex 5-hour/7-day snapshot fields, without another upstream usage call. It never performs active probes, billing calls, generic proxying, credential mutation, or upstream modification.
- Serves the built SPA from Fastify in production. Unknown
/api/*routes remain JSON 404 responses; browser routes fall back toindex.html.
See architecture, security, the pinned upstream API notes, operations, and troubleshooting.
- Node.js 22
- pnpm 10
pnpm install
cp .env.example .env
# Replace every placeholder. Generate each cryptographic key separately.
pnpm devThe web dev server runs through Vite and proxies /api and /health to the Fastify server on port 3000. For a production-style local run:
pnpm build
# Set WEB_DIST=apps/web/dist in .env
pnpm start-
SUB2API_BASE_URL: the existing upstream origin. No credentials in the URL. -
SUB2API_ADMIN_API_KEY: upstream admin key, sent only asx-api-keyto the two allowlisted admin operations. On a new upstream install, finish the upstream setup/bootstrap flow first and create/configure this key there; this dashboard does not bootstrap upstream. -
PUBLIC_ORIGIN: exact browser origin, without a path, query, fragment, or embedded credentials. -
GROUP_ALLOWLIST_JSON: deployment-owned allowlist. Example:[ { "slug": "team-a", "upstreamId": 1, "label": "Team A", "platformHint": "Anthropic" } ] -
SESSION_COOKIE_KEY,CSRF_HMAC_KEY,ACCOUNT_REF_HMAC_KEY: different random values, each at least 32 bytes.SESSION_COOKIE_KEYis the separate cookie key; the encrypted session vault uses its own Redis vault keyring. The example environment file contains safe generation commands and no real secrets. -
Authenticated sessions survive browser close and dashboard restart through a dedicated encrypted Redis vault backed by a persistent AOF volume. Sessions expire after 7 days idle or 30 days absolute. The anonymous/2FA cookie remains a browser-session cookie with a 10-minute lifetime. Redis outages fail closed with HTTP 503 and do not clear the browser cookie.
-
Redis records and AOF files contain sensitive AES-256-GCM ciphertext. Protect the Redis volume and vault keyring as secrets. Retain old vault keys until all sessions encrypted with them have expired.
-
The initial migration requires one login. Losing the Redis volume or vault keys, reaching either session expiry, logging out, or upstream invalidating the session requires login again.
-
Cookie, CSRF, account-reference, and Redis vault keys must never be reused.
-
WEB_DIST: SPA output directory. Useapps/web/distfrom the repository root or/app/webin the provided container.
All variables are documented in .env.example.
| Command | Purpose |
|---|---|
pnpm dev |
Run server and web development processes |
pnpm start |
Start the built Fastify server |
pnpm build |
Build contracts, web, and server in workspace order |
pnpm test |
Run all Vitest projects; does not require a browser |
pnpm test:contract |
Run shared contract regression tests |
pnpm test:e2e |
Run the opt-in Playwright Chromium flow with an in-process mock upstream |
pnpm lint |
Lint the workspace |
pnpm typecheck |
Type-check the workspace |
pnpm format:check |
Check formatting |
Install the browser once before E2E testing: pnpm exec playwright install chromium. The default pnpm test remains browser-independent.
docker build -t sub2api-dashboard .
# Redis is mandatory. Use docker-compose.example.yml, or attach the container
# to a network where REDIS_URL resolves to a dedicated persistent Redis service.
docker compose -f docker-compose.example.yml up -d --buildThe multi-stage image uses Node 22 and pnpm 10 to build contracts, web, and server. Runtime is non-root, contains production dependencies and built artifacts only, serves WEB_DIST=/app/web, checks /health/live, and stops with SIGTERM.
For a hardened single-instance example using environment-provided secrets, a read-only root filesystem, tmpfs, dropped capabilities, and no-new-privileges, see docker-compose.example.yml.
- Browser cookies contain only an encrypted session ID. Authenticated sessions survive browser close and dashboard restart through the dedicated encrypted Redis vault/AOF volume; they expire after 7 days idle or 30 days absolute. The anonymous/2FA cookie remains a browser-session cookie with a 10-minute lifetime. Redis outages fail closed with HTTP 503 without clearing the browser cookie.
- The single dashboard instance remains required because caches, single-flight coordination, and rate limits are process-local. Losing the Redis volume or vault keys, absolute expiry, logout, or upstream invalidation requires login; the initial migration requires one login.
- Compliance failures are returned as HTTP 423 with
COMPLIANCE_LOCKED; resolve setup/compliance in upstream. - Upgrade upstream only after completing the compatibility checks in operations.