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