Skip to content

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:

  1. 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: false on 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 {}
  2. 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 EventStore and SnapshotStore can 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);
    }
    }
  3. 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);
    }