Skip to content

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

ModelTableNotes
CirclecirclesOwner-managed group with kind + name
CircleMembercircle_membersAccepted membership with bondLevel
CircleInvitationcircle_invitationsPending invite before membership
UserFollowuser_followsUnidirectional follow graph
UserMuteuser_mutesSilence another user
UserBlockuser_blocksBilateral silence
ConnectionIntentconnection_intentsDiscovery intent (1 per user)

Circle Fields

FieldTypeNotes
idUUID
ownerIdUUID
kindStringgeneral | family | work | inner | custom
nameString
accentString?Color token

Constraints: Max 10 members per circle enforced at service level. Max 10 custom circles per user (built-ins exempt). One invite per (circle, invitee, status).

Bond Levels

Each CircleMember carries a bondLevel: acquaintance (default) or inner-circle. Owners promote/demote members via PATCH /circles/me/:id/members/:memberId. Bond level drives inner-visibility posts (see Posts).


Key Methods

MethodDescription
createCircle(userId, dto)Create a custom circle (max 10 custom per user)
addMember(circleId, memberId, ownerId)Directly add accepted member
sendInvitation(circleId, inviterId, inviteeId, bondLevel)Two-step consent flow
acceptInvitation(invitationId, userId) / declineInvitation(...)Recipient responds
revokeInvitation(invitationId, userId)Owner cancels pending invite
pendingInvitesTo(viewerId, targetId)Sender-side: my outstanding invites to a user
myMemberships(userId)"Circles you're in" — others' circles I belong to
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/circles/. Require JwtAuthGuard.

MethodPathDescription
GET/circles/meList my circles (owned + member of)
POST/circles/meCreate a circle
PATCH/circles/me/:idUpdate circle name/accent
DELETE/circles/me/:idDelete (owner only)
GET/circles/me/:id/membersList members
POST/circles/me/:id/membersDirectly add member
PATCH/circles/me/:id/members/:memberIdUpdate member bond level
DELETE/circles/me/:id/members/:memberIdRemove member
POST/circles/me/:id/invitationsSend invitation
GET/circles/me/:id/invitationsList pending invites for a circle (owner)
DELETE/circles/me/:id/invitations/:invitationIdRevoke (inviter only)
GET/circles/invitations/meInvitations addressed to me
POST/circles/invitations/:invitationId/acceptAccept an invite I received
POST/circles/invitations/:invitationId/declineDecline an invite I received
GET/circles/me/check/:userIdDo I share a circle with this user?
GET/circles/me/pending/:userIdMy outstanding invites sent to this user
GET/circles/memberships/me"Circles you're in" — circles owned by others that I belong to
POST/circles/users/:id/followFollow a user
DELETE/circles/users/:id/followUnfollow
GET/circles/users/:id/follow-statsFollower/following counts
GET/circles/users/:id/followersA user's followers
GET/circles/users/:id/followingWho a user follows

Circle Invite Visibility (both sides)

The consent flow is legible from both ends:

  • Recipient side — the circle.invited notification (carrying payload.invitationId) renders inline Confirm / Decline buttons in the mobile inbox, and a gold banner on the profile screen surfaces pending invitations addressed to me ("N circle invitations · tap to confirm or decline").
  • Sender sideGET /circles/me/pending/:userId powers an "Invite pending" signal under the Add to circle CTA and a tap-to-cancel Pending pill in the circle picker; the circles screen lists "Invitations sent · awaiting reply" with a cancel action, and "Circles you're in" answers whose / which circle am I in via GET /circles/memberships/me.

Events Emitted

EventPayloadConsumed 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/accept
        → CircleMember row created
        → Invitation status → 'accepted'
        → Event: social.invitation.accepted
        → Owner receives notification

Invitee → POST /circles/invitations/:id/decline
        → Invitation status → 'declined'
        → No membership created

Privacy 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.

Regulus — invite-only social-knowledge platform