Skip to content

Admin Panel

The admin panel is accessible at /admin and provides platform-wide visibility into users, projects, jobs, provider calls, and system health. It is protected at three layers: ORPC middleware, Better Auth admin plugin, and a frontend layout guard.

RoutePurpose
/adminPlatform metrics: user/project counts, jobs by status, total cost
/admin/usersUser list with search, role management (user/admin), ban/unban
/admin/users/[userId]User detail: profile, billing, projects, jobs, sessions
/admin/projectsCross-user project listing with search
/admin/jobsJob monitor with tabs (all/stuck/failed), retry/cancel actions
/admin/providersProvider analytics: call counts, error rates, latency, cost
/admin/healthSystem health: stuck jobs, recent failures, provider errors (24h)
/admin/logsActivity log: provider calls with filters (provider, status, job type, errors)
  1. ORPC requireAdmin middleware (packages/api/src/index.ts) — checks session.user.role === "admin" on every admin procedure. Returns UNAUTHORIZED (401) or FORBIDDEN (403).
  2. Better Auth admin plugin (packages/auth/src/index.ts) — independently protects /api/auth/admin/* endpoints with its own role check.
  3. Frontend layout guard (apps/web/src/routes/admin/+layout.svelte) — redirects non-admin users to /dashboard. UX only, not a security barrier.

All admin procedures live in the admin namespace of appRouter (packages/api/src/routers/index.ts):

ProcedureDescription
admin.usersListSearch/paginated user list
admin.userDetailUser profile + projects, jobs, billing, sessions
admin.userSetRoleChange role (user/admin)
admin.userBanBan user with optional reason and expiry
admin.userUnbanRemove ban
admin.projectsListAllAll projects cross-user with search
admin.jobsListAllAll jobs with status/type filters
admin.jobsStuckJobs stuck longer than threshold
admin.jobAdminRetryRetry a failed job (no ownership check)
admin.jobAdminCancelCancel an active job
admin.providerCallsStatsAggregated provider stats
admin.providerCallsRecentRecent provider calls with filters
admin.platformMetricsPlatform-wide user/project/job/cost totals
admin.systemHealthFailed jobs (1h), stuck jobs, provider errors (24h)
admin.activityLogPaginated provider call log with filters (provider, status, jobType, errorsOnly)

The Better Auth admin plugin registers endpoints under /api/auth/admin/*:

  • POST /api/auth/admin/list-users
  • POST /api/auth/admin/ban-user
  • POST /api/auth/admin/unban-user
  • POST /api/auth/admin/set-role
  • POST /api/auth/admin/impersonate-user
  • POST /api/auth/admin/remove-user

These are automatically routed through the existing catch-all app.on(["POST", "GET"], "/api/auth/*", ...).

The adminClient() plugin is added to apps/web/src/lib/auth-client.ts, enabling client-side access to Better Auth admin APIs.

Option 1: Environment variable (emergency backdoor)

Section titled “Option 1: Environment variable (emergency backdoor)”

Set ADMIN_USER_IDS with comma-separated user IDs:

Terminal window
wrangler secret put ADMIN_USER_IDS
# Enter value: d8jdYK0avWgxQkUSrIsVynWFao0nQbgS

Better Auth treats these users as admins regardless of their role column value.

After migration 0006_admin_role.sql is applied:

Terminal window
wrangler d1 execute dubbit-metadata --remote \
--command "UPDATE user SET role='admin' WHERE id='<USER_ID>';"
Terminal window
wrangler d1 execute dubbit-metadata --remote \
--command "SELECT id, email FROM user WHERE email='<EMAIL>';"

Migration 0006_admin_role.sql adds:

  • user.role (text, default 'user')
  • user.banned (integer/boolean, default false)
  • user.ban_reason (text, nullable)
  • user.ban_expires (integer/timestamp, nullable)
  • session.impersonated_by (text, nullable)
  • Index user_role_idx on user.role
VariableWherePurpose
ADMIN_USER_IDSWorker secretComma-separated user IDs treated as admin by Better Auth

No additional env vars are required. The admin plugin uses the existing BETTER_AUTH_SECRET and database connection.