Create Event Handlers
Sets up RabbitMQ event publishers and consumers following ModuleImplementationGuide.md Section 9.
Tenant-only: Events use tenantId only for tenant context. Do not include organizationId in event payloads or in createBaseEvent; use tenantId only.
Event Naming Convention
Reference: ModuleImplementationGuide.md Section 9.1
Format: {domain}.{entity}.{action}
✅ Correct:
user.created
auth.login.success
notification.email.sent
secret.rotated
❌ Wrong:
userCreated
loginSuccess
emailSent
Standard Actions:
created, updated, deleted
started, completed, failed
sent, received, expired, rotated
Event Structure
Reference: ModuleImplementationGuide.md Section 9.2
typescript
1interface DomainEvent<T = unknown> {
2 // Identity
3 id: string; // Unique event ID (UUID)
4 type: string; // Event type (domain.entity.action)
5
6 // Metadata
7 timestamp: string; // ISO 8601
8 version: string; // Event schema version
9 source: string; // Module that emitted
10 correlationId?: string; // Request correlation
11
12 // Context
13 tenantId: string; // Tenant context (required; all users and data are scoped by tenant)
14 userId?: string; // Actor
15
16 // Payload
17 data: T; // Event-specific data
18}
Event Publisher
Reference: containers/auth/src/events/publishers/AuthEventPublisher.ts
src/events/publishers/[Module]EventPublisher.ts
typescript
1import { randomUUID } from 'crypto';
2import { EventPublisher, getChannel, closeConnection } from '@coder/shared';
3import { log } from '../../utils/logger';
4import { getConfig } from '../../config';
5
6let publisher: EventPublisher | null = null;
7
8export async function initializeEventPublisher(): Promise<void> {
9 if (publisher) return;
10
11 const config = getConfig();
12 if (!config.rabbitmq?.url) {
13 log.warn('RabbitMQ URL not configured, events will not be published');
14 return;
15 }
16
17 try {
18 await getChannel();
19 publisher = new EventPublisher(config.rabbitmq.exchange || 'coder_events');
20 log.info('Event publisher initialized', { exchange: config.rabbitmq.exchange });
21 } catch (error: any) {
22 log.error('Failed to initialize event publisher', error);
23 }
24}
25
26export async function closeEventPublisher(): Promise<void> {
27 try {
28 await closeConnection();
29 publisher = null;
30 } catch (error: any) {
31 log.error('Error closing event publisher', error);
32 }
33}
34
35function getPublisher(): EventPublisher | null {
36 if (!publisher) {
37 const config = getConfig();
38 publisher = new EventPublisher(config.rabbitmq.exchange || 'coder_events');
39 }
40 return publisher;
41}
42
43export function createBaseEvent(
44 type: string,
45 userId?: string,
46 tenantId?: string,
47 correlationId?: string,
48 data?: any
49) {
50 return {
51 id: randomUUID(),
52 type,
53 timestamp: new Date().toISOString(),
54 version: '1.0',
55 source: '[module-name]',
56 correlationId,
57 tenantId,
58 userId,
59 data: data || {},
60 };
61}
62
63export async function publishEvent(event: any, routingKey?: string): Promise<void> {
64 const pub = getPublisher();
65 if (!pub) {
66 log.warn('Event publisher not initialized, skipping event', { type: event.type });
67 return;
68 }
69
70 try {
71 await pub.publish(routingKey || event.type, event);
72 log.debug('Event published', { type: event.type, id: event.id });
73 } catch (error: any) {
74 log.error('Failed to publish event', error, { type: event.type });
75 }
76}
Usage in Services
typescript
1import { publishEvent, createBaseEvent } from '../events/publishers/ModuleEventPublisher';
2
3// Publish event (tenantId for tenant context)
4const event = createBaseEvent(
5 'resource.created',
6 userId,
7 tenantId,
8 correlationId,
9 {
10 resourceId: resource.id,
11 name: resource.name,
12 }
13);
14
15await publishEvent(event);
Event Consumer
Reference: ModuleImplementationGuide.md Section 9.4
src/events/consumers/[Resource]Consumer.ts
typescript
1import { EventConsumer } from '@coder/shared';
2import { log } from '../../utils/logger';
3import { getConfig } from '../../config';
4
5let consumer: EventConsumer | null = null;
6
7export async function initializeEventConsumer(): Promise<void> {
8 if (consumer) return;
9
10 const config = getConfig();
11 if (!config.rabbitmq?.url) {
12 log.warn('RabbitMQ URL not configured, events will not be consumed');
13 return;
14 }
15
16 try {
17 consumer = new EventConsumer({
18 queue: config.rabbitmq.queue || '[module-name]_service',
19 exchange: config.rabbitmq.exchange || 'coder_events',
20 bindings: config.rabbitmq.bindings || [],
21 });
22
23 // Register handlers
24 consumer.on('other.resource.created', handleResourceCreated);
25 consumer.on('other.resource.updated', handleResourceUpdated);
26
27 await consumer.start();
28 log.info('Event consumer initialized', { queue: config.rabbitmq.queue });
29 } catch (error: any) {
30 log.error('Failed to initialize event consumer', error);
31 }
32}
33
34async function handleResourceCreated(event: any): Promise<void> {
35 log.info('Resource created event received', { resourceId: event.data.resourceId });
36 // Handle the event
37}
38
39async function handleResourceUpdated(event: any): Promise<void> {
40 log.info('Resource updated event received', { resourceId: event.data.resourceId });
41 // Handle the event
42}
43
44export async function closeEventConsumer(): Promise<void> {
45 try {
46 if (consumer) {
47 await consumer.stop();
48 consumer = null;
49 }
50 } catch (error: any) {
51 log.error('Error closing event consumer', error);
52 }
53}
Event Documentation
Reference: ModuleImplementationGuide.md Section 9.5
logs-events.md (if events are logged)
Create in module root if module publishes events that get logged:
markdown
1# [Module Name] - Logs Events
2
3## Published Events
4
5### {domain}.{entity}.{action}
6
7**Description**: When this event is triggered.
8
9**Triggered When**:
10- Condition 1
11- Condition 2
12
13**Event Type**: `{domain}.{entity}.{action}`
14
15**Event Schema**:
16
17\`\`\`json
18{
19 "$schema": "http://json-schema.org/draft-07/schema#",
20 "type": "object",
21 "required": ["id", "type", "timestamp", "version", "source", "data"],
22 "properties": {
23 "id": { "type": "string", "format": "uuid" },
24 "type": { "type": "string" },
25 "timestamp": { "type": "string", "format": "date-time" },
26 "version": { "type": "string" },
27 "source": { "type": "string" },
28 "tenantId": { "type": "string", "format": "uuid" },
29 "userId": { "type": "string", "format": "uuid" },
30 "data": {
31 "type": "object",
32 "properties": {
33 "resourceId": { "type": "string" }
34 }
35 }
36 }
37}
38\`\`\`
notifications-events.md (if events trigger notifications)
Create in module root if module publishes events that trigger notifications.
Configuration
Add to config/default.yaml:
yaml
1rabbitmq:
2 url: ${RABBITMQ_URL}
3 exchange: coder_events
4 queue: [module-name]_service
5 bindings:
6 - "other.resource.created"
7 - "other.resource.updated"
Checklist