Admin & CRM
The platform-admin surface is the internal operator console: cross-account entitlement tooling (GATE-6) plus the CRM. It is not tenant-facing; nothing here is reachable by ordinary users, and CRM data is invisible to app-side APIs by construction.
The perimeter
Admin lives on admin.podcasterplus.com behind three stacked, independent gates. Each is an AND-gate; none substitutes for another.
Layers, in processing order
- Cloudflare Access fronts the
admin.host at the edge; only Access-approved identities reach the origin at all. - Host binding: a request for
/api/admin/*that is not on the admin host (app., the apex,*.pages.dev) is a flat 404, never a redirect, so the surface is not advertised. For pages,adminPerimeterHandleinsrc/hooks.server.tsdoes the same for the(admin)route group. - Access JWT origin verification: on the deployed admin host the
Cf-Access-Jwt-Assertionheader is REQUIRED and verified againstCF_ACCESS_TEAM_DOMAIN/CF_ACCESS_AUD; missing or invalid assertion, or unset config, is a fail-closed 403. Verification is skipped only on localhost. This closes the direct-to-*.pages.devbypass: a request that dodges the Access-fronted DNS cannot mint the JWT. Both the API middleware and the SvelteKit hook delegate to the shared verifier insrc/lib/server/admin-access.tsso the two surfaces can never drift. requireAuth(): a normal Supabase session. Auth cookies are scoped to.podcasterplus.com, so a login performed on the admin host shares the session withapp..requirePlatformAdmin()(src/api/middleware/admin.ts): THE authorization gate, the third authorization axis (global platform staff; not ReBAC, not billing membership, not an RLS policy). It reads theplatform_adminstable on the service-role client injected byrequireAuth(). An Access-approved, authenticated user with noplatform_adminsrow still gets 403.
Every /api/admin/* route composes the full chain: requireAdminPerimeter() then requireAuth() then (optionally) rateLimit() then requirePlatformAdmin() then the handler.
platform_admins is service-role-only (no client RLS policies; GATE-6 migration 20260615140000), which is why the (admin) layout guard (src/routes/(admin)/+layout.server.ts) checks staff status through a service-role client: that guard is defense-in-depth UX (it redirects an authenticated non-operator away from a console shell that would only 403), while the Hono middleware chain is the real boundary.
Route inventory (/api/admin, src/api/routes/admin/index.ts)
| Endpoint | Purpose |
|---|---|
GET /accounts | Cross-account directory |
GET /accounts/:id/entitlements | Effective caps + overrides for one account |
GET /accounts/:id/usage-events | Usage ledger |
GET /accounts/:id/managed-episodes | Slot occupancy detail |
POST /accounts/:id/free-slot | admin_free_slot RPC (archive a slot-holding episode, audit-logged) |
POST /accounts/:id/overrides | Grant an entitlement override |
POST /overrides/bulk | admin_bulk_override_account bulk grant |
GET /action-log | The immutable admin_action_log |
GET /accounts/:id/deletion-preview | What admin_delete_account would remove |
POST /accounts/:id/delete | admin_delete_account RPC (refuses platform-admin accounts) |
POST /accounts/:id/enterprise | Enterprise provisioning |
The CRM routers are mounted onto the same admin app (.route('/', adminCrmRoutes) etc. at the bottom of index.ts), so all CRM endpoints live under /api/admin/... and inherit the perimeter. See CRM for their inventory.
Console pages (src/routes/(admin)/admin/)
| Page | Content |
|---|---|
/admin | Console landing |
/admin/accounts/[id] | Account 360 with CRM tabs (Overview, Timeline, Revenue, Analytics, Support, Notes & Tasks) |
/admin/action-log | Audit log browser |
/admin/bulk-override | Bulk entitlement overrides |
/admin/crm (+ contacts, pipeline, tasks) | CRM working surfaces |
Related
- CRM: data model, endpoints, sync worker, webhooks
- Admin API route stub
- Billing & entitlements for the entitlement model the admin tooling operates on
- CRM Sync worker stub