js

How to Build a Scalable Authorization System with NestJS, CASL, and PostgreSQL

Learn to implement a flexible, role-based authorization system using NestJS, CASL, and PostgreSQL that grows with your app.

How to Build a Scalable Authorization System with NestJS, CASL, and PostgreSQL

I’ve been thinking a lot about building secure applications lately. It’s one thing to let users log in, but it’s a whole different challenge to control what they can actually do once they’re inside. I’ve seen too many projects start with simple checks and then spiral into a mess of complex permission logic. That’s why I want to talk about building a solid authorization system from the ground up. Let’s build something that grows with your application, not something that holds it back.

Think about the last application you used. Could you edit other people’s documents? Could you see sensitive admin panels? The system that answers these questions is called authorization, and getting it right is crucial. I’m going to show you how to implement a flexible, powerful system using NestJS, CASL, and PostgreSQL. This approach combines the structure of roles with the precision of specific rules.

First, we need to set up our project. Make sure you have Node.js and PostgreSQL installed. We’ll start by creating a new NestJS application.

nest new rbac-demo
cd rbac-demo
npm install @casl/ability @nestjs/typeorm typeorm pg

Now, let’s configure our database connection. We’ll use TypeORM to manage our PostgreSQL database. Create a .env file for your database credentials.

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST,
      port: +process.env.DB_PORT,
      username: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
      autoLoadEntities: true,
      synchronize: true,
    }),
  ],
})
export class AppModule {}

The heart of our system will be the database design. We need entities for users, roles, permissions, and the resources we want to protect. Let’s start with the User entity.

// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Role } from './role.entity';

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  email: string;

  @Column()
  password: string;

  @ManyToMany(() => Role, { eager: true })
  @JoinTable()
  roles: Role[];
}

Next, we define roles and permissions. A role is like a job title, and permissions are the specific abilities that come with it.

// role.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Permission } from './permission.entity';

@Entity()
export class Role {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  name: string;

  @ManyToMany(() => Permission, { eager: true })
  @JoinTable()
  permissions: Permission[];
}

The Permission entity is where things get interesting. This is where we define what actions can be performed on what resources.

// permission.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

export enum Action {
  CREATE = 'create',
  READ = 'read',
  UPDATE = 'update',
  DELETE = 'delete',
  MANAGE = 'manage',
}

@Entity()
export class Permission {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'enum', enum: Action })
  action: Action;

  @Column()
  subject: string;

  @Column({ type: 'jsonb', nullable: true })
  conditions: any;
}

Now, let’s create a resource to protect. We’ll use a simple Post entity as an example.

// post.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user.entity';

@Entity()
export class Post {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  title: string;

  @Column()
  content: string;

  @ManyToOne(() => User)
  author: User;

  @Column({ default: false })
  published: boolean;
}

Have you ever wondered how to check if a user can edit a specific post, not just any post? This is where CASL comes in. CASL is a library that lets you define abilities in a clean, readable way. Let’s create a factory that builds abilities based on a user’s roles and permissions.

// ability.factory.ts
import { Injectable } from '@nestjs/common';
import { Ability, AbilityBuilder, AbilityClass } from '@casl/ability';
import { User } from './user.entity';
import { Post } from './post.entity';
import { Action } from './permission.entity';

type Subjects = typeof Post | typeof User | 'all';
export type AppAbility = Ability<[Action, Subjects]>;

@Injectable()
export class AbilityFactory {
  createForUser(user: User) {
    const { can, build } = new AbilityBuilder<AppAbility>(
      Ability as AbilityClass<AppAbility>
    );

    // Load all permissions from user's roles
    user.roles.forEach(role => {
      role.permissions.forEach(permission => {
        if (permission.conditions) {
          can(permission.action, permission.subject, permission.conditions);
        } else {
          can(permission.action, permission.subject);
        }
      });
    });

    // Add ownership rule: users can manage their own posts
    can(Action.UPDATE, Post, { author: { id: user.id } });
    can(Action.DELETE, Post, { author: { id: user.id } });

    return build();
  }
}

We need a way to check these abilities in our endpoints. We’ll create a guard that uses our ability factory. This guard will protect routes based on the user’s permissions.

// abilities.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AbilityFactory } from './ability.factory';
import { CHECK_ABILITY, RequiredRule } from './abilities.decorator';

@Injectable()
export class AbilitiesGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private abilityFactory: AbilityFactory,
  ) {}

  canActivate(context: ExecutionContext): boolean {
    const rules = this.reflector.get<RequiredRule[]>(
      CHECK_ABILITY,
      context.getHandler(),
    ) || [];

    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const ability = this.abilityFactory.createForUser(user);

    return rules.every(rule => ability.can(rule.action, rule.subject));
  }
}

To make our guard easy to use, we’ll create a custom decorator. This decorator lets us specify what ability is required right on the controller method.

// abilities.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Action } from './permission.entity';

export interface RequiredRule {
  action: Action;
  subject: string;
}

export const CHECK_ABILITY = 'check_ability';
export const CheckAbilities = (...requirements: RequiredRule[]) =>
  SetMetadata(CHECK_ABILITY, requirements);

Now let’s see how this all comes together in a controller. We’ll protect a post update endpoint so that only users with the right permissions can use it.

// posts.controller.ts
import { Controller, Put, Param, Body, UseGuards } from '@nestjs/common';
import { PostsService } from './posts.service';
import { AbilitiesGuard } from './abilities.guard';
import { CheckAbilities } from './abilities.decorator';
import { Action } from './permission.entity';

@Controller('posts')
@UseGuards(AbilitiesGuard)
export class PostsController {
  constructor(private postsService: PostsService) {}

  @Put(':id')
  @CheckAbilities({ action: Action.UPDATE, subject: 'Post' })
  async updatePost(@Param('id') id: string, @Body() updateData: any) {
    return this.postsService.update(id, updateData);
  }
}

But what about checking ownership? The guard above checks if the user can update any post. We need to also verify they’re updating their own post. We can modify our service to include this check.

// posts.service.ts
import { Injectable, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './post.entity';
import { AbilityFactory } from './ability.factory';

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post)
    private postsRepository: Repository<Post>,
    private abilityFactory: AbilityFactory,
  ) {}

  async update(id: string, updateData: any, user: any) {
    const post = await this.postsRepository.findOne({ where: { id } });
    const ability = this.abilityFactory.createForUser(user);

    if (ability.cannot('update', post)) {
      throw new ForbiddenException('You cannot update this post');
    }

    return this.postsRepository.update(id, updateData);
  }
}

Let’s talk about seeding some initial data. We need roles and permissions to test our system. Here’s how you might create an admin role and a user role.

// seed.ts
async function seed() {
  // Create permissions
  const manageAll = permissionRepository.create({
    action: Action.MANAGE,
    subject: 'all',
  });

  const readPost = permissionRepository.create({
    action: Action.READ,
    subject: 'Post',
  });

  // Create roles
  const adminRole = roleRepository.create({
    name: 'admin',
    permissions: [manageAll],
  });

  const userRole = roleRepository.create({
    name: 'user',
    permissions: [readPost],
  });

  // Create users
  const admin = userRepository.create({
    email: '[email protected]',
    password: await hash('password', 10),
    roles: [adminRole],
  });

  await userRepository.save(admin);
}

What happens when your application grows and you need more complex rules? CASL handles this well. You can add conditions to your permissions. For example, you might want users to only edit their own posts, or only view published posts.

// Adding conditional permissions
const editOwnPost = permissionRepository.create({
  action: Action.UPDATE,
  subject: 'Post',
  conditions: { authorId: '${user.id}' },
});

const viewPublished = permissionRepository.create({
  action: Action.READ,
  subject: 'Post',
  conditions: { published: true },
});

Remember to test your authorization logic thoroughly. Write tests that simulate different users trying to perform different actions. Make sure your admin can do everything they should, and regular users are properly restricted.

// ability.factory.spec.ts
describe('AbilityFactory', () => {
  it('should allow admin to manage all', () => {
    const admin = createAdminUser();
    const ability = factory.createForUser(admin);
    expect(ability.can('manage', 'all')).toBe(true);
  });

  it('should allow user to read only published posts', () => {
    const user = createRegularUser();
    const ability = factory.createForUser(user);
    const publishedPost = { published: true };
    const draftPost = { published: false };

    expect(ability.can('read', 'Post', publishedPost)).toBe(true);
    expect(ability.can('read', 'Post', draftPost)).toBe(false);
  });
});

As your application evolves, you might need to add new resource types or actions. The beauty of this system is that you can extend it without rewriting everything. Just add new permissions and assign them to roles.

Consider performance too. Loading all permissions for every request might be heavy. You could cache a user’s abilities for a short time, or store pre-computed ability rules in the database. Always measure and optimize based on your actual usage patterns.

Security is not a feature you add at the end. It’s a foundation you build from the beginning. This system gives you that foundation. It’s clear, it’s testable, and it scales with your needs. Start with simple roles, add conditions as needed, and always keep security in mind.

I hope this guide helps you build more secure applications. What permission challenges have you faced in your projects? Share your thoughts in the comments below. If you found this useful, please like and share it with other developers who might benefit from it. Let’s build safer software together.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: nestjs,authorization,casl,postgresql,rbac



Similar Posts
Blog Image
Type-Safe NestJS Microservices with Prisma and RabbitMQ: Complete Inter-Service Communication Tutorial

Learn to build type-safe microservices with NestJS, Prisma, and RabbitMQ. Complete guide to inter-service communication, error handling, and production deployment.

Blog Image
Build Full-Stack TypeScript Apps: Complete Next.js and Prisma Integration Guide for Modern Developers

Learn how to integrate Next.js with Prisma ORM for type-safe full-stack TypeScript apps. Build modern web applications with seamless database operations.

Blog Image
How to Build a Scalable Video Conferencing App with WebRTC and Node.js

Learn how to go from a simple peer-to-peer video call to a full-featured, scalable conferencing system using WebRTC and Mediasoup.

Blog Image
How to Build Type-Safe APIs in Node.js with Effect-TS and Fastify

Discover how to eliminate runtime surprises by designing APIs in TypeScript where every failure is typed and handled safely.

Blog Image
Building Production-Ready GraphQL APIs with NestJS, Prisma, and Redis: Complete Scalable Backend Guide

Build scalable GraphQL APIs with NestJS, Prisma & Redis. Complete guide covering authentication, caching, real-time subscriptions & deployment. Start building today!

Blog Image
Build Complete Multi-Tenant SaaS with NestJS, Prisma, and PostgreSQL Row Level Security

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Complete guide covers authentication, data isolation & deployment.