Group & Chapter Admin Handbook#
This handbook is the operational reference for the people who run a group day-to-day: the group’s admin and moderators. It covers the create flow, membership management, post moderation, chapter event creation, and broadcast announcements.
Who this is for: anyone with MembershipRole = admin or moderator on a group (interest or chapter). The platform-admin lens is in documentation/role-guides/admin/phase2-admin-overview.md.
Source of truth: backend/internal/groups/service.go, backend/internal/groups/store.go, backend/internal/groups/types.go. RPC contract: protos/bits/groups/v1/groups.proto.
1. Roles inside a group#
Three roles, persisted as strings in groups_membership.role (backend/internal/groups/types.go:47):
admin— full control. Can transfer admin to another member, change visibility, archive the group, promote/demote moderators, ban members.moderator— can approve/decline join requests, delete posts, ban members. Cannot change visibility or transfer admin.member— post, comment, react.
The platform admin role overrides everything: a platform admin can take any moderator action on any group regardless of group membership.
Last-admin protection#
The store rejects any role change or removal that would leave a group with zero admins (ErrLastAdminProtect → 422). Before stepping down, promote a successor.
2. Creating a group#
Any verified user can create an interest group. Chapter creation requires the groups.create capability — typically reserved for platform admins.
/app/groups/new walks through:
- Name (≤ 200 chars).
- Slug — must match
^[a-z0-9][a-z0-9_-]*[a-z0-9]$|^[a-z0-9]$, ≤ 120 chars, globally unique. Slug collisions return 409. - Kind —
interest(default) orchapter(admin-only). - Visibility —
public | institute | alumni | private. - Description (≤ 4 000 chars), avatar URL, banner URL.
The first creator is set to admin automatically.
3. Managing membership#
Public / institute / alumni groups#
Members join directly. The flow does not require approval — the API call mints a GroupMembership row in state active.
Private groups (and any group that wants gated entry)#
Members must RequestMembership. The request lands in GroupRequest with state pending. Admins/moderators see the queue on /app/groups/<slug> → Members tab → Requests sub-tab.
For each request you can:
- Approve — flips request state to
approved, mints aGroupMembershiprow, publishesgroups.member.joinedevent. Audit row written. - Decline — flips to
declined. Audit row written. The user can request again in the future (declines are not bans).
Cross-group request IDs are rejected as NotFound to prevent enumeration.
Banning a member#
Use Remove member with the Ban option on /app/groups/<slug> → Members tab. This sets GroupMembership.state = banned. Banned members cannot rejoin; their existing posts remain (consider deleting separately if needed). Banning is reversible — re-adding a banned user via the same API explicitly resets state to active.
Promoting / demoting#
UpdateMemberRole flips a member’s role between admin, moderator, and member. Last-admin protection applies — promoting yourself out of admin while you are the only admin is rejected.
4. Moderating posts#
Group posts are subject to the same moderation pipeline as the main feed (backend/internal/groups/moderation.go → ContentModerator port → Stage 12 classifier).
What you (as group admin/moderator) can do:
- Delete a post — soft-delete via
DeleteGroupPost. The post is hidden from listings but preserved in audit. Audit row written. - Pin an announcement-kind post — composing with
kind = announcement(admin/moderator only) auto-pins to the top of the Posts tab. - Report a post — the existing
ReportContentRPC acceptstarget_type=group_post. Reports flow to/admin/moderationfor the platform team.
Banned members cannot post — the API returns 403 PermissionDenied (ErrBanned).
5. Chapter events#
Note: applicable to chapter-kind groups only (
Group.kind = chapter). Interest groups do not have an Events tab.
Chapter events are stored on the calendar (calendar_event.kind = 'chapter') and bound to the chapter via calendar_event.scope_ref = <chapter group UUID> (backend/internal/calendar/store.go:39, backend/internal/calendar/service.go:180).
To create a chapter event:
- As chapter admin/moderator, navigate to the calendar admin (
/admin/calendar) — the chapter scope option appears for chapters you administer. - Author the event: title, body, start/end, location, optional cover image.
- Save. The event appears on:
- The chapter group’s Events tab (
/app/groups/<slug>→ Events). - Each chapter member’s
/app/calendaronce they have toggled chapter event subscription on. - The Stage 19 search index under the
eventssurface.
- The chapter group’s Events tab (
Visibility is otherwise unconditional inside the chapter — every chapter member sees every published chapter event.
6. Announcement broadcasts#
Two flavours:
a) Group-internal announcement post#
Compose a post with kind = announcement on the group’s Posts tab. This is a regular GroupPost row that gets pinned to the top of the Posts tab. Notifications fan out via the groups.post.created topic with an extra kind=announcement flag so member preferences can treat it as higher-priority.
b) Platform-wide announcement (admin only)#
For announcements that should appear on every member’s Campus tab, you need the platform admin role and use /admin/cms/announcements. Set category = chapter and audience = chapter:<your-chapter-uuid> to scope it to your chapter. See documentation/campus-tab-and-chapter-events.md.
If you don’t have admin, ask your Bits admin to publish on your behalf.
7. Notifications you’ll generate#
Every action you take in the admin role fans out a notification to the relevant party. Topics:
groups.member.joined— to existing members when someone joins (or is approved).groups.post.created— to subscribed members when a new post is published. Default-on for chapter members; default-off for interest groups (members opt in per group).groups.request.received— to admins/moderators when someone requests to join.
All admin-side topics honour the recipient’s /app/profile/notifications preferences.
8. Audit trail#
Every mutating action you take writes an audit row via s.writeAudit(...) (e.g. inside ApproveRequest in backend/internal/groups/service.go). The action string is bits.groups.v1.GroupsService/<MethodName>. Platform admins can replay any action via /admin/audit-logs.
9. Common operational issues#
- “A request has been pending for weeks” — there is no automatic SLA; pending requests do not expire. Either approve, decline, or hand the moderation duty to another member by promoting them.
- “I can’t archive a group” — Phase 2 does not expose an archive button to group admins; only platform admins can archive (via
archived_at). Request via Bits support. - “A member’s posts are spammy but they are not banned” — delete the posts individually; bans persist forever in V2 (no time-limited bans). Use bans only for abuse, not for low-quality content.
- “I want to make my interest group bigger by inviting alumni” — invites are not implemented in V2. Members must self-discover and join. Use cross-posting on the main feed plus a chapter announcement to drive traffic.