Skip to content

Groups Module

Location: apps/api/src/modules/groups/

Aggregate roots: Group, GroupMember, GroupJoinRequest, GroupInvite

Topic-centred study/research collaboration spaces (ADR-005). A group has an owner, a join policy, an optional interest anchor, and a computed standing score — the group-level analogue of reputation.


Core Concepts

FieldValuesNotes
kindstudy | research | generaldefaults to study
joinPolicyopen | request | invitedefaults to request
roleowner | moderator | memberone owner per group
memberCountintmaintained transactionally on join/leave/remove

Open groups join instantly; request/invite groups go through GroupJoinRequest (member-initiated) or GroupInvite (manager-initiated). Archive is a soft delete (archivedAt).


Group Standing

A rep-weighted "group reputation", computed per group at list/detail time (constants in groups.constants.ts):

score = posts × 6 + contributors × 10 + max(0, memberWeightSum − 1) × 3
memberWeight(member) = max(1, log₂(totalRep + 1))
  • posts / contributors come from group-scoped posts (Post.groupId), aggregated in the DB with groupBy (one query for post counts, one by (groupId, authorId) for distinct contributors).
  • memberWeightSum reads reputation_snapshots directly (cross-context Prisma read, no service import) — a group of respected experts ranks above one padded with empty accounts. Zero-rep members count as weight 1.
  • Tiers: seedling (0+) → growing (40+) → established (150+) → pillar (400+).

HTTP Endpoints

All routes under /api/v1/groups/. Require JwtAuthGuard.

MethodPathDescription
POST/groupsCreate a group (creator becomes owner)
GET/groups?interestId=&q=&limit=Discover groups (+ standing, isMember)
GET/groups/mineGroups the viewer belongs to
GET/groups/:idDetail + viewer relationship + standing
PATCH/groups/:idOwner/moderator edits metadata
DELETE/groups/:idOwner archives (soft delete)
POST/groups/:id/joinInstant join (open groups only)
POST/groups/:id/requestRequest to join (request/invite)
DELETE/groups/:id/requestCancel my pending request
POST/groups/requests/:requestId/respondApprove/reject request (manager)
GET/groups/:id/requestsPending join requests (manager)
POST/groups/:id/leaveLeave the group
DELETE/groups/:id/members/:memberIdRemove a member (manager)
PATCH/groups/:id/members/:memberId/rolePromote/demote moderator ⇄ member (owner)
POST/groups/:id/inviteInvite a user (owner/moderator)
GET/groups/:id/invitesPending invites for the group (manager)
GET/groups/invites/mineInvites the viewer received
POST/groups/invites/:inviteId/respondAccept/decline (invitee)
DELETE/groups/invites/:inviteIdRevoke a pending invite (manager)
POST/groups/:id/transferTransfer ownership to a member (owner)

Ownership Transfer

Only the owner can transfer. Atomic transaction: previous owner is demoted to moderator (keeps manage rights, can later leave), target member becomes owner, Group.ownerId updates. The owner can never leave directly — transfer or archive first.


Events Emitted

EventPayload
groups.group.created{ groupId, ownerId }
groups.member.joined{ groupId, memberId }
groups.join.requested{ groupId, userId, requestId }
groups.join.approved{ groupId, memberId }
groups.invite.sent{ groupId, inviteId, inviterId, inviteeId }
groups.invite.accepted{ groupId, inviteId, inviterId, memberId }
groups.ownership.transferred{ groupId, previousOwnerId, newOwnerId }

Notifications subscribes to the invite/request/ownership events.


Invariants

  • Moderators can remove plain members only; only the owner removes moderators or sets roles.
  • memberCount is incremented only when a membership row is actually created (an approved request for someone who already accepted an invite must not drift the count).
  • Approve/accept paths use upsert + $transaction — idempotent under races between requests and invites.
  • One pending request and one pending invite per (group, user).

Admin

/admin/groups lists groups with archive/restore; /admin/groups/:id gives full detail management — remove members (POST /admin/groups/:id/members/:memberId/remove) and resolve join requests (POST /admin/groups/requests/:requestId/resolve). All actions are audit-logged.

See also: /modules/posts (group-scoped posts), /modules/reputation.

Regulus — invite-only social-knowledge platform