Skip to content

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 a SELECT … FOR UPDATE row lock; /identity/refresh carries a tight @Throttle. Performance: a ReputationSnapshot(interestId, value) covering index backs the leaderboard, group standings use a single groupBy, and messaging.listMine takes a bounded take. See ADR-009 for the admin User-FK exception.

Context Map

mermaid
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| Notifications

Contexts Reference

identity

Aggregate roots: User, InviteCode, Session, Persona

Responsibilities:

  • Invite-only registration: reserve → register flow with 30-minute TTL
  • Invite code generation: format REG-XXXXXX, allocation formula clamp(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 code
  • identity.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_selected
  • interests.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 in reputation_snapshots
  • Peer voting: +1 / 0 / -1 per (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, optional restoreDelta

Events consumed:

  • interests.user_selected → +5 per selected interest (one-shot bootstrap)
  • identity.invite_code.used → +3 to inviter in their top interest
  • posts.created → +3 to author in post's interest
  • posts.replied → +2 to replier
  • posts.reacted (first unique per user) → +1 to post author
  • posts.bookmarked (first unique) → +1 to post author
  • posts.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.invited
  • circle.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, splitShare 0–100 %, primary author keeps ≥ 10 %
  • Threaded replies with kind: reply | reaction | criticism | contribution
  • Scheduled publish (publishAt), soft delete, edit, pin (max 3)
  • #journalism tag: gate rep ≥ 20 in any interest
  • Series: create, order, follow for notifications
  • Materialised counters: reactions, replies, shares, bookmarks, views

Events emitted:

  • posts.created
  • posts.replied
  • posts.reacted
  • posts.bookmarked
  • posts.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_KEY is absent
  • POST /ai/map-interests — LLM maps free-text to catalog interests
  • POST /ai/apply-suggestions — writes accepted suggestions to UserInterest
  • POST /ai/enhance-post — AI content enhancement in composer
  • AssistantSuggestion.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 PostCoAuthor rows
  • 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 = 5 in interest
  • WikiVote.value = +1 | -1; WikiProposal.netVotes is 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 intent
  • GET /discovery/mentors — users with expertise gap ≥ N in a shared interest
  • isFollowing wired to follow graph
  • isActive: false opts 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 (@RequiresRep to propose)
  • Application → approval; reputation-weighted governance voting; proposal execution
  • Treasury debits take a SELECT … FOR UPDATE lock (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
  • removedAt soft-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 /debates and /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) and expo-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 pending assets 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:

PathPurpose
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

Regulus — invite-only social-knowledge platform