Skip to content

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

  1. Cloudflare Access fronts the admin. host at the edge; only Access-approved identities reach the origin at all.
  2. 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, adminPerimeterHandle in src/hooks.server.ts does the same for the (admin) route group.
  3. Access JWT origin verification: on the deployed admin host the Cf-Access-Jwt-Assertion header is REQUIRED and verified against CF_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.dev bypass: 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 in src/lib/server/admin-access.ts so the two surfaces can never drift.
  4. requireAuth(): a normal Supabase session. Auth cookies are scoped to .podcasterplus.com, so a login performed on the admin host shares the session with app..
  5. 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 the platform_admins table on the service-role client injected by requireAuth(). An Access-approved, authenticated user with no platform_admins row 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)

EndpointPurpose
GET /accountsCross-account directory
GET /accounts/:id/entitlementsEffective caps + overrides for one account
GET /accounts/:id/usage-eventsUsage ledger
GET /accounts/:id/managed-episodesSlot occupancy detail
POST /accounts/:id/free-slotadmin_free_slot RPC (archive a slot-holding episode, audit-logged)
POST /accounts/:id/overridesGrant an entitlement override
POST /overrides/bulkadmin_bulk_override_account bulk grant
GET /action-logThe immutable admin_action_log
GET /accounts/:id/deletion-previewWhat admin_delete_account would remove
POST /accounts/:id/deleteadmin_delete_account RPC (refuses platform-admin accounts)
POST /accounts/:id/enterpriseEnterprise 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/)

PageContent
/adminConsole landing
/admin/accounts/[id]Account 360 with CRM tabs (Overview, Timeline, Revenue, Analytics, Support, Notes & Tasks)
/admin/action-logAudit log browser
/admin/bulk-overrideBulk entitlement overrides
/admin/crm (+ contacts, pipeline, tasks)CRM working surfaces

Internal documentation - Not for public distribution