Multitenancy
Ocoda Event Sourcing provides built-in support for multitenancy. This allows you to separate the event- and snapshot-streams of different tenants. This can be achieved by using pools. A pool is a collection of streams that belong to the same tenant. By default the event- and snapshot-store create a shared collection of streams for all tenants. To enable multitenancy you need to:
-
Disable the creation of a default pool in the store configuration
Section titled “Disable the creation of a default pool in the store configuration”Set
useDefaultPool: falseon the configuration of both stores, so no shared collection is created.import { Module } from '@nestjs/common';import {EventSourcingModule,InMemoryEventStore,InMemorySnapshotStore,} from '@ocoda/event-sourcing';@Module({imports: [EventSourcingModule.forRoot({events: Events,eventStore: {driver: InMemoryEventStore,useDefaultPool: false,},snapshotStore: {driver: InMemorySnapshotStore,useDefaultPool: false,},}),]})export class AppModule {}import { Module } from '@nestjs/common';import { EventSourcingModule } from '@ocoda/event-sourcing';import {PostgresEventStore,PostgresSnapshotStore,type PostgresEventStoreConfig,type PostgresSnapshotStoreConfig,} from '@ocoda/event-sourcing-postgres';@Module({imports: [EventSourcingModule.forRoot<PostgresEventStoreConfig, PostgresSnapshotStoreConfig>({events: Events,eventStore: {driver: PostgresEventStore,connectionString: process.env.DATABASE_URL,useDefaultPool: false,},snapshotStore: {driver: PostgresSnapshotStore,connectionString: process.env.DATABASE_URL,useDefaultPool: false,},}),]})export class AppModule {}Every pool is stored in its own pair of tables in the same database, e.g.
tenant-1-eventsandtenant-1-snapshots, and has its own global positions:readAll({ pool })reads one pool. The MariaDB stores, on MariaDB or MySQL, and the MongoDB stores work the same way, with tables or collections. -
Create a pool for each tenant
Section titled “Create a pool for each tenant”The ensureCollection methods are used to create a pool for a tenant. The pool is created if it does not exist, so it’s safe to call them again for a tenant that already has a pool.
await eventStore.ensureCollection('tenant-1');await snapshotStore.ensureCollection('tenant-1');For example, in a service that provisions new tenants. The
EventStoreandSnapshotStorecan be injected anywhere, see Module configuration.import { Injectable } from '@nestjs/common';import { EventStore, SnapshotStore } from '@ocoda/event-sourcing';@Injectable()export class TenantProvisioningService {constructor(private readonly eventStore: EventStore,private readonly snapshotStore: SnapshotStore,) {}async provision(tenantId: string): Promise<void> {await this.eventStore.ensureCollection(tenantId);await this.snapshotStore.ensureCollection(tenantId);}} -
Provide a pool when interacting with the event-store or snapshot-repository
Section titled “Provide a pool when interacting with the event-store or snapshot-repository”The library doesn’t resolve the tenant for you, so determine it in your application (e.g. from a request header or a token) and pass it as the pool.
async getById(accountId: AccountId, tenantId: string): Promise<Account> {const eventStream = EventStream.for<Account>(Account, accountId);const account = await this.accountSnapshotRepository.load(accountId, tenantId);const eventCursor = this.eventStore.getEvents(eventStream, {fromVersion: account.version + 1,pool: tenantId,});await account.loadFromHistory(eventCursor);return account;}async save(account: Account, tenantId: string): Promise<void> {const events = account.getUncommittedEvents();const stream = EventStream.for<Account>(Account, account.id);await this.eventStore.appendEvents(stream, events, {expectedVersion: account.committedVersion,pool: tenantId,});account.markCommitted(events);await this.accountSnapshotRepository.save(account.id, account, tenantId);}
MIT 2026 © Ocoda.