Groups & ministries
Small groups, ministries, classes — anywhere your church gathers in something smaller than the whole. A group page is a roster + an event list with a leader role (a member who shepherds that group, nothing else).
Groups run on the Groups community module (Community settings). Turning the module off removes the nav entry, the routes, and the group surfaces.
How do members find a group?
A member opens Groups in their portal. Each row shows the group's name, category (small group, ministry, class, …), meeting info, member count, and an Open or Closed badge.
- Open groups let a member tap Join and they're in immediately.
- Closed groups show Request to join — that sends a request to the group's leader. The member's badge becomes Requested until a leader (or church admin) approves them.
The list is organised into two sections (no filter chips):
- Your groups — the groups the member already belongs to, plus any with a pending join request.
- Discover groups — open groups they can join, and closed groups they can request to join (each shows a Join or Request action).
Tapping a row opens the group page: about, roster, upcoming events, and a Join / Leave button. Leaders also see their own management controls here.
What does a group leader do?
A group leader is a member of a group with role = 'leader'. It is a
resource-scoped role — they manage their group and nothing else:
- Approve pending requests when the group is closed.
- Promote or demote other members of their group.
- Schedule, edit, and remove events on the group's calendar — recurring (a standing weekly meeting) or one-off (a potluck). They show on the group page's Events tab and in the church's regular Events list, labelled with the group's name.
- Moderate discussion posts (Pin / Hide / Delete) via the Post actions menu.
Every one of these is reachable from both web and mobile — a leader on
their phone has the same powers as a leader on a laptop. On mobile, the
leader-only Pending requests section sits at the top of a group's
About tab when there's something to act on; the Events tab gains
a Schedule button and a per-row ⋮ menu (Edit / Remove) when the viewer
is a leader.
The leader role is enforced by an is_group_leader(group_id) database
function — never by the church-level role. A member can be a leader of one
group and a regular member of another at the same time.
How does an admin start a group?
Admins open Community → Manage groups, click New group, fill in the sheet (name, category, meeting info, description, open or closed), and save. The new group appears on the member side.
To assign a leader, the admin opens the group page (the Manage roster link from the admin list takes you straight there) and promotes any active member to leader. From that moment on, the new leader has the controls above.
To add members in bulk, open the group's Roster tab and click Add from directory at the top of the roster card. The dialog lists every member of your church (search by name or email), lets you tick several at once, and adds them all to the group with one click. The operation is idempotent — picking someone who's already on the roster is a silent no-op, so you can use the same dialog whenever you need to sweep in newcomers without worrying about double-adds.
To archive a group (it's ended, season's over), use the Archive button on the admin Groups list. The group is soft-deleted: it disappears from the member browse and group-list APIs, but the rows survive. Archived groups collect in an Archived disclosure on the admin Groups list, where each row has a Restore button to bring it back. Cascade is via the church FK — a church drop still removes everything.
How does a member start a group?
Members don't have to wait on staff. The Start a group button on the member Groups page (web and mobile) opens a short proposal form: group name, what it's about, when and where it would meet, and whether it's open to join or request to join. Submitting sends the proposal to the church's pastors for review and shows a quiet Proposed card in the member's "Your groups" section while it waits. The member can withdraw a pending proposal at any time.
When a pastor decides, the member gets a notification either way:
- Approved — the group goes live immediately with the proposer as its leader, exactly as submitted (admins can adjust anything afterwards via the group's Settings tab). The notification deep-links into the new group.
- Declined — the member sees a "Not approved" card with the pastor's note (if one was added), plus Try again and Dismiss actions.
Reviewing group requests (admin)
Member proposals queue up in a Group requests section on Community → Groups, and the sidebar's Groups entry shows a badge while any are waiting. Each card shows who's asking, the proposed name, description, meeting rhythm, and the open/closed choice, with two actions:
- Approve — creates the group and seats the proposer as its active leader in one step. The proposer is notified.
- Decline — optionally add a short note (the member sees it). Declines are reversible: a collapsed Declined list below the queue lets you restore any declined proposal back to pending — restoring is silent (the member is only notified when a real decision lands).
Exporting rosters
The admin Groups list has an Export rosters button at the top. Tapping
it pulls every group in your church plus the active and pending members on
each one and downloads a single CSV file (group-rosters-YYYY-MM-DD.csv).
The file is one row per (group × member) pair with the following columns:
| Group | Category | Member | Phone | Role | Status | Joined |
|---|
The CSV is RFC-4180 quoted (commas, quotes, and newlines inside cells are safe), so it imports cleanly into Excel, Numbers, Google Sheets, and the mail-merge tools most churches use. Email isn't on the export — emails live on the auth user record, not the member profile, and we only surface phone numbers from the profile here.
Admin KPIs on the group detail
Each group's admin detail page (the 5-tab surface you reach by clicking a group name in the list) shows a four-card KPI strip above the tabs:
- Active members — the current roster count plus how many joined in the last 30 days.
- Attendance · 4w — the average rate across the four most recent past
group-event occurrences (
goingRSVPs ÷ occurrences considered). A recurring event counts its series regulars; a one-off counts that date's RSVPs. - Pending requests — open join requests waiting on a leader.
- Posts · 30d — the number of discussion posts in the last 30 days.
The roster table also pulls per-member attendance: a percentage column
("Attendance"), coloured by threshold (≥85% green, 70–84% muted, <70%
warning), plus a Last seen column with the most recent occurrence the
member was going for. Both are derived from the same 4-occurrence lookback
across the group's events. You can sort the roster by either column.
Events + RSVPs on mobile
The mobile Events tab on a group shows every upcoming group event with date/time, location, a Recurring badge for standing meetings, and an inline RSVP row. The RSVP surface depends on the event:
- Recurring events (a standing weekly meeting) use a series-level "I'm a regular" RSVP — one tap marks you as a regular attendee across every occurrence, so you never re-confirm week after week.
- One-off events use a per-occurrence Going / Maybe RSVP keyed to that single date.
Each row shows the running going / maybe counts; Clear removes your
RSVP. The same controls live on web under the group's Events tab.
The mobile group's primary action row also reflects the viewer's RSVP state for the next upcoming event: tap Going in the RSVP sheet and the row's pill swaps to a "Going for THU" affirmative until you clear or change it. It derives from the same group-events query the Events tab uses, so the pill stays consistent across both surfaces.
The Discussion inline tab also shows a small count chip when there are new posts in the last 7 days in that group, so you can see at a glance whether to dig into the conversation.
Who sees what?
| Item | Members of the church | Group members | Group leader | Pastoral staff (admins) |
|---|---|---|---|---|
| A live group's listing | ✅ | ✅ | ✅ | ✅ |
| A hidden (moderation) group | ❌ | ❌ | ✅ (their own) | ✅ |
| An archived (soft-deleted) group | ❌ | ❌ | ✅ (their own) | ✅ |
| Active roster | ✅ | ✅ | ✅ | ✅ |
| Pending requests | ❌ | ❌ | ✅ | ✅ |
| Joining an open group | ✅ (self-serve) | — | — | — |
| Joining a closed group | requests only | — | approves | approves |
| Promote / demote / approve | ❌ | ❌ | ✅ | ✅ |
| Create group events | ❌ | ❌ | ✅ (for their group) | ✅ |
RLS enforces these rules on the database. The leader privilege is
specifically scoped to the group's group_id, not a blanket admin tier.
Group events
A group's calendar is its events. Setting events.group_id scopes an
event to a group: members see it on the group page's Events tab and in the
member Events list labelled with the group name, while a group_id-less event
is a church-wide event (the unchanged default). Group events are
member-only — RLS hides them from the public church site.
A group leader schedules events from a leader-facing scheduler on the group's Events tab (web + mobile) — no admin access required. The form is locked to their group (no group picker, member-only by construction) and offers a recurrence editor: leave it off for a one-off, or set a weekly / monthly / custom cadence for a standing meeting. RLS permits the write when the caller is a church admin (any event) or the group's active leader (an event scoped to that group). Church admins can still create group events from the admin event form by picking the group.
Regular attendees vs per-occurrence RSVP
Because a recurring group event is a standing gathering, it uses a
series-level RSVP: a member marks themselves a regular once and it
holds for every occurrence — no weekly re-confirm. One-off group events use
the ordinary per-occurrence Going / Maybe RSVP. The group page rolls these up
into the going / maybe counts and the attendance KPIs above.
Module gate
module_groups = false in Community settings takes the
entire Groups surface offline:
- The Groups nav entry hides on web and the segment goes to an empty state on mobile.
- The
/groupsand/groups/<id>routes still resolve but the listing is empty (RLS does not return anything because the module check is layered in every read). - Group events that already exist remain visible in the church-wide events list — disabling the module hides the group surface, not the historical events themselves.
Discussion walls
Once a group exists, each group page has a Discussion section — a posts feed with comments. It is active-member-only by RLS: a non-member of the group sees no posts, even if the group itself is browsable. A member can mute a group from its menu to stop new-post notifications while staying in the group.
Posting + commenting
A member taps New post on the Discussion list to open the compose dialog and writes to the wall (1–8000 chars; optional title up to 120 chars). The dialog is local to the page — compose is not a deep-linkable URL, so reloading or sharing the group URL never lands you on an open composer.
The composer accepts one optional attachment per post — either an image (JPEG, PNG, etc.) or any other file, up to 20 MB. On web, two buttons ("Image" / "File") open the native file picker; on mobile, the same two options run through Expo's image-library + document picker. The preview shows the filename, size, and (for images) a thumbnail. Tap Remove to discard the pick before posting.
Editing a post (from the Post actions menu) shows the existing attachment pre-filled — you can keep it, Replace with a new image or file, or Remove it. Removing on save deletes the attachment from the post; the underlying storage blob is left in place and swept by a background job (orphan-blob cleanup), the same hygiene level we use for attachments staged then discarded.
The Discussion feed (member web)
On member web the Discussion tab is a single-column feed — each post is a full-width card with the author, the full body, the attachment (if any), and two pill buttons at the bottom: the heart and the comment toggle. Pinned posts float to the top with a brand-tinted bar; the rest order newest-first.
Tap the comment pill on a card to expand the thread inline (no separate page, no two-pane). The latest 2 comments show by default; if there are more, a small "See N previous comments" link reveals the rest. The reply composer sits at the bottom of the expanded thread as a "Write a comment…" placeholder that swaps to a textarea on click — the new comment appears in the thread instantly via an optimistic update.
Deep links to a single post open the group feed with ?post=<id> — the
feed scrolls the matching card into view with its comments auto-expanded.
Searching the discussion
A small search icon sits at the top-right of the member feed; tap it to slide in a search input. Type two or more characters and the feed swaps to server-side search of the whole archive (case-insensitive, post title + body). Clear the input — or tap the icon to close it — to drop back to the regular newest-first feed.
A few things to know:
- The search runs after a short pause in typing, so it doesn't fire on every keystroke.
- While searching, pinned posts read inline with the rest — they don't float to the top of the results list (that treatment is for the regular feed only).
- Special characters (
%,_,\) are matched literally — typing them searches for them rather than acting as wildcards. - On admin web the Discussion tab keeps its two-pane moderation queue (list left, full post + actions right) — that surface is for triage, not feed reading.
Post actions menu
Every discussion post has a … menu in the upper right (top of the post
detail on web, kebab on the post card on mobile). The menu is conditional —
members see only the actions they're allowed to take:
- Edit post — author and leaders/admins. Opens the same compose dialog
pre-filled with the post's title and body, and saves through
updateGroupPost. - Pin / Unpin — leaders + admins only. Pinned posts float to the top of the Discussion list with a brand-tinted strip; up to 3 pinned per group.
- Hide / Un-hide — leaders + admins only. A hidden post stays visible to the author and to leaders + admins, and disappears for every other group member until un-hidden. Use this to pause a thread without destroying it.
- Report post — everyone except the author. Opens a small reason form (up to 1000 chars); the report lands in the moderation queue with the reporter's note for context.
- Delete post — author and leaders/admins. Soft-deletes (
deleted_at = now()); disappears for everyone. The action shows a confirm dialog before it fires.
All controls are surfaced through this single menu — there are no extra inline buttons next to posts. The same set of actions is available on web (member + admin) and mobile, so leaders manage their groups from their phone.
Leader moderation on comments
Posts go through the Post actions menu above —
leaders + admins also have the same Hide and Remove affordances on
individual comments inside a thread. A group leader does NOT see leader
controls on a different group they're a regular member of — the
is_group_leader(group_id) check is strictly resource-scoped.
Who sees what — discussion
| Item | Active group member | Non-member of the group | Group leader | Pastoral staff (admins) |
|---|---|---|---|---|
| Live (non-hidden) post | ✅ | ❌ (RLS) | ✅ | ✅ |
| Hidden post | ✅ (own only) | ❌ | ✅ | ✅ |
| Live comment | ✅ | ❌ | ✅ | ✅ |
| Hidden comment | ✅ (own only) | ❌ | ✅ | ✅ |
| Post / comment — send | ✅ | ❌ | ✅ | ✅ |
| Hide a post or comment | ❌ | ❌ | ✅ | ✅ |
| Remove a post or comment | ✅ (own only) | ❌ | ✅ (any) | ✅ (any) |
Future / fast-follows
- Reactions on posts — fast-follow; the schema doesn't carry one yet.
- Rich media in posts — out of scope this slice.
- Group attendance — folded into Phase 4.10's check-in model, or shipped as a leader-only fast-follow.
- Group categories as an enum — currently free-form text so churches can use their own taxonomy; might curate later if a clear default set emerges.
- Group-level giving / funds — out of scope; group fundraising stays a church-wide fund for now.
More topics