Social Module
Location: apps/api/src/modules/social/
Aggregate roots: Circle, CircleMember, CircleInvitation, UserFollow, ConnectionIntent
The Social context handles the personal network layer: circles for private audiences, bidirectional consent-based membership, and follow graph.
Prisma Models
| Model | Table | Notes |
|---|---|---|
| Circle | circles | Owner-managed group with kind + name |
| CircleMember | circle_members | Accepted membership with bondLevel |
| CircleInvitation | circle_invitations | Pending invite before membership |
| UserFollow | user_follows | Unidirectional follow graph |
| UserMute | user_mutes | Silence another user |
| UserBlock | user_blocks | Bilateral silence |
| ConnectionIntent | connection_intents | Discovery intent (1 per user) |
Circle Fields
| Field | Type | Notes |
|---|---|---|
| id | UUID | |
| ownerId | UUID | |
| kind | String | general | family | work | inner | custom |
| name | String | |
| accent | String? | Color token |
Constraints: Max 10 members per circle enforced at service level. One invite per (circle, invitee, status).
Key Methods
| Method | Description |
|---|---|
createCircle(userId, dto) | Create a circle (max 3 custom per user in Alpha) |
addMember(circleId, memberId, ownerId) | Directly add accepted member |
sendInvitation(circleId, inviterId, inviteeId, bondLevel) | Two-step consent flow |
respondToInvitation(invitationId, userId, accept) | Accept or decline |
revokeInvitation(invitationId, userId) | Owner cancels pending invite |
removeMember(circleId, memberId, ownerId) | Remove a member |
getCircles(userId) | List all circles (owned + member of) |
follow(followerId, followingId) | Unidirectional follow |
unfollow(followerId, followingId) | Remove follow |
upsertIntent(userId, dto) | Set/update discovery intent |
getMyIntent(userId) | Fetch current intent |
HTTP Endpoints
All routes under /api/v1/social/. Require JwtAuthGuard.
| Method | Path | Description |
|---|---|---|
| GET | /circles | List my circles |
| POST | /circles | Create a circle |
| PATCH | /circles/:id | Update circle name/accent |
| DELETE | /circles/:id | Delete (owner only) |
| GET | /circles/:id/members | List members |
| POST | /circles/:id/members | Directly add member |
| DELETE | /circles/:id/members/:memberId | Remove member |
| POST | /circles/:id/invitations | Send invitation |
| GET | /circles/invitations/me | My pending invitations |
| POST | /circles/invitations/:id/respond | Accept or decline |
| DELETE | /circles/invitations/:id | Revoke (inviter only) |
| POST | /users/:id/follow | Follow a user |
| DELETE | /users/:id/follow | Unfollow |
| GET | /me/following | Users I follow |
| GET | /me/followers | Users following me |
| PUT | /intent | Upsert connection intent |
| GET | /intent | Get my intent |
Events Emitted
| Event | Payload | Consumed by |
|---|---|---|
social.member.added | { circleId, memberId } | Notifications |
social.member.removed | { circleId, memberId } | Notifications |
social.invitation.sent | { invitationId, circleId, inviterId, inviteeId } | Notifications |
social.invitation.accepted | { invitationId, circleId, memberId } | Notifications |
social.user.followed | { followerId, followingId } | Notifications |
Circle Invitation Flow
Owner → POST /circles/:id/invitations
→ CircleInvitation row status='pending'
→ Event: social.invitation.sent
→ Invitee receives notification
Invitee → POST /circles/invitations/:id/respond { accept: true }
→ CircleMember row created
→ Invitation status → 'accepted'
→ Event: social.invitation.accepted
→ Owner receives notification
Invitee → respond { accept: false }
→ Invitation status → 'declined'
→ No membership createdPrivacy rule for DMs: Two users can only open a DM if they share at least one circle on either side or have an existing conversation.