Admin console basics
The admin console (apps/admin) is Bloom's operations surface: a separate SPA on its own
origin, beside the user dashboard, for the people who run Bloom - not for project owners.
It is grant-gated, read-only by design, and every write it can perform is an explicitly
guarded, audited action. In the current deployment it lives at https://bloom-admin.pages.dev
(Cloudflare Pages), reaching the API through a same-origin /api/* proxy.

The screens follow the approved design of record - the frames on this page are those boards.
Signing in
Sign-in is Google, against the admin session surface. A Google account alone is not enough:
you need an admin grant, and its role - viewer, operator, or superadmin -
determines your permission set. An ungranted account lands on the access-denied page with no
session cookie set; a granted one gets an admin session cookie scoped to the admin origin
only. The server re-checks the grant on every admin API call - nothing about your access is
decided client-side.
Reading data
The console is a read-only resource browser over seven resources: users,
admin_grants, sessions, oauth_identities, telegram_links, workspaces, and
global_credentials.
- Resource lists expose exactly the search/sort/filter controls the backend whitelists - no more. List state (filter, search, sort, page) round-trips through the URL, so a filtered view is a shareable link.
- Record detail shows the record read-only, with related panels cross-linked by
user_id- from a user, jump straight to their sessions, grants, identities, and links. - The raw-JSON inspector (
/{resource}/{id}/raw) shows the exact API payload, making the structural-redaction contract visible: what the API redacts simply is not there, for anyone, at any role. - Overview aggregates only what the API can truthfully produce: per-resource totals, grants by role, recent sessions.
- ⌘K / Ctrl+K opens a command palette for jumping between resources.

There are deliberately no create/edit/delete or bulk controls anywhere - the API has no generic mutation routes, and the console renders no affordance the server would refuse ("no fake affordances").
Acting: guarded verbs
The only write path is the guarded action:
POST /api/admin/{resource}(/{id})?/actions/{verb} - one explicit verb per operation (grant
admin, change a role, revoke a session, ...), each gated on its exact permission. A button
appears only when your grant carries that permission.
Safety scales with destructiveness:
- Reversible-until-sent actions (revoking a session) fire optimistically with an undo window - the request is deferred, and Undo cancels it.
- Irreversible actions get hard confirms: deleting a user first shows a real preview of the cascade it would trigger, then requires typing the account's email.
Every action - including denied attempts - lands in the audit trail with before/after
snapshots, readable at GET /api/admin/audit_log.
Running it locally
pnpm install # once, at the repo root
cd apps/admin
pnpm dev # http://localhost:5174
The dev server proxies /api to the deployed API (https://bloom-server.exe.xyz by
default; override with VITE_DEV_API_TARGET). Unauthenticated you get the login screen, and
an ungranted Google account lands on access-denied. Completing the admin sign-in against a
local API requires pointing the API back at the SPA
(BLOOM_ADMIN_DASHBOARD_URL=http://localhost:5174) - see the
apps/admin README,
which also documents the security model in depth.