Leave Localhost logoLeave LocalhostDocs
Security

Super Admin Access

Platform super admins can open the /admin panel. This is platform-scoped authorization, deliberately separate from organization/workspace roles: a workspace owner is not a platform admin.

Platform super admins can open the /admin panel. This is platform-scoped authorization, deliberately separate from organization/workspace roles: a workspace owner is not a platform admin.

Source: convex/admin/access.ts and convex/admin/accessPolicy.ts.

How access is granted

Access is decided server-side from two env vars (see Environment Variables):

  • SUPER_ADMIN_EMAILS — comma-separated allow-list of emails. Blank/unset means nobody is an admin and the panel is effectively off.
  • SUPER_ADMIN_REQUIRE_2FAtrue (default) or false. When on, an allow-listed user must also have Better Auth two-factor enabled.

The decision (decideSuperAdminAccess) runs in order: authenticated → mapped to an app users row → email verified → email in the allow-list → 2FA enrolled (if required). Each step has a stable reason the UI can show.

The verified-email check is still enforced even though new email/password accounts must verify before sign-in. It protects legacy or externally imported unverified accounts and keeps the admin policy independent of provider-specific behavior. Google/Microsoft OAuth and magic link all set emailVerified; an unverified password account does not. (The check runs before the allow-list so an unverified session never learns who is allow-listed.)

Set the vars on the Convex deployment, not just .env:

bun --cwd packages/backend convex env set SUPER_ADMIN_EMAILS "you@example.com"
bun --cwd packages/backend convex env set SUPER_ADMIN_REQUIRE_2FA "true"

Security properties

  • No client input is trusted. The email and 2FA status are read from the Better Auth session server-side; no user id or email argument can grant access. (getSuperAdminAccess only returns a display-safe snapshot.)
  • Verified email required. An allow-listed address with an unverified email is denied (email_not_verified), so an unclaimed admin email cannot be taken over via unverified signup.
  • Server-enforced everywhere. Every admin query calls requireSuperAdmin; every admin mutation re-resolves access with resolveSuperAdminAccess. The Next.js route also gates server-side (404 for non-admins), but that is convenience — the backend is the boundary.
  • Denials are auditable. An authenticated non-admin hitting an admin write produces an admin.access_denied audit event. Because a Convex mutation rolls back all writes when it throws, the admin writes return a structured failure result instead of throwing, so the denial/failure audit actually commits. Anonymous callers are not recorded (avoids a write-spam vector). Denied reads cannot be audited (a Convex query cannot write).
  • Writes require step-up. Admin writes additionally require sensitive-action verification — see Admin: Workspaces.

Why an env allow-list and not a database table

The starter intentionally has no super_admins table. An env allow-list is easier to reason about, has no bootstrap problem, and avoids shipping a second admin-management product inside the starter. To add runtime grants later:

  • Add a super_admins table keyed by app user.
  • Add grant/revoke mutations protected by requireSuperAdminInMutation.
  • Require sensitive-action step-up for every grant/revoke.

Removing it

See Removing the Admin Panel and Audit Log.

On this page