Skip to main content

Admin RBAC — API Enforcement Map

Companion to admin-rbac-2026-04-18.md (§5 permissions matrix, §7 enforcement layers). This doc is the line-item audit: every API route under src/app/api/premium/*, src/app/api/admin/*, src/app/api/care/*, src/app/api/training/*, plus a few cross-cutting mutations, and the capability each one now enforces.

API Gate Stack

Rules applied

  1. API is authoritative. UI hiding is convenience; server 403s are the security boundary.
  2. Legacy gates kept alongside Model C. Every role === 'admin', ROLE_TABS[role].includes(...), ROLE_SETTINGS[role].includes(...), ROLE_REQUEST_TYPES[role] check was left in place with a // LEGACY — remove after Phase 4 role-column drop. marker. Remove them in Phase 4 when the role column is dropped.
  3. No new capability keys. If a route's best mapping wasn't clean, the closest existing cap was picked and flagged (see "Founder decisions needed" at the bottom).
  4. Gate helpers. requireCapability(request, cap) for query/header-token routes; requireCapabilityFromToken(token, headers, cap) from the new helper file src/lib/rbac-route-helpers.ts for body-token routes (body-parsing consumes the stream, so the authoritative guard cannot read it without modifying rbac-server.ts).
  5. Signature/public routes untouched. Webhooks (/api/stripe/webhook, /api/voice/twiml, /api/voice/fallback, /api/voice/dial-status, /api/voice/voicemail, /api/telnyx/voice-webhook, /api/mailerlite/webhook), public subscription forms (/api/care/subscribe, /api/premium/resolve-slug), public chatbot endpoints (/api/chatbot/stream, /api/chatbot/unified), public onboarding (/api/onboard, /api/onboard/check-setup, /api/onboard/notify, /api/onboard/resend-link), founder-token endpoints (/api/admin/founder-stats, /api/admin/search-churches, /api/admin/provision-number), and ADMIN_SECRET endpoints (/api/admin/voices, /api/admin/voices/library) are out of scope for RBAC — they gate on different mechanisms.

Mapping table

Columns:

  • Route / Method — the API handler.
  • Legacy gate — the pre-Model-C check kept in place during rollout.
  • New cap — the requireCapability(...) key now enforced.
  • Notes — edge cases, flagged decisions.

/api/premium/*

RouteMethodLegacy gateNew capNotes
/api/premium/requestsGETROLE_REQUEST_TYPES[role] includes typeinbox:prayer:read / inbox:visitor:read / inbox:callback:read (keyed on ?type=)Highest-traffic authoritative gate. Read cap selected per request type.
/api/premium/requestsPATCHsameinbox:prayer:update / inbox:visitor:update / inbox:callback:updateCovers both update_notes and default status update.
/api/premium/teamPOSTrole !== 'admin'settings:team:removeBoth remove and toggle actions gate on remove.
/api/premium/team-linkGETrole !== 'admin'settings:team:inviteReturns a member's login URL — higher than :team:view.
/api/premium/updatePOSTROLE_SETTINGS[role] per section; role !== 'admin' for team_addPer-section map (see table below)JSON body path and FormData path both gated.
/api/premium/groupsGET, POST(none — new)groups:manageAlready shipped — reference implementation.
/api/premium/groups/[id]PATCH, DELETE(none — new)groups:manageAlready shipped.
/api/premium/members/[id]PATCH(none — new)groups:manageAlready shipped.
/api/premium/resolve-slugGET(public)Public lookup by pewsearch slug. No auth.

Per-section cap map for /api/premium/update:

section valueCapability
basic, contactsettings:church_profile:edit
hours, availabilitysettings:hours:edit
expect, staff, ministries, eventstrain:church_knowledge:edit
team_addsettings:team:invite
care_enablesettings:notifications:edit
chatbot, chatbot_enable, voice_agent, voice_selection, voicetrain:agents:edit
pastor_pulsetrain:pastor_pulse:edit
notifications_voicesettings:notifications:edit
integrations, widgetsettings:integrations:edit
crisis_message, human_escalationtrain:safety:edit
websitewebsite:sections:edit

/api/admin/*

RouteMethodLegacy gateNew capNotes
/api/admin/agentsGET, POSTALLOWED_ROLES.includes(role) (admin, office_admin)train:agents:edit
/api/admin/theologyGET, POSTinline role checktrain:theology:editPastor template has this; office_admin does NOT (spec §4.2).
/api/admin/kb-proxyGET, POST, PUT, DELETEauthAndCheck(feature)train:faqs:edit (FAQ actions) / train:documents:upload (document actions)Mapped by feature string passed into authAndCheck.
/api/admin/kb-proxy/uploadPOSTtoken + plantrain:documents:upload
/api/admin/analytics-proxyGETtoken + planhome:metrics:viewActivity metrics surface → home:metrics.
/api/admin/moderationGETinlineinbox:safety:read
/api/admin/moderationPOST, DELETEinlineinbox:safety:resolveManual restrictions = resolve action.
/api/admin/moderation/moderate-documentPOSTinlineinbox:safety:resolve
/api/admin/toolsGET, POSTTOOL_CONFIG_ROLEStrain:agents:edit
/api/admin/trainingGET, POSTinline admin-tokentrain:faqs:editReview → promote writes a canned FAQ.
/api/admin/auditGETrole !== 'admin'audit:viewAdmin-only.
/api/admin/safety-statsGETgeneric authinbox:safety:read
/api/admin/backup-ownerGET, POST, DELETErole !== 'admin'church:transfer_ownershipAdmin-only cap.
/api/admin/revoke-sessionsPOSTrole !== 'admin'settings:team:removeRevokes all member sessions → team-remove power.
/api/admin/adopt-templatesGET, POSTinline admin-tokentrain:faqs:editTemplates populate FAQs.
/api/admin/translatePOSTresolveTokenOrHeaders + nothinghome:overview:viewLow-privilege admin UI helper — every team member with dashboard access.
/api/admin/resourcesGET, POST, PUT, DELETEALLOWED_ROLES + plan gatetrain:church_knowledge:editLocal resources feed the chatbot's pastoral-care lookups.
/api/admin/exportPOSTrate-limit + token lookupaudit:viewFLAG: no perfect cap for bulk data export. audit:view chosen (admin-only). Dedicated data:export cap may be warranted.
/api/admin/photo-extractPOSTadmin or office_admintrain:church_knowledge:editVision OCR fills in church profile fields.
/api/admin/founder-statsGETFOUNDER_TOKEN— (founder-only; not RBAC-gated)
/api/admin/search-churchesGETFOUNDER_TOKEN— (founder-only)
/api/admin/provision-numberPOST, DELETE, GETFOUNDER_TOKEN— (founder-only)
/api/admin/voicesPOSTADMIN_SECRET— (founder-only; voice-pool admin)
/api/admin/voices/libraryGETADMIN_SECRET— (founder-only)

/api/care/*

RouteMethodLegacy gateNew capNotes
/api/care/membersGETROLE_TABS[role].includes('care')settings:notifications:editFLAG: see "Founder decisions needed" below — no dedicated care:* cap exists yet.
/api/care/membersDELETE['admin','office_admin'].includes(role)settings:notifications:editSame flag.
/api/care/broadcastPOST['admin','office_admin'] + ROLE_TABS[role].includes('care')settings:notifications:editSame flag.
/api/care/subscribePOST(public form)Public congregant opt-in.

/api/training/*

RouteMethodLegacy gateNew capNotes
/api/training/sessionsGET, POSTROLE_TRAINING_PATHS[role] + plantrain:simulator:use
/api/training/simulatePOSTplan-onlytrain:simulator:use
/api/training/scenariosGETROLE_TRAINING_PATHS + plantrain:simulator:use
/api/training/evaluatePOSTplan-onlytrain:simulator:use

/api/upload/*

RouteMethodLegacy gateNew capNotes
/api/upload/hero-photoPOST['admin','office_admin']website:media:upload
/api/upload/hero-source-photoPOSTALLOWED_ROLES = {admin, office_admin}website:media:upload
/api/upload/logoPOST['admin','office_admin']website:media:upload
/api/upload/staff-photoPOST['admin','office_admin']train:church_knowledge:editStaff photo is knowledge data, not site media.

Out of scope (signature-verified or public)

RouteWhy it's out
/api/stripe/*Signature-verified webhooks + checkout creators (customer intent ≠ auth).
/api/voice/twiml, /voice/fallback, /voice/dial-status, /voice/voicemailTwilio/Telnyx signature-verified webhooks.
/api/telnyx/voice-webhookTelnyx webhook.
/api/mailerlite/*Webhook + external API integration.
/api/chatbot/stream, /api/chatbot/unifiedPublic chatbot endpoints (CORS + rate-limit + moderation).
/api/contactPublic contact form.
/api/onboard, /onboard/check-setup, /onboard/notify, /onboard/resend-linkPublic onboarding + webhook flow.
/api/founder/*Founder-only tools gated on FOUNDER_TOKEN, not team RBAC.
/api/ops/*Internal ops ingest with signed payloads.
/api/sermons/*, /api/social/*, /api/health/*, /api/cron/*Separate products / cron authorization.
/api/test-reports/*Public test report system (no auth by design).
/api/churches/searchPublic directory search.

Founder decisions needed

  1. Care tab capabilities. Spec §11 item 6 flagged that care:broadcast / care:subscribers:view caps don't exist yet. Current fallback: settings:notifications:edit. This is tighter than legacy (admin+office+pastor only, not care_team) — may want to loosen back to include care_team via either a new cap or direct grant during onboarding.
  2. Bulk data export. /api/admin/export returns every prayer/visitor/callback/call/care-member/knowledge row. Currently gated on audit:view (admin-only). A dedicated data:export cap might be cleaner for compliance audit trails (GDPR-style requests).
  3. /api/admin/translate permissiveness. Gated on home:overview:view because translation is a generic helper across the dashboard. If that feels too open (every template group gets it), tighten to train:church_knowledge:edit.

Legacy-gate removal checklist (Phase 4)

When the role column is dropped, grep for // LEGACY — remove after Phase 4 role-column drop. markers and delete the next-immediately-following role check. All such markers are co-located in:

  • src/app/api/premium/requests/route.ts
  • src/app/api/premium/team/route.ts
  • src/app/api/premium/team-link/route.ts
  • src/app/api/premium/update/route.ts
  • src/app/api/care/members/route.ts
  • src/app/api/care/broadcast/route.ts
  • src/app/api/admin/agents/route.ts
  • src/app/api/admin/theology/route.ts
  • src/app/api/admin/tools/route.ts
  • src/app/api/admin/audit/route.ts
  • src/app/api/admin/backup-owner/route.ts
  • src/app/api/admin/revoke-sessions/route.ts
  • src/app/api/admin/resources/route.ts
  • src/app/api/admin/photo-extract/route.ts
  • src/app/api/upload/hero-photo/route.ts
  • src/app/api/upload/hero-source-photo/route.ts
  • src/app/api/upload/staff-photo/route.ts
  • src/app/api/upload/logo/route.ts

Row count

  • Premium namespace: 9 routes (8 gated, 1 public).
  • Admin namespace: 24 routes (18 RBAC-gated, 5 founder-token-only, 1 skipped).
  • Care namespace: 3 routes (2 gated, 1 public).
  • Training namespace: 4 routes (all gated on train:simulator:use).
  • Upload namespace: 4 routes (all gated).
  • Total gated by Model C: 41 routes across 5 namespaces.