During my internship, I was given a very interesting task — to learn and implement multitenancy in a full-stack application using NestJS for the backend, Prisma as the ORM, and React (TypeScript) for the frontend.
This was my first real dive into the world of SaaS architecture, and it turned out to be one of the most valuable lessons I’ve learned so far.
What Is Multitenancy (and Why It Matters)
Before I started writing any code, I had to understand what multitenancy actually means.
Simply put, multitenancy is a software architecture where a single instance of an application serves multiple customers (called tenants), while keeping their data separated and secure.
Think of it like:
One app, one database, but many independent organizations using it at the same time.
For example:
A company A’s users should only see their company’s products.
Company B’s users should only see their data — even though both are using the same backend and database.
Common Multitenancy Strategies
While researching, I found that there are three main strategies to implement multitenancy:
Database per tenant – every tenant gets a separate database.
Schema per tenant – all tenants share a database, but each has a different schema.
Shared schema with a tenant ID – all tenants share the same schema, but every record includes a tenantId.
For this project, I used the third approach (shared schema with tenantId) because it’s simple, efficient, and perfect for learning.
The Stack I Used
Backend: NestJS + Prisma + MongoDB
Frontend: React (TypeScript)
Auth: JWT Authentication
Tenancy Context: A custom TenantService + TenantContextInterceptor
How I Implemented It
- Tenant Context Handling
I created a TenantService that stores the shopId (which represents the tenant) for each request.
Then, I used a NestJS interceptor called TenantContextInterceptor to extract the tenant ID from the authenticated user:
`@Injectable()
export class TenantContextInterceptor implements NestInterceptor {
constructor(private readonly tenantService: TenantService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable {
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user || !user.shopId) {
throw new InternalServerErrorException(
'Tenant context (shopId) not found for authenticated user.',
);
}
this.tenantService.setShopId(user.shopId);
return next.handle();
}
}
This interceptor automatically sets the correct tenant context before any controller runs.
- Filtering Data by Tenant
Inside the ProductsService, I made sure every operation is filtered by the tenant’s shopId.
That means no user can ever access another tenant’s products:
async findAll(): Promise {
const shopId = this.getShopId();
return this.prisma.product.findMany({
where: { shopId },
});
}
`
And when creating a product:async create(createProductDto: CreateProductDto): Promise {
const shopId = this.getShopId();
return this.prisma.product.create({
data: {
...createProductDto,
shopId,
},
});
}
4. Frontend Integration
On the frontend, I used React (TypeScript) with an AuthContext that stores the authenticated user and their shopId.
Every request made through my productService automatically includes the user’s token — and since the backend extracts the shopId from it, the data stays tenant-specific.
💡 What I Learned
Building this taught me how multitenancy works in practice, especially in SaaS environments.
The key lessons I learned:
Always isolate tenant data — never trust the client side alone.
Middleware and interceptors are perfect places for tenant context logic.
Prisma makes it simple to filter by tenant fields efficiently.
React’s context and hooks make the frontend tenant-aware in a clean way.
🚀 Final Thoughts
This task really helped me connect the dots between theory and real-world implementation.
I now have a clear understanding of how SaaS applications manage multiple organizations securely within a single app.
If you’re starting out, I’d highly recommend trying the shared-schema multitenancy approach first — it’s simple, scalable, and helps you understand the fundamentals deeply.
Top comments (0)