Admin
Platform administrators are users whose role is admin — an axis 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, like the session guard, redirecting 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, where it would sit in shell history and in ps; and it goes through the kit’s own auth config
rather than downloading a CLI, so it runs inside the api container as well as from a checkout.
Further admins are promoted 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/*; Director 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: they are one click beside a search result, and 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 swallows its failures into a log line, so this 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. 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 the escape hatch 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
and 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, which is the single
source of truth for it — 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 — with one sentence per
fault in problems, which this page lists under a Degraded label rather than a green tick. It is
deliberately not a 503: 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, so an operator’s change is never attributed to the person they were impersonating. - 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 deliberately not exposed in the UI.