Event Catalog
The Event Catalog is the authoritative reference for every event published on the Platform-Kernel event bus. It covers schema definitions, Kafka topic configuration, publisher-to-consumer mappings, and replay/retention rules.
Events are the primary mechanism for cross-service communication and frontend real-time updates (via the WebSocket bridge in the Notify Service).
Source of truth: All schemas are generated from Protobuf definitions in
proto/platform/events/v1/schemas/v1/. Any breaking schema change requires
a new event type — never modify existing field semantics.
See also:
- EventService gRPC Reference — Publish / Subscribe RPCs
- WebSocket Protocol — Real-time push to browsers
- gRPC Overview — buf pipeline,
buf breakingCI
Topic Overview
| Topic | Domain | Events | Partition key | Retention |
|---|---|---|---|---|
platform.auth.events | IAM | 9 | user_id | 7 days |
platform.money.events | Money | 8 | wallet_id | 7 days |
platform.module.events | Module Registry | 6 | module_id | 7 days |
platform.data.events | Data Layer (CDC) | — | record_id | 7 days |
platform.files.events | File Storage | — | file_id | 7 days |
platform.notify.events | Notify | — | user_id | 7 days |
platform.audit.events | Audit | — | entity_id | 30 days |
platform.billing.events | Billing | — | tenant_id | 7 days |
The Data Layer, Files, Notify, Audit, and Billing topics are internal CDC or operational streams. Their schemas are defined in respective service layers and are not part of the typed Protobuf schema package.
This catalog documents the three core typed topics (auth, money, module)
whose schemas live in proto/platform/events/v1/schemas/v1/.
Event Envelope
Every event, regardless of topic, is wrapped in an EventEnvelope before
publishing. Consumers receive the envelope and unpack the typed payload with
proto.Unmarshal / Any.unpack.
// proto/platform/events/v1/schemas/v1/common.proto
message EventEnvelope {
string event_id = 1; // UUID v7 — idempotency key
string event_type = 2; // domain.entity.action
string source = 3; // Publishing service
string tenant_id = 4; // Gateway-injected — never trust client
string schema_version = 5; // "1.0.0"
google.protobuf.Timestamp timestamp = 6;
string trace_id = 7; // OpenTelemetry trace ID
string correlation_id = 8; // Links related events (saga)
google.protobuf.Any payload = 9; // Typed domain event
string entity_id = 10; // Kafka partition key
}
| Field | Description |
|---|---|
event_id | UUID v7. Time-sortable. Used as Kafka message key for idempotent replay. |
entity_id | Set to the domain partition key (e.g., user_id, wallet_id). Deterministic partition routing per entity. |
correlation_id | Links multiple events in a saga or request chain. |
tenant_id | Injected by the Gateway — SDK and consumers MUST NOT trust caller-provided values. |
Schema Versioning Policy
| Rule | Detail |
|---|---|
| Backward-compatible changes | Add optional fields freely — old consumers ignore unknown fields (proto3 default) |
| Breaking changes | Create a new event_type (e.g., auth.user.created.v2) — never mutate existing field numbers or semantics |
| CI guard | buf breaking --use WIRE_JSON runs on every PR — wire-breaking changes fail the pipeline |
| Deprecation window | Old event_type kept alive for 2 full retention windows (14 days for 7-day topics) before removal |
| Schema version field | schema_version in envelope is informational — event_type is the canonical discriminator |
Replay Rules
| Rule | Detail |
|---|---|
| Consumer seek | EventService.ReplayFrom seeks a consumer group to any past timestamp within retention |
| Replay bound | Cannot replay beyond Kafka topic retention (KAFKA_RETENTION_HOURS=168 for 7-day topics) |
| Financial event dedup | money.* events: permanent dedup via processed_event_ids PostgreSQL table (no TTL) |
| Non-financial dedup | Other events: Valkey dedup, 24-hour TTL — safe to replay within 24h without side-effects |
| Ordering guarantee | Events with the same entity_id are always processed in order within a consumer group (Kafka partition guarantee) |
| Bulk suppress | When consumer lag > 100 000 events, lag alerts suppressed for 30 minutes (KAFKA_BULK_IMPORT_SUPPRESS_MINUTES=30) |
platform.auth.events
Kafka config:
| Parameter | Value |
|---|---|
| Topic | platform.auth.events |
| Partition key | user_id |
| Retention | 7 days (KAFKA_RETENTION_HOURS=168) |
| Publisher | IAM Service (services/iam) |
| Proto package | platform.events.v1.schemas.v1 |
| Proto file | proto/platform/events/v1/schemas/v1/auth.proto |
Consumer mapping:
| Consumer service | Consumer group | Events consumed |
|---|---|---|
| Audit Service | audit.auth-event-handler | All auth events |
| Notify Service | notify.user-event-handler | auth.user.created, auth.user.logged_in |
| Billing Service | billing.user-event-handler | auth.user.created (tenant init) |
auth.user.created
Published when a new user account is successfully created.
message UserCreatedEvent {
string user_id = 1;
string email = 2;
string display_name = 3;
string role = 4; // "member" | "admin" | "owner"
string tenant_id = 5;
google.protobuf.Timestamp created_at = 6;
}
Example envelope:
{
"eventId": "019614a8-0000-7000-8000-000000000001",
"eventType": "auth.user.created",
"source": "iam",
"tenantId": "tenant-acme",
"schemaVersion": "1.0.0",
"timestamp": "2026-04-22T10:56:00Z",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"payload": {
"@type": "type.googleapis.com/platform.events.v1.schemas.v1.UserCreatedEvent",
"userId": "usr-01234",
"displayName": "Alice Smith",
"role": "member",
"tenantId": "tenant-acme",
"createdAt": "2026-04-22T10:56:00Z"
}
}
auth.user.updated
Published when a user's profile is modified. Uses a sparse update pattern:
only changed fields are populated. Consumers must treat absent optional fields
as "unchanged".
message UserUpdatedEvent {
string user_id = 1;
optional string email = 2; // Set only if email changed
optional string display_name = 3;
optional bool is_active = 4;
string updated_by = 5;
google.protobuf.Timestamp updated_at = 6;
}
auth.user.deleted
Published when a user account is soft-deleted.
message UserDeletedEvent {
string user_id = 1;
string deleted_by = 2;
string reason = 3; // "self" | "admin" | "policy"
google.protobuf.Timestamp deleted_at = 4;
}
auth.user.logged_in
Published on every successful authentication, regardless of method.
message UserLoggedInEvent {
string user_id = 1;
string session_id = 2;
string ip_address = 3;
string user_agent = 4;
string auth_method = 5; // "password" | "oauth" | "api_key"
google.protobuf.Timestamp logged_in_at = 6;
}
auth.user.logged_out
Published when a session is terminated.
message UserLoggedOutEvent {
string user_id = 1;
string session_id = 2;
string reason = 3; // "manual" | "expired" | "revoked"
google.protobuf.Timestamp logged_out_at = 4;
}
auth.role.changed
Published when a user's role is upgraded or downgraded. Triggers RBAC cache invalidation on the Gateway.
message RoleChangedEvent {
string user_id = 1;
string previous_role = 2;
string new_role = 3;
string changed_by = 4; // User ID of the admin/owner actor
string tenant_id = 5;
google.protobuf.Timestamp changed_at = 6;
}
auth.password.changed
Published when a user's password is changed or reset via any method.
message PasswordChangedEvent {
string user_id = 1;
string method = 2; // "self_change" | "admin_reset" | "forgot_password"
google.protobuf.Timestamp changed_at = 3;
}
Security note: Password hashes are never included in events. Only the method enum is published — sufficient for audit without exposing secrets.
auth.api_key.created
Published when a new API key is generated for a user.
message APIKeyCreatedEvent {
string key_id = 1;
string user_id = 2;
string key_prefix = 3; // First 8 chars only — never full key
string name = 4; // User-provided label
google.protobuf.Timestamp expires_at = 5;
google.protobuf.Timestamp created_at = 6;
}
auth.api_key.revoked
Published when an API key is revoked. Triggers immediate Valkey cache
invalidation for the _apikey:{prefix} key on the Gateway.
message APIKeyRevokedEvent {
string key_id = 1;
string user_id = 2;
string revoked_by = 3;
string reason = 4;
google.protobuf.Timestamp revoked_at = 5;
}
platform.money.events
Kafka config:
| Parameter | Value |
|---|---|
| Topic | platform.money.events |
| Partition key | wallet_id |
| Retention | 7 days |
| Publisher | Money Service (services/money) |
| Proto file | proto/platform/events/v1/schemas/v1/money.proto |
Financial event dedup: All
money.*events are deduplicated using a permanent PostgreSQL table (processed_event_ids). Valkey TTL-based dedup is NOT used for financial events. This prevents duplicate ledger entries regardless of replay or consumer restart.
Consumer mapping:
| Consumer service | Consumer group | Events consumed |
|---|---|---|
| Audit Service | audit.money-event-handler | All money events |
| Notify Service | notify.money-event-handler | Credit / debit / transfer events |
| Billing Service | billing.wallet-event-handler | money.wallet.credited (revenue tracking) |
money.wallet.created
message WalletCreatedEvent {
string wallet_id = 1;
string tenant_id = 2;
string currency = 3; // ISO 4217 (e.g. "USD", "EUR")
string owner_id = 4;
string owner_type = 5; // "user" | "merchant" | "system"
google.protobuf.Timestamp created_at = 6;
}
money.wallet.credited
message WalletCreditedEvent {
string wallet_id = 1;
string transaction_id = 2; // Idempotency key — used for dedup
string tenant_id = 3;
int64 amount = 4; // Minor units (cents). int64 — no float precision
string currency = 5;
int64 balance_after = 6; // Running balance (minor units)
string source = 7; // "deposit"|"refund"|"transfer_in"|"bonus"
string reference_id = 8; // External payment processor reference
string description = 9;
google.protobuf.Timestamp credited_at = 10;
}
Example:
{
"walletId": "wlt-99887766",
"transactionId": "txn-aabbcc001122",
"tenantId": "tenant-acme",
"amount": 10000,
"currency": "USD",
"balanceAfter": 150000,
"source": "deposit",
"referenceId": "stripe_pi_3P4K2",
"description": "Invoice #INV-2026-042 payment",
"creditedAt": "2026-04-22T10:56:00Z"
}
amount = 10000= $100.00 USD. Always store and compare in minor units (cents). Never usefloat64for money.
money.wallet.debited
message WalletDebitedEvent {
string wallet_id = 1;
string transaction_id = 2;
string tenant_id = 3;
int64 amount = 4;
string currency = 5;
int64 balance_after = 6;
string destination = 7; // "withdrawal"|"purchase"|"transfer_out"|"fee"
string reference_id = 8;
string description = 9;
google.protobuf.Timestamp debited_at = 10;
}
money.transfer.completed
message TransferCompletedEvent {
string transfer_id = 1; // Idempotency key
string from_wallet_id = 2;
string to_wallet_id = 3;
string tenant_id = 4;
int64 amount = 5;
string currency = 6;
string initiated_by = 7;
string description = 8;
google.protobuf.Timestamp completed_at = 9;
}
money.transfer.failed
message TransferFailedEvent {
string transfer_id = 1;
string from_wallet_id = 2;
string to_wallet_id = 3;
string tenant_id = 4;
int64 amount = 5;
string currency = 6;
string failure_reason = 7; // "insufficient_funds"|"wallet_frozen"|"limit_exceeded"
string initiated_by = 8;
google.protobuf.Timestamp failed_at = 9;
}
money.payment.processed
message PaymentProcessedEvent {
string payment_id = 1;
string wallet_id = 2;
string tenant_id = 3;
int64 amount = 4;
string currency = 5;
string provider = 6; // "stripe" | "apcopay"
string provider_ref = 7; // Provider's transaction reference
string status = 8; // "succeeded"|"failed"|"pending"|"refunded"
google.protobuf.Timestamp processed_at = 9;
}
money.wallet.frozen
message WalletFrozenEvent {
string wallet_id = 1;
string tenant_id = 2;
string frozen_by = 3; // "system" | "admin" | user_id
string reason = 4; // "suspicious_activity"|"compliance"|"manual"
google.protobuf.Timestamp frozen_at = 5;
}
money.wallet.unfrozen
message WalletUnfrozenEvent {
string wallet_id = 1;
string tenant_id = 2;
string unfrozen_by = 3;
string reason = 4;
google.protobuf.Timestamp unfrozen_at = 5;
}
platform.module.events
Kafka config:
| Parameter | Value |
|---|---|
| Topic | platform.module.events |
| Partition key | module_id |
| Retention | 7 days |
| Publisher | Module Registry Service (services/module-manager) |
| Proto file | proto/platform/events/v1/schemas/v1/module.proto |
Consumer mapping:
| Consumer service | Consumer group | Events consumed |
|---|---|---|
| Audit Service | audit.module-event-handler | All module events |
| Gateway | gateway.module-cache-invalidator | module.registry.activated, module.registry.deactivated |
| Notify Service | notify.module-event-handler | module.registry.activated |
module.registry.registered
message ModuleRegisteredEvent {
string module_id = 1;
string name = 2;
string version = 3; // Semantic version (e.g. "1.2.0")
string manifest_hash = 4; // SHA-256 of manifest.json
string tenant_id = 5;
string registered_by = 6;
google.protobuf.Timestamp registered_at = 7;
}
module.registry.activated
message ModuleActivatedEvent {
string module_id = 1;
string tenant_id = 2;
string activated_by = 3;
google.protobuf.Timestamp activated_at = 4;
}
Gateway side-effect: This event triggers Gateway to invalidate
modules:active:{tenantId}Valkey cache and reload xDS routing config via the Domain Resolver (LDS/RDS update). Zero-downtime, no restart.
module.registry.deactivated
message ModuleDeactivatedEvent {
string module_id = 1;
string tenant_id = 2;
string deactivated_by = 3;
string reason = 4; // "manual"|"policy"|"billing"|"violation"
google.protobuf.Timestamp deactivated_at = 5;
}
module.registry.updated
message ModuleUpdatedEvent {
string module_id = 1;
string previous_version = 2;
string new_version = 3;
string tenant_id = 4;
string updated_by = 5;
bool requires_migration = 6; // true → DB schema migration needed
google.protobuf.Timestamp updated_at = 7;
}
module.registry.config_changed
Published when a module's runtime configuration is changed via the admin API. Config values are intentionally omitted from the event — they may contain secrets. The event signals that a config cache refresh is needed.
message ModuleConfigChangedEvent {
string module_id = 1;
string tenant_id = 2;
string changed_by = 3;
string config_key = 4; // Which key changed (no value — may be secret)
google.protobuf.Timestamp changed_at = 5;
}
module.registry.health_changed
Published when the Gateway's health probe detects a module status transition.
message ModuleHealthChangedEvent {
string module_id = 1;
string previous_status = 2; // "healthy" | "degraded" | "unhealthy"
string new_status = 3;
string reason = 4;
google.protobuf.Timestamp changed_at = 5;
}
Publisher → Consumer Matrix
| Event type | Audit | Notify | Billing | Gateway |
|---|---|---|---|---|
auth.user.created | ✅ | ✅ | ✅ | — |
auth.user.updated | ✅ | — | — | — |
auth.user.deleted | ✅ | — | — | — |
auth.user.logged_in | ✅ | ✅ | — | — |
auth.user.logged_out | ✅ | — | — | — |
auth.role.changed | ✅ | — | — | ✅ (RBAC cache) |
auth.password.changed | ✅ | — | — | — |
auth.api_key.created | ✅ | — | — | — |
auth.api_key.revoked | ✅ | — | — | ✅ (key cache) |
money.wallet.created | ✅ | — | — | — |
money.wallet.credited | ✅ | ✅ | ✅ | — |
money.wallet.debited | ✅ | ✅ | — | — |
money.transfer.completed | ✅ | ✅ | — | — |
money.transfer.failed | ✅ | ✅ | — | — |
money.payment.processed | ✅ | — | ✅ | — |
money.wallet.frozen | ✅ | ✅ | — | — |
money.wallet.unfrozen | ✅ | ✅ | — | — |
module.registry.registered | ✅ | — | — | — |
module.registry.activated | ✅ | ✅ | — | ✅ (xDS reload) |
module.registry.deactivated | ✅ | — | — | ✅ (xDS reload) |
module.registry.updated | ✅ | — | — | — |
module.registry.config_changed | ✅ | — | — | — |
module.registry.health_changed | ✅ | ✅ | — | — |