Multi-Tenant Event Store Stream Design
A multi-tenant event store must enforce tenant scope before every read, write, replay, subscription, snapshot, and projection operation. Tenant identity is an authorization boundary carried independently from aggregate identity; matching entity IDs in two tenants must never share history or derived state.
Problem
Why this pattern exists
Adding tenant_id field without making it mandatory in query and storage paths creates cross-tenant failure modes. Background rebuilds, admin endpoints, snapshots, cache keys, metrics, and WebSocket filters can bypass controller checks even when ordinary HTTP requests appear isolated.
Embedding tenant only inside entity string is brittle. It invites parsing inconsistencies and makes authorization depend on naming convention. Treat tenant scope as typed request context, validate it at boundary, include it in storage/index keys, and reject missing scope unless endpoint is explicitly system-level with audited privilege.
Design decisions
Make boundaries explicit
- 01
Scope before lookup
Resolve authenticated tenant and authorization before accessing event IDs, streams, snapshots, or cursors. Filtering after global lookup can leak existence, counts, timing, or payloads.
- 02
Keep aggregate identity independent
Use tenant scope plus stable entity ID as compound boundary. Two tenants may both own order-42 without collision; moving data between tenants becomes explicit migration, not string rename.
- 03
Partition derived state
Tenant-facing projection compute and caches must key by tenant before entity. Administrative global views require separate code path, role, telemetry, and audit trail.
AllSource implementation
Apply pattern to durable Core history
AllSource hosted architecture authenticates tenant at control and Query Service layers, while Core remains source of truth for events and operational metadata. Query Service owns per-tenant user-facing projections; Core stores enabled set as opaque tenant metadata and serves tenant-scoped event history. This keeps hot ingest engine from becoming tenant-specific compute layer.
Propagate tenant context to event ingestion, queries, durable-consumer registration, WebSocket delivery, snapshot lookup, and replay. Use fail-closed defaults: missing or invalid tenant context returns error, not global results. Run adversarial tests with same entity and consumer IDs across tenants, including historical reconstruction and reconnect paths.
tenant acme + entity order-42 -> independent stream
tenant orbit + entity order-42 -> independent stream
query scope: tenant resolved from authenticated context
projection key: (tenant_id, entity_id)
missing tenant: reject
system-wide operation: explicit admin path + audit eventFailure modes
Detect weak implementations early
Entity snapshot cache keys only by entity ID.
Fix: Include tenant in key or disable unsafe snapshot fast path for tenant-scoped reads.
WebSocket reconnect receives events outside tenant filter.
Fix: Bind tenant authorization to durable consumer and server-side subscription filter.
Missing tenant parameter returns global data.
Fix: Fail closed; reserve global access for explicit audited administration route.
Production checklist
Ready when each statement is true
- Tenant scope is typed authenticated context, not optional query text.
- All indexes, caches, snapshots, consumers, and projections include tenant boundary.
- Missing scope fails closed.
- Administrative cross-tenant paths are separate and audited.
- Isolation tests reuse same IDs across tenants and every access path.
Related patterns
Continue through adjacent decisions
Aggregate streams
An aggregate stream is the ordered history for one consistency boundary, such as an order, account, or workflow. Give every aggregate a stable entity ID, append only facts owned by that boundary, and derive current state by folding its events in version order.
Read nextProjections and read models
A projection folds ordered events into a query-specific read model. It is derived state, not a second source of truth: operators must be able to discard it, replay source events, and build a replacement without rewriting event history or stopping existing readers.
Read nextDurable subscriptions
A durable subscription gives a named consumer a server-tracked position in the event log. After reconnect, event store replays committed events after last acknowledged position, then switches consumer to live delivery. Processing remains at least once, so handlers must be idempotent.
Read nextStore history once
