Skip to main content

Admin Nav → Capability Map

Single source of truth the frontend uses to decide what to render in the admin dashboard nav AND what every test must verify. Pair of this doc and the RBAC spec + API enforcement map is authoritative for all admin UI gating.

1. Summary

This doc maps every admin-dashboard nav element — the 4 top tabs (Home / Inbox / Train AI / Website), the Settings slide-over, the header controls, and every sub-section within each — to the capability (from the 50 in src/lib/rbac.ts) and the plan predicate (from src/lib/tier-config.ts) required to render it. When the capability gate fails, the element is hidden. When the plan gate fails, the element is either hidden OR shown-locked-with-upsell CTA — behavior is explicit per row. The frontend (AdminDashboard.rbac.ts, the slide-over components) and every Playwright test (knowledge/tests/registry.yaml) read from this doc. If code disagrees with this doc, this doc wins — propose a change here first, then update code.

Gating Flow

Capability count note. rbac.ts exports 52 keys (6 Home + 15 Inbox + 8 Train + 5 Website + 8 Settings + 2 Care + 8 Admin-only). The older admin-rbac-2026-04-18.md header says "42". The code is authoritative — the 10 extra came from splitting inbox:prayer:readinbox:prayer:read:confidential, inbox:callback:readinbox:callback:read:reason, adding the 2 care caps (founder decision 1a), and adding inbox:prayer:assign + inbox:prayer:care_message (founder decision 2026-04-18 — dedicated caps for the Inbox card "Assign to prayer team" and "Send care message" buttons, replacing the P1 proxies on settings:team:invite and care:broadcast). See §12 "Reconciliation needed".


2. Predicates glossary (canonical)

Defined ONCE here; every row below references these by name. Plan values come from VALID_PLAN_KEYS in tier-config.ts.

PredicateDefinitionSource
hasChat(plan)normalizePlanChannel(plan) === 'chat' || 'both' OR isProWebsitePlan(plan) (Pro Website bundles a chatbot per pricing.yaml). True for every plan except voice-only.tier-config.ts + pricing.yaml
hasVoice(plan, channel)planIncludesVoice(plan, channel)tier-config.ts
hasProVoice(plan, channel)planIncludesProVoice(plan, channel) — voice + Pro tier or higher (PCO integration).tier-config.ts
hasProWebsite(plan)isProWebsitePlan(plan) — matches cwa_pro_website, ps_pro_website, pro_website.tier-config.ts
hasSuite(plan)plan.startsWith('cwa_suite_') OR legacy suite* / bundle.derived
isProTier(plan)canAccess(plan, 'analytics') — Pro or Suite.tier-config.ts TIER_FEATURES
isStarterTier(plan)normalizePlanTier(plan) === 'starter' AND NOT Pro Website.tier-config.ts
isActive(church)church.status === 'active' (NOT 'cancelled', 'paused', 'past_due'). See §10 "Edge cases" for the FA-046 churn-return gap.premium_churches.status

All predicates are pure functions of plan, channel, and optionally status; compute once per render.


3. Top-level tabs

Four visible tabs (post-P1 IA collapse). Upgrade is a header CTA, not a tab (see §5).

3.1 Home

FieldValue
Required capabilityhome:overview:view
Required plan predicatenone — always visible to any authenticated member
Fallback if cap missinghidden (lockout page per §8.5)
Fallback if plan missingn/a — no plan gate
Default landing ruleroute /admin/[token] → Home tab; first render = welcome card + setup checklist (if home:checklist:edit) + activity tiles (if home:metrics:view)
Test handle[data-tab="home"], [data-testid="home-panel"]

3.2 Inbox

FieldValue
Required capabilityANY of: inbox:calls:read, inbox:prayer:read, inbox:visitor:read, inbox:callback:read, inbox:safety:read
Required plan predicatenone — Inbox reads data the AI has already collected; plan-gating is per-chip (Calls requires hasVoice)
Fallback if cap missinghidden
Fallback if plan missingn/a at tab level; Calls chip gated on hasVoice(plan) at sub-section level (§4)
Default landing rulefirst chip the viewer has access to, in order: Calls > Prayer > Visitors > Callbacks > Safety. Empty-state copy if no items yet.
Test handle[data-tab="inbox"], chips carry [data-chip="calls|prayer|visitor|callback|safety"]

3.3 Train AI

FieldValue
Required capabilityANY of: train:church_knowledge:edit, train:theology:edit, train:agents:edit, train:faqs:edit, train:safety:edit, train:simulator:use, train:documents:upload, train:pastor_pulse:edit
Required plan predicatehasChat(plan) OR hasVoice(plan, channel) — training only matters if there's an AI to train
Fallback if cap missinghidden
Fallback if plan missinghidden (no AI to train — a pure directory-listing plan has no reason to show this)
Default landing rulefirst sub-section the viewer has access to: Church Knowledge > Theology > Agents > FAQs > Safety > Simulator > Documents > Pastor Pulse
Test handle[data-tab="train"], sub-sections: [data-section="knowledge|theology|agents|faqs|safety|simulator|documents|pulse"]

3.4 Website

FieldValue
Required capabilityANY of: website:sections:edit, website:design:edit, website:publish, website:preview, website:media:upload
Required plan predicatehasProWebsite(plan)
Fallback if cap missinghidden
Fallback if plan missinghidden — not locked. A plan without Pro Website has NO website to edit; showing a locked tab creates the false impression that a site exists behind the gate. Upsell to Pro Website lives on the Upgrade CTA in the header, not as a ghost tab.
Default landing rulesections panel (Hero) if website:sections:edit; otherwise preview only
Test handle[data-tab="website"]

4. Sub-sections within each tab

Format: surface — capability — plan predicate — fallback. Every row maps to code and to a Playwright step.

4.1 Home sub-sections

Sub-sectionCapabilityPlan predicateCap fallbackPlan fallbackTest handle
Welcome card / overviewhome:overview:viewnonehiddenn/a[data-home="welcome"]
Activity metrics (calls/prayer/visitor tiles)home:metrics:viewnone (tile omits call count if !hasVoice)hidden tilecall tile hidden if !hasVoice[data-home="metrics"]
Financial metrics (giving totals tile)home:metrics:financial:viewnone (future: requires giving integration)hidden tilehidden until integration lands[data-home="giving"]
Setup checklist railhome:checklist:editnonehiddenn/a[data-home="checklist"]
Share-link cardhome:share_link:viewhasChat || hasProWebsitehiddenhidden (nothing to share)[data-home="share"]
Remove demo examples buttonhome:examples:removenonehiddenn/a[data-home="remove-examples"]

4.2 Inbox sub-sections

Sub-sectionCapabilityPlan predicateCap fallbackPlan fallbackTest handle
Calls list chip + detailinbox:calls:readhasVoice(plan, channel)hiddenchip hidden[data-chip="calls"], [data-testid="call-row"]
Call transcript modalinbox:calls:transcripthasVoicehidden buttonhidden[data-action="open-transcript"]
Call delete buttoninbox:calls:deletehasVoicehidden buttonhidden[data-action="delete-call"]
Prayer list chipinbox:prayer:readhasChat || hasVoicehiddenhidden[data-chip="prayer"]
Prayer confidential textinbox:prayer:read:confidentialsameredacted to "Confidential — contact the pastor"redacted[data-field="prayer-text"]
Prayer status / notes updateinbox:prayer:updatesamehidden dropdownhidden[data-action="update-prayer"]
Assign prayer request to teaminbox:prayer:assignhasChat || hasVoicehidden buttonhidden[data-action="assign-prayer"]
Send care message (prayer / visitor / callback card)inbox:prayer:care_messagehasChat || hasVoicehidden buttonhidden[data-action="send-care-message"]
Visitors list chipinbox:visitor:readhasChat || hasVoicehiddenhidden[data-chip="visitor"]
Visitor status / notes updateinbox:visitor:updatesamehiddenhidden[data-action="update-visitor"]
Callbacks list chipinbox:callback:readhasChat || hasVoicehiddenhidden[data-chip="callback"]
Callback reason (un-redacted)inbox:callback:read:reasonsameredacted to "Pastoral inquiry"redacted[data-field="callback-reason"]
Callback status updateinbox:callback:updatesamehiddenhidden[data-action="update-callback"]
Safety flags chipinbox:safety:readhasChat || hasVoicehiddenhidden[data-chip="safety"]
Safety resolve actionsinbox:safety:resolvesamehiddenhidden[data-action="resolve-safety"]
Assignee picker (cross-cutting)inbox:item:assignhasChat || hasVoiceread-only label (no dropdown)hidden[data-testid="inbox-assignee-chip"]

inbox:item:assign (added Inbox sprint PR 4/5, 2026-04-19) gates the assignee-picker dropdown on every inbox row. Template grants: admin + office_admin only. Narrower than inbox:prayer:assign (which also includes pastor) because the cross-cutting picker mutates every row's assignee — day-to-day routing stays with admin + office_admin. Pastors can still be extended via a custom group. Viewers without the cap see a flat read-only chip when an item is already assigned, so they can still tell who's handling what.

4.3 Train AI sub-sections

Sub-sectionCapabilityPlan predicateCap fallbackPlan fallbackTest handle
Church Knowledge editortrain:church_knowledge:edithasChat || hasVoicehiddenhidden[data-section="knowledge"]
Theology lens pickertrain:theology:edithasChat || hasVoicehiddenhidden[data-section="theology"]
Agent personality editortrain:agents:edithasChat || hasVoicehiddenhidden[data-section="agents"]
FAQ editortrain:faqs:edithasChat || hasVoicehiddenhidden[data-section="faqs"]
Safety / crisis message editortrain:safety:edithasChat || hasVoicehiddenhidden[data-section="safety"]
Simulatortrain:simulator:usehasChat || hasVoice (simulator needs at least one AI channel)hiddenhidden[data-section="simulator"]
Document uploadtrain:documents:uploadisProTier(plan) (documents are a Pro feature per TIER_FEATURES)hiddenlocked-with-upsell "Upload documents unlocks with Pro — [Upgrade →]"[data-section="documents"]
Pastor Pulse editortrain:pastor_pulse:edithasChat || hasVoicehiddenhidden[data-section="pulse"]

4.4 Website sub-sections

All gated on hasProWebsite(plan) at the tab level (§3.4). If tab is visible, individual caps gate sub-sections.

Sub-sectionCapabilityPlan predicateCap fallbackPlan fallbackTest handle
Section editor (Hero, About, Services, Staff, Events, Give, Contact)website:sections:edithasProWebsitehidden paneln/a (tab already hidden)[data-section="sections"]
Design panel (template, colors, videos)website:design:edithasProWebsitehidden paneln/a[data-section="design"]
Publish buttonwebsite:publishhasProWebsitehiddenn/a[data-action="publish"]
Preview buttonwebsite:previewhasProWebsitehiddenn/a[data-action="preview"]
Media uploadwebsite:media:uploadhasProWebsitehiddenn/a[data-action="upload-media"]

4.5 Settings slide-over (header gear)

Triggered by the gear icon (§5). Tabs inside: Account / Team / Notifications / Integrations.

Sub-sectionCapabilityPlan predicateCap fallbackPlan fallbackTest handle
Church profile (name, description, contact, social)settings:church_profile:editnonehiddenn/a[data-settings="profile"]
Office hours gridsettings:hours:edithasChat || hasVoicehiddenhidden[data-settings="hours"]
Notifications (email/phone, alert routing)settings:notifications:edithasChat || hasVoicehiddenhidden[data-settings="notifications"]
Integrations (PCO, Cal.com, giving URL)settings:integrations:editisProTier(plan) || hasProWebsite (Pro Website gets PCO for directory sync)hiddenlocked-with-upsell "Integrations unlock with Pro — [Upgrade →]"[data-settings="integrations"]
Sharing / embed / QRsettings:sharing:viewhasChat || hasProWebsitehiddenhidden[data-settings="sharing"]
Team roster (view)settings:team:viewnonehiddenn/a[data-settings="team"]
Team invitesettings:team:invitenonehidden buttonn/a[data-action="invite-member"]
Team removesettings:team:removenonehidden buttonn/a[data-action="remove-member"]

4.6 Care sub-surface

Care (congregation broadcast) is folded into the Inbox tab visually (per rbac-catalog.ts which assigns care:* caps to the Inbox category). A "Send broadcast" action button sits in Inbox header; a "Subscribers" panel is reachable from Inbox side-nav.

Sub-sectionCapabilityPlan predicateCap fallbackPlan fallbackTest handle
Broadcast composer (email/SMS)care:broadcasthasChat || hasVoicehiddenhidden[data-action="care-broadcast"]
Subscribers rostercare:subscribers:viewhasChat || hasVoicehiddenhidden[data-panel="care-subscribers"]

4.7 Billing (inside Settings slide-over, admin-only section)

Sub-sectionCapabilityPlan predicateCap fallbackPlan fallbackTest handle
Billing view (subscription, invoices)billing:viewnonehiddenn/a[data-settings="billing"]
Billing manage (upgrade/downgrade, card)billing:managenonehiddenn/a[data-action="manage-billing"]
Cancel subscriptionbilling:cancelisActive(church)hiddenhidden (already cancelled)[data-action="cancel-sub"]

5. Header controls

ControlCapabilityPlan predicateCap fallbackPlan fallbackTest handle
Gear icon (opens Settings slide-over)ANY of settings:* (see §4.5)nonehiddenn/a[data-header="gear"]
Bell icon (notifications)home:overview:view (everyone)nonehiddenn/a[data-header="bell"]
Upgrade CTAnone (every authenticated member sees it, to nudge billing conversations)!hasSuite(plan) — Suite is the top tier, no higher upgradehiddenhidden for Suite[data-header="upgrade"]
Sign-outnone (always available to authenticated)nonenever hiddenn/a[data-header="signout"]

6. Mobile bottom nav

Same 4 tabs. Mobile-specific adaptations:

TabMobile visibilityNotes
Homesame as desktop (§3.1)always
Inboxsame as desktop (§3.2)filter chips become a horizontal scrollable strip
Train AIsame as desktop (§3.3)sub-sections become a vertical accordion
Websitesame as desktop — hidden if !hasProWebsite(plan)on mobile the editor is read-only (preview + publish only) — website:sections:edit buttons render a "Open on desktop to edit" toast

Header gear/bell/upgrade collapse into an overflow menu (). Same caps + plan predicates apply.


7. Full plan × tab matrix

Cells: V = visible, H = hidden, L = locked-with-upsell. Assumes viewer has the required capability for each tab (this matrix is the PLAN gate only; §8 layers the role gate on top).

Plan keyHomeInboxTrain AIWebsiteSettingsCareUpgrade
cwa_starter_chat / _annualVV (no Calls chip)VHVVV
cwa_starter_voice / _annualVV (no chat-only chips)VHVVV
cwa_starter_both / _annualVVVHVVV
cwa_pro_chat / _annualVV (no Calls chip)VHVVV
cwa_pro_voice / _annualVV (no chat-only chips)VHVVV
cwa_pro_both / _annualVVVHVVV
cwa_suite_chat / _annualVV (no Calls chip)VHVVH
cwa_suite_both / _annualVVVHVVH
cwa_bundleVVVHVVH
cwa_pro_websiteVVVVVVV
ps_pro_website (legacy)VVVVVVV
pro_website (legacy)VVVVVVV
ps_premium (directory-only)V (no call tile, no share card)HHHV (profile only)HV

Notes:

  • Pro Website is the ONLY non-voice, non-chat-branded plan that still shows the Website tab.
  • ps_premium buyers purchased a directory listing; the admin dashboard is effectively a profile editor + billing view. Inbox/Train/Website are hidden because there is no AI and no website.
  • Document upload (§4.3) inside Train AI is a sub-row that locks-with-upsell on any non-Pro plan; the tab itself stays V.

8. Role × capability matrix

Rows = 12 TemplateKey values + custom. Columns grouped by surface. Pulled VERBATIM from TEMPLATE_GROUP_CAPABILITIES in rbac.ts (the code, not the spec's prose §4 — code wins). Cells: = template grants the cap, blank = does not.

8.1 Home (6 caps)

Roleoverviewmetricsmetrics:financialchecklistshare_linkexamples:remove
admin
office_admin
pastor
prayer_team
care_team
treasurer
volunteer_coordinator
worship_team
usher_team
kids_ministry
youth_ministry
tech_team
custom(see the group's own capability set)

8.2 Inbox (15 caps)

Rolecalls:readcalls:transcriptcalls:deleteprayer:readprayer:read:confprayer:updateprayer:assignprayer:care_messagevisitor:readvisitor:updatecallback:readcallback:read:reasoncallback:updatesafety:readsafety:resolve
admin
office_admin
pastor
prayer_team
care_team
treasurer
volunteer_coordinator
worship_team
usher_team
kids_ministry
youth_ministry
tech_team

8.3 Train AI (8 caps)

Roleknowledgetheologyagentsfaqssafetysimulatordocumentspastor_pulse
admin
office_admin
pastor
prayer_team
care_team
treasurer
volunteer_coordinator
worship_team
usher_team
kids_ministry
youth_ministry
tech_team

8.4 Website (5 caps)

Rolesectionsdesignpublishpreviewmedia
admin
office_admin
pastor
prayer_team
care_team
treasurer
volunteer_coordinator
worship_team
usher_team
kids_ministry
youth_ministry
tech_team

8.5 Settings (8 caps)

Roleprofilehoursnotificationsintegrationssharingteam:viewteam:inviteteam:remove
admin
office_admin
pastor
prayer_team
care_team
treasurer
volunteer_coordinator
worship_team
usher_team
kids_ministry
youth_ministry
tech_team

8.6 Care (2 caps)

Rolebroadcastsubscribers:view
admin
office_admin
pastor
care_team
all others

8.7 Admin-only (8 caps) — never grantable via group UI

Rolebilling:viewbilling:managebilling:cancelchurch:deletechurch:transfergroups:manageapi_keysaudit:view
admin
all others

custom role: by construction the create-group UI excludes admin-only caps (filterGrantableCapabilities() in rbac-catalog.ts), so no custom group can ever hold any of these.


9. Upsell-lock behavior spec

When a sub-section is marked locked-with-upsell in §4:

Rendering:

  • Show the row/tab/button in the nav with an inline lock icon (Lucide Lock) at text size.
  • Label color text-stone-400 (disabled) instead of the active text-stone-900.
  • aria-disabled="true", cursor: not-allowed.
  • Do NOT render the actual UI behind the lock (no ghost fields, no skeleton) — that invites confusion about whether data exists.

Interaction:

  • Click does NOT navigate. Instead, render an inline popover anchored to the locked element with copy per §9.1 and a primary Upgrade → button.
  • Upgrade → routes to /admin/[token]#upgrade with a ?context= param naming the locked feature (e.g. ?context=documents) so the Upgrade CTA copy can echo it back: "You tried to upload a document — Pro plans include unlimited document training."
  • Keyboard: Enter or Space on the locked element opens the popover same as click.

Hide vs lock decision rule:

  • Hide when the plan simply doesn't include the product (no Voice plan → no Calls chip; no Pro Website → no Website tab).
  • Lock-with-upsell when the plan COULD upgrade to unlock the feature within its product line (Starter Chat + documents upload = upgrade to Pro Chat).

9.1 Canonical upsell copy

FeatureCopy
Voice-required feature (Calls chip)"Calls unlock with a Voice or Bundle plan — [Upgrade →]"
Pro-required feature (document upload, integrations)"{Feature} unlocks with Pro — [Upgrade →]"
Suite-required feature (marketplace, custom agents)"{Feature} unlocks with Suite — [Upgrade →]"
Pro Website required (Website tab)(hidden, not locked — no upsell shown in nav)
Cancel when already cancelledbilling:cancel is simply hidden when !isActive(church)

10. Edge cases

  1. User loses capability mid-session. Admin removes a member from a template group while the member has the dashboard open. On next page navigation or API call: the tab disappears and any API request returns 403. In-session polling: every 60 secondsGET /api/premium/me/capabilities returns the freshly-computed effectivePermissions; if the set differs from the cached provider value, re-render the nav (hide newly-lost tabs). No hard reload. Tested in spec §9 scenario 11.

  2. Plan downgrade mid-session (subscription paused/cancelled via Stripe webhook). Same polling interval picks up the new plan / status from premium_churches. Tab visibility re-computes. Data the user ALREADY loaded (e.g., a prayer list in memory) continues to render until next navigation — this is acceptable because the data is not sensitive beyond what the member's old plan already granted.

  3. groups:manage without Admin template. A custom group CANNOT grant groups:manage (it's admin-only, stripped by filterGrantableCapabilities()). But what about Admin members who are NOT in the Pastor template? Team panel (settings:team:view) is a SEPARATE cap from groups:manage. Admin always has both. Pastor has settings:team:view only. So: Admin can manage groups AND see team; Pastor can see team roster but cannot manage group capabilities. Correct.

  4. Custom group with ZERO capabilities. A member whose only group grants zero caps AND has zero direct caps sees the lockout page (§8.5 of the spec): "Your account has no permissions. Please contact your church admin." home:overview:view is NOT implicitly granted — if the empty group is the member's only source, Home itself is hidden.

  5. Churn-return auth hole (FA-046) — KNOWN GAP. Current behavior: a member whose premium_churches.status = 'cancelled' retains a working access_token and CAN still hit the dashboard. The isActive(church) predicate is NOT yet wired into the tab-visibility resolver. Until FA-046 is fixed, a cancelled church viewing /admin/[token] sees the full UI (minus Cancel button per §4.7). Flag all tabs' Plan fallback as "best-effort" under this condition. Fix: add isActive() as a top-level guard in the dashboard layout that routes cancelled churches to /billing-lapsed.

  6. Multi-channel plan without Pro Website add-on (e.g., cwa_pro_both with no Pro Website purchased). Website tab HIDDEN (§3.4). Upgrade CTA highlights Pro Website as an available add-on.

  7. Legacy role-only member (Phase 1 rollout). Members whose group_ids and capabilities arrays are both empty fall through to legacyRoleToCapabilities(role) in rbac.ts. The resulting cap set is re-checked against this doc exactly as if it came from a group. No separate rendering logic.


11. Test matrix

Playwright data-provider format. Each row = one scenario. Run matrix: 10 canonical plans × 9 canonical roles = 90 test cases. Machine-readable (no surrounding prose inside cells).

Columns:

  • plan — plan key
  • role — TemplateKey or custom
  • expected_visible_tabs — comma-separated subset of home,inbox,train,website
  • expected_home_sections — comma-separated subset of welcome,metrics,giving,checklist,share,remove-examples
  • expected_inbox_chips — comma-separated subset of calls,prayer,visitor,callback,safety
  • expected_upgrade_ctavisible or hidden
planroleexpected_visible_tabsexpected_home_sectionsexpected_inbox_chipsexpected_upgrade_cta
cwa_starter_chatadminhome,inbox,trainwelcome,metrics,checklist,share,remove-examplesprayer,visitor,callback,safetyvisible
cwa_starter_chatpastorhome,inbox,trainwelcome,metrics,checklist,share,remove-examplesprayer,visitor,callback,safetyvisible
cwa_starter_chatoffice_adminhome,inbox,trainwelcome,metrics,checklist,share,remove-examplesprayer,visitor,callback,safetyvisible
cwa_starter_chatprayer_teamhome,inboxwelcome,metrics,shareprayervisible
cwa_starter_chatcare_teamhome,inboxwelcome,metrics,shareprayer,visitor,callbackvisible
cwa_starter_chattreasurerhomewelcome,metrics,giving,sharevisible
cwa_starter_chatvolunteer_coordinatorhome,inboxwelcome,metrics,sharevisitor,callbackvisible
cwa_starter_chatusher_teamhome,inboxwelcome,sharevisitorvisible
cwa_starter_chattech_teamhomewelcome,sharevisible
cwa_starter_voiceadminhome,inbox,trainwelcome,metrics,checklist,share,remove-examplescalls,prayer,visitor,callback,safetyvisible
cwa_starter_voicepastorhome,inbox,trainwelcome,metrics,checklist,share,remove-examplescalls,prayer,visitor,callback,safetyvisible
cwa_starter_voiceprayer_teamhome,inboxwelcome,metrics,shareprayervisible
cwa_starter_voiceusher_teamhome,inboxwelcome,sharevisitorvisible
cwa_starter_bothadminhome,inbox,trainwelcome,metrics,checklist,share,remove-examplescalls,prayer,visitor,callback,safetyvisible
cwa_pro_chatadminhome,inbox,trainwelcome,metrics,checklist,share,remove-examplesprayer,visitor,callback,safetyvisible
cwa_pro_chatpastorhome,inbox,trainwelcome,metrics,checklist,share,remove-examplesprayer,visitor,callback,safetyvisible
cwa_pro_voiceadminhome,inbox,trainwelcome,metrics,checklist,share,remove-examplescalls,prayer,visitor,callback,safetyvisible
cwa_pro_bothadminhome,inbox,trainwelcome,metrics,checklist,share,remove-examplescalls,prayer,visitor,callback,safetyvisible
cwa_pro_bothcare_teamhome,inboxwelcome,metrics,shareprayer,visitor,callbackvisible
cwa_suite_chatadminhome,inbox,trainwelcome,metrics,checklist,share,remove-examplesprayer,visitor,callback,safetyhidden
cwa_suite_bothadminhome,inbox,trainwelcome,metrics,checklist,share,remove-examplescalls,prayer,visitor,callback,safetyhidden
cwa_bundleadminhome,inbox,trainwelcome,metrics,checklist,share,remove-examplescalls,prayer,visitor,callback,safetyhidden
cwa_pro_websiteadminhome,inbox,train,websitewelcome,metrics,checklist,share,remove-examplesprayer,visitor,callback,safetyvisible
cwa_pro_websitepastorhome,inbox,train,websitewelcome,metrics,checklist,share,remove-examplesprayer,visitor,callback,safetyvisible
cwa_pro_websitetech_teamhome,websitewelcome,sharevisible
cwa_pro_websiteprayer_teamhome,inbox,websitewelcome,metrics,shareprayervisible
ps_pro_websiteadminhome,inbox,train,websitewelcome,metrics,checklist,share,remove-examplesprayer,visitor,callback,safetyvisible
pro_websiteadminhome,inbox,train,websitewelcome,metrics,checklist,share,remove-examplesprayer,visitor,callback,safetyvisible
ps_premiumadminhomewelcomevisible

Full cartesian expansion (remaining 60 rows of plan × role) follows the same derivation: visible_tabs = intersection(role's cap-granted tabs, plan's plan-gated tabs); home_sections same rule; inbox_chips = intersection of role's inbox caps with plan's channel. Generator script: scripts/derive-admin-nav-test-matrix.mjs (to be built). 30 scenarios above are the distilled canonical set — each exercises at least one unique row/column.


12. How to keep this doc fresh

Rebuild triggers. Regenerate or hand-update this doc when ANY of these files change:

  • churchwiseai-web/src/lib/rbac.ts — capability enum or TEMPLATE_GROUP_CAPABILITIES.
  • churchwiseai-web/src/lib/rbac-catalog.ts — capability labels or category assignment.
  • churchwiseai-web/src/lib/tier-config.ts — plan keys or any predicate.
  • churchwiseai-web/src/app/admin/[token]/components/AdminDashboard.rbac.tsCAP_TABS or canSeeTab.

Proposed CI check. A derive-script to run in pnpm derive --check:

// knowledge/scripts/check-admin-nav-map.ts
// 1. Parse ALL_CAPABILITIES from rbac.ts
// 2. Grep this doc for every capability key; fail if any are missing
// 3. Parse VALID_PLAN_KEYS from tier-config.ts
// 4. Grep §7 plan matrix for every plan key; fail if any are missing
// 5. Parse TEMPLATE_GROUP_CAPABILITIES; assert §8 matrix row-for-row matches

Gate PRs that touch the rebuild-trigger files on this check passing.

last-verified date at the top must be bumped by hand when a human or agent does a full pass comparing code → doc. Current: 2026-04-19.

Reconciliation needed

Flagged conflicts between this doc, the RBAC spec, and the code:

  1. Capability count. Spec §3 header says "42 capabilities"; code exports 52 via ALL_CAPABILITIES. Resolution: the 2 care:* caps were added in Phase 1 as a founder decision (see rbac.ts comment around line 76), and inbox:prayer:assign + inbox:prayer:care_message were added 2026-04-18 to back the Inbox card "Assign to prayer team" and "Send care message" buttons with dedicated caps instead of the short-lived proxies on settings:team:invite / care:broadcast. The 42 → 52 gap is real; the spec should be regenerated from code. This doc uses 52 (code wins).

  2. Care tab fate. Spec §11 Q6 flagged this as undecided. Resolution picked here: Care is folded into Inbox (broadcast action + subscribers panel) per rbac-catalog.ts assigning both care:* caps to the Inbox category and per the P1 sprint plan §Slice 2 (Inbox merges Calls + Requests + Care). If founder changes mind and Care becomes its own tab, update §3 (add 3.5 Care tab), §4 (move §4.6 Care), and §7 (add Care column to plan matrix). Nothing else downstream should change — the caps are already correct.

  3. settings:team:view on Pastor. Spec §5 Settings matrix shows settings:team:view = ✓ for Pastor; code TEMPLATE_GROUP_CAPABILITIES.pastor also grants it. Match. (Spec §4.3 prose says "settings:team:view" explicitly — consistent.)

  4. inbox:calls:delete on Pastor. Spec §5 matrix marks it Admin-only. Code agrees (Pastor template omits it). Match.

  5. home:metrics:financial:view on Pastor. Spec §5 matrix: Admin + Treasurer only. Code agrees (Pastor template omits it). Match.

  6. Pastor Pulse for Worship Team. Spec §4.8 and matrix both grant train:pastor_pulse:edit. Code agrees. Match.

  7. Simulator tab vs Train-AI simulator sub-section. Undefined at the section level in spec §5 (the simulator sub-tab appears in Train AI surfaces). This doc treats it as a Train AI sub-section (§4.3). No conflict — spec and code agree that train:simulator:use is the gate; just naming clarity.

  8. Pastor Pulse naming. Spec §4 and code both use train:pastor_pulse:edit. The UI label in rbac-catalog.ts is "Update weekly pulse" — pastor-friendly. This doc uses the code name in tables and the UI label in prose. No conflict.

No unreconcilable conflicts. All items 1-8 are aligned in code + this doc; item 1 requires updating the older spec header (cosmetic).