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.
/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.
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.
/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. 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 the version (read from the root package.json), the environment and
runtime, uptime, counts (users, organizations, workspaces, entitling subscriptions), a database probe
with migration state (journal shipped in the build vs. drizzle.__drizzle_migrations), 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, because a
restart cannot fix an environment, so the container health check asserts liveness only.
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.