Admin RBAC Architecture — 2026-04-18
Status: Ready for implementation. Model C (per-person access_tokens + multi-group membership) with hybrid template-based custom groups.
Scope note: This document cannot be written to disk — the planning agent is read-only. The spec is delivered below for the parent agent to persist at C:/dev/knowledge/architecture/admin-rbac-2026-04-18.md.
Role Hierarchy
Section 1 — Current state audit
1.1 Roles today (hardcoded enum in src/lib/premium-shared.ts)
9 TeamRole values, single-select per team member (church_team_members.role TEXT NOT NULL):
| Role | Current tabs | Settings sections | Request types | Training paths | Financial | Pastoral |
|---|---|---|---|---|---|---|
admin | all 9 | all 19 | prayer, visitor, callback, safety | 10 agent paths | ✓ | ✓ |
office_admin | 8 (no upgrade) | 17 (no team, no chatbot toggle) | prayer, visitor, callback, safety | same as admin | — | ✓ |
prayer_team | overview, requests | — | prayer only | prayer | — | — |
care_team | overview, requests, care | — | prayer, visitor, callback | pastoral_care, prayer | — | — |
treasurer | overview | — | — | giving | ✓ | — |
volunteer_coordinator | overview, requests | — | visitor, callback | welcome, volunteer | — | — |
worship_leader | overview | — | — | sermon_prep | — | — |
spiritual_leader | overview, training | — | prayer | small_group, bible_study, prayer, new_member | — | — |
care_leader | overview, training | — | prayer, visitor, callback | pastoral_care, prayer, welcome, new_member, youth | — | — |
1.2 Enforcement sites today
| Location | Check | Authoritative? |
|---|---|---|
AdminDashboard.tsx:198 | ROLE_TABS[role] filters tabs | UI-only |
RequestManager.tsx:188-189 | PASTORAL_ROLES.includes() + ROLE_REQUEST_TYPES[role] | UI-only |
SettingsTab.tsx:257,261 | ROLE_SETTINGS[role] filters sub-tabs | UI-only |
api/premium/requests/route.ts:31,69 | ROLE_REQUEST_TYPES[resolved.role] | API (authoritative) |
api/premium/update/route.ts:68,148 | ROLE_SETTINGS[role] | API (authoritative) |
api/care/members/route.ts:19 | ROLE_TABS[role]?.includes('care') | API (authoritative) |
api/care/broadcast/route.ts:49 | same | API (authoritative) |
api/training/sessions/route.ts:125 | ROLE_TRAINING_PATHS[role] | API (authoritative) |
api/premium/team/route.ts:35 | resolved.role !== 'admin' | API (authoritative) |
voice-queries.ts:237,342 | PASTORAL_ROLES for prayer/callback redaction | Query-layer |
1.3 Inconsistencies found
- Tab-order-vs-gating drift.
ROLE_TABS.adminstill lists'social'butAdminDashboard.tsx:205hides it for everyone (if (t.key === 'social') return false). Gate in data; UI shadows it. Dead code. treasurerhasrequestsnowhere buttraininggivesgivingpath. A treasurer has zero value in the Inbox today, yet the founder's locked list calls out financial visibility as a real concern.prayer_teamcan see Inbox → prayer, butROLE_REQUEST_TYPES.prayer_team = ['prayer']. Consistent — but the Dashboard OverviewrecentActivityquery invoice-queries.ts:246-270filters per-role and returns{data: []}literal for unauthorized types. Good pattern to preserve.spiritual_leader/care_leader/worship_leaderroles exist but the founder's locked list usesworship_team/ has nospiritual_leaderorcare_leader. Mapping decision: keep 3 current roles as templates but rename per founder's list (see §4).- Two overlapping identity systems coexist.
church_team_members(legacy, token-based) andchurch_admin_identities+church_identity_roles(new, email/magic-link). Model C layers on top ofchurch_team_membersbecause that is where per-personaccess_tokenlives.church_identity_rolesis orthogonal (owner/backup metadata for account recovery) and is not touched here. - Financial visibility is declared but never enforced.
FINANCIAL_ROLESis exported but not referenced anywhere outside of docs. Gap before launch — Treasurer can't actually see giving because there's no giving surface yet, but the moment a giving surface ships, the taxonomy must be in place.
1.4 The hard migration fact
Today's model has
roleasTEXT— single role per member. Must becomegroup_ids UUID[]for Model C.
Keep role TEXT during rollout as a read-only fallback; new code reads group_ids + capabilities. Phase 4 backfills and drops role.
Section 2 — Target data model
2.1 New table: church_custom_groups
CREATE TABLE church_custom_groups (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
church_id uuid NOT NULL REFERENCES premium_churches(id) ON DELETE CASCADE,
name text NOT NULL,
description text,
capabilities text[] NOT NULL DEFAULT '{}', -- array of capability keys from Section 3
origin text NOT NULL DEFAULT 'custom' CHECK (origin IN ('template','custom')),
template_key text, -- e.g. 'worship_team'; null for origin='custom'
is_deletable boolean NOT NULL DEFAULT true, -- Admin template is false-locked
sort_order int NOT NULL DEFAULT 100,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (church_id, name)
);
CREATE INDEX idx_custom_groups_church ON church_custom_groups(church_id);
CREATE INDEX idx_custom_groups_template ON church_custom_groups(church_id, template_key) WHERE template_key IS NOT NULL;
-- Validate that every element of capabilities is a known capability key
-- (enforced at application layer; Postgres array-of-enum is awkward).
2.2 Modify church_team_members
ALTER TABLE church_team_members
ADD COLUMN group_ids uuid[] NOT NULL DEFAULT '{}',
ADD COLUMN capabilities text[] NOT NULL DEFAULT '{}';
-- Index for effective-permissions lookup
CREATE INDEX idx_team_members_group_ids ON church_team_members USING GIN (group_ids);
-- Keep role TEXT NOT NULL during Phase 1-3 rollout.
-- Phase 4 migration: backfill group_ids from role, then DROP COLUMN role.
2.3 Back-compat strategy
- Phase 1 (data model + seed). Add columns nullable-effective via defaults. On each
premium_churchesINSERT (new church), trigger seeds all 11 templates intochurch_custom_groups. Existing churches get seeded via a one-shotUPDATEmigration run in the same release. - Phase 2 (dual-read).
resolveToken()returns both legacyroleand neweffectivePermissions. Every capability check falls back to the legacy ROLE_* maps whengroup_idsis empty. New code writes both. - Phase 3 (legacy write-only).
rolebecomes computed-in-app from the primary group's template_key for UI display continuity. All authorization useseffectivePermissions. - Phase 4 (drop). Backfill
group_idsfromrolefor every extant row, verify, droprolecolumn, remove legacy constants.
2.4 Seed rule for new churches
On premium_churches INSERT, a server-side function seedTemplateGroups(premiumId) creates 11 rows in church_custom_groups with origin='template', template_key set per §4, and is_deletable = false for the Admin template (only). Admin group membership auto-assigned to the primary owner church_team_members row that already exists.
2.5 RLS posture
church_custom_groups: RLS enabled. Service-role policy: full. Member-role policy (viaauth.uid()mapped topremium_id): SELECT only within theirchurch_id.church_team_members.group_ids/capabilities: already covered by existing row-scoped RLS on that table.- Principle for this project: API is authoritative. RLS is defense-in-depth for the three-Stripe-customer launch window, not the primary gate.
Section 3 — Capability taxonomy
42 capabilities, namespaced surface:resource:action[:qualifier]. Key format is ^[a-z_]+:[a-z_]+(:[a-z_]+){1,2}$. These map to the 4-tab IA (Home / Inbox / Train AI / Website) + Settings slide-over + Billing.
Home
| Key | Label | Description | Category | Admin-only | Default seed groups |
|---|---|---|---|---|---|
home:overview:view | View Home dashboard | See the Home tab with welcome card, share-link card, and setup checklist | Home | — | all 11 |
home:metrics:view | See activity counts | View this-week tiles: calls, prayer requests, visitor contacts (counts only, no detail) | Home | — | all 11 |
home:metrics:financial:view | See giving totals | View giving totals tile on Home (sums only, no donor names) | Home | — | Admin, Treasurer |
home:checklist:edit | Manage setup checklist | Mark items done, skip, hide the checklist rail | Home | — | Admin, Office Admin, Pastor |
home:share_link:view | See share link | Copy church website / chatbot share link | Home | — | all 11 |
home:examples:remove | Remove demo examples | Remove pre-loaded sample prayer, sample call from fresh-account state | Home | — | Admin, Office Admin, Pastor |
Inbox
| Key | Label | Description | Category | Admin-only | Default seed groups |
|---|---|---|---|---|---|
inbox:calls:read | See call history | View voice call log list with summaries | Inbox | — | Admin, Office Admin, Pastor |
inbox:calls:transcript | Read call transcripts | Open full verbatim transcript + listen to recording | Inbox | — | Admin, Office Admin, Pastor |
inbox:calls:delete | Delete call records | Permanently delete a call log row | Inbox | — | Admin |
inbox:prayer:read | See prayer requests | View prayer request list (excluding confidential) | Inbox | — | Admin, Office Admin, Pastor, Prayer Team, Care Team |
inbox:prayer:read:confidential | Read confidential prayer requests | View prayer requests marked is_confidential = true (un-redacted) | Inbox | — | Admin, Office Admin, Pastor |
inbox:prayer:update | Update prayer status / notes | Mark prayed / acknowledged / archived; add internal notes | Inbox | — | Admin, Office Admin, Pastor, Prayer Team, Care Team |
inbox:visitor:read | See visitor contacts | View first-time visitor intake list | Inbox | — | Admin, Office Admin, Pastor, Care Team, Volunteer Coordinator |
inbox:visitor:update | Update visitor status / notes | Mark contacted / converted; add notes | Inbox | — | Admin, Office Admin, Pastor, Care Team, Volunteer Coordinator |
inbox:callback:read | See callback requests | View callback list with names and phone | Inbox | — | Admin, Office Admin, Pastor, Care Team |
inbox:callback:read:reason | Read callback reasons | Read the un-redacted reason field (redacts to "Pastoral inquiry" without this cap) | Inbox | — | Admin, Office Admin, Pastor |
inbox:callback:update | Update callback status | Mark scheduled / completed / failed | Inbox | — | Admin, Office Admin, Pastor, Care Team |
inbox:safety:read | See safety flags | View moderation / safety-flag tab | Inbox | — | Admin, Office Admin, Pastor |
inbox:safety:resolve | Resolve safety flags | Dismiss, escalate, or mark reviewed | Inbox | — | Admin, Office Admin, Pastor |
Train AI
| Key | Label | Description | Category | Admin-only | Default seed groups |
|---|---|---|---|---|---|
train:church_knowledge:edit | Edit church knowledge | Add/edit staff, ministries, What-to-Expect, service info used by the AI | Train AI | — | Admin, Office Admin, Pastor |
train:theology:edit | Edit theology lens | Pick denominational lens, set doctrinal confidence | Train AI | — | Admin, Pastor |
train:agents:edit | Edit agent personalities | Modify AI agent tone, persona overrides, prompts | Train AI | — | Admin, Pastor |
train:faqs:edit | Edit FAQs | Add, edit, delete FAQ Q&A pairs | Train AI | — | Admin, Office Admin, Pastor |
train:safety:edit | Edit safety / crisis message | Configure AI safety rules, crisis escalation message | Train AI | — | Admin, Pastor |
train:simulator:use | Use AI simulator | Run AI simulator to test agent responses | Train AI | — | Admin, Office Admin, Pastor |
train:documents:upload | Upload training documents | Upload PDFs/docs for AI to ingest | Train AI | — | Admin, Pastor |
train:pastor_pulse:edit | Update weekly pulse | Edit sermon topic/series, weekly announcement, theme verse | Train AI | — | Admin, Office Admin, Pastor, Worship Team |
Website
| Key | Label | Description | Category | Admin-only | Default seed groups |
|---|---|---|---|---|---|
website:sections:edit | Edit website sections | Edit Hero, About, Services, Staff, Events, Give, Contact sections | Website | — | Admin, Office Admin, Pastor |
website:design:edit | Edit website design | Change template, colors, hero video, transition videos | Website | — | Admin, Pastor |
website:publish | Publish website changes | Promote draft → live | Website | — | Admin, Pastor |
website:preview | Preview website | Open public preview URL | Website | — | all 11 |
website:media:upload | Upload website media | Upload hero photo, logo, slideshow images | Website | — | Admin, Office Admin, Pastor |
Settings (slide-over)
| Key | Label | Description | Category | Admin-only | Default seed groups |
|---|---|---|---|---|---|
settings:church_profile:edit | Edit church profile | Church name, description, contact email, phone, social | Settings | — | Admin, Office Admin, Pastor |
settings:hours:edit | Edit office hours | Office hours grid, pastor availability | Settings | — | Admin, Office Admin, Pastor |
settings:notifications:edit | Configure notifications | Notification email/phone, alert routing | Settings | — | Admin, Office Admin, Pastor |
settings:integrations:edit | Manage integrations | Connect Planning Center, Cal.com, giving URLs | Settings | — | Admin, Pastor |
settings:sharing:view | View sharing & embed | Share links, embed code, QR codes | Settings | — | Admin, Office Admin, Pastor |
settings:team:view | View team roster | See who's on the team + what groups they're in | Settings | — | Admin, Pastor |
settings:team:invite | Invite team members | Add new members; assign to groups | Settings | — | Admin |
settings:team:remove | Remove team members | Deactivate or delete members | Settings | — | Admin |
Admin-only / Billing (never grantable via group UI)
| Key | Label | Description | Category | Admin-only | Default seed groups |
|---|---|---|---|---|---|
billing:view | View billing | See subscription, invoices | Billing (admin-only) | ✓ | Admin |
billing:manage | Manage billing | Upgrade, downgrade, update card | Billing (admin-only) | ✓ | Admin |
billing:cancel | Cancel subscription | End the subscription | Billing (admin-only) | ✓ | Admin |
church:delete | Delete church | Permanently delete the church and all data | Billing (admin-only) | ✓ | Admin |
church:transfer_ownership | Transfer primary ownership | Move primary-owner role to another identity | Billing (admin-only) | ✓ | Admin |
groups:manage | Manage groups & capabilities | Create/edit/delete custom groups; edit template group capabilities | Billing (admin-only) | ✓ | Admin |
api_keys:manage | Manage API keys | Create/rotate/revoke API keys for integrations | Billing (admin-only) | ✓ | Admin |
audit:view | View audit log | See audit log of team actions | Billing (admin-only) | ✓ | Admin |
Total: 42 capabilities. 8 are admin-only (never selectable in custom-group UI). 34 are group-grantable.
3.1 Redaction semantics
inbox:prayer:read(yes) +inbox:prayer:read:confidential(no) → prayer_text foris_confidential=truerows replaced with"Confidential — contact the pastor". Same row, masked field.inbox:callback:read(yes) +inbox:callback:read:reason(no) → reason field replaced with"Pastoral inquiry".home:metrics:view(yes) +home:metrics:financial:view(no) → giving tile hidden entirely (row-level, not field-level, because the tile is the data).
3.2 Why this shape
- Namespacing by surface lets the admin UI auto-generate categories (all
inbox:*under "Inbox"). - Verb suffix (
read/update/edit/resolve/view) is consistent and tight.readis list+detail;editis save;updateis status/notes transitions;viewis analytical/reporting. - Qualifiers after a 3rd colon carve out redaction boundaries (
:confidential,:reason) without inventing separate surfaces.
Section 4 — Template group definitions (11 templates)
Keys match what the admin UI will scaffold on every new church.
4.1 Admin
- Key:
admin - Name: "Admin"
- Description: "Full access to everything. Can manage team, billing, and every setting."
- Capabilities: all 42 capabilities (including the 8 admin-only). This is the only template that receives admin-only capabilities and is
is_deletable = false. - Typical sample: 1–2 per church (pastor + office manager).
4.2 Office Admin
- Key:
office_admin - Name: "Office Admin"
- Description: "Manages day-to-day content, requests, and church profile. Can't change billing or team."
- Capabilities: all of Home; all of Inbox except
inbox:calls:delete;train:church_knowledge:edit,train:faqs:edit,train:simulator:use,train:pastor_pulse:edit;website:sections:edit,website:preview,website:media:upload;settings:church_profile:edit,settings:hours:edit,settings:notifications:edit,settings:sharing:view. - Typical sample: 1–2 per church.
4.3 Pastor
- Key:
pastor - Name: "Pastor"
- Description: "Sees all Inbox, edits theology and agent personality, publishes website changes."
- Capabilities: all of Home; all of Inbox (including
:confidentialand:reason); alltrain:*except:safety(see note); all Website;settings:team:view,settings:church_profile:edit.- Note:
train:safety:editincluded for Pastor. Only the 8 admin-only caps are excluded.
- Note:
- Typical sample: 1–2 per church (lead pastor + associate).
4.4 Prayer Team
- Key:
prayer_team - Name: "Prayer Team"
- Description: "Sees non-confidential prayer requests and can mark them prayed."
- Capabilities:
home:overview:view,home:metrics:view,home:share_link:view;inbox:prayer:read,inbox:prayer:update;website:preview. - Typical sample: 3–8 per church.
4.5 Care Team
- Key:
care_team - Name: "Care Team"
- Description: "Sees prayer, visitor, and callback requests. Follows up with congregation."
- Capabilities:
home:overview:view,home:metrics:view,home:share_link:view;inbox:prayer:read,inbox:prayer:update,inbox:visitor:read,inbox:visitor:update,inbox:callback:read,inbox:callback:update;website:preview. - Typical sample: 3–6 per church.
4.6 Treasurer
- Key:
treasurer - Name: "Treasurer"
- Description: "Sees giving totals and financial reports. No access to pastoral content."
- Capabilities:
home:overview:view,home:metrics:view,home:metrics:financial:view,home:share_link:view;website:preview. - Typical sample: 1–2 per church.
4.7 Volunteer Coordinator
- Key:
volunteer_coordinator - Name: "Volunteer Coordinator"
- Description: "Manages visitor follow-up and volunteer callbacks. Does not see prayer requests."
- Capabilities:
home:overview:view,home:metrics:view,home:share_link:view;inbox:visitor:read,inbox:visitor:update,inbox:callback:read,inbox:callback:update;website:preview. - Typical sample: 1–2 per church.
4.8 Worship Team
- Key:
worship_team - Name: "Worship Team"
- Description: "Edits weekly pulse (sermon topic, theme verse) so the AI speaks to this week's theme."
- Capabilities:
home:overview:view,home:metrics:view,home:share_link:view;train:pastor_pulse:edit;website:preview. - Typical sample: 3–6 per church.
4.9 Usher Team
- Key:
usher_team - Name: "Usher Team"
- Description: "Receives first-time visitor contacts so they can welcome them Sunday."
- Capabilities:
home:overview:view,home:share_link:view;inbox:visitor:read;website:preview. - Typical sample: 4–10 per church.
4.10 Kids Ministry
- Key:
kids_ministry - Name: "Kids Ministry"
- Description: "Sees visitor contacts flagged with children. Receives kids-specific callbacks."
- Capabilities:
home:overview:view,home:metrics:view,home:share_link:view;inbox:visitor:read,inbox:visitor:update,inbox:callback:read,inbox:callback:update;website:preview. - Typical sample: 2–4 per church.
4.11 Youth Ministry
- Key:
youth_ministry - Name: "Youth Ministry"
- Description: "Sees visitor contacts and callbacks related to youth. Can update status."
- Capabilities: same as Kids Ministry.
- Typical sample: 2–4 per church.
4.12 Tech Team
- Key:
tech_team - Name: "Tech Team"
- Description: "Edits website design, media uploads, integrations. Does not see pastoral content."
- Capabilities:
home:overview:view,home:share_link:view;train:simulator:use;website:sections:edit,website:design:edit,website:preview,website:media:upload;settings:integrations:edit,settings:sharing:view. - Typical sample: 1–3 per church.
Founder wrote "11 templates" but specified 12 (Admin through Tech Team). Shipping all 12. The Admin template is non-deletable; the other 11 can be renamed, customized, or deleted.
Section 5 — Permissions matrix
Legend: ✓ = full access · ⊙ = redacted (visible but sensitive fields masked) · blank = hidden. Columns abbreviated: Admin (A), Office Admin (OA), Pastor (P), Prayer (Pr), Care (C), Treasurer (T), Volunteer Coord (V), Worship (W), Usher (U), Kids (K), Youth (Y), Tech (X). "AO" column flags admin-only capabilities.
Home tab
| Surface / element | Capability | A | OA | P | Pr | C | T | V | W | U | K | Y | X | AO |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Welcome card | home:overview:view | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| Share-link card | home:share_link:view | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| Setup checklist rail | home:checklist:edit | ✓ | ✓ | ✓ | — | |||||||||
| Metric tile: calls this week | home:metrics:view | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ||
| Metric tile: prayer count | home:metrics:view | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ||
| Metric tile: visitor count | home:metrics:view | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ||
| Metric tile: giving totals | home:metrics:financial:view | ✓ | ✓ | — | ||||||||||
| Example-data card (sample prayer/call) | home:examples:remove | ✓ | ✓ | ✓ | — | |||||||||
| "View live preview" link | home:share_link:view | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
Inbox tab (filter chips)
| Surface | Capability | A | OA | P | Pr | C | T | V | W | U | K | Y | X | AO |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Chip: Calls | inbox:calls:read | ✓ | ✓ | ✓ | — | |||||||||
| Call detail — transcript | inbox:calls:transcript | ✓ | ✓ | ✓ | — | |||||||||
| Call detail — delete | inbox:calls:delete | ✓ | — | |||||||||||
| Chip: Prayer requests | inbox:prayer:read | ✓ | ✓ | ✓ | ✓ | ✓ | — | |||||||
| Prayer detail — confidential text | inbox:prayer:read:confidential | ✓ | ✓ | ✓ | ⊙ | ⊙ | — | |||||||
| Prayer row status dropdown | inbox:prayer:update | ✓ | ✓ | ✓ | ✓ | ✓ | — | |||||||
| Chip: Visitors | inbox:visitor:read | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ||||
| Visitor row update | inbox:visitor:update | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | |||||
| Chip: Callbacks | inbox:callback:read | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | |||||
| Callback row — reason text | inbox:callback:read:reason | ✓ | ✓ | ✓ | ⊙ | ⊙ | ⊙ | ⊙ | — | |||||
| Callback status dropdown | inbox:callback:update | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | |||||
| Chip: Safety flags | inbox:safety:read | ✓ | ✓ | ✓ | — | |||||||||
| Safety flag resolve actions | inbox:safety:resolve | ✓ | ✓ | ✓ | — |
Train AI tab (sub-tabs)
| Surface | Capability | A | OA | P | Pr | C | T | V | W | U | K | Y | X | AO |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Church Knowledge sub-tab | train:church_knowledge:edit | ✓ | ✓ | ✓ | — | |||||||||
| Theology sub-tab | train:theology:edit | ✓ | ✓ | — | ||||||||||
| Agent Personalities sub-tab | train:agents:edit | ✓ | ✓ | — | ||||||||||
| FAQs sub-tab | train:faqs:edit | ✓ | ✓ | ✓ | — | |||||||||
| Safety / Crisis sub-tab | train:safety:edit | ✓ | ✓ | — | ||||||||||
| Simulator sub-tab | train:simulator:use | ✓ | ✓ | ✓ | ✓ | — | ||||||||
| Document Upload sub-tab | train:documents:upload | ✓ | ✓ | — | ||||||||||
| Pastor Pulse card | train:pastor_pulse:edit | ✓ | ✓ | ✓ | ✓ | — |
Website tab (editor sections)
| Surface | Capability | A | OA | P | Pr | C | T | V | W | U | K | Y | X | AO |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Section panel: Hero | website:sections:edit | ✓ | ✓ | ✓ | ✓ | — | ||||||||
| Section panel: About | website:sections:edit | ✓ | ✓ | ✓ | ✓ | — | ||||||||
| Section panel: Services | website:sections:edit | ✓ | ✓ | ✓ | ✓ | — | ||||||||
| Section panel: Staff | website:sections:edit | ✓ | ✓ | ✓ | ✓ | — | ||||||||
| Section panel: Events | website:sections:edit | ✓ | ✓ | ✓ | ✓ | — | ||||||||
| Section panel: Give | website:sections:edit | ✓ | ✓ | ✓ | ✓ | — | ||||||||
| Section panel: Contact | website:sections:edit | ✓ | ✓ | ✓ | ✓ | — | ||||||||
| Design: template picker | website:design:edit | ✓ | ✓ | ✓ | — | |||||||||
| Design: colors | website:design:edit | ✓ | ✓ | ✓ | — | |||||||||
| Design: videos | website:design:edit | ✓ | ✓ | ✓ | — | |||||||||
| Publish button | website:publish | ✓ | ✓ | — | ||||||||||
| Media upload | website:media:upload | ✓ | ✓ | ✓ | ✓ | — | ||||||||
| Preview button | website:preview | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
Settings slide-over
| Surface | Capability | A | OA | P | Pr | C | T | V | W | U | K | Y | X | AO |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Church Profile section | settings:church_profile:edit | ✓ | ✓ | ✓ | — | |||||||||
| Hours section | settings:hours:edit | ✓ | ✓ | ✓ | — | |||||||||
| Notifications section | settings:notifications:edit | ✓ | ✓ | ✓ | — | |||||||||
| Integrations section | settings:integrations:edit | ✓ | ✓ | ✓ | — | |||||||||
| Sharing section | settings:sharing:view | ✓ | ✓ | ✓ | ✓ | — | ||||||||
| Team roster | settings:team:view | ✓ | ✓ | — | ||||||||||
| Team — Invite | settings:team:invite | ✓ | — | |||||||||||
| Team — Remove | settings:team:remove | ✓ | — | |||||||||||
| Billing section | billing:view, billing:manage, billing:cancel | ✓ | ✓ | |||||||||||
| Delete church (danger zone) | church:delete | ✓ | ✓ | |||||||||||
| Transfer ownership | church:transfer_ownership | ✓ | ✓ | |||||||||||
| Manage Groups UI | groups:manage | ✓ | ✓ | |||||||||||
| API Keys | api_keys:manage | ✓ | ✓ | |||||||||||
| Audit log | audit:view | ✓ | ✓ |
Adversarial check — snoop scenarios closed:
- Prayer Team snoops on giving. Prayer Team has zero
home:metrics:financial:viewand zeroinbox:callback:read:reason→ giving tile absent from their Home; callback reasons redacted if they somehow added Care Team too. Confirmed. - Tech Team snoops on confidential prayer. Tech Team has no
inbox:*caps at all. Chip-gated + API-gated. Confirmed. - Usher snoops on callback reasons. Usher only has
inbox:visitor:read. Callback chip hidden, API returns 403. Confirmed. - Volunteer Coordinator reads confidential callback reason. Has
inbox:callback:readbut not:reason→ reason masked to "Pastoral inquiry". Confirmed (matches today's redaction pattern invoice-queries.ts:288). - Kids Ministry pivots to prayer. No
inbox:prayer:*caps. API returns 403 on/api/premium/requests?type=prayer. Confirmed. - Worship Team edits theology. Only has
train:pastor_pulse:edit. Theology form returns 403 on PATCH. Confirmed.
Section 6 — Effective-permissions algorithm
6.1 Types (new file: src/lib/rbac.ts)
// src/lib/rbac.ts — client-safe; no DB imports.
export type Capability =
| 'home:overview:view' | 'home:metrics:view' | 'home:metrics:financial:view'
| 'home:checklist:edit' | 'home:share_link:view' | 'home:examples:remove'
| 'inbox:calls:read' | 'inbox:calls:transcript' | 'inbox:calls:delete'
| 'inbox:prayer:read' | 'inbox:prayer:read:confidential' | 'inbox:prayer:update'
| 'inbox:visitor:read' | 'inbox:visitor:update'
| 'inbox:callback:read' | 'inbox:callback:read:reason' | 'inbox:callback:update'
| 'inbox:safety:read' | 'inbox:safety:resolve'
| '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'
| 'website:sections:edit' | 'website:design:edit' | 'website:publish'
| 'website:preview' | 'website:media:upload'
| 'settings:church_profile:edit' | 'settings:hours:edit'
| 'settings:notifications:edit' | 'settings:integrations:edit'
| 'settings:sharing:view' | 'settings:team:view'
| 'settings:team:invite' | 'settings:team:remove'
| 'billing:view' | 'billing:manage' | 'billing:cancel'
| 'church:delete' | 'church:transfer_ownership'
| 'groups:manage' | 'api_keys:manage' | 'audit:view';
export const ADMIN_ONLY_CAPABILITIES: ReadonlySet<Capability> = new Set([
'billing:view', 'billing:manage', 'billing:cancel',
'church:delete', 'church:transfer_ownership',
'groups:manage', 'api_keys:manage', 'audit:view',
]);
export interface ResolvedMember {
id: string;
church_id: string;
premium_id: string;
name: string;
group_ids: string[];
capabilities: Capability[]; // direct per-person grants
is_active: boolean;
}
export interface GroupSpec {
id: string;
name: string;
capabilities: Capability[];
}
/** Pure union of group capabilities + direct grants. Deduped via Set. */
export function effectivePermissions(
member: Pick<ResolvedMember, 'group_ids' | 'capabilities'>,
groupsById: Map<string, GroupSpec>,
): Set<Capability> {
const out = new Set<Capability>(member.capabilities);
for (const gid of member.group_ids) {
const g = groupsById.get(gid);
if (!g) continue;
for (const c of g.capabilities) out.add(c);
}
return out;
}
export function can(
member: Pick<ResolvedMember, 'group_ids' | 'capabilities'>,
cap: Capability,
groupsById: Map<string, GroupSpec>,
): boolean {
return effectivePermissions(member, groupsById).has(cap);
}
export class ForbiddenError extends Error {
constructor(public readonly capability: Capability) {
super(`Forbidden: missing capability ${capability}`);
this.name = 'ForbiddenError';
}
}
export function assertCan(
member: Pick<ResolvedMember, 'group_ids' | 'capabilities'>,
cap: Capability,
groupsById: Map<string, GroupSpec>,
): void {
if (!can(member, cap, groupsById)) throw new ForbiddenError(cap);
}
6.2 Request-scoped cache
// src/lib/rbac-resolver.ts — server-only
import 'server-only';
import { cache } from 'react';
import { supabase } from './supabase';
import type { ResolvedMember, GroupSpec } from './rbac';
// React cache() dedupes per request — identical args return the same promise.
export const getGroupsForPremium = cache(async (premiumId: string): Promise<Map<string, GroupSpec>> => {
const { data } = await supabase
.from('church_custom_groups')
.select('id, name, capabilities')
.eq('church_id', premiumId);
const m = new Map<string, GroupSpec>();
for (const g of data ?? []) m.set(g.id, g as GroupSpec);
return m;
});
Performance note: one SELECT per request per premium, cached by React's cache() for the duration of the request. Typical result set ≤ 20 rows × ~20 capability strings → ~2 KB. Effective-permission computation is a single linear pass over group_ids (max ~5 per person) + direct grants — sub-millisecond.
6.3 Backwards compatibility helper during Phases 1–3
// src/lib/rbac-legacy.ts — compat bridge during rollout
import { LEGACY_ROLE_CAPABILITIES } from './rbac-legacy-map'; // derived from Section 4
export function legacyRoleCapabilities(role: string): Capability[] {
return LEGACY_ROLE_CAPABILITIES[role] ?? [];
}
export function effectiveWithFallback(
member: { role?: string; group_ids: string[]; capabilities: Capability[] },
groupsById: Map<string, GroupSpec>,
): Set<Capability> {
if (member.group_ids.length > 0 || member.capabilities.length > 0) {
return effectivePermissions(member, groupsById);
}
// Pre-migration: fall back to legacy role
return new Set(legacyRoleCapabilities(member.role ?? 'admin'));
}
Section 7 — Enforcement layers (UI, API, DB)
Principle: API always authoritative. UI hiding is UX, not security. DB RLS is defense-in-depth.
7.1 Per-surface enforcement table (key surfaces)
| Surface | UI | API | DB (RLS) |
|---|---|---|---|
| Home metric tile: giving totals | Hidden if !can('home:metrics:financial:view') | /api/admin/giving/summary → 403 without cap | RLS on church_giving (future) limited to members whose capabilities @> ARRAY['home:metrics:financial:view'] OR whose group_ids intersect a group that contains it |
| Inbox chip: Prayer | Hidden if !can('inbox:prayer:read') | /api/premium/requests?type=prayer → 403 | voice_prayer_requests row-scoped RLS by church_id (existing); redaction applied in API layer |
| Prayer detail: confidential text | Shown redacted if !can('inbox:prayer:read:confidential') | API returns prayer_text: 'Confidential — contact the pastor' when cap missing; pattern already used in voice-queries.ts:348-353 | — |
| Callback detail: reason field | Shown redacted if !can('inbox:callback:read:reason') | API returns reason: 'Pastoral inquiry' when cap missing; already in voice-queries.ts:288 | — |
| Train AI → Theology | Sub-tab hidden if !can('train:theology:edit') | /api/admin/theology POST → 403 | — |
| Train AI → Agent personality | Sub-tab hidden if !can('train:agents:edit') | /api/admin/agent-config PATCH → 403 | — |
| Website Publish button | Button hidden if !can('website:publish') | /api/admin/website/publish → 403 | — |
| Settings → Team → Invite | Button hidden if !can('settings:team:invite') | /api/premium/team POST where action=invite → 403 | — |
| Settings → Billing | Entire section hidden if !can('billing:view') | /api/billing/* → 403 | RLS on stripe_* tables (service-role only anyway) |
| Settings → Manage Groups | Section hidden if !can('groups:manage') | /api/admin/groups → 403 | church_custom_groups RLS: SELECT is allowed to all members of church_id; INSERT/UPDATE/DELETE require service-role OR member with groups:manage (enforced in API) |
| Team remove | Button hidden if !can('settings:team:remove') | /api/premium/team POST action=remove → 403 | — |
7.2 API middleware pattern (new)
Replace the current ad-hoc resolved.role !== 'admin' checks with a single helper:
// src/lib/api-rbac.ts — new
import 'server-only';
import { NextResponse } from 'next/server';
import { resolveTokenOrHeaders } from './premium-queries';
import { getGroupsForPremium } from './rbac-resolver';
import { effectivePermissions, type Capability } from './rbac';
export async function requireCapability(
request: Request,
token: string | null,
cap: Capability,
): Promise<
| { ok: true; resolved: Awaited<ReturnType<typeof resolveTokenOrHeaders>> & {}; caps: Set<Capability> }
| { ok: false; response: Response }
> {
const resolved = await resolveTokenOrHeaders(token, request.headers);
if (!resolved) return { ok: false, response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) };
const groups = await getGroupsForPremium(resolved.premium.id);
// During rollout: hydrate member's group_ids + capabilities from DB (not on resolved).
// Phase 2+: have resolveToken also fetch these fields.
const caps = effectivePermissions(
{ group_ids: (resolved as any).group_ids ?? [], capabilities: (resolved as any).capabilities ?? [] },
groups,
);
if (!caps.has(cap)) {
return { ok: false, response: NextResponse.json({ error: `Forbidden: ${cap}` }, { status: 403 }) };
}
return { ok: true, resolved, caps };
}
7.3 UI hook pattern
// src/hooks/useCapabilities.ts — new
'use client';
import { createContext, useContext } from 'react';
import type { Capability } from '@/lib/rbac';
export const CapabilityContext = createContext<Set<Capability>>(new Set());
export const useCan = (cap: Capability) => useContext(CapabilityContext).has(cap);
The server page wraps the dashboard tree with <CapabilityContext.Provider value={caps}>. Every component reads via useCan(...).
Section 8 — Admin Group Management UI
All mockups live under Settings slide-over → Team sub-tab → "Manage Groups" link.
8.1 Team Members screen (updated)
┌──────────────────────────────────────────────────────────────────────────┐
│ Settings · Team [← back to dashboard] │
│ │
│ 12 members · 8 groups [+ Invite member] │
│ ────────────────────────────────────────────────────────────────────── │
│ │
│ Pastor Ruth Groups: Admin · Pastor │
│ ruth@buchananbaptist.org Last seen: 12 min ago │
│ [✎ edit groups] [copy link] [remove] │
│ ────────────────────────────────────────── │
│ Sarah Chen Groups: Prayer Team │
│ sarah.c@buchananbaptist.org Last seen: 2 days ago │
│ [✎ edit groups] [copy link] [remove] │
│ ────────────────────────────────────────── │
│ Tom Walker Groups: Treasurer │
│ tom@buchananbaptist.org Last seen: never │
│ [✎ edit groups] [copy link] [remove] │
│ │
│ ────────────────────────────────────────────────────────────────────── │
│ GROUPS [+ Create group] │
│ │
│ · Admin (2 members) [rename] [edit] · locked │
│ · Pastor (1 member) [rename] [edit] [delete] │
│ · Prayer Team (5 members) [rename] [edit] [delete] │
│ · Care Team (0 members) [rename] [edit] [delete] │
│ · Treasurer (1 member) [rename] [edit] [delete] │
│ · Worship Team (3 members) [rename] [edit] [delete] │
│ │
└──────────────────────────────────────────────────────────────────────────┘
8.2 Invite Member modal
┌─────────────────────────────────────────────────────────────┐
│ Invite a team member [✕] │
│ ───────────────────────────────────────────────────── │
│ Name [ Sarah Chen ] │
│ Email [ sarah.c@buchananbaptist.org ] │
│ │
│ Groups (pick one or more) │
│ [x] Prayer Team │
│ [ ] Care Team │
│ [ ] Worship Team │
│ [ ] Usher Team │
│ [ ] Kids Ministry │
│ [ ] Youth Ministry │
│ [ ] Volunteer Coordinator │
│ [ ] Tech Team │
│ [ ] Office Admin │
│ [ ] Pastor │
│ │
│ ▸ Advanced: extra capabilities (optional) │
│ Use when this person needs one-off access beyond │
│ their groups. │
│ [click to expand] │
│ │
│ We'll send Sarah a magic link to this email. │
│ │
│ [Cancel] [Send invite] │
└─────────────────────────────────────────────────────────────┘
8.3 Create / Edit Group modal
┌─────────────────────────────────────────────────────────────┐
│ Edit group: Prayer Team [✕] │
│ ───────────────────────────────────────────────────── │
│ Name [ Prayer Team ] │
│ Description [ Sees non-confidential prayer requests] │
│ │
│ Capabilities │
│ ───────────── │
│ │
│ HOME │
│ [x] See Home dashboard │
│ [x] See activity counts │
│ [ ] See giving totals │
│ [x] See share link │
│ │
│ INBOX │
│ [x] See prayer requests │
│ [ ] Read confidential prayer requests │
│ [x] Update prayer status / notes │
│ [ ] See visitor contacts │
│ [ ] See callback requests │
│ [ ] Read callback reasons │
│ [ ] Update callback status │
│ [ ] See safety flags │
│ [ ] Resolve safety flags │
│ [ ] See call history │
│ │
│ TRAIN AI │
│ [ ] Edit church knowledge │
│ [ ] Edit theology lens │
│ ... (collapsed by default — expand) │
│ │
│ WEBSITE │
│ [x] Preview website │
│ [ ] Edit website sections │
│ ... │
│ │
│ SETTINGS │
│ [ ] Edit church profile │
│ ... │
│ │
│ ───────────── │
│ Admin-only capabilities (billing, delete, manage groups) │
│ never appear here. Admins get those automatically. │
│ │
│ [Cancel] [Save group] │
└─────────────────────────────────────────────────────────────┘
8.4 Group deletion confirmation
┌───────────────────────────────────────────────────────────┐
│ Delete "Prayer Team"? [✕] │
│ ──────────────────────────────────────────────────── │
│ │
│ ⚠ 5 people will lose this access: │
│ · Sarah Chen │
│ · Mark Davis │
│ · Linda Park │
│ · Joe Patel │
│ · Amanda Ng │
│ │
│ They will remain team members but will lose all │
│ capabilities granted by this group. │
│ │
│ If any of them are only in this group, they will │
│ no longer see any content in the Inbox. │
│ │
│ This cannot be undone. (You can re-create the group │
│ from the Prayer Team template later.) │
│ │
│ [Cancel] [Yes, delete Prayer Team] │
└───────────────────────────────────────────────────────────┘
8.5 Empty states
- Zero groups beyond Admin (someone deleted them all): prompt "Restore templates?" button that re-seeds from the template registry.
- Zero members in a group: "No one is in this group yet. [Invite someone →]"
- Member with zero groups and zero direct caps (possible if admin removes everything): dashboard renders a lockout page: "Your account has no permissions. Please contact your church admin."
8.6 Error states
- Invite email already exists → inline: "A team member with this email already exists. [Edit their groups →]"
- Save group with name collision → inline: "A group named 'Prayer Team' already exists. Pick a different name."
- Save group with zero capabilities → warning toast: "This group grants no access. Add at least one capability to make it useful." (Allow save.)
- Delete last Admin-group member → block with "Admin group must have at least one member. Add someone else before removing yourself."
Section 9 — Test surface
| # | Scenario | Expected |
|---|---|---|
| 1 | Member in [Prayer Team] opens Inbox | Only the Prayer chip is visible. /api/premium/requests?type=visitor returns 403. |
| 2 | Member in [Prayer Team, Care Team] opens Inbox | Prayer + Visitor + Callback chips visible (union). inbox:callback:read:reason absent → reason field shows "Pastoral inquiry". |
| 3 | Member in [Usher Team] + direct grant inbox:prayer:read | Visitor + Prayer chips visible. Confidential prayer still masked (no :confidential cap). |
| 4 | Member in [Office Admin] where Admin deletes the Office Admin group | Member loses all Office Admin caps on next request. /api/admin/theology GET → 403. Member still has a valid access_token and logs in to a lockout or reduced view. |
| 5 | Admin opens Create Group modal | Billing, delete-church, transfer-ownership, manage-groups, api-keys, audit:view checkboxes are absent from the capability list. |
| 6 | Template rename (Admin renames "Prayer Team" to "Intercessors") | Renamed in UI; existing members retain membership; template_key remains 'prayer_team'. |
| 7 | Template → custom conversion (admin edits caps on a template group) | origin stays 'template', template_key stays set, capabilities now diverge. "Restore template defaults" button offered. |
| 8 | Admin creates brand-new custom group "Hospitality" with inbox:visitor:read only | Row inserted with origin='custom', template_key=NULL. Members can be assigned. |
| 9 | API 403 parity test: Treasurer hits every Inbox endpoint | All 403. No endpoint returns 200 with redacted data (Treasurer has zero inbox caps). |
| 10 | UI-hidden-but-API-open regression test: Prayer Team member calls /api/admin/theology directly | 403. Not 200-with-empty. |
| 11 | Revocation immediacy: admin removes [Care Team] from Joe; Joe's next page load | Joe sees Inbox without Visitor/Callback chips; matching API calls return 403 within same session (no token rotation). |
| 12 | Per-member access_token stability under role changes | Joe's access_token is unchanged when groups change. Only group_ids array is updated. |
| 13 | Admin-group minimum-one-member invariant | Removing the last Admin member blocked with "Admin group must have at least one member." |
| 14 | Ownership transfer and Admin group | Transferring primary ownership also ensures the new owner is in the Admin group (auto-added). |
| 15 | Group-delete un-assigns correctly | After delete, every previously-in-group member has that group_id removed from their group_ids array. Idempotent. |
| 16 | Confidential prayer redaction | Prayer Team member viewing a prayer with is_confidential=true sees the redacted placeholder. Office Admin viewing the same row sees full text. |
| 17 | Multi-group union does NOT accidentally grant admin-only cap | Even if someone assembles 10 custom groups, none can grant billing:view because the create-group UI never exposes it (server validates on group save). |
| 18 | Direct grant of admin-only cap attempt | Server rejects with 400: "Admin-only capabilities cannot be granted directly." |
| 19 | Phase 2 fallback: member with empty group_ids but legacy role='prayer_team' | effectiveWithFallback() returns the template caps for prayer_team. |
| 20 | Legacy endpoint compatibility | /api/premium/requests continues to accept token query param and apply new capability checks with no change to API surface. |
Section 10 — Rollout phases
Phase 1 — Data model + template seeding + hardcoded template-access (~2 days)
Goal: Ship new schema and seed. Every new church gets 12 templates. No admin UI yet. Capability checks shim-mapped from legacy role (no behavior change).
Files touched:
- new
supabase/migrations/20260419000000_rbac_model_c.sql— DDL from §2. - new
src/lib/rbac.ts— types + effective-permissions helpers. - new
src/lib/rbac-resolver.ts— cached group fetch. - new
src/lib/rbac-templates.ts— 12-template definitions matching §4. - new
src/lib/rbac-legacy-map.ts— legacy-role → capabilities map. src/app/api/stripe/webhook/route.ts— seed templates onprovisionNewChurch.- new
src/app/api/admin/groups/seed-existing/route.ts— one-shot endpoint to seed all extant churches (internal only).
Migrations: additive only. No destructive changes to church_team_members.
Back-compat: resolveToken() unchanged; capability checks fall through to legacy map via effectiveWithFallback().
Phase 2 — Admin Group Management UI for template editing (~2 days)
Goal: Admins can rename, edit capabilities, delete templates (except Admin). No custom group creation yet.
Files touched:
- new
src/app/admin/[token]/components/settings/TeamTab.tsx— rewritten Team section. - new
src/app/admin/[token]/components/settings/GroupEditorModal.tsx. - new
src/app/admin/[token]/components/settings/GroupDeleteConfirmModal.tsx. - new
src/app/api/admin/groups/route.ts— GET (list), PATCH (rename/edit caps). - new
src/app/api/admin/groups/[id]/route.ts— DELETE. - new
src/lib/api-rbac.ts—requireCapability()middleware from §7.2. - update
src/app/admin/[token]/page.tsx— wrap in<CapabilityContext.Provider>.
Back-compat: member rows keep role; new group_ids membership preferred on read.
Phase 3 — Custom group creation + direct-grant UI (~1 day)
Goal: Admins can create brand-new groups and grant per-person capabilities.
Files touched:
- new
src/app/admin/[token]/components/settings/InviteMemberModal.tsx— with group-picker + advanced per-cap grants. - new
src/app/api/admin/groups/route.tsPOST — create custom group. - update
src/app/api/premium/team/route.ts— acceptgroup_ids[],capabilities[]. - new
src/app/api/admin/members/[id]/capabilities/route.ts— PATCH direct grants. - new
src/hooks/useCapabilities.ts.
Back-compat: role column still written as the template_key of the first group for legacy display.
Phase 4 — Customer migration + drop legacy role (~half day)
Goal: Backfill + remove the legacy field.
Migration script:
-- For every extant church_team_members row, find the matching template
-- group_id (by role → template_key) and insert into group_ids.
UPDATE church_team_members m
SET group_ids = ARRAY[g.id]::uuid[]
FROM church_custom_groups g
WHERE g.church_id = m.premium_id
AND g.template_key = m.role
AND array_length(m.group_ids, 1) IS NULL;
Verify query: find rows where group_ids = '{}' AND capabilities = '{}' → those are orphans. Fix by adding to legacy role→template_key group. Then:
ALTER TABLE church_team_members DROP COLUMN role;
Code cleanup: remove TeamRole, ROLE_TABS, ROLE_SETTINGS, ROLE_REQUEST_TYPES, ROLE_TRAINING, ROLE_TRAINING_PATHS, PASTORAL_ROLES, FINANCIAL_ROLES, SIMULATOR_ONLY_ROLES, getRoleLabel. Roughly 25 call-sites from the grep in §1.2.
Back-compat: none needed post-Phase 4. Everything is capability-driven.
Section 11 — Open questions / founder decisions needed
-
Should Treasurer see the prayer request count on Home (Option A), or should the counts tile be capability-gated per-data-type (Option B)?
- A: Prayer count visible to Treasurer (they only skip the contents). Simpler.
- B: Counts tile is sliced — Treasurer sees only Giving/visitor counts, not prayer count. More privacy-preserving.
- My default recommendation: A, to keep tile implementation simple. The count alone is not sensitive.
-
Pastor template caps: include
train:safety:editandwebsite:publish, or reserve those strictly to Admin?- A: Include (current spec). A senior pastor realistically edits these.
- B: Exclude. Only Admin can edit safety / publish.
- Recommendation: A. A church typically has one pastor who IS the spiritual authority; withholding these makes the template useless.
-
home:examples:remove— is this a real capability or just an admin action?- A: Keep as capability. Allows Office Admin to clear demo data.
- B: Drop; fold into
home:checklist:edit. Less noise in capability list. - Recommendation: B. It's a one-click one-time action, not worth a dedicated cap.
-
Direct-grant ergonomics: flat checkbox list vs. "add a capability" searchable picker?
- A: Flat checkbox list of all 34 grantable caps (expanded by category, collapsed by default).
- B: "Add capability" button that opens a searchable picker; selected caps appear as chips.
- Recommendation: A for parity with the group editor. Consistency > novelty.
-
Worship Team access to
inbox:prayer:read?- The founder's prior brief implied Worship needs pulse-editing only. But in practice worship leaders pray at the end of service and often need prayer awareness.
- A: Exclude (current spec).
- B: Include, on the argument that worship leaders pastorally engage.
- Recommendation: A (keep tight by default); Admin can add as a direct grant case-by-case.
-
Where does
care:broadcast(email the congregation) live?- Not present in the 42 caps because the redesign may move Care out of Inbox. If it stays, it needs
inbox:care:broadcastandinbox:care:subscribers:viewcaps. Flag for when the Care tab fate is decided.
- Not present in the 42 caps because the redesign may move Care out of Inbox. If it stays, it needs
-
"Pastor" template and primary-owner auto-assignment.
- When a church signs up, the primary owner is auto-added to Admin. Should they also auto-join the Pastor template? Or should the UI prompt "You're the admin — are you also the pastor? [yes/no]" at the checklist stage?
- Recommendation: prompt once on Home during onboarding. Default is yes.
Critical Files for Implementation
C:/dev/churchwiseai-web/src/lib/premium-shared.ts— source of ROLE_* constants to be superseded; legacy fallback map lives here until Phase 4.C:/dev/churchwiseai-web/src/lib/premium-queries.ts—resolveTokenmust start returninggroup_idsandcapabilitieson the resolved member.C:/dev/churchwiseai-web/src/app/admin/[token]/components/AdminDashboard.tsx— top-level gating consumer; must switch fromROLE_TABS[role]tocan()calls under the new IA.C:/dev/churchwiseai-web/src/app/api/premium/requests/route.ts— highest-traffic authoritative gate; prototype for therequireCapability()middleware rollout.C:/dev/churchwiseai-web/src/lib/database.types.ts— must be regenerated after migration addsgroup_ids,capabilities, and thechurch_custom_groupstable.