Add fine-grained authorization to a NestJS app
Build a NestJS app for a blogging platform that registers users in Permit.io and uses a NestJS guard to allow only users with the Author role through a protected route. This tutorial is for Node.js backend developers who want to enforce Permit.io policies in NestJS controllers.
When you finish, your NestJS app has two endpoints:
| Endpoint | What it does |
|---|---|
POST /register | Syncs a user to Permit.io and assigns the user the Reader role in the default tenant |
GET /posts | Runs PermitGuard, which calls permit.check() and returns 403 unless the user has permission to create a Post |
Prerequisites
- A Permit.io account. See Create a Permit.io account.
- Node.js and npm, to run the NestJS app and install the Permit CLI.
- Docker, to run the policy decision point (PDP) container.
- A NestJS project created with the Nest CLI (
nest new). See NestJS first steps.
1. Configure the policy in Permit
Create the blogging platform policy with the Permit CLI. If your environment already has a policy with a Post resource and an Author role that can create posts, skip to 2. Get your API key.
Install the Permit CLI
The Permit CLI creates policies and runs the PDP from your terminal. Install the CLI with npm:
npm install -g @permitio/cli
Run permit to confirm that the CLI is installed.
Sign in with the Permit CLI
Authenticate the CLI with your Permit.io account:
permit login
The command opens a browser window where you sign in. After you sign in, the CLI uses your default environment. To use a different environment, run permit env select and choose the environment.
Apply the blogging platform template
Permit CLI templates create a policy with predefined resources, roles, and rules. To see the available templates, run permit env template list. The template source files are in the Permit CLI repository.

Apply the blogging-platform template to your environment:
permit env template apply --template blogging-platform
The CLI prints a success message when the template is applied.
Review the policy in the Policy Editor
In the Permit dashboard, select your project and open the Policy screen.

The blogging-platform template creates:
| Policy element | What the template defines |
|---|---|
| Resources | Post (with a premium boolean attribute) and Comment, each with create, read, update, and delete actions |
| Roles | Admin (all actions), Author (create and read posts, read comments), Reader (create and read comments), and Premium Reader (read posts and comments) |
| Relationship | A Post is the parent of its Comment instances. An Author of a post instance becomes a Moderator of the comments on that post. This rule is relationship-based access control (ReBAC). |
| Resource set | Free Post contains posts where premium is false. Readers can read free posts. This rule is attribute-based access control (ABAC). |
This tutorial uses one rule from the policy: the Author role can create a Post, and the Reader role cannot. To change which role can perform an action, check or clear the box in the Policy Editor.
2. Get your API key
Your NestJS app and the PDP authenticate with Permit.io with your environment API key. Copy the API key of the environment where you applied the template. See Get your API key.
Anyone with the environment API key can change that environment's policy through the Permit API. Load the key from an environment variable, and don't commit it.
3. Run the PDP
The PDP evaluates each permit.check() call against your policy. Start a PDP container with the Permit CLI:
permit pdp run
The command starts the PDP in Docker and prints the container ID and name. The PDP listens on port 7766, so your app connects to it at http://localhost:7766.

The Free Post resource set is an ABAC rule, and the Cloud PDP doesn't evaluate ABAC rules, so run the container PDP for this policy. To run the container with docker run instead, or to check that the PDP is healthy, see Run the PDP.
4. Build the NestJS app
Install the Node.js SDK
In your NestJS project directory, install the Permit Node.js SDK:
npm install permitio
For all SDK options, see the Node.js SDK quickstart.
Create the Permit client provider
Create the file src/permit/permit.provider.ts with the following code. Your project structure can differ; adjust the import paths in the next steps if it does.
// src/permit/permit.provider.ts
import { Permit } from 'permitio';
export const permit = new Permit({
token: process.env.PERMIT_API_KEY!,
pdp: process.env.PDP_URL!,
});
The file creates one Permit client that the controller and the guard import. The client reads two environment variables:
| Variable | Value |
|---|---|
PERMIT_API_KEY | Your environment API key from 2. Get your API key |
PDP_URL | The PDP address from 3. Run the PDP: http://localhost:7766 |
Create a guard that calls permit.check()
A NestJS guard runs before a route handler and decides whether the request reaches the handler. Create the file src/permit/permit.guard.ts with the following code:
// src/permit/permit.guard.ts
import { CanActivate, ExecutionContext, Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { permit } from './permit.provider';
// This guard checks if the user has access to the resource
@Injectable()
export class PermitGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.headers['x-user'];
const action = 'create';
const resource = 'Post';
if (!user) {
throw new HttpException('Missing permission info', HttpStatus.BAD_REQUEST);
}
try {
const permitted = await permit.check(user, action, resource);
return permitted;
} catch (err) {
throw new HttpException('Permission check failed', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
PermitGuard reads the user key from the x-user request header and calls permit.check() with the action create and the resource Post. permit.check() resolves to true or false. When the guard returns false, NestJS rejects the request with HTTP 403. When the header is missing, the guard returns HTTP 400.
To protect other routes, such as commenting or editing, change the action and resource values, or read them from the route.
In a production app, take the user key from your authenticated session. This example reads the user key from a header so that you can test the route with curl.
Add the register and posts routes to AppController
Replace the contents of src/app.controller.ts with the following code. The Nest CLI registers AppController in src/app.module.ts, so you don't need to change the module.
// src/app.controller.ts
import { Controller, Post, Body, HttpException, HttpStatus, UseGuards, Get } from '@nestjs/common';
import { permit } from './permit/permit.provider';
import { PermitGuard } from './permit/permit.guard';
@Controller()
export class AppController {
@Post('register')
async register(@Body() body: { email: string, first_name: string, last_name: string }) {
const { email, first_name, last_name } = body;
if (!email || !first_name || !last_name) {
throw new HttpException('Missing required fields', HttpStatus.BAD_REQUEST);
}
try {
const user = await permit.api.users.sync({
key: email,
email,
first_name,
last_name,
});
const assignedRole = {
user: email,
role: 'Reader',
tenant: 'default'
};
const response = await permit.api.users.assignRole(assignedRole);
// Continue with the rest of the code
return {
message: 'User registered and role assigned',
user,
response
};
} catch (err) {
throw new HttpException('Failed to sync user', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// middleware to check if the user has access to the resource
@UseGuards(PermitGuard)
@Get('posts')
getPosts() {
return { message: "You have passed the auth check" };
}
}
- The
registerhandler syncs the user to Permit.io withpermit.api.users.sync(), then assigns the user the Reader role in thedefaulttenant. The user's email address is the user key in Permit.io. - The
getPostshandler returns a message only whenPermitGuardallows the request.@UseGuards(PermitGuard)applies the guard to this route.
Start the NestJS app
Set the environment variables and start the app, replacing <YOUR_API_KEY> with your API key:
export PERMIT_API_KEY=<YOUR_API_KEY>
export PDP_URL=http://localhost:7766
npm run start
A project created with nest new listens on http://localhost:3000.
5. Test the permission check
Register two users, give one of them the Author role, and confirm that the guard allows only that user through GET /posts.
Register two users
In a second terminal, register John and Emma:
curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{"email": "john@example.com", "first_name": "John", "last_name": "Doe"}'
curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{"email": "emma@example.com", "first_name": "Emma", "last_name": "Den"}'
Each request returns "message": "User registered and role assigned", the synced user under user, and the role assignment under response, with "role": "Reader" and "tenant": "default". Both users appear in the Directory screen of the Permit dashboard.
Assign John the Author role
Both users have the Reader role, which can't create posts. Give John the Author role in the Permit dashboard:
- Open the Directory screen and select
john@example.comto open the Edit User panel. - Under Permissions Per Tenant, select the Default Tenant.
- In Top Level Access, add the Author role.
- Click Save.

For other ways to assign roles, including the API and SDK, see Sync users.
Check that the guard allows John and blocks Emma
Send a GET /posts request as John:
curl http://localhost:3000/posts -H "x-user: john@example.com"
The PDP allows the request because John has the Author role. The app returns {"message":"You have passed the auth check"}.
Send the same request as Emma:
curl http://localhost:3000/posts -H "x-user: emma@example.com"
The PDP denies the request because Emma has only the Reader role. NestJS returns HTTP 403 with {"message":"Forbidden resource","error":"Forbidden","statusCode":403}.
Each check also appears in the Audit Log screen of the Permit dashboard, with the user, action, resource, and decision.
Next steps
- Check permissions with the Node.js SDK: SDK installation, configuration, and more
permit.check()examples. - Check permissions with permit.check(): check against tenants, resource instances, and attributes.
- Build RBAC policies: create roles and permissions for your own resources.
- Permit Elements overview: embed user management and other UI components in your app.