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
- API is authoritative. UI hiding is convenience; server 403s are the security boundary.
- 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 therolecolumn is dropped. - 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).
- Gate helpers.
requireCapability(request, cap)for query/header-token routes;requireCapabilityFromToken(token, headers, cap)from the new helper filesrc/lib/rbac-route-helpers.tsfor body-token routes (body-parsing consumes the stream, so the authoritative guard cannot read it without modifyingrbac-server.ts). - 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), andADMIN_SECRETendpoints (/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/*
| Route | Method | Legacy gate | New cap | Notes |
|---|---|---|---|---|
/api/premium/requests | GET | ROLE_REQUEST_TYPES[role] includes type | inbox:prayer:read / inbox:visitor:read / inbox:callback:read (keyed on ?type=) | Highest-traffic authoritative gate. Read cap selected per request type. |
/api/premium/requests | PATCH | same | inbox:prayer:update / inbox:visitor:update / inbox:callback:update | Covers both update_notes and default status update. |
/api/premium/team | POST | role !== 'admin' | settings:team:remove | Both remove and toggle actions gate on remove. |
/api/premium/team-link | GET | role !== 'admin' | settings:team:invite | Returns a member's login URL — higher than :team:view. |
/api/premium/update | POST | ROLE_SETTINGS[role] per section; role !== 'admin' for team_add | Per-section map (see table below) | JSON body path and FormData path both gated. |
/api/premium/groups | GET, POST | (none — new) | groups:manage | Already shipped — reference implementation. |
/api/premium/groups/[id] | PATCH, DELETE | (none — new) | groups:manage | Already shipped. |
/api/premium/members/[id] | PATCH | (none — new) | groups:manage | Already shipped. |
/api/premium/resolve-slug | GET | (public) | — | Public lookup by pewsearch slug. No auth. |
Per-section cap map for /api/premium/update:
section value | Capability |
|---|---|
basic, contact | settings:church_profile:edit |
hours, availability | settings:hours:edit |
expect, staff, ministries, events | train:church_knowledge:edit |
team_add | settings:team:invite |
care_enable | settings:notifications:edit |
chatbot, chatbot_enable, voice_agent, voice_selection, voice | train:agents:edit |
pastor_pulse | train:pastor_pulse:edit |
notifications_voice | settings:notifications:edit |
integrations, widget | settings:integrations:edit |
crisis_message, human_escalation | train:safety:edit |
website | website:sections:edit |
/api/admin/*
| Route | Method | Legacy gate | New cap | Notes |
|---|---|---|---|---|
/api/admin/agents | GET, POST | ALLOWED_ROLES.includes(role) (admin, office_admin) | train:agents:edit | |
/api/admin/theology | GET, POST | inline role check | train:theology:edit | Pastor template has this; office_admin does NOT (spec §4.2). |
/api/admin/kb-proxy | GET, POST, PUT, DELETE | authAndCheck(feature) | train:faqs:edit (FAQ actions) / train:documents:upload (document actions) | Mapped by feature string passed into authAndCheck. |
/api/admin/kb-proxy/upload | POST | token + plan | train:documents:upload | |
/api/admin/analytics-proxy | GET | token + plan | home:metrics:view | Activity metrics surface → home:metrics. |
/api/admin/moderation | GET | inline | inbox:safety:read | |
/api/admin/moderation | POST, DELETE | inline | inbox:safety:resolve | Manual restrictions = resolve action. |
/api/admin/moderation/moderate-document | POST | inline | inbox:safety:resolve | |
/api/admin/tools | GET, POST | TOOL_CONFIG_ROLES | train:agents:edit | |
/api/admin/training | GET, POST | inline admin-token | train:faqs:edit | Review → promote writes a canned FAQ. |
/api/admin/audit | GET | role !== 'admin' | audit:view | Admin-only. |
/api/admin/safety-stats | GET | generic auth | inbox:safety:read | |
/api/admin/backup-owner | GET, POST, DELETE | role !== 'admin' | church:transfer_ownership | Admin-only cap. |
/api/admin/revoke-sessions | POST | role !== 'admin' | settings:team:remove | Revokes all member sessions → team-remove power. |
/api/admin/adopt-templates | GET, POST | inline admin-token | train:faqs:edit | Templates populate FAQs. |
/api/admin/translate | POST | resolveTokenOrHeaders + nothing | home:overview:view | Low-privilege admin UI helper — every team member with dashboard access. |
/api/admin/resources | GET, POST, PUT, DELETE | ALLOWED_ROLES + plan gate | train:church_knowledge:edit | Local resources feed the chatbot's pastoral-care lookups. |
/api/admin/export | POST | rate-limit + token lookup | audit:view | FLAG: no perfect cap for bulk data export. audit:view chosen (admin-only). Dedicated data:export cap may be warranted. |
/api/admin/photo-extract | POST | admin or office_admin | train:church_knowledge:edit | Vision OCR fills in church profile fields. |
/api/admin/founder-stats | GET | FOUNDER_TOKEN | — (founder-only; not RBAC-gated) | |
/api/admin/search-churches | GET | FOUNDER_TOKEN | — (founder-only) | |
/api/admin/provision-number | POST, DELETE, GET | FOUNDER_TOKEN | — (founder-only) | |
/api/admin/voices | POST | ADMIN_SECRET | — (founder-only; voice-pool admin) | |
/api/admin/voices/library | GET | ADMIN_SECRET | — (founder-only) |
/api/care/*
| Route | Method | Legacy gate | New cap | Notes |
|---|---|---|---|---|
/api/care/members | GET | ROLE_TABS[role].includes('care') | settings:notifications:edit | FLAG: see "Founder decisions needed" below — no dedicated care:* cap exists yet. |
/api/care/members | DELETE | ['admin','office_admin'].includes(role) | settings:notifications:edit | Same flag. |
/api/care/broadcast | POST | ['admin','office_admin'] + ROLE_TABS[role].includes('care') | settings:notifications:edit | Same flag. |
/api/care/subscribe | POST | (public form) | — | Public congregant opt-in. |
/api/training/*
| Route | Method | Legacy gate | New cap | Notes |
|---|---|---|---|---|
/api/training/sessions | GET, POST | ROLE_TRAINING_PATHS[role] + plan | train:simulator:use | |
/api/training/simulate | POST | plan-only | train:simulator:use | |
/api/training/scenarios | GET | ROLE_TRAINING_PATHS + plan | train:simulator:use | |
/api/training/evaluate | POST | plan-only | train:simulator:use |
/api/upload/*
| Route | Method | Legacy gate | New cap | Notes |
|---|---|---|---|---|
/api/upload/hero-photo | POST | ['admin','office_admin'] | website:media:upload | |
/api/upload/hero-source-photo | POST | ALLOWED_ROLES = {admin, office_admin} | website:media:upload | |
/api/upload/logo | POST | ['admin','office_admin'] | website:media:upload | |
/api/upload/staff-photo | POST | ['admin','office_admin'] | train:church_knowledge:edit | Staff photo is knowledge data, not site media. |
Out of scope (signature-verified or public)
| Route | Why it's out |
|---|---|
/api/stripe/* | Signature-verified webhooks + checkout creators (customer intent ≠ auth). |
/api/voice/twiml, /voice/fallback, /voice/dial-status, /voice/voicemail | Twilio/Telnyx signature-verified webhooks. |
/api/telnyx/voice-webhook | Telnyx webhook. |
/api/mailerlite/* | Webhook + external API integration. |
/api/chatbot/stream, /api/chatbot/unified | Public chatbot endpoints (CORS + rate-limit + moderation). |
/api/contact | Public contact form. |
/api/onboard, /onboard/check-setup, /onboard/notify, /onboard/resend-link | Public 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/search | Public directory search. |
Founder decisions needed
- Care tab capabilities. Spec §11 item 6 flagged that
care:broadcast/care:subscribers:viewcaps 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. - Bulk data export.
/api/admin/exportreturns every prayer/visitor/callback/call/care-member/knowledge row. Currently gated onaudit:view(admin-only). A dedicateddata:exportcap might be cleaner for compliance audit trails (GDPR-style requests). /api/admin/translatepermissiveness. Gated onhome:overview:viewbecause translation is a generic helper across the dashboard. If that feels too open (every template group gets it), tighten totrain: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.tssrc/app/api/premium/team/route.tssrc/app/api/premium/team-link/route.tssrc/app/api/premium/update/route.tssrc/app/api/care/members/route.tssrc/app/api/care/broadcast/route.tssrc/app/api/admin/agents/route.tssrc/app/api/admin/theology/route.tssrc/app/api/admin/tools/route.tssrc/app/api/admin/audit/route.tssrc/app/api/admin/backup-owner/route.tssrc/app/api/admin/revoke-sessions/route.tssrc/app/api/admin/resources/route.tssrc/app/api/admin/photo-extract/route.tssrc/app/api/upload/hero-photo/route.tssrc/app/api/upload/hero-source-photo/route.tssrc/app/api/upload/staff-photo/route.tssrc/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.