The fastest way to add multi-tenancy to your Node.js application β‘
Stop spending weeks implementing multi-tenancy from scratch. Get production-ready tenant isolation, automatic data filtering, and enterprise-grade authorization in minutes, not months.
# Get started in 30 seconds
npm install @saaskit/multitenancy-core|
β Before: The Pain
|
β After: Pure Joy
|
1. Install the toolkit:
npm install @saaskit/multitenancy-core @saaskit/multitenancy-adapters2. Add one middleware:
import { createTenantMiddleware } from '@saaskit/multitenancy-core';
app.use(createTenantMiddleware({
resolution: { type: 'subdomain' },
dataStore: yourDataStore // We'll show you how!
}));3. That's it! π Your app now has:
- β Automatic tenant detection
- β Secure context isolation
- β Ready for multi-tenancy
π Smart Tenant Context - Automatic tenant detection & isolation
- AsyncLocalStorage magic - Context follows your requests everywhere
- Multiple resolution strategies - Subdomain, header, JWT, or custom
- Zero performance overhead - Built for production scale
- Type-safe everywhere - Full TypeScript support
π Bulletproof Data Isolation - Never leak tenant data again
- ORM integrations - Prisma, Sequelize, Mongoose
- Automatic query filtering - Set it once, works everywhere
- Multi-database support - Separate databases per tenant
- Admin override - Safe cross-tenant operations
π₯ Enterprise Authorization - RBAC + ABAC in one package
- Pre-built roles - Admin, member, viewer out of the box
- Flexible permissions - Fine-grained access control
- Policy engine - Complex authorization rules made simple
- Audit logging - Track every action automatically
π― Framework Freedom - Works with your favorite stack
- Express - Drop-in middleware
- NestJS - Decorators and guards
- Fastify - High-performance plugins
- Any Node.js app - Framework-agnostic core
// 1. Setup your data store (implement once, use everywhere)
const tenantDataStore = {
async getTenantById(id: string) {
return await prisma.tenant.findUnique({ where: { id } });
},
async getTenantBySubdomain(subdomain: string) {
return await prisma.tenant.findUnique({ where: { subdomain } });
},
async getUserTenant(userId: string, tenantId: string) {
return await prisma.tenantUser.findUnique({
where: { userId_tenantId: { userId, tenantId } },
include: { roles: true }
});
}
};
// 2. Add tenant middleware (handles everything automatically)
app.use(createTenantMiddleware({
resolution: { type: 'subdomain' },
dataStore: tenantDataStore,
allowNoTenant: false // Strict tenant isolation
}));
// 3. Apply Prisma adapter (queries auto-filtered by tenant)
import { applyPrismaAdapter } from '@saaskit/multitenancy-adapters';
applyPrismaAdapter(prisma, {
tenantField: 'tenantId',
models: ['User', 'Project', 'Task', 'Invoice']
});
// 4. Use anywhere - tenant context is automatic!
app.get('/api/projects', async (req, res) => {
// Only returns projects for current tenant - automatically!
const projects = await prisma.project.findMany();
res.json(projects);
});
// 5. Add role-based protection
app.delete('/api/projects/:id',
requireRole('admin', 'project-manager'),
async (req, res) => {
await prisma.project.delete({ where: { id: req.params.id } });
res.json({ success: true });
}
);// app.module.ts
import { MultitenancyModule } from '@saaskit/multitenancy-nestjs';
@Module({
imports: [
MultitenancyModule.forRoot({
resolution: { type: 'header', headerName: 'x-tenant-id' },
dataStore: TypeOrmTenantDataStore
})
]
})
export class AppModule {}
// projects.controller.ts
@Controller('projects')
@UseGuards(TenantAuthGuard)
export class ProjectsController {
@Get()
@Roles('member', 'admin')
async findAll(@TenantContext() context: TenantContext) {
// Automatically filtered by tenant
return this.projectsService.findAll();
}
@Delete(':id')
@Permissions('projects:delete')
async remove(@Param('id') id: string) {
return this.projectsService.remove(id);
}
}// Register the plugin
await fastify.register(require('@saaskit/multitenancy-fastify'), {
resolution: { type: 'subdomain' },
dataStore: mongoTenantDataStore
});
// Apply Mongoose plugin globally
import { mongooseTenantPlugin } from '@saaskit/multitenancy-adapters';
mongoose.plugin(mongooseTenantPlugin, {
tenantField: 'tenantId',
indexTenant: true // Automatic indexing
});
// Use in routes
fastify.get('/api/users', {
preHandler: [fastify.requireTenant, fastify.requireRole('admin')]
}, async (request, reply) => {
// Auto-filtered by tenant
const users = await User.find();
return users;
});Choose the strategy that fits your architecture:
| Strategy | Use Case | Example | Setup |
|---|---|---|---|
| π Subdomain | Customer-facing SaaS | acme.yoursaas.com |
{
resolution: {
type: 'subdomain'
}
} |
| π Header | API-first, mobile apps | X-Tenant-ID: acme |
{
resolution: {
type: 'header',
headerName: 'x-tenant-id'
}
} |
| π« JWT Token | Microservices, SPAs | { tenantId: "acme" } |
{
resolution: {
type: 'token',
tokenClaim: 'tenantId'
}
} |
| βοΈ Custom | Complex routing logic | API key, path, etc. |
{
resolution: {
type: 'custom',
customResolver: async (req) => {
const apiKey = req.headers['x-api-key'];
const client = await getClientByApiKey(apiKey);
return client?.tenantId;
}
}
} |
import { RoleManager } from '@saaskit/multitenancy-auth';
const roleManager = new RoleManager({
roles: [
{
name: 'admin',
permissions: [
'users:*', // All user operations
'projects:*', // All project operations
'billing:*', // Billing management
'settings:*' // Tenant settings
]
},
{
name: 'project-manager',
permissions: [
'users:read',
'projects:*', // Full project access
'tasks:*'
]
},
{
name: 'member',
permissions: [
'users:read',
'projects:read',
'projects:write', // Can edit projects
'tasks:*'
]
},
{
name: 'viewer',
permissions: [
'users:read',
'projects:read',
'tasks:read'
]
}
]
});// Set up permission inheritance
roleManager.setPermissionHierarchy({
'users:manage': ['users:read', 'users:write', 'users:delete'],
'projects:manage': ['projects:read', 'projects:write', 'projects:delete'],
'admin:*': ['users:manage', 'projects:manage', 'billing:manage']
});
// Now 'admin:*' automatically includes all sub-permissions!import { PolicyEngine } from '@saaskit/multitenancy-auth';
const policyEngine = new PolicyEngine();
// Resource ownership policy
policyEngine.registerPolicy({
id: 'resource-ownership',
name: 'Users can manage their own resources',
effect: 'allow',
actions: ['read', 'write', 'delete'],
resources: ['project', 'task', 'document'],
conditions: [{
attribute: 'resource.attributes.ownerId',
operator: 'eq',
value: '${subject.id}' // Dynamic value
}]
});
// Time-based access policy
policyEngine.registerPolicy(
PolicyEngine.createTimeBasedPolicy(
'business-hours',
'Allow admin access only during business hours',
['admin:*'],
['*'],
{ start: '09:00', end: '17:00', timezone: 'UTC' }
)
);
// IP-based restrictions
policyEngine.registerPolicy({
id: 'ip-whitelist',
name: 'Restrict admin access to office IPs',
effect: 'allow',
actions: ['admin:*'],
resources: ['*'],
conditions: [{
attribute: 'environment.ipAddress',
operator: 'in',
value: ['192.168.1.0/24', '10.0.0.0/8']
}]
});// schema.prisma
model User {
id String @id @default(cuid())
email String
tenantId String // Required field
@@unique([email, tenantId])
@@index([tenantId])
}
model Project {
id String @id @default(cuid())
name String
tenantId String // Required field
ownerId String
owner User @relation(fields: [ownerId], references: [id])
@@index([tenantId])
@@index([tenantId, ownerId])
}// Apply the adapter
import { applyPrismaAdapter } from '@saaskit/multitenancy-adapters';
applyPrismaAdapter(prisma, {
tenantField: 'tenantId',
models: ['User', 'Project', 'Task'], // Auto-filtered models
exclude: ['SystemLog'], // Skip certain models
onViolation: 'throw' // or 'warn' for development
});
// All queries now automatically filtered by tenant!
const users = await prisma.user.findMany(); // Only current tenant's users
const projects = await prisma.project.findMany(); // Only current tenant's projects// models/User.js
import { DataTypes } from 'sequelize';
import { applySequelizeAdapter } from '@saaskit/multitenancy-adapters';
const User = sequelize.define('User', {
email: DataTypes.STRING,
tenantId: {
type: DataTypes.STRING,
allowNull: false
}
});
// Apply tenant filtering
applySequelizeAdapter(User, {
tenantField: 'tenantId',
autoScope: true
});
// Usage - automatically scoped to tenant
const users = await User.findAll(); // Only current tenant's users// models/User.js
import mongoose from 'mongoose';
import { mongooseTenantPlugin } from '@saaskit/multitenancy-adapters';
const userSchema = new mongoose.Schema({
email: String,
name: String
// tenantId added automatically by plugin
});
// Apply the plugin
userSchema.plugin(mongooseTenantPlugin, {
tenantField: 'tenantId',
indexTenant: true,
autoPopulate: true
});
const User = mongoose.model('User', userSchema);
// Usage - automatically scoped
const users = await User.find(); // Only current tenant's usersPerfect for enterprise customers who need dedicated databases:
import { TenantConnectionManager } from '@saaskit/multitenancy-adapters';
const connectionManager = new TenantConnectionManager({
default: process.env.DEFAULT_DATABASE_URL,
resolver: async (tenantId: string) => {
const tenant = await getTenant(tenantId);
return tenant.dedicatedDb ? tenant.databaseUrl : 'default';
},
pooling: {
max: 10,
idleTimeout: 30000
}
});
// Use in your middleware
app.use(async (req, res, next) => {
const tenantId = getCurrentTenantId();
const connection = await connectionManager.getConnection(tenantId);
req.db = connection;
next();
});import { TenantManager } from '@saaskit/multitenancy-core';
const tenantManager = new TenantManager({
onCreate: async (tenant) => {
// Setup default data, send welcome email, etc.
await setupDefaultData(tenant.id);
await sendWelcomeEmail(tenant.ownerEmail);
},
onSuspend: async (tenant) => {
// Cleanup resources, notify users
await cleanupResources(tenant.id);
},
onDelete: async (tenant) => {
// Full cleanup, data export, etc.
await exportTenantData(tenant.id);
await deleteTenantData(tenant.id);
}
});
// Create a new tenant
const newTenant = await tenantManager.create({
name: 'Acme Corp',
subdomain: 'acme',
plan: 'professional',
ownerEmail: '[email protected]'
});Track everything automatically:
import { AuditLogger } from '@saaskit/multitenancy-core';
const auditLogger = AuditLogger.getInstance({
store: new DatabaseAuditStore(prisma),
sensitiveFields: ['password', 'apiKey', 'secret']
});
// Automatic logging with decorator
@AuditLog('user:update')
async function updateUser(id: string, data: any) {
return await prisma.user.update({
where: { id },
data
});
}
// Manual logging
await auditLogger.log({
action: 'project:create',
resource: { type: 'project', id: project.id },
result: 'success',
metadata: { name: project.name }
});
// Query audit logs
const recentActivity = await auditLogger.query({
actions: ['user:login', 'user:logout'],
dateRange: { start: yesterday, end: now },
limit: 100
});π Tenant not detected / Context is undefined
Common causes:
- Middleware not registered or registered after routes
- Async context lost in callbacks
- Missing tenant data in resolution strategy
Solutions:
// β
Correct: Register middleware BEFORE routes
app.use(createTenantMiddleware(config));
app.use('/api', apiRoutes);
// β Wrong: Middleware after routes
app.use('/api', apiRoutes);
app.use(createTenantMiddleware(config));
// β
Preserve async context in callbacks
import { tenantContext } from '@saaskit/multitenancy-core';
setTimeout(() => {
// Use runAsync to preserve context
tenantContext.runAsync(currentContext, async () => {
const tenant = tenantContext.getCurrentTenant();
// ... your code
});
}, 1000);β‘ Performance Issues
Optimization tips:
- Enable connection pooling:
applyPrismaAdapter(prisma, {
tenantField: 'tenantId',
models: ['User', 'Project'],
caching: {
enabled: true,
ttl: 300 // 5 minutes
}
});- Add database indexes:
-- Always index tenant fields
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
CREATE INDEX idx_projects_tenant_id ON projects(tenant_id);
-- Composite indexes for common queries
CREATE INDEX idx_projects_tenant_owner ON projects(tenant_id, owner_id);- Use tenant-aware caching:
import { TenantCache } from '@saaskit/multitenancy-core';
const cache = new TenantCache(redisClient);
// Automatically scoped to current tenant
await cache.set('user-preferences', preferences);
const cached = await cache.get('user-preferences');π Data Leakage Prevention
Best practices:
- Always validate tenant access:
app.get('/api/projects/:id', async (req, res) => {
const project = await prisma.project.findUnique({
where: { id: req.params.id }
});
// β
Double-check tenant ownership
const currentTenant = tenantContext.getCurrentTenantId();
if (project.tenantId !== currentTenant) {
return res.status(404).json({ error: 'Project not found' });
}
res.json(project);
});- Use strict mode:
app.use(createTenantMiddleware({
resolution: { type: 'subdomain' },
dataStore: tenantDataStore,
allowNoTenant: false, // β
Strict: Reject requests without tenant
validateAccess: true // β
Validate user belongs to tenant
}));- Enable audit logging:
// Track all data access
@AuditLog('data:access')
async function getData() {
// Your data access code
}π§ͺ Testing Multi-Tenant Code
import { TenantContextManager } from '@saaskit/multitenancy-core';
describe('Multi-tenant API', () => {
const tenantContext = TenantContextManager.getInstance();
it('should isolate data by tenant', async () => {
// Setup test tenants
const tenant1 = { id: 'tenant1', name: 'Acme Corp' };
const tenant2 = { id: 'tenant2', name: 'Globex Corp' };
// Test with tenant1 context
await tenantContext.runAsync({ tenant: tenant1 }, async () => {
const projects = await getProjects();
expect(projects).toHaveLength(3); // tenant1 has 3 projects
});
// Test with tenant2 context
await tenantContext.runAsync({ tenant: tenant2 }, async () => {
const projects = await getProjects();
expect(projects).toHaveLength(1); // tenant2 has 1 project
});
});
it('should enforce role-based access', async () => {
const memberContext = {
tenant: { id: 'tenant1' },
user: { id: 'user1' },
roles: ['member']
};
await tenantContext.runAsync(memberContext, async () => {
await expect(deleteProject('project1')).rejects.toThrow('Insufficient permissions');
});
});
});Step-by-step migration from custom solution
Before: Manual tenant filtering
// β Before: Manual and error-prone
app.get('/api/users', async (req, res) => {
const tenantId = req.headers['x-tenant-id']; // Manual extraction
if (!tenantId) return res.status(400).json({ error: 'Missing tenant' });
const users = await prisma.user.findMany({
where: { tenantId: tenantId } // Manual filtering - easy to forget!
});
res.json(users);
});After: Automatic with SaaS Toolkit
// β
After: Automatic and bulletproof
app.use(createTenantMiddleware({
resolution: { type: 'header', headerName: 'x-tenant-id' },
dataStore: tenantDataStore
}));
applyPrismaAdapter(prisma, {
tenantField: 'tenantId',
models: ['User'] // Automatic filtering
});
app.get('/api/users', async (req, res) => {
const users = await prisma.user.findMany(); // Automatically filtered!
res.json(users);
});Migration steps:
- Install the toolkit
- Replace manual tenant extraction with middleware
- Apply ORM adapters
- Remove manual filtering from queries
- Add role-based authorization
- Test thoroughly
Migration guides for popular alternatives
From @clerk/backend:
// Before
import { clerkMiddleware } from '@clerk/backend';
app.use(clerkMiddleware);
// After: More control and flexibility
import { createTenantMiddleware } from '@saaskit/multitenancy-core';
app.use(createTenantMiddleware({
resolution: { type: 'token', tokenClaim: 'org_id' },
dataStore: clerkTenantDataStore
}));From @casl/ability:
// Before: Manual ability setup for each request
const ability = defineAbilityFor(user);
if (ability.cannot('delete', 'Project')) {
throw new ForbiddenError();
}
// After: Automatic context-aware authorization
import { requirePermission } from '@saaskit/multitenancy-core';
app.delete('/projects/:id', requirePermission('projects:delete'), handler);Before going live, ensure you have:
- Tenant isolation tested - Verify no data leakage between tenants
- Database indexes added - Index all
tenantIdfields for performance - Error handling configured - Proper error responses for invalid tenants
- Audit logging enabled - Track all sensitive operations
- Rate limiting per tenant - Prevent resource exhaustion
- Backup strategy - Tenant-aware backup and restore
- Monitoring set up - Track tenant-specific metrics
- Security review completed - Regular security audits
// production.ts
const config = {
tenant: {
resolution: { type: 'subdomain' },
dataStore: new CachedTenantDataStore(redis, database),
allowNoTenant: false,
validateAccess: true,
onError: (error, req, res) => {
logger.error('Tenant resolution failed', {
error: error.message,
ip: req.ip,
userAgent: req.get('user-agent')
});
res.status(400).json({ error: 'Invalid tenant configuration' });
}
},
database: {
pooling: { max: 20, idleTimeout: 30000 },
caching: { enabled: true, ttl: 300 },
indexing: { autoCreate: false } // Create indexes manually in production
},
audit: {
enabled: true,
store: new DatabaseAuditStore(prisma),
retention: { days: 365 },
sensitive: ['password', 'apiKey', 'token', 'secret']
},
security: {
rateLimit: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // Per tenant
keyGenerator: (req) => `${req.tenant?.id}:${req.ip}`
}
}
};Creates middleware for automatic tenant resolution and context management.
interface TenantMiddlewareOptions {
resolution: TenantResolutionOptions;
dataStore: TenantDataStore;
onError?: (error: Error, req: any, res: any) => void;
allowNoTenant?: boolean;
validateAccess?: boolean;
}Singleton for accessing current tenant context.
// Get current tenant
const tenant = tenantContext.getCurrentTenant();
const tenantId = tenantContext.getCurrentTenantId();
// Get current user and roles
const user = tenantContext.getCurrentUser();
const roles = tenantContext.getCurrentRoles();
const permissions = tenantContext.getCurrentPermissions();
// Check permissions
const canEdit = tenantContext.hasPermission('projects:write');
const isAdmin = tenantContext.hasRole('admin');// Require authentication
app.use(requireTenantAuth);
// Require specific roles (any of)
app.use(requireRole('admin', 'moderator'));
// Require specific permissions (any of)
app.use(requirePermission('users:read', 'users:write'));import { applyPrismaAdapter, createPrismaAdapter } from '@saaskit/multitenancy-adapters';
// Apply to existing client
applyPrismaAdapter(prisma, {
tenantField: 'tenantId',
models: ['User', 'Project'],
exclude: ['SystemLog'],
onViolation: 'throw' | 'warn' | 'ignore'
});
// Create new tenant-aware client
const TenantPrismaClient = createTenantPrismaClient(PrismaClient, options);
const prisma = new TenantPrismaClient();import { applySequelizeAdapter, TenantModel } from '@saaskit/multitenancy-adapters';
// Apply to specific model
applySequelizeAdapter(User, {
tenantField: 'tenantId',
autoScope: true
});
// Use base model class
class User extends TenantModel {
// Your model definition
}import { mongooseTenantPlugin, createTenantModel } from '@saaskit/multitenancy-adapters';
// Apply as plugin
userSchema.plugin(mongooseTenantPlugin, {
tenantField: 'tenantId',
indexTenant: true,
autoPopulate: false
});
// Create tenant-specific models
const TenantUser = createTenantModel(User, tenantId);const roleManager = new RoleManager({
roles: RoleDefinition[],
permissionHierarchy: Record<string, string[]>
});
// Role management
roleManager.registerRole(role);
roleManager.getRole(name);
roleManager.getRolePermissions(roleName);
roleManager.hasRole(name);
// Permission management
roleManager.setPermissionHierarchy(hierarchy);
roleManager.addPermissionImplication(parent, children);const policyEngine = new PolicyEngine();
// Policy management
policyEngine.registerPolicy(policy);
policyEngine.registerPolicies(policies);
policyEngine.evaluate(context);
// Policy templates
PolicyEngine.createOwnershipPolicy(resourceType);
PolicyEngine.createRolePolicy(role, actions, resources);
PolicyEngine.createTimeBasedPolicy(id, name, actions, resources, startTime, endTime);const authManager = new AuthorizationManager({
roleManager,
policyEngine
});
// Authorization checks
const canAccess = await authManager.can(action, resource);
const cannotAccess = await authManager.cannot(action, resource);
// Bulk checks
const permissions = await authManager.getPermissions(subject);
const roles = await authManager.getRoles(subject);We love contributions! Here's how to get started:
# Clone and setup
git clone https://github.com/saaskit/multitenancy.git
cd multitenancy
npm install
# Run tests
npm test
# Build packages
npm run build
# Try the example
cd examples/express-demo
npm run dev- π― New adapters - Add support for more ORMs/databases
- π§ Framework integrations - Add support for Koa, Hapi, etc.
- π Examples - More real-world examples and tutorials
- π Bug fixes - Check our issues
- π Documentation - Improve guides and API docs
- β‘ Performance - Optimize hot paths and memory usage
- Write tests for new features
- Follow TypeScript best practices
- Update documentation
- Add examples for new features
- Ensure backward compatibility
- GraphQL integration - Schema stitching and resolvers
- Admin dashboard - Web UI for tenant management
- Advanced caching - Redis integration and cache invalidation
- Tenant analytics - Usage metrics and insights
- Webhook system - Event-driven tenant lifecycle
- Python support - Django and FastAPI adapters
- Multi-region - Geographical tenant distribution
- Tenant migration - Tools for moving tenants between databases
- Advanced RBAC - Hierarchical roles and dynamic permissions
- Compliance tools - GDPR, SOC2, HIPAA helpers
Vote on features at our discussions!
MIT License - see LICENSE for details.
Free for commercial use β No attribution required β Modify as needed β
|
π Documentation docs.saaskit.dev |
π¬ Discord Join our community |
π Issues Report bugs |
π‘ Discussions Share ideas |
Need help with production deployment, custom features, or architecture review?
π§ [email protected]
We offer:
- π Architecture consulting - Design review and best practices
- β‘ Performance optimization - Scale to millions of tenants
- π Security audits - Comprehensive security review
- π Training - Team training and workshops
- π Custom development - Bespoke features and integrations
If this toolkit saved you weeks of development time, give us a star! β
It helps other developers discover the project and motivates us to keep improving it.
Built with β€οΈ by the SaaSKit team
Making multi-tenancy accessible to every developer