Admin Panel
Overview
Section titled “Overview”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.
| Route | Purpose |
|---|---|
/admin | Platform metrics: user/project counts, jobs by status, total cost |
/admin/users | User list with search, role management (user/admin), ban/unban |
/admin/users/[userId] | User detail: profile, billing, projects, jobs, sessions |
/admin/projects | Cross-user project listing with search |
/admin/jobs | Job monitor with tabs (all/stuck/failed), retry/cancel actions |
/admin/providers | Provider analytics: call counts, error rates, latency, cost |
/admin/health | System health: stuck jobs, recent failures, provider errors (24h) |
/admin/logs | Activity log: provider calls with filters (provider, status, job type, errors) |
Architecture
Section titled “Architecture”Security (3 layers)
Section titled “Security (3 layers)”- ORPC
requireAdminmiddleware (packages/api/src/index.ts) — checkssession.user.role === "admin"on every admin procedure. ReturnsUNAUTHORIZED(401) orFORBIDDEN(403). - Better Auth
adminplugin (packages/auth/src/index.ts) — independently protects/api/auth/admin/*endpoints with its own role check. - Frontend layout guard (
apps/web/src/routes/admin/+layout.svelte) — redirects non-admin users to/dashboard. UX only, not a security barrier.
Backend: ORPC admin procedures
Section titled “Backend: ORPC admin procedures”All admin procedures live in the admin namespace of appRouter (packages/api/src/routers/index.ts):
| Procedure | Description |
|---|---|
admin.usersList | Search/paginated user list |
admin.userDetail | User profile + projects, jobs, billing, sessions |
admin.userSetRole | Change role (user/admin) |
admin.userBan | Ban user with optional reason and expiry |
admin.userUnban | Remove ban |
admin.projectsListAll | All projects cross-user with search |
admin.jobsListAll | All jobs with status/type filters |
admin.jobsStuck | Jobs stuck longer than threshold |
admin.jobAdminRetry | Retry a failed job (no ownership check) |
admin.jobAdminCancel | Cancel an active job |
admin.providerCallsStats | Aggregated provider stats |
admin.providerCallsRecent | Recent provider calls with filters |
admin.platformMetrics | Platform-wide user/project/job/cost totals |
admin.systemHealth | Failed jobs (1h), stuck jobs, provider errors (24h) |
admin.activityLog | Paginated provider call log with filters (provider, status, jobType, errorsOnly) |
Better Auth admin endpoints
Section titled “Better Auth admin endpoints”The Better Auth admin plugin registers endpoints under /api/auth/admin/*:
POST /api/auth/admin/list-usersPOST /api/auth/admin/ban-userPOST /api/auth/admin/unban-userPOST /api/auth/admin/set-rolePOST /api/auth/admin/impersonate-userPOST /api/auth/admin/remove-user
These are automatically routed through the existing catch-all app.on(["POST", "GET"], "/api/auth/*", ...).
Frontend: auth client
Section titled “Frontend: auth client”The adminClient() plugin is added to apps/web/src/lib/auth-client.ts, enabling client-side access to Better Auth admin APIs.
Enabling admin access
Section titled “Enabling admin access”Option 1: Environment variable (emergency backdoor)
Section titled “Option 1: Environment variable (emergency backdoor)”Set ADMIN_USER_IDS with comma-separated user IDs:
wrangler secret put ADMIN_USER_IDS# Enter value: d8jdYK0avWgxQkUSrIsVynWFao0nQbgSBetter Auth treats these users as admins regardless of their role column value.
Option 2: Manual SQL update
Section titled “Option 2: Manual SQL update”After migration 0006_admin_role.sql is applied:
wrangler d1 execute dubbit-metadata --remote \ --command "UPDATE user SET role='admin' WHERE id='<USER_ID>';"Finding a user ID
Section titled “Finding a user ID”wrangler d1 execute dubbit-metadata --remote \ --command "SELECT id, email FROM user WHERE email='<EMAIL>';"Database migration
Section titled “Database migration”Migration 0006_admin_role.sql adds:
user.role(text, default'user')user.banned(integer/boolean, defaultfalse)user.ban_reason(text, nullable)user.ban_expires(integer/timestamp, nullable)session.impersonated_by(text, nullable)- Index
user_role_idxonuser.role
Configuration
Section titled “Configuration”| Variable | Where | Purpose |
|---|---|---|
ADMIN_USER_IDS | Worker secret | Comma-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.