Storage & Caching
Storage is optional. When configured, MTModule maintains a registry of known tenants. Unknown tenants are rejected before allowTenant is even called.
Storage interface
Any object implementing the following interface can be passed as storage:
interface Storage<T> {
add(tenant: string, settings?: T): Promise<TenantEntity<T>>;
remove(tenant: string): Promise<number>;
exists(tenant: string): Promise<Boolean>;
updateSettings(tenant: string, settings: T): Promise<TenantEntity<T>>;
get(tenant?: string): Promise<TenantEntity<T> | Array<TenantEntity<T>>>;
}
interface TenantEntity<T> {
tenant: string;
settings: T;
}
MTService.storage exposes the configured storage instance so you can call it directly from application services.
Sequelize storage
Setup model
Extend TenantsStorageSequelizeModel to create your own storage model:
// models/tenants-storage.model.ts
import { TenantsStorageSequelizeModel } from 'nestjs-mtenant';
export class TenantsStorage extends TenantsStorageSequelizeModel<TenantsStorage> {}
Register the model in your Sequelize connection, then reference it in MTModule:
import { MTModule, SEQUELIZE_STORAGE } from 'nestjs-mtenant';
MTModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
for: [User, Book],
storage: SEQUELIZE_STORAGE,
storageRepository: TenantsStorage,
}),
})
The SEQUELIZE_STORAGE constant equals the string 'sequelize'.
Table schema
| Column | Type | Notes |
|---|---|---|
id |
INTEGER | Primary key, auto-increment |
tenant |
VARCHAR | Unique, not null |
settings |
BLOB | Up to 64 KB of JSON |
createdAt |
DATETIME | Auto-managed |
updatedAt |
DATETIME | Auto-managed |
deletedAt |
DATETIME | Soft-delete (paranoid) |
TypeORM storage
Setup entity
Import and register TenantsStorageTypeOrmEntity in your DataSource:
import { TenantsStorageTypeOrmEntity } from 'nestjs-mtenant';
const dataSource = new DataSource({
entities: [User, Book, TenantsStorageTypeOrmEntity],
// ...
});
Reference it in MTModule:
import { MTModule, TYPEORM_STORAGE } from 'nestjs-mtenant';
MTModule.forRoot({
for: [User, Book],
storage: TYPEORM_STORAGE,
storageRepository: TenantsStorageTypeOrmEntity,
dataSource,
})
The TYPEORM_STORAGE constant equals the string 'typeorm'.
Table schema
| Column | Type | Notes |
|---|---|---|
id |
INTEGER | Primary key, auto-generated |
tenant |
VARCHAR | Unique, not null |
settings |
TEXT | JSON |
createdAt |
DATETIME | Auto-managed |
updatedAt |
DATETIME | Auto-managed |
deletedAt |
DATETIME | Soft-delete |
Custom storage
Pass any object that satisfies the Storage<T> interface:
import { Storage, TenantEntity } from 'nestjs-mtenant';
class InMemoryStorage<T> implements Storage<T> {
private store = new Map<string, TenantEntity<T>>();
async add(tenant: string, settings?: T): Promise<TenantEntity<T>> {
const entry = { tenant, settings: settings ?? ({} as T) };
this.store.set(tenant, entry);
return entry;
}
async remove(tenant: string): Promise<number> {
return this.store.delete(tenant) ? 1 : 0;
}
async exists(tenant: string): Promise<boolean> {
return this.store.has(tenant);
}
async updateSettings(tenant: string, settings: T): Promise<TenantEntity<T>> {
const entry = { tenant, settings };
this.store.set(tenant, entry);
return entry;
}
async get(tenant?: string) {
if (tenant) return this.store.get(tenant)!;
return [...this.store.values()];
}
}
MTModule.forRoot({
for: [User],
storage: new InMemoryStorage(),
})
IoRedis caching
Add a caching layer over any storage backend to avoid hitting the database on every request. The cache stores exists and get results keyed by tenant name.
Setup
import { MTModule, SEQUELIZE_STORAGE, IOREDIS_CACHE } from 'nestjs-mtenant';
import IORedis from 'ioredis';
MTModule.forRootAsync({
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
for: [User, Book],
storage: SEQUELIZE_STORAGE,
storageRepository: TenantsStorage,
cache: IOREDIS_CACHE,
cacheClient: new IORedis({ host: config.get('REDIS_HOST') }),
cacheOptions: {
expire: 600, // TTL in seconds (default: 3600)
prefix: 'MY_APP_MTENANT_CACHE/', // key prefix (default: 'MTENANT_PCACHE/')
},
}),
})
The IOREDIS_CACHE constant equals the string 'ioredis'.
Cache invalidation
The cache is invalidated automatically on add(), remove(), and updateSettings(). You do not need to manage cache keys manually.
Custom cache
Implement the Cache interface to plug in any caching backend:
import { Cache } from 'nestjs-mtenant';
class NodeCacheAdapter implements Cache {
constructor(private readonly client: NodeCache) {}
set(key: string, value: string, expire?: number): boolean {
return this.client.set(key, value, expire ?? 0);
}
has(key: string): boolean {
return this.client.has(key);
}
get(key: string): string {
return this.client.get<string>(key) ?? '';
}
remove(key: string): boolean {
this.client.del(key);
return true;
}
}
MTModule.forRoot({
for: [User],
storage: SEQUELIZE_STORAGE,
storageRepository: TenantsStorage,
cache: new NodeCacheAdapter(new NodeCache()),
})
Using storage from application code
Inject MTService to interact with the tenant registry from your own services:
import { Injectable } from '@nestjs/common';
import { MTService, StoredTenantEntity } from 'nestjs-mtenant';
import { TenantSettingsDto } from './tenant-settings.dto';
@Injectable()
export class TenancyService {
constructor(private readonly mt: MTService) {}
add(tenant: string, settings: TenantSettingsDto) {
return this.mt.storage.add(tenant, settings);
}
remove(tenant: string) {
return this.mt.storage.remove(tenant);
}
exists(tenant: string) {
return this.mt.storage.exists(tenant);
}
get(tenant?: string) {
return this.mt.storage.get(tenant);
}
updateSettings(tenant: string, settings: TenantSettingsDto) {
return this.mt.storage.updateSettings(tenant, settings);
}
}