Admin
Platform administrators are users whose role is admin. This role is independent of
organization roles: an organization owner is not an admin, and an admin has no implicit membership
anywhere.
The same role list drives Better Auth’s admin({ adminRoles }), the API’s requireAdmin middleware
and the /admin route guard (a universal load that redirects non-admins to /app).
Bootstrap
Section titled “Bootstrap”ADMIN_PASSWORD='…' bun run admin:create -- --email you@example.com --name 'You'# add --yes to promote an account that already existsWith ADMIN_PASSWORD unset the script prompts for a password. It is never read from the command
line, because arguments stay in shell history and are visible in ps. The script uses the kit’s own
auth config and downloads no CLI, so it also runs inside the api container.
Promote further admins from /admin/users.
/admin/users: people (Better Auth admin plugin)
Section titled “/admin/users: people (Better Auth admin plugin)”Search, ban and unban with a reason and an optional expiry, set role, revoke sessions, impersonate.
These come from the Better Auth admin plugin at /api/auth/admin/*. Starterdough adds no procedures
for them.
Impersonation sessions last one hour and cannot target other admins. While one is active the app shell shows a banner with Stop impersonating. Starting an impersonation and revoking a user’s sessions both ask for confirmation first, because neither can be undone.
Impersonate from the web app. The desktop and mobile shells hide the button: the admin’s own session waits in a cookie that a shell never gets back, so it could start an impersonation but never stop it (see Authentication).
/admin/organizations: tenants
Section titled “/admin/organizations: tenants”Every organization with member and workspace counts, effective plan and current subscription
(admin.organizations.list). A subscription whose seats disagrees with the member count is
flagged here. Seat sync logs its failures instead of throwing, so this page is where drift is
visible before the daily reconcile repairs it.
Force-cancel a subscription at period end or immediately
(admin.organizations.cancelSubscription). The API asks Stripe, mirrors the answer into the local
row and audits it with the admin as actor. It answers 412 when billing is not configured, 404
without a live subscription and 502 with Stripe’s message on a Stripe error.
Revoke API keys and webhooks (admin.organizations.revokeMachineAccess,
POST /api/v1/admin/organizations/{organizationId}/revoke-machine-access) stops an organization’s
own systems at once: it revokes every key the organization holds that is not revoked yet, and
switches off every webhook endpoint that is on, as an owner’s own switch would, so a pending
delivery ends without an attempt. Each one is audited in the organization’s log as
api_key.revoke or webhook.disable, with the admin as the actor and by: 'platform-admin' in
its metadata, and the answer counts them (apiKeysRevoked, webhooksDisabled). The page asks
once more first, since a revoked key stays revoked. This is what a ban cannot do: banning a person
ends their sessions, and stops the keys they created while it lasts, because they saw those keys’
secrets, but not the keys other members created or the organization’s webhooks, and an admin
cannot impersonate a banned owner to revoke them. It suspends nothing: an owner or admin who is
not banned can create keys and switch endpoints on again, so ban them too, or delete the
organization, to keep it out.
Delete an organization (admin.organizations.delete,
DELETE /api/v1/admin/organizations/{organizationId}) is for a tenant nobody owns any more. Better
Auth needs an owner to promote one, and only an owner may delete an organization. It takes the
organization’s slug as confirmSlug, refuses with PRECONDITION_FAILED while a live subscription
exists, and reports how many stored objects went with it (objectsDeleted, objectsFailed). The
page keeps the button disabled until the slug is typed out. The organization’s audit log goes with
it, so the record of who deleted it is one info line in the API’s log, platform admin deleted an organization, with the organization’s id, name and slug and the admin’s user id. It lasts only as
long as those logs: the default stack rotates each container’s at 10 MB × 5, and a LOG_LEVEL of
warn or error drops the line. If you need that answer later, ship the API’s logs somewhere
that keeps them.
/admin/flags: feature flags
Section titled “/admin/flags: feature flags”Flags live in feature_flag (a global default) and feature_flag_override (per organization).
Admins manage both (admin.flags.list | upsert | delete | setOverride). Clients read the resolved
map for an organization from system.flags (GET /api/v1/flags?organizationId=…).
In the app, the (app) layout load fetches the flags for the active organization once per session.
Components read flag('key') from #lib/flags.js. An unreachable API means “nothing is on”.
Keys are stable slugs used literally in code: lowercase letters, numbers, dots, dashes and underscores.
/admin/system: health
Section titled “/admin/system: health”admin.system.status reports your product’s version and the kit release (version and
kitVersion in the root package.json), the environment and runtime, uptime, counts (users,
organizations, workspaces, entitling subscriptions), a database probe with the state of both
migration histories (the journals shipped in the build vs. drizzle.__drizzle_migrations for the
kit’s and drizzle.__product_migrations for yours), the AI service’s /health, and which optional
subsystems are configured: billing (with any unpriced plans), the email provider and sender, social
sign-in and email verification. Never secrets.
The AI service answers /health with HTTP 200 and ok: false when its configuration cannot
serve a request (no token check, or an OCR backend that cannot run there). It lists one sentence per
fault in problems, which this page shows under a Degraded label. It is not a 503: the
container health check asserts liveness only, and a 503 would mark the container unhealthy and fail
every deploy, which waits for healthy, over its environment.
Consequences to know
Section titled “Consequences to know”- Admin actions on users are not in the per-organization audit log (they have no tenant). Rely on Better Auth’s request logging or add a global log when needed.
- An action an admin takes while impersonating is audited in that organization’s log with the
impersonated user as the actor and the admin’s id in
impersonated_by. - Flag reads are one indexed query per page load with no caching. Add a short cache when flags are read on hot paths.
- Deleting users is not exposed in the UI.