ADR-024: Platform admin panel
Status: accepted, 3 November 2026.
Context
Section titled “Context”Everything the platform operator could do so far happened on the command line or in Forge: user:reset-password, onboarding:funnel, a database console for “which shops exist and who owns them”. With real shops arriving, the operator needs to see every shop and account in one place, find a shop by its owner’s email or its domain, know which shops are stuck in onboarding or have never confirmed their address, and act in the few cases that need the platform rather than the shop: closing a shop that must not trade, and helping a person who cannot get in. The product’s tenant model (tenancy) was built so that no request ever sees two shops; an operator’s view is the one deliberate exception, and it has to be built so that the exception cannot become a hole.
Decision
Section titled “Decision”- One place, outside every tenant. The panel lives on the root domain at
/admin. The paths do not exist on a shop host. No Wayfinder, no workspace slug: plain relative URLs on the host the pages are served from. - A flag only the command line sets.
users.is_platform_admin, not mass assignable, changed only byadmin:grantandadmin:revoke, each of which writes to the audit log. No page grants or revokes it, so there is no privilege-escalation path through the application. - Two-factor required, 404 for everyone else. The
platform.adminmiddleware answers 404 to any account without the flag, signed in or not, and shows an account with the flag but without two-factor a page that leads to its two-factor settings. The same content security policy and response headers as the control panel apply. - Read across tenants, write almost nothing. The read model (
PlatformDirectory) drops the workspace global scope on purpose and never writes. The only state changes areworkspaces.is_active(suspend and reactivate, with a reason) and two emails the person could have requested themselves (a password reset link, the address confirmation). No impersonation, no editing of a shop’s data, no password setting, no money figures, no customer PII. - Password again for every change, and an append-only log. Changes sit behind Fortify’s
password.confirm, are rate limited, and are recorded inplatform_admin_actionsin the same transaction: who, what, the subject and its name at the time, the reason, the address, the time. Rows are never updated or deleted; the model has noupdated_at.
Alternatives considered
Section titled “Alternatives considered”- A
superadminrole inside workspaces. Rejected: roles are per workspace and the permission matrix is tested against the tenant boundary; an operator who is a member of every shop would blur that boundary and appear in every team list. - A separate application or a third-party admin package. Rejected for now: a second deploy to keep patched for a panel with four pages, and the packages that generate admin UIs would bypass the services and conventions (money, ledger, append-only history) the rest of the code relies on. The panel is ordinary Inertia pages with the existing components.
- Impersonation (“sign in as”). Deliberately left out. It is the most useful support tool and the most dangerous one: it turns every administrator account into every owner account. If it is ever added it needs its own ADR, an explicit time-boxed session, a banner in the shop, the owner’s consent or a recorded reason, and its own rows in the audit log. Until then, an operator who must act inside a shop asks its owner for an invitation (ADR-021).
- Deleting shops or accounts from the panel. Rejected: both cascade into business records that the append-only rules protect (ADR-002, ADR-011). Suspension closes a shop without destroying anything; removal, when it is ever needed, is a command with a written procedure.
Consequences
Section titled “Consequences”- Operators stop needing a database console for the everyday questions, and a shop that must not trade can be closed in a minute, with the reason on record.
- The audit log is the first platform-level record of operator actions; later operator powers (billing adjustments, domain changes, ownership transfer) must write to it too.
- Administrator accounts carry a standing requirement: two-factor on, and a shop of their own to set it up in.
admin:listshows who is locked out. - The read model’s cross-tenant queries are a pattern to copy carefully: every query drops the workspace scope explicitly and the panel writes nothing through them.
See the platform admin panel for the pages, the commands and the tests.