Granting permissions β
Permissions are the small verbs the platform actually checks. Can this user record a donation? Can they delete a member? Can they approve a report? Each verb has a key like giving.record or members.delete, and the answer is yes if any of the user's roles has that key toggled on in role_permissions_v2.
There are 68 keys. They ship with the platform β you can't add new ones, you can't rename existing ones, and they don't change between churches. What you control is the wiring: which roles grant which keys. That wiring lives in the permission matrix.

Opening the matrix β
From Users & Roles, click the Roles & Permissions tab. The matrix appears under the role list, with one row per permission and one column per role. The filter box in the top-left narrows the visible rows by resource, action, key, or description β type giving to see only the seven giving keys, or delete to see every destructive permission across all modules at once.
How the matrix is organised β
Permissions are grouped by resource β members, giving, reports, events, and so on. Within each group they're sorted by action (view before edit before delete). The grouping is purely visual; under the hood every permission is a flat key. The full taxonomy:
| Resource | Actions you'll see |
|---|---|
dashboard | view |
members | view, create, edit, delete, merge |
attendance | view, mark |
giving | view, record, manage, donate |
reports | view, view_all, submit, manage, delete, export, approve, unlock |
org_units | view, create, edit, delete, manage |
schools | view, manage |
demographics | view |
map | view, manage |
ministries | view, manage |
groups | view, manage |
events | view, create, edit |
messaging | view, send |
notifications | view, send |
users | manage |
custom_fields | view, manage |
config | manage |
billing | manage |
website | manage, publish, blog, sermons |
podcast | manage |
workflows | view, manage, execute, enroll |
forms | submit, manage, view_submissions |
zapier | view, manage |
That's 68 in total. The canonical list lives in src/shared/lib/access-policy.ts on the frontend and supabase/functions/_shared/access-policy.ts on the backend β both files are kept in sync.
Toggling a permission β
Find the row (the permission) and the column (the role), then flip the switch. The change writes to role_permissions_v2 immediately β no save button, no confirmation step. Users with that role pick up the new behaviour on their next page navigation or React Query refetch (typically within seconds).
The toggle is symmetric: switching off removes the row from role_permissions_v2; switching on inserts it. Both actions are written to the audit log with the actor's email and the timestamp, so you can always reconstruct who changed what.
Common combinations β
Most useful roles bundle a few keys together. A reference for the patterns we see most often:
Read + write a resource β
members.view + members.edit β the most common pair. Lets someone update phone numbers and addresses without being able to add or delete anyone. Good for a data-quality team member.
giving.view + giving.record β record donations and see history. The classic Treasurer shape. Add giving.manage if they also create funds; leave it off if you want a separate fund manager.
attendance.view + attendance.mark β exactly what the check-in volunteer needs. Pair with scoped permissions to limit them to one service or campus.
Create + edit but no delete β
members.create + members.edit (without members.delete). Common for ministry leaders β they can add visitors and update existing records but can't wipe anyone out. Deletes typically require an admin.
events.create + events.edit (without delete) β same shape for calendar work. Lets a worship leader add the rehearsal series without being able to remove last year's archive.
View-only across the board β
Every view key with nothing else is the Viewer role. Useful for board members, auditors, and probationary staff. There's a dedicated dashboard.view so a Viewer can land somewhere that looks meaningful.
Manage vs lower verbs β
For some resources, manage is a superset that implies the lower verbs. giving.manage includes the ability to record and view in the UI but the explicit giving.record and giving.view permissions still need to be on for the edge functions and RLS to allow the operations. Always grant the lower verbs alongside manage β the matrix doesn't auto-grant them.
config.manage is the one big exception: it's a single switch that gates every settings page (channels, modules, PWA, visitor config, data logs). There are no separate read/write keys for those.
Where the check actually runs β
A permission isn't checked once β it's checked in three places, in this order:
- The UI uses
usePermissions().has("giving.record")to hide buttons. This is purely cosmetic; a forged client request can still call the API. - The edge function calls
assertPermission(ctx, "giving.record")from_shared/caller-context.ts. If the user doesn't have the key, the function returns 403 before touching the database. - Row-Level Security in Postgres re-checks via the
has_permission()SQL helper for the final layer of defence.
This three-layer check means a misconfigured role can never accidentally leak data β even if the UI gets out of sync with the database, the edge function and RLS will reject the request.
Filtering the matrix β
For churches with many custom roles, the matrix can be wide. The filter input narrows it by typing:
givingβ only the giving section.deleteβ every destructive action across the platform.members.mergeβ find a specific key.reportβ also matches reports.
The filter checks resource, action, key, and description β whichever hits first wins.
Next β
- Scoped permissions β pair a permission grant with an org-unit limit.
- Audit log of role changes β see who toggled what.
- Default roles β what the seeded roles include.