Introduction
Multi-tenancy allows a single application to serve multiple clients (tenants) while keeping their data isolated. This approach is essential for SaaS products to reduce costs and simplify maintenance. Instead of deploying one instance per client, a tenant identifier is used to filter access. This tutorial walks you through implementing a simple and secure solution with Prisma and TypeScript.
Prerequisites
- Node.js 20+
- Basic knowledge of TypeScript
- PostgreSQL installed
- npm or yarn
Project Initialization
mkdir multi-tenancy-tutorial
cd multi-tenancy-tutorial
npm init -y
npm install express prisma @prisma/client
npx prisma initThis command creates the project and installs the required dependencies. Prisma will manage the database schema with the tenantId field for data isolation.
Prisma Schema with Tenant
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
tenantId String
name String
}The User model includes a required tenantId field. Every query will automatically filter on this field to isolate data by client.
Isolation Middleware
import { Request, Response, NextFunction } from 'express';
export const tenantMiddleware = (req: Request, res: Response, next: NextFunction) => {
const tenantId = req.headers['x-tenant-id'] as string;
if (!tenantId) {
return res.status(400).json({ error: 'Tenant ID requis' });
}
(req as any).tenantId = tenantId;
next();
};This middleware extracts the tenantId from headers and attaches it to the request. It ensures every call is associated with a valid tenant.
Prisma Configuration with Tenant
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const getTenantPrisma = (tenantId: string) => {
return prisma.$extends({
query: {
user: {
findMany: ({ args }) => {
args.where = { ...args.where, tenantId };
return args;
}
}
}
});
};This Prisma extension automatically injects the tenantId filter on queries. It prevents manual filter omissions and strengthens security.
Example Route with Isolation
import express from 'express';
import { tenantMiddleware } from '../middleware/tenant';
import { getTenantPrisma } from '../prisma';
const router = express.Router();
router.use(tenantMiddleware);
router.get('/', async (req, res) => {
const tenantId = (req as any).tenantId;
const prismaTenant = getTenantPrisma(tenantId);
const users = await prismaTenant.user.findMany();
res.json(users);
});
export default router;The route applies the middleware then uses the extended Prisma instance. Results are automatically limited to the current tenant.
Best Practices
- Always validate the tenantId on input
- Use Prisma extensions to centralize logic
- Test isolation with multiple tenants
- Add indexes on tenantId for performance
- Log tenant access for auditing
Common Mistakes
- Forgetting the tenantId filter in a manual query
- Storing the tenantId in the body instead of headers
- Not handling cases where tenantId is missing
- Ignoring performance on large tables without indexes
Going Further
Deepen your knowledge with our dedicated advanced multi-tenancy course. Discover our Learni trainings.