This feature is part of the incoming v0.4.0 release from the arcanagov branch.
Admin Console
The Arcana Admin Console is a unified operations surface for platform administrators. It provides site-wide notice management, user analytics, usage monitoring, rate limit controls, and a full audit trail — all secured behind identity-based authentication with MFA.
Overview
The Admin Console consolidates all operator tasks into a single secured surface at /admin. It replaces ad-hoc shared-token access with person-bound identity, allowlist enforcement, and multi-factor authentication.
Goals
- Site-wide notices — Schedule-aware, color-customizable banners for all users without redeploying.
- Unified admin shell — One surface for notices, users, usage, rate limits, feedback, and audit.
- Real security — Person-bound identity, allowlist, MFA, audit trail, step-up for destructive actions.
- Design consistency — Same density, type, color, and layout language as the console.
- Privacy-aware analytics — Aggregates for maps and usage; no gratuitous raw PII.
Authentication
Admin access requires a multi-layer authentication pipeline:
- Operator opens
/admin - If no Supabase session → redirect to
/auth?next=/admin - After sign-in, the server validates:
- Valid JWT (signature, expiration, audience)
- Email (normalized) is in
ADMIN_ALLOWLISTorapp_metadata.admin === true - MFA level:
currentLevel === 'aal2'(if aal1, force MFA challenge)
- Every admin API re-runs all checks — never trusts client-side "isAdmin" flags
Allowlist Configuration
# Site environment variables
[email protected],[email protected]
# Optional: comma-separated Supabase user UUIDs
ADMIN_USER_IDS=
Authorization Matrix
| Action | Allowlist | AAL2 | Step-up / Confirm | Audit |
|---|---|---|---|---|
| View overview | ✓ | ✓ | — | optional |
| Read audit log | ✓ | ✓ | — | — |
| Edit notice | ✓ | ✓ | — | ✓ |
| View usage / user aggregates | ✓ | ✓ | — | optional |
| Reset rate limit (one user) | ✓ | ✓ | confirm modal | ✓ |
| Reset rate limits (ALL) | ✓ | ✓ | re-MFA + type phrase | ✓ |
| Mint license (machine) | admin key only | n/a | n/a | ✓ |
| Delete feedback | ✓ | ✓ | confirm | ✓ |
Site Notices
Site notices are schedule-aware banners that appear across the marketing site, auth pages, and console. They can be configured with custom colors, severity levels, and dismissibility.
Notice Properties
| Property | Type | Description |
|---|---|---|
enabled | boolean | Toggle notice on/off |
severity | info | warn | critical | dev | Visual severity level |
title | string (max 80) | Notice headline |
body | string (max 400) | Notice body text (plain text) |
link_url | string (https only) | Optional call-to-action link |
link_label | string (max 40) | Link display text |
colors | { bg, fg, border } | Custom hex colors |
starts_at | ISO 8601 UTC | Schedule start (or null for immediate) |
ends_at | ISO 8601 UTC | Schedule end (or null for indefinite) |
surfaces | marketing | console | auth | all | Where the banner appears |
dismissible | boolean | Whether users can close the banner |
Default Colors by Severity
| Severity | Background | Foreground | Border |
|---|---|---|---|
info | #1e3a5f | #e8eef8 | #3b82f6 |
warn | #422006 | #fef3c7 | #eab308 |
critical | #450a0a | #fecaca | #dc2626 |
dev | #1a1c20 | #f5f0e8 | #1d4ed8 |
Public Status API
Clients fetch active notices from a public endpoint:
GET /api/site-status
Returns the active notice (if any) with a server_time field for clock skew correction. Cached with Cache-Control: public, max-age=30, stale-while-revalidate=60.
Console Modules
Overview Dashboard
The landing page after admin login. Shows:
- Active notice status (on/off + schedule window)
- User counts: total, active 24h, free/pro split
- Usage: 24h requests, error rate
- Rate limit events: 24h count
- Last 10 audit entries
Users
Aggregate user analytics — counts, tiers, activity windows. No raw PII dump; user IDs shown as prefixes unless in support mode.
Usage
Time-series charts for requests, tokens, and cost. Server-side aggregation only — no raw request logs downloadable in v1.
Geo Map
Country-level choropleth with sortable table. Filters for date range, tier, and free/paid split. No individual user location pins — privacy-first design with country aggregates only.
Rate Limits
Inspect and reset rate limits per user or globally. Global resets require re-MFA and typing the confirmation phrase RESET ALL RATE LIMITS.
Feedback
Existing feedback admin UI migrated under the admin shell. Token-based login replaced with identity-based admin session.
Audit Log
Append-only log of all admin actions. Filterable by action type, actor, and date. Each entry includes actor email, action, target, metadata JSON, hashed IP, and request ID.
Routes
| Path | Module |
|---|---|
/admin | Overview dashboard |
/admin/notices | Site banner CRUD |
/admin/users | User counts, tiers, activity |
/admin/usage | Usage charts and totals |
/admin/map | Geo choropleth + filters |
/admin/rate-limits | Inspect / reset rate limits |
/admin/feedback | Feedback management |
/admin/licenses | License management (optional) |
/admin/audit | Audit log table |
Security Design
Threat Model
| Threat | Impact | Mitigation |
|---|---|---|
| Stolen shared admin token | Full ops access | Replace with person-bound identity |
| XSS on site origin | Session/token theft | Strict CSP; no long-lived secrets in JS |
| Phished operator password | Rate limit wipe | MFA AAL2 required; step-up on destructive actions |
| Insider abuse | Silent mass reset | Audit log; typed confirmation; dual control later |
| Enumeration of /admin | Targeted attacks | noindex; Access; no public links |
| Geo/IP leakage | Privacy/regulatory | Country aggregates only; hash IP at rest |
Trust Boundaries
| Boundary | Who Crosses | Mechanism |
|---|---|---|
| Browser → public status | Anyone | No auth; rate-limit lightly |
| Browser → admin APIs | Operators only | JWT + allowlist + MFA |
| Site → proxy admin | Site Function only | Short admin key (never in browser) |
| Proxy admin (machine) | CI / device mint | Admin key break-glass |
Audit Log Schema
All admin mutations are logged to an append-only D1 table:
CREATE TABLE admin_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at INTEGER NOT NULL, -- unix ms
actor_id TEXT, -- supabase uid
actor_email TEXT NOT NULL,
action TEXT NOT NULL, -- e.g. notice.update
target TEXT, -- e.g. user_id, 'global'
meta_json TEXT, -- JSON: before/after, counts
ip_hash TEXT, -- sha256(ip + server_pepper)
user_agent TEXT,
request_id TEXT -- CF ray id
);
Application may INSERT + SELECT. No delete API. Retention: 2 years.
API Contracts
Site APIs (Pages Functions)
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/site-status | Public | Active notice for clients |
GET | /api/admin/me | Admin JWT | Actor profile, AAL, allowlist status |
GET | /api/admin/notice | Admin | Full notice including inactive |
PUT | /api/admin/notice | Admin | Update notice |
GET | /api/admin/audit | Admin | Paginated audit log |
GET | /api/admin/proxy/* | Admin | BFF to proxy admin (server secret) |
Proxy Admin APIs
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /v1/admin/stats/overview | Admin key | Users, requests, tiers |
GET | /v1/admin/stats/usage | Admin key | Time series data |
GET | /v1/admin/stats/geo | Admin key | Country buckets |
POST | /v1/admin/rate-limits/reset | Admin key | Reset rate limits |
The site acts as a BFF (Backend for Frontend) — browser calls site Functions with JWT, site Functions call proxy with admin key. The browser never holds the proxy admin key.
Design Tokens
The Admin Console inherits the console design system and adds admin-specific semantic tokens. Full dark and light theme support.
Color Tokens (Dark)
:root {
--admin-bg: #0a0a0a;
--admin-surface: #191919;
--admin-surface-2: #1a1c20;
--admin-border: #212327;
--admin-text: #ffffff;
--admin-text-muted: #7d8187;
--admin-primary: #1d4ed8; /* cobalt */
--admin-primary-hover: #3b82f6;
--admin-success: #22c55e;
--admin-warning: #eab308;
--admin-error: #dc2626;
--admin-danger-bg: rgba(220, 38, 38, 0.08);
--admin-danger-border: rgba(220, 38, 38, 0.35);
--admin-danger-text: #fca5a5;
}
Typography
| Role | Font | Size | Weight |
|---|---|---|---|
| Page title | sans | 2xl (1.375rem) | 500–600 |
| Section label | mono | xs (0.625rem) | 400, uppercase |
| Nav item | sans | sm (0.8125rem) | 400 |
| Body | sans | sm–base | 400 |
| Code / IDs | mono | xs–sm | 400 |
Implementation Phases
Phase 1 — Admin Shell + Auth
/adminlayout with sidebar navigation- Supabase login + allowlist + AAL2 gate
/api/admin/meendpointadmin_auditD1 table- Migrate feedback under admin shell
- Design tokens in
admin-console.css
Exit criteria: Only allowlisted MFA users can enter; feedback still works; unauthenticated users blocked.
Phase 2 — Notices
- KV notice storage
- GET public status + PUT admin notice
- Notices UI with live preview
- Banner injection on marketing + console
- Audit logging on notice updates
Exit criteria: Schedule a banner for a 10-minute window; all surfaces show/hide correctly.
Phase 3 — Ops Metrics + Rate Limits
- Overview stats BFF
- Users and usage pages
- Rate limit inspect + per-user reset
- Mass reset with step-up authentication
- Audit all mutations
Phase 4 — Map + Hardening
- Geo buckets at edge
- Map UI with choropleth
- Admin subdomain and/or Cloudflare Access
- Privacy review
Operations Runbooks
Publish a Maintenance Banner
- Sign in to admin (MFA required)
- Navigate to Notices → enable → set UTC window → choose colors → set surfaces to
all - Save → verify banner appears on marketing + console within 60 seconds
- After the window expires, banner auto-hides; disable manually if needed
Emergency Rate-Limit Wipe
- MFA admin → Rate limits → Danger zone
- Enter reason + type
RESET ALL RATE LIMITS - Confirm step-up if prompted
- Verify audit row; spot-check that a free user can request again
Offboard an Operator
- Remove email from
ADMIN_ALLOWLIST(and Cloudflare Access if enabled) - Revoke Supabase sessions if needed
- Rotate machine admin keys if that operator had environment access