js

Complete Multi-Tenant SaaS Guide: NestJS, Prisma, PostgreSQL Row-Level Security from Setup to Production

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Master tenant isolation, security & architecture. Start building now!

Complete Multi-Tenant SaaS Guide: NestJS, Prisma, PostgreSQL Row-Level Security from Setup to Production

I’ve been thinking a lot lately about what separates a simple application from a truly robust SaaS platform. The complexity isn’t just in the features—it’s in how you handle multiple customers securely and efficiently. That’s why I want to walk you through building a multi-tenant system with NestJS, Prisma, and PostgreSQL’s row-level security. This combination creates a foundation that scales while keeping data completely isolated between customers.

Let me show you how to set this up properly.

First, we need to establish our database schema with tenant isolation built into its core. Every table that contains tenant-specific data requires a tenantId field. This becomes our anchor point for data separation.

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

But how do we ensure that users from one tenant can’t accidentally access another tenant’s data? That’s where PostgreSQL’s row-level security comes into play.

ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation_policy ON users
  USING (tenant_id = current_setting('app.current_tenant_id')::text);

Now, what happens when a request comes in? We need to identify which tenant it belongs to and set that context before any database operation. This is where middleware becomes crucial.

@Injectable()
export class TenantMiddleware implements NestMiddleware {
  constructor(private readonly configService: ConfigService) {}

  use(req: Request, res: Response, next: NextFunction) {
    const tenantId = this.extractTenantFromRequest(req);
    if (tenantId) {
      req['tenantId'] = tenantId;
    }
    next();
  }
}

Have you considered how this middleware integrates with your database operations? We need a Prisma client that’s aware of the current tenant context.

@Injectable()
export class TenantPrismaService {
  constructor(
    private readonly configService: ConfigService,
    private readonly request: Request
  ) {}

  getClient(): PrismaClient {
    const tenantId = this.request['tenantId'];
    const client = new PrismaClient();
    
    // Set the tenant context for this connection
    client.$executeRaw`SELECT set_config('app.current_tenant_id', ${tenantId}, true)`;
    
    return client;
  }
}

What about authentication and authorization? We need guards that understand multi-tenancy.

@Injectable()
export class TenantGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const userTenantId = request.user.tenantId;
    const requestTenantId = request.tenantId;

    return userTenantId === requestTenantId;
  }
}

Testing becomes particularly important in a multi-tenant environment. You need to verify that data isolation works as expected.

describe('Multi-tenant Data Isolation', () => {
  it('should not allow cross-tenant data access', async () => {
    const tenantAClient = await createTestClient('tenant-a');
    const tenantBClient = await createTestClient('tenant-b');

    // Create data in tenant A
    await tenantAClient.user.create({ data: { email: '[email protected]' } });

    // Try to access from tenant B
    const result = await tenantBClient.user.findMany();
    expect(result).toHaveLength(0);
  });
});

Performance considerations are different in multi-tenant systems. Database indexes need special attention.

model User {
  id       String @id @default(cuid())
  email    String
  tenantId String
  
  @@index([tenantId])
  @@index([email, tenantId])
}

What about database migrations? They need to handle both schema changes and RLS policies.

async function runMigrations() {
  // Schema migrations
  await prisma.$executeRaw`ALTER TABLE users ADD COLUMN IF NOT EXISTS new_column TEXT`;
  
  // RLS policy updates
  await prisma.$executeRaw`
    CREATE POLICY IF NOT EXISTS user_select_policy ON users
    FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id')::text)
  `;
}

Error handling requires special consideration too. You don’t want to leak tenant information in error messages.

@Catch(PrismaClientKnownRequestError)
export class PrismaClientExceptionFilter implements ExceptionFilter {
  catch(exception: PrismaClientKnownRequestError, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse();
    
    // Sanitize error messages to remove tenant-specific information
    const safeMessage = this.sanitizeErrorMessage(exception.message);
    
    response.status(500).json({
      error: 'Database operation failed',
      message: safeMessage
    });
  }
}

Building a multi-tenant application requires thinking about every layer of your stack. From database design to API endpoints, each component must respect tenant boundaries. The patterns I’ve shown here provide a solid foundation, but remember that every application has unique requirements.

What challenges have you faced with multi-tenancy? I’d love to hear about your experiences and solutions. If you found this helpful, please share it with others who might benefit from these patterns. Your comments and questions help make these guides better for everyone.

Keywords: multi-tenant SaaS NestJS, PostgreSQL row-level security, Prisma multi-tenancy, NestJS tenant isolation, SaaS application architecture, PostgreSQL RLS tutorial, multi-tenant database design, NestJS Prisma integration, tenant-aware middleware, SaaS security best practices



Similar Posts
Blog Image
Complete Guide: Building Full-Stack Applications with Next.js and Prisma Integration in 2024

Learn how to integrate Next.js with Prisma for powerful full-stack development. Build type-safe apps with seamless database operations. Start today!

Blog Image
Complete Guide to Building Multi-Tenant SaaS Applications with NestJS, Prisma and PostgreSQL RLS Security

Learn to build secure multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Complete guide with authentication, tenant isolation & performance tips.

Blog Image
Node.js Event-Driven Microservices: Complete RabbitMQ MongoDB Architecture Tutorial 2024

Learn to build scalable event-driven microservices with Node.js, RabbitMQ & MongoDB. Master message queues, Saga patterns, error handling & deployment strategies.

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
Complete Guide to Integrating Next.js with Prisma ORM for Full-Stack TypeScript Applications

Learn how to integrate Next.js with Prisma ORM for powerful full-stack development. Build type-safe applications with seamless database management and API routes.

Blog Image
Build High-Performance File Upload System: Multer, Sharp, AWS S3 in Node.js

Build a high-performance Node.js file upload system with Multer, Sharp & AWS S3. Learn secure uploads, image processing, and scalable storage solutions.