Events
Overview
The Identity Platform uses an event-driven architecture built on Redis Streams. Events decouple the platform from products and enable reliable, scalable, idempotent communication.
Each event is published to a Redis stream and includes:
- event_type: A namespaced identifier (e.g.,
user.created, authentication.login)
- event_id: A UUID that uniquely identifies THIS event occurrence
- correlation_id: A UUID that ties this event to a request chain (e.g., a login request may trigger
user.updated, session.created, authentication.login_success)
- timestamp: ISO 8601 UTC
- version: Schema version of this event (semver:
1.0.0)
- data: Event-specific payload (product-agnostic)
- metadata: Correlation info, source, IP, etc.
Event Structure
{
"event_id": "uuid",
"correlation_id": "uuid",
"event_type": "user.created",
"version": "1.0.0",
"timestamp": "2024-01-15T10:30:00.000Z",
"source": "identity-platform",
"data": {
"user_id": "uuid",
"email": "user@acme.com"
},
"metadata": {
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"request_id": "uuid"
}
}
Event Types
User Events
| Event Type |
Version |
Trigger |
Data |
user.created |
1.0.0 |
New user registered |
user_id, email, phone, created_at |
user.updated |
1.0.0 |
User profile updated |
user_id, changed_fields[], updated_at |
user.deleted |
1.0.0 |
User deleted (GDPR) |
user_id, deleted_at |
user.disabled |
1.0.0 |
User status → suspended/banned |
user_id, status, reason |
user.verified |
1.0.0 |
Email/phone verified |
user_id, channel (email/phone), verified_at |
Organization Events
| Event Type |
Version |
Trigger |
Data |
organization.created |
1.0.0 |
New org created |
org_id, name, created_by |
organization.updated |
1.0.0 |
Org details updated |
org_id, changed_fields[], updated_at |
Membership Events
| Event Type |
Version |
Trigger |
Data |
membership.created |
1.0.0 |
User added to org |
user_id, org_id, role, status |
membership.updated |
1.0.0 |
Role/status changed |
user_id, org_id, changed_fields[] |
membership.removed |
1.0.0 |
User removed from org |
user_id, org_id, removed_by |
Authentication Events
| Event Type |
Version |
Trigger |
Data |
authentication.login |
1.0.0 |
Successful login |
user_id, session_id, product_key, ip, device |
authentication.logout |
1.0.0 |
Successful logout |
user_id, session_id, product_key |
authentication.failed |
1.0.0 |
Failed login attempt |
user_id (if known), email, ip, reason |
authentication.mfa_required |
1.0.0 |
MFA step reached |
user_id, method |
authentication.mfa_verified |
1.0.0 |
MFA passed |
user_id, method |
Session Events
| Event Type |
Version |
Trigger |
Data |
session.created |
1.0.0 |
New session |
session_id, user_id, product_key, session_type |
session.expired |
1.0.0 |
Session timed out |
session_id, user_id, reason |
session.revoked |
1.0.0 |
Session revoked |
session_id, user_id, product_key, reason |
Security Events
| Event Type |
Version |
Trigger |
Data |
security.event |
1.0.0 |
Any security event logged |
event_type, severity, user_id, ip, user_agent |
security.token_revoked |
1.0.0 |
Token blacklisted |
user_id, token_type, reason |
Event Properties
idempotency
Every event has a unique event_id (UUID). Consumers must deduplicate by event_id. This allows safe retry of failed deliveries without duplicate processing.
correlation
Events triggered by the same request share a correlation_id. Example:
correlation_id: req_abc123
↓ triggers
event: user.created (user_id=123)
event: session.created (user_id=123, session_id=456)
event: authentication.login (user_id=123, session_id=456)
versioning
Event payloads are versioned with semver. When the schema changes in a backward-compatible way (adding optional fields), the version increments the PATCH. Breaking changes (removing fields, changing types) increment MAJOR. Consumers MUST check version before processing.
Delivery Guarantees
| Guarantee |
Mechanism |
| At-least-once |
Redis Streams with consumer groups |
| Deduplication |
event_id uniqueness |
| Ordering |
Per-stream ordering preserved |
| Durability |
Redis AOF persistence |
| Retry |
Consumer must ACK (XACK); unacked claimed by other consumers |
| DLQ |
Failed messages (max retries exceeded) routed to events.dlq.stream |
Consumer Contract
Products consuming events MUST:
- Use consumer groups per product + service combination
- Acknowledge (XACK) every processed message
- Idempotently handle duplicates via
event_id
- Validate
version before processing (reject unknown MAJOR versions)
- Route failed messages to DLQ after max retries
- Preserve correlation_id across their own downstream events
Internal vs External Events
| Channel |
Audience |
Persistence |
Delivery |
| SecurityEvent (DB table) |
Internal admin/audit |
Permanent (2 years) |
Queryable via API |
| Redis Streams |
Products + internal services |
7 days |
Consumer group |
| Audit log (SecurityEvent) |
Compliance |
Permanent |
Archive |
The SecurityEvent model in PostgreSQL is the internal audit log. These are NOT the same as Redis Stream events but are correlated:
User logs in
→ SecurityEvent (DB: login_success, severity=info) [audit trail]
→ Redis Stream event (user.created, authentication.login) [product notifications]
Stream Names
| Stream |
Events |
events.user |
user.created, user.updated, user.deleted, user.disabled, user.verified |
events.organization |
organization.created, organization.updated |
events.membership |
membership.created, membership.updated, membership.removed |
events.authentication |
authentication.login, authentication.logout, authentication.failed |
events.session |
session.created, session.expired, session.revoked |
events.security |
security.event, security.token_revoked |
events.dlq |
Dead-letter queue for failed deliveries |