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
// 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
| Event | Publisher | Subscribers |
|---|---|---|
identity.user.registered | identity | economy (wallet bootstrap), invite-tree, contribution, personas, social, notifications |
identity.invite_code.used | identity | reputation (+3 to inviter), notifications |
identity.persona.created | identity | — (forward-compat) |
interests.user_selected | interests, ai-assistant | reputation (+5 per interest) |
Posts
| Event | Publisher | Subscribers |
|---|---|---|
posts.created | posts | reputation (+3), contribution, notifications, federation (AP fan-out) |
posts.replied | posts | reputation (+2), contribution, notifications |
posts.reacted | posts | reputation (+1 unique), contribution, notifications |
posts.bookmarked | posts | reputation (+1 unique), contribution, notifications |
posts.reposted | posts | reputation (+1 unique), notifications |
posts.coauthor.invited | posts | notifications |
posts.scheduled.published | posts | notifications |
mention.detected | posts | notifications |
moderation.report.filed | posts | — (forward-compat) |
series.new_part | posts | notifications |
Social / messaging
| Event | Publisher | Subscribers |
|---|---|---|
social.invitation.sent / .accepted | social | notifications |
social.invitation.declined | social | — (forward-compat) |
social.member.added / .removed | social | notifications |
social.member.bond_changed | social | — (forward-compat) |
social.user.followed | social | notifications |
social.circle.created | social | — (forward-compat) |
messaging.message.posted | messaging | notifications |
messaging.member.added | messaging | notifications |
messaging.conversation.created | messaging | — (forward-compat) |
Groups / guilds
| Event | Publisher | Subscribers |
|---|---|---|
groups.join.requested / .join.approved | groups | notifications |
groups.invite.sent / .invite.accepted | groups | notifications |
groups.ownership.transferred | groups | notifications |
groups.group.created, groups.member.joined | groups | — (forward-compat) |
guilds.application.submitted / .approved | guilds | notifications |
guilds.proposal.opened / .executed | guilds | notifications |
guilds.guild.founded, guilds.member.joined | guilds | — (forward-compat) |
Economy / content / reputation / admin
| Event | Publisher | Subscribers |
|---|---|---|
economy.payment.sent | economy | contribution, notifications |
economy.post.boosted | economy | notifications |
marketplace.order.placed / .released | economy (marketplace) | — (forward-compat) |
education.course.completed | education | reputation |
education.lesson.completed | education | reputation |
education.course.published | education | — (forward-compat) |
events.rsvp.created | events | notifications |
reputation.milestone | reputation | notifications |
reputation.vote.cast | reputation | notifications |
wiki.proposal.approved | wiki | notifications |
ai.session.completed | ai-assistant | notifications |
ai.session.started, ai.turn.answered | ai-assistant | — (forward-compat) |
user.banned / user.unbanned | admin | notifications |
post.hidden / post.unhidden | admin | notifications |
admin.broadcast.sent | admin | notifications |
admin.user.notification.requested | admin | notifications |
admin.reputation.adjust.requested | admin | reputation |
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.
// 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.