Skip to content

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).

Terminal window
ADMIN_PASSWORD='' bun run admin:create -- --email you@example.com --name 'You'
# add --yes to promote an account that already exists

With 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.

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.

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.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.

  • 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.