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
Build Real-Time Web Apps: Complete Guide to Integrating Svelte with Supabase Database

Learn to build real-time web apps with Svelte and Supabase integration. Get instant APIs, live data sync, and seamless user experiences. Start building today!

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

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

Blog Image
How to Build a Production-Ready File Upload System in Node.js with Fastify

Learn to build a secure, scalable Node.js file upload system with Fastify, S3, resumable uploads, and progress tracking for production.

Blog Image
How React Native and Firebase Supercharge Mobile App Development

Discover how combining React Native with Firebase simplifies backend setup, enabling faster, scalable mobile app development.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

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

Blog Image
Build High-Performance GraphQL APIs with NestJS, Prisma, and Redis Caching

Learn to build high-performance GraphQL APIs with NestJS, Prisma ORM, and Redis caching. Master resolvers, DataLoader optimization, real-time subscriptions, and production deployment strategies.