Skip to content

Event Bus

Regulus uses NestJS EventEmitter2 as the in-process event bus. All cross-bounded-context communication flows through events — direct service imports across module boundaries are forbidden.

Pattern

typescript
// Publisher (posts module)
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class PostsService {
  constructor(private readonly events: EventEmitter2) {}

  async createPost(userId: string, dto: CreatePostDto): Promise<Post> {
    const post = await this.prisma.post.create({ data: { ... } });

    await this.events.emitAsync('posts.created', {
      postId: post.id,
      authorId: post.authorId,
      interestId: post.interestId,
    });

    return post;
  }
}

// Subscriber (reputation module)
import { OnEvent } from '@nestjs/event-emitter';

@Injectable()
export class ReputationService {
  @OnEvent('posts.created')
  async handlePostCreated(event: { postId: string; authorId: string; interestId: string | null }) {
    if (event.interestId) {
      await this.record({
        userId: event.authorId,
        interestId: event.interestId,
        delta: +3,
        reason: 'posts.created',
      });
    }
  }
}

Event Catalogue

Generated from a grep of emitAsync( / .emit( and @OnEvent( across apps/api/src/modules. The bus is configured with wildcard: true and . as the delimiter, so subscribers may bind to namespaces. notifications is the dominant consumer; reputation and contribution are the other cross-cutting sinks.

Identity / interests

EventPublisherSubscribers
identity.user.registeredidentityeconomy (wallet bootstrap), invite-tree, contribution, personas, social, notifications
identity.invite_code.usedidentityreputation (+3 to inviter), notifications
identity.persona.createdidentity(forward-compat)
interests.user_selectedinterests, ai-assistantreputation (+5 per interest)

Posts

EventPublisherSubscribers
posts.createdpostsreputation (+3), contribution, notifications, federation (AP fan-out)
posts.repliedpostsreputation (+2), contribution, notifications
posts.reactedpostsreputation (+1 unique), contribution, notifications
posts.bookmarkedpostsreputation (+1 unique), contribution, notifications
posts.repostedpostsreputation (+1 unique), notifications
posts.coauthor.invitedpostsnotifications
posts.scheduled.publishedpostsnotifications
mention.detectedpostsnotifications
moderation.report.filedposts(forward-compat)
series.new_partpostsnotifications

Social / messaging

EventPublisherSubscribers
social.invitation.sent / .acceptedsocialnotifications
social.invitation.declinedsocial(forward-compat)
social.member.added / .removedsocialnotifications
social.member.bond_changedsocial(forward-compat)
social.user.followedsocialnotifications
social.circle.createdsocial(forward-compat)
messaging.message.postedmessagingnotifications
messaging.member.addedmessagingnotifications
messaging.conversation.createdmessaging(forward-compat)

Groups / guilds

EventPublisherSubscribers
groups.join.requested / .join.approvedgroupsnotifications
groups.invite.sent / .invite.acceptedgroupsnotifications
groups.ownership.transferredgroupsnotifications
groups.group.created, groups.member.joinedgroups(forward-compat)
guilds.application.submitted / .approvedguildsnotifications
guilds.proposal.opened / .executedguildsnotifications
guilds.guild.founded, guilds.member.joinedguilds(forward-compat)

Economy / content / reputation / admin

EventPublisherSubscribers
economy.payment.senteconomycontribution, notifications
economy.post.boostedeconomynotifications
marketplace.order.placed / .releasedeconomy (marketplace)(forward-compat)
education.course.completededucationreputation
education.lesson.completededucationreputation
education.course.publishededucation(forward-compat)
events.rsvp.createdeventsnotifications
reputation.milestonereputationnotifications
reputation.vote.castreputationnotifications
wiki.proposal.approvedwikinotifications
ai.session.completedai-assistantnotifications
ai.session.started, ai.turn.answeredai-assistant(forward-compat)
user.banned / user.unbannedadminnotifications
post.hidden / post.unhiddenadminnotifications
admin.broadcast.sentadminnotifications
admin.user.notification.requestedadminnotifications
admin.reputation.adjust.requestedadminreputation

Forward-compat events

About 14 events are emitted but have no consumer yet — they are intentional hooks for upcoming features (e.g. marketplace.order.placed, guilds.guild.founded, social.circle.created, ai.turn.answered, moderation.report.filed). Emitting them now keeps publishers stable so a future subscriber can bind without touching the publisher.

Type Safety

10 domain-event classes are typed in packages/domain/src/events/UserRegisteredEvent, InviteCodeReservedEvent, InviteCodeUsedEvent, UserProfileUpdatedEvent, UserInterestAddedEvent, UserInterestUpdatedEvent, UserInterestRemovedEvent, ConversationCreatedEvent, MessageSentEvent, AssistantSessionCompletedEvent. These cover the original Alpha A core flows.

The Wave 3–6 contexts (groups, guilds, economy, content, federation) emit string-literal events with inline-typed payloads rather than dedicated classes — the bus accepts both. When promoting a forward-compat event to a stable contract, add a typed class here so producer and consumer share one shape. Always import existing classes from @regulus/domain — never re-define their shapes inline.

typescript
// packages/domain/src/events/posts.events.ts
export class PostCreatedEvent {
  constructor(public readonly payload: {
    postId: string;
    authorId: string;
    interestId: string | null;
    format: string;
  }) {}
}

Error Handling

emitAsync is used for all events with side-effects. Errors in subscribers are caught and logged — they do not roll back the publisher's transaction. For critical cross-context consistency (e.g. wallet bootstrap), use emitAsync and await before responding.

Regulus — invite-only social-knowledge platform