Skip to main content

Event Model

Every event on the platform — whether published by a kernel service or a third-party module — uses the same standard envelope. This guarantees that every consumer can inspect the metadata without deserialising the payload and that OpenTelemetry tracing spans are automatically correlated across services.


Event Envelope

FieldTypeRequiredDescription
idUUID v7Unique event identifier. Time-sortable. Used as idempotency key.
typestringEvent name in {domain}.{entity}.{verb} format, e.g. auth.user.created
sourcestringModule or service that published the event, e.g. crm, iam
tenantIdUUIDInjected by API Gateway from JWT. Cannot be set by the publisher.
dataobjectEvent-specific payload. Schema defined per event type. Max 1 MB total envelope.
schemaVersionstringSemver string, e.g. "1.0.0". Used for consumer compatibility checks.
timestampISO 8601UTC time of event creation on the producer side.
traceIdstringOpenTelemetry trace ID. Propagated from the originating HTTP request.

Example envelope (JSON over the wire)

{
"id": "01j9pa9ev300000000000000",
"type": "auth.user.created",
"source": "iam",
"tenantId": "01j9p3kz5f00000000000000",
"data": {
"userId": "01j9pa5mz700000000000000",
"email": "[email protected]",
"roles": ["member"]
},
"schemaVersion": "1.0.0",
"timestamp": "2026-04-15T10:30:00.000Z",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}

Event Type Naming Convention

Event types follow the {domain}.{entity}.{verb} convention:

PartRuleExamples
domainBounded context slugauth, crm, money, billing
entityNoun (singular)user, contact, wallet, deal
verbPast tensecreated, updated, deleted, credited, changed

All parts are lowercase with dots as separators. No hyphens or underscores in the type string.

✅ auth.user.created
✅ crm.deal.won
✅ money.wallet.credited
❌ auth_user_created (underscores)
❌ CRM.Contact.Created (uppercase)
❌ crm.contact-created (hyphen)

Protobuf Schema

All event schemas are declared as Protobuf messages located in proto/events/{domain}/ in the monorepo. Protobuf guarantees language-agnostic type safety across Go consumers and TypeScript SDK.

// proto/events/auth/user_created.proto
syntax = "proto3";
package platform.events.auth;

message UserCreatedEvent {
string id = 1;
string type = 2;
string source = 3;
string tenant_id = 4;
string schema_version = 5;
string timestamp = 6;
string trace_id = 7;

message Data {
string user_id = 1;
string email = 2;
repeated string roles = 3;
}
Data data = 8;
}

Backward Compatibility (Protobuf Schema CI)

Schema changes go through automated backward compatibility checks on every pull request:

Change typeAllowedReason
Add new fieldConsumers that don't know the field ignore it
Add new messageNo impact on existing consumers
Remove field❌ Blocked by CIConsumers expecting the field break
Rename field❌ Blocked by CIWire equivalent to remove + add
Change field type❌ Blocked by CIDeserialisation fails in existing consumers
CI step: buf breaking --against ".git#branch=main"
Config: buf.yaml at repository root
breaking:
use: [FILE]

A PR that contains a breaking .proto change fails CI and cannot be merged. The only valid strategy for a breaking change is to create a new event type (auth.user.created.v2) alongside the old one and migrate consumers before removing the old type.


schemaVersion and Consumer Compatibility

schemaVersion follows semver:

ChangeVersion bump
New optional field addedPatch (1.0.01.0.1)
New required field addedMinor (1.0.01.1.0)
Breaking changeNew event type — never bump major in-place

The SDK exposes event.schemaVersion so that consumers can implement version-conditional logic if they need to handle multiple schema generations during a migration window:

kernel.events().subscribe('auth.user.created', async (event) => {
if (event.schemaVersion === '1.0.0') {
// handle original schema
} else if (event.schemaVersion >= '1.1.0') {
// handle extended schema with new fields
}
});

id Field — UUID v7

id is always a UUID v7 (time-ordered). This provides:

  • Idempotency: the same event re-delivered gets the same id. Consumers store id to detect and skip duplicates.
  • Time-sortable: events can be sorted by id without a secondary timestamp sort, which is useful for ClickHouse analytics.
  • Globally unique: no coordination required between producers.

traceId — Distributed Tracing

traceId is the OpenTelemetry W3C trace ID from the originating HTTP request. It propagates automatically through:

HTTP request (traceId: 4bf92...)

▼ Gateway injects traceId into event envelope
Kafka event (traceId: 4bf92...)

▼ Consumer extracts traceId, continues trace span
ClickHouse audit log (traceId: 4bf92...)

Every log line, audit record, and ClickHouse analytics row associated with a single user action shares the same traceId, making end-to-end debugging trivial.