Reza Golkhandani // Senior Software Engineer | Backend platforms
Two NestJS Packages for Database-per-Tenant, and the Design Behind Them
NestJS | MongoDB | MySQL | Open source
Two NestJS Packages for Database-per-Tenant, and the Design Behind Them
The goal
As the first engineer at a startup building a restaurant management system, I needed tenancy that could not be forgotten. Row-level filtering depends on every developer remembering a clause on every query. The goal was isolation as a guarantee: a database per tenant, with the mechanics hidden so feature code never sees a tenant at all. The two packages that came out of it are on npm.
The approach
Resolve the tenant once per request in the framework layer, and hand every service a model that is already bound to the right database.
What I built
mongoose-tenancy-nest. A request-scoped provider reads the tenant identifier, switches the shared connection to that tenant's database using the driver's cached useDb, registers any dependent models the main model references, and returns a bound model. Services inject it by name.
static modelProvider(name, main, dependencies?) {
return {
scope: Scope.REQUEST,
provide: name,
useFactory: (tenancy, request) =>
tenancy.getModelForTenancy(request.headers.tenant, main, dependencies),
inject: [MongooseTenancy, 'REQUEST'],
};
}
Two details that matter: the database handle is cached, so switching is a lookup rather than a connection, and dependent models register on the same handle before the main one so population across collections works.
mysql2-nestjs. A dynamic module wrapping a connection pool, with register and registerAsync so options come from configuration or a factory. The pool is created lazily on first use and probed with a trivial query, so a bad configuration fails loudly at startup rather than on the first customer request. One injection token exposes it everywhere.
The result
The forgotten-filter class of bug disappeared, because there was nothing to forget. Per-customer backup became "copy this database". Both packages have been in use since and are small enough to read in one sitting. Today I would keep request scope to the model provider alone, take the tenant from a verified token claim, and bound the pool cache; all three are on the roadmap.
Takeaways
- Row-level tenancy is a convention. Database-per-tenant is a guarantee.
- Hide tenancy in the framework layer so feature code cannot get it wrong.
- Publish the small thing. Code people can read in a sitting gets better faster.