Bounded Contexts
Each folder under apps/api/src/modules/<name>/ is a bounded context. Cross-context service imports are forbidden by ESLint rule no-cross-context-import. Integration happens through the NestJS EventEmitter2 event bus, and all cross-context references are UUID-only (no Postgres foreign keys cross a boundary).
As of the 2026-06 audit there are 25 bounded contexts registered in apps/api/src/app.module.ts (plus shared infrastructure modules that are not contexts). The Wave 3–6 content contexts (events, deliberation, education, projects, marketplace, videos, audio, federation) joined the original Alpha A set.
Audit 2026-06-08. Security: the per-interest leaderboard now sits behind
JwtAuthGuard; guild-treasury debits take aSELECT … FOR UPDATErow lock;/identity/refreshcarries a tight@Throttle. Performance: aReputationSnapshot(interestId, value)covering index backs the leaderboard, group standings use a singlegroupBy, andmessaging.listMinetakes a boundedtake. See ADR-009 for the admin User-FK exception.
Context Map
graph LR
Identity -->|invite_code.used| Reputation
Identity -->|invite_code.used| InviteTree
Interests -->|user_selected| Reputation
Posts -->|post.created| Reputation
Posts -->|post.replied| Reputation
Posts -->|post.reacted| Reputation
Posts -->|post.bookmarked| Reputation
Posts -->|post.reposted| Reputation
Posts -->|emoji_pay.sent| Economy
Posts -->|post.created| Contribution
Economy -->|wallet.bootstrap| Identity
Reputation -->|reputation.changed| Notifications
Social -->|circle.invited| Notifications
Messaging -->|message.posted| NotificationsContexts Reference
identity
Aggregate roots: User, InviteCode, Session, Persona
Responsibilities:
- Invite-only registration:
reserve → registerflow with 30-minute TTL - Invite code generation: format
REG-XXXXXX, allocation formulaclamp(5 + rep/50 + age/14d, 5, 30), default batch 5 - JWT-based auth: login, refresh, logout
- Personas: max 5 per user, one default (
Main), accent color tokens - Account lifecycle: password reset, GDPR deletion, ban/unban
- Profile: username, bio, avatar, city, country
Events emitted:
identity.invite_code.used— when registration consumes a codeidentity.user_registered— new user created
Events consumed: none (bootstrap EmojiWallet is triggered by economy module on identity.user_registered)
interests
Aggregate roots: Interest (catalog), UserInterest
Responsibilities:
- 3-level catalog (L1 category → L2 sub-interest → L3 deep-interest)
- Catalog tree cached for 10 minutes in-process
- Per-user depth 1–5 (Curious / Reading / Practicing / Deep / Lifework)
- Self-rated expertise 1–5 (distinct from depth)
- Rollup reputation to parents using
RATIOS = [1.0, 0.5, 0.25, 0.12, 0.06]
Events emitted:
interests.user_selectedinterests.depth_changed
Events consumed: none
reputation
Aggregate roots: ReputationEvent (ledger), ReputationSnapshot, ReputationVote, ReputationAppeal
Responsibilities:
- Per-interest reputation only — no global aggregate exposed publicly
- Append-only ledger (
reputation_events); snapshots materialised inreputation_snapshots - Peer voting:
+1 / 0 / -1per(voter, target, interest)triple - Sybil dampening: votes from users with very low total rep are down-weighted
- Level labels: Curious (0–9) / Reader (10–29) / Practitioner (30–49) / Adept (50–69) / Master (70–89) / Lifework (90+)
- Appeal + redemption flow:
pending → accepted/rejected, optionalrestoreDelta
Events consumed:
interests.user_selected→ +5 per selected interest (one-shot bootstrap)identity.invite_code.used→ +3 to inviter in their top interestposts.created→ +3 to author in post's interestposts.replied→ +2 to replierposts.reacted(first unique per user) → +1 to post authorposts.bookmarked(first unique) → +1 to post authorposts.reposted(first unique) → +1 to post author
social
Aggregate roots: Circle, CircleMember, CircleInvitation, UserFollow, UserBlock, UserMute
Responsibilities:
- Circle CRUD (max 10 per user)
- Built-in circle kinds:
general | family | work | inner - Bidirectional consent: owner invites → invitee accepts/declines
- Bond levels:
inner-circle | acquaintance - Unidirectional follow graph
- Block / mute between users
Events emitted:
circle.invitedcircle.member_joined
posts
Aggregate roots: Post, PostReply, PostReaction, PostBookmark, PostInterest, PostCoAuthor, PostSeries, PostBoost
Responsibilities:
- All content formats:
note | essay | photo | video | quote | thread | repost - Visibility:
public | circles | inner - Content depth 0–5 (surface → thesis)
- Multi-interest tagging (up to 5 via
PostInterest) - Co-authorship: invite → accept/decline,
splitShare0–100 %, primary author keeps ≥ 10 % - Threaded replies with kind:
reply | reaction | criticism | contribution - Scheduled publish (
publishAt), soft delete, edit, pin (max 3) #journalismtag: gate rep ≥ 20 in any interest- Series: create, order, follow for notifications
- Materialised counters: reactions, replies, shares, bookmarks, views
Events emitted:
posts.createdposts.repliedposts.reactedposts.bookmarkedposts.reposted
ai-assistant
Aggregate roots: AssistantSession, AssistantTurn, AssistantSuggestion
Responsibilities:
- 5-question reflection flow; each session = 5 turns
- Anthropic Claude Sonnet 4.6 via Vercel AI SDK for question generation and interest mapping
- Scripted fallback when
ANTHROPIC_API_KEYis absent POST /ai/map-interests— LLM maps free-text to catalog interestsPOST /ai/apply-suggestions— writes accepted suggestions toUserInterestPOST /ai/enhance-post— AI content enhancement in composerAssistantSuggestion.confidence: 0–1,depth: 1–5,reason: max 280 chars
economy
Aggregate roots: EmojiWallet, EmojiPayment (append-only), PostBoost
Responsibilities:
- EmojiPay: send mana on a post or reply with an emotion
- 8 emotions:
love | awe | joy | curiosity | gratitude | insight | calm | courage - 6 tiers: 0=1, 1=3, 2=10, 3=25, 4=50, 5=100 mana
- Rep thresholds per tier (in post's primary interest): 0/0/10/20/35/50
- Wallet: 50 starter mana, 5/day regen, 50 regen cap
- Co-author mana split on accepted
PostCoAuthorrows - Boost: tier 1 = ×1.5 for 24 h (10 mana), tier 2 = ×2 for 48 h (25 mana)
- Nightly regen cron; boost expiry cron
- All financial rows are append-only. Reversal = new row with
reversed: true.
notifications
Aggregate roots: Notification, NotificationPreference, PushToken
Responsibilities:
- In-app notification inbox
- Expo Push Notifications (
ExponentPushToken[...]) - Per-kind toggles stored as JSON in
NotificationPreference.kinds - Per-channel toggles:
push | email | inApp - Notifications created by subscribing to events from other contexts
messaging
Aggregate roots: Conversation, ConversationMember, Message, MessageReaction
Responsibilities:
- DM and group conversations (up to 50 members)
- DM restricted to users who share at least one circle
- Per-conversation mute (
ConversationMember.mutedAt) - Message emoji reactions (grapheme validated server-side)
- Real-time delivery via Centrifugo
wiki
Aggregate roots: WikiEntry, WikiProposal, WikiVote
Responsibilities:
- Per-interest knowledge base (one canonical entry per interest)
- Propose → community vote → auto-approve at
netVotes ≥ 3 - Rep gate for proposing/voting:
WIKI_MIN_REP = 5in interest WikiVote.value=+1 | -1;WikiProposal.netVotesis materialised counter- Status:
pending | approved | rejected
contribution
Aggregate roots: ContributionEvent (append-only), ContributionSnapshot
Responsibilities:
- Tracks overall platform contribution per user
- Event kinds:
post_created | reply_created | reaction_given | bookmark_given | emoji_pay_sent - Invite-tree inheritance: 50 % to inviter / 25 % to grand-inviter / 10 % to great-grand-inviter
- Snapshot recomputed nightly; also updated incrementally
discovery
Aggregate roots: ConnectionIntent
Responsibilities:
- Intent-based discovery:
networking | friends | romantic | mentor | mentee | collab GET /discovery/candidates— users with matching intentGET /discovery/mentors— users with expertise gap ≥ N in a shared interestisFollowingwired to follow graphisActive: falseopts user out of discovery entirely
invite-tree
Aggregate roots: InviteNode — see /modules/invite-tree
Responsibilities:
- Materialised path tree (dot-separated UUIDs) of "who invited whom"
- Subtree queries via
path LIKE 'root_uuid.%' - Ancestors / descendants / lineage endpoints
- Used by Contribution context for inheritance ratios
Events consumed: identity.user.registered → inserts the node under the inviter.
geo
Aggregate roots: none (read model over a country/city reference table).
Responsibilities:
GET /geo/countries,GET /geo/cities— typeahead reference data for profile + discovery- Country list is cached server-side (admin/geo caching from the audit series)
groups
Aggregate roots: Group, GroupMember, GroupJoinRequest, GroupInvite — full detail at /modules/groups
Responsibilities (ADR-005):
- Topic-centred study/research collaboration; group-scoped posts via optional
Post.groupId - Join flow: request → manager approves, or manager-initiated invite → invitee accepts
- Ownership transfer; per-member "standing" computed via a single Prisma
groupBy(audit perf fix)
Events emitted: groups.group.created, groups.member.joined, groups.join.requested, groups.join.approved, groups.invite.sent, groups.invite.accepted, groups.ownership.transferred
guilds
Aggregate roots: Guild, GuildMember, GuildApplication, GuildProposal, GuildVote (treasury lives in economy) — see /modules/guilds
Responsibilities (ADR-006):
- Merit-gated, self-governing interest-domain communities (
@RequiresRepto propose) - Application → approval; reputation-weighted governance voting; proposal execution
- Treasury debits take a
SELECT … FOR UPDATElock (audit security fix)
Events emitted: guilds.guild.founded, guilds.member.joined, guilds.application.submitted, guilds.application.approved, guilds.proposal.opened, guilds.proposal.executed
events
Aggregate roots: Event, EventRsvp — see /modules/content-contexts
Responsibilities (v2.0 §3.5.4):
- Community gatherings under
/events; per-interest tagging; RSVP tracking removedAtsoft-remove for admin moderation
Events emitted: events.rsvp.created → notifications.
deliberation
Aggregate roots: Debate, DebateArgument, DebateVote, Problem, Solution, SolutionVote — see /modules/content-contexts
Responsibilities (§3.2.5–3.2.6):
#Debates— structured pro/con arguments with per-user voting#Problem— open problems with proposed solutions and solution votes- Controllers
/debatesand/problems; UUID-only refs; soft-remove
education
Aggregate roots: Course, Lesson, Flashcard, Enrollment, LessonProgress — see /modules/content-contexts
Responsibilities (§3.2.1 + §4):
- Courses → lessons → flashcards; enrolment and per-lesson progress
- Status lifecycle for course publishing
Events emitted: education.course.published, education.course.completed (→ reputation), education.lesson.completed (→ reputation)
projects
Aggregate roots: Project, ProjectMember — see /modules/content-contexts
Responsibilities (Wave 6 §3.3.5):
- Collaborative product/service creation; member roster; per-interest tagging; soft-remove
economy / marketplace
Aggregate roots: EmojiWallet, EmojiPayment (append-only), PostBoost; marketplace: Offering, MarketOrder, MarketLedgerEntry (append-only); guild treasury: GuildTreasury, GuildLedgerEntry (append-only) — see /modules/economy
Responsibilities:
- EmojiPay: send mana on a post/reply with one of 8 emotions across 6 tiers; rep-gated tiers
- Wallet: 50 starter mana, 5/day regen (cap 50); nightly regen + boost-expiry crons
- Marketplace: mana-priced service offerings with escrow recorded append-only in the ledger
- Guild treasury: pooled mana, funded by members, disbursed by the founder; row-locked debits
- All financial rows are append-only. Reversal = new row, never UPDATE.
Events emitted: economy.payment.sent (→ notifications, contribution), economy.post.boosted, marketplace.order.placed, marketplace.order.released
videos / audio
Aggregate roots: VideoVisit, VideoReaction, VideoRepost, VideoComment; AudioPost — full detail at /modules/videos
Responsibilities (Wave 6 §3.5.3):
#VideoVisit— 60-second video cards per interest, with reactions, reposts, comments#AudioPost— voice/audio notes per interest- Native runtime gated:
expo-video(runtime 1.1) andexpo-audio(runtime 1.3)
media
Aggregate roots: MediaAsset — see /modules/media
Responsibilities:
- Presigned-PUT upload pipeline to Cloudflare R2:
request → confirm(pending → committed) - Object addressing keyed by
(bucket, key); lazy width/height/duration stamping - Cron sweeps
pendingassets older than 24 h
federation
Aggregate roots: RemoteFollower, RemoteFollowing, RemotePost (+ apPublicKey/apPrivateKey on User) — see /modules/federation
Responsibilities (ADR-008, live):
- ActivityPub bridge: per-user RSA keypairs, Webfinger actor identifiers
acct:[email protected] - Outbound:
posts.created→ fan-out to remote followers - Inbound: follow remote actors, cache their posts in
RemotePost
Events consumed: posts.created → builds + signs the outbound Create activity.
admin (cross-cutting orchestration)
No domain aggregate roots. Admin endpoints orchestrate other contexts and own only the AuditLogEntry ledger, FeatureFlag, and Report review state.
Responsibilities:
- Users: list, role change, ban/unban (
user.banned/user.unbanned), GDPR delete - Posts: list, hide/unhide (
post.hidden/post.unhidden) - Reports: review queue (open / reviewed / dismissed)
- Reputation: votes viewer, appeal resolution, broadcast adjustments
- Economy, wiki, groups/guilds, Wave 3–6 content moderation (soft-remove)
- Feature flags; append-only audit log of all privileged mutations; broadcast notifications
Admin orchestration exception (ADR-009)
The admin context is the one whitelisted exception to the no-cross-context rule: it may read other contexts' read models directly (still no writes to foreign aggregates — those go through events such as admin.reputation.adjust.requested). ADR-009 (draft) additionally records the single permitted User foreign key exception: moderation tables that must cascade with account deletion may keep a real FK to users, since identity owns that table and the FK does not cross into a peer aggregate. Every other cross-context reference stays UUID-only.
shared (not a context)
Shared infrastructure used by all contexts:
| Path | Purpose |
|---|---|
shared/auth/ | JwtAuthGuard, JwtStrategy, AdminGuard |
shared/access/ | AccessMatrixService, @RequiresRep decorator |
shared/prisma/ | PrismaService (singleton) |
shared/events/ | Typed event definitions (@regulus/domain) |
shared/guards/ | ThrottlerGuard configuration |
shared/feature-flags/ | FeatureFlagService + @RequiresFlag guard |
shared/realtime/ | CentrifugoService |
shared/email/ | EmailService (Resend) |
shared/health/ | GET /health liveness probe |