js

How to Build Multi-Tenant SaaS Authentication with NestJS, Prisma, JWT and RBAC

Learn to build secure multi-tenant SaaS auth with NestJS, Prisma & JWT. Complete guide covers tenant isolation, RBAC, and scalable architecture.

How to Build Multi-Tenant SaaS Authentication with NestJS, Prisma, JWT and RBAC

I’ve been thinking about multi-tenant systems lately while designing a new SaaS product. The challenge? Creating one application that securely serves multiple clients with completely isolated data. If you’re building scalable software, this approach saves resources while maintaining security. Today I’ll show you how I built a production-ready authentication system using NestJS, Prisma, and JWT.

When starting, I asked: How can we efficiently separate tenant data without duplicating code? The solution lies in middleware and database design. We’ll use a shared database with separate schemas, identified through subdomains or headers. This keeps costs manageable while preventing data leaks between clients.

First, let’s set up the foundation:

npm i @nestjs/jwt passport-jwt @prisma/client bcryptjs
npx prisma init

The database schema defines our multi-tenant structure. Notice how every user, role, and token links to a specific tenant:

model Tenant {
  id        String   @id @default(cuid())
  subdomain String   @unique
  users     User[]
}

model User {
  id       String @id @default(cuid())
  email    String
  tenant   Tenant @relation(fields: [tenantId], references: [id])
  tenantId String
}

Ever wonder how the system knows which tenant you’re accessing? The middleware extracts tenant context from incoming requests:

// tenant.middleware.ts
@Injectable()
export class TenantMiddleware implements NestMiddleware {
  async use(req: Request, res: Response, next: NextFunction) {
    const tenantId = req.headers['x-tenant-id'] || req.hostname.split('.')[0];
    const tenant = await this.prisma.tenant.findUnique({ 
      where: { subdomain: tenantId }
    });
    
    if (!tenant) throw new BadRequestException('Invalid tenant');
    req['tenant'] = tenant;
    next();
  }
}

Now the authentication service. We’ll implement secure password handling and token generation:

// auth.service.ts
@Injectable()
export class AuthService {
  async login(email: string, password: string, tenantId: string) {
    const user = await this.prisma.user.findFirst({
      where: { email, tenantId }
    });
    
    if (!user || !bcrypt.compareSync(password, user.password)) {
      throw new UnauthorizedException();
    }
    
    return this.generateTokens(user, tenantId);
  }
  
  private generateTokens(user: User, tenantId: string) {
    const payload = { 
      sub: user.id, 
      tenant: tenantId,
      email: user.email 
    };
    
    return {
      access_token: this.jwtService.sign(payload),
      refresh_token: this.jwtService.sign(payload, { expiresIn: '7d' })
    };
  }
}

What happens if a user tries accessing another tenant’s data? Our JWT strategy adds tenant validation:

// jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private tenantService: TenantService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  async validate(payload: any) {
    const validTenant = await this.tenantService.isActive(payload.tenant);
    if (!validTenant) throw new UnauthorizedException();
    return { userId: payload.sub, tenant: payload.tenant };
  }
}

For role-based access, we create a custom guard that checks permissions:

// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private prisma: PrismaService
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    const userRoles = await this.prisma.userRole.findMany({
      where: { userId: user.userId },
      include: { role: true }
    });

    return userRoles.some(ur => requiredRoles.includes(ur.role.name));
  }
}

Testing is critical. I always verify these scenarios:

  • Can users from Tenant A access Tenant B’s data?
  • Do refresh tokens work only within their original tenant?
  • Are admin roles restricted to their own tenant?

Performance tip: Add indexing on tenant columns. Prisma makes this straightforward:

model User {
  tenantId String
  @@index([tenantId])
}

Common pitfalls I’ve encountered:

  • Forgetting tenant context in background jobs
  • Caching tokens without tenant isolation
  • Not auditing cross-tenant access attempts

Building this system taught me that security and scalability aren’t mutually exclusive. With proper tenant isolation at every layer - database, middleware, and tokens - we create robust SaaS applications. What other multi-tenant challenges have you faced?

If you found this guide helpful, share it with your team. Have questions or improvements? Let’s discuss in the comments below - your real-world experiences help everyone build better systems.

Keywords: multi-tenant SaaS authentication, NestJS multi-tenant architecture, Prisma JWT authentication, tenant isolation database, role-based access control RBAC, JWT refresh tokens implementation, NestJS Prisma integration, SaaS authentication system, multi-tenant security middleware, scalable tenant management



Similar Posts
Blog Image
How to Build Production-Ready GraphQL API with NestJS, Prisma, Redis Caching

Build a production-ready GraphQL API with NestJS, Prisma, and Redis. Learn authentication, caching, subscriptions, and optimization techniques.

Blog Image
How to Build High-Performance GraphQL Subscriptions with Apollo Server, Redis, and PostgreSQL

Learn to build real-time GraphQL subscriptions with Apollo Server 4, Redis PubSub, and PostgreSQL. Complete guide with authentication, scaling, and production deployment tips.

Blog Image
Build Scalable Real-time Apps with Socket.io Redis Adapter and TypeScript in 2024

Learn to build scalable real-time apps with Socket.io, Redis adapter & TypeScript. Master chat rooms, authentication, scaling & production deployment.

Blog Image
Complete Guide to Event-Driven Microservices with Node.js, TypeScript, and Apache Kafka

Master event-driven microservices with Node.js, TypeScript, and Apache Kafka. Complete guide covers distributed systems, Saga patterns, CQRS, monitoring, and production deployment. Build scalable architecture today!

Blog Image
Build Complete Multi-Tenant SaaS API with NestJS Prisma PostgreSQL Row-Level Security Tutorial

Learn to build a secure multi-tenant SaaS API using NestJS, Prisma & PostgreSQL Row-Level Security. Complete guide with tenant isolation, authentication & performance optimization.

Blog Image
Build Production-Ready GraphQL API with NestJS, Prisma, PostgreSQL: Authentication, Real-time Subscriptions & Deployment Guide

Learn to build a production-ready GraphQL API with NestJS, Prisma, and PostgreSQL. Includes JWT authentication, real-time subscriptions, and deployment guide.