Fine-Grained Authorization in NestJS Without the Boilerplate
A practical look at how
permify-toolkitremoves the friction of using Permify in a TypeScript and NestJS project.
Introduction
I have been working with NestJS for a few years now, and for most of that time, authorization was the part I dreaded.
Not authentication. That part is boring in a good way.
Authorization. The part where you have to answer:
"Can this specific user actually do this specific thing to this specific resource?"
...at runtime, correctly, without it becoming a maze of if statements scattered across your controllers.
I eventually landed on Permify as the answer. If you have not heard of it, Permify is an open-source, Google Zanzibar-inspired authorization service. You define your permission model as a schema, push it to the Permify server, and then check permissions over gRPC from your app. It is genuinely powerful, it scales, and the model is clean once you understand it.
The problem was the integration layer.
The Problem with Raw Permify in Node.js
The Permify Node client exists, but using it in a real NestJS app without any abstraction means you are doing a lot of repetitive work:
- Manually wiring the gRPC client in your module
- Keeping a separate
.permfile that lives outside your TypeScript codebase - Writing small scripts to push schemas and seed relationships during development
- Watching the config between your app and those scripts drift apart over time
I kept rebuilding the same setup across projects:
- The NestJS module wiring.
- The schema push script.
- The seed script.
- The shared config that inevitably became two separate configs because someone edited one and forgot the other.
I got tired of it. So I built permify-toolkit.
What is permify-toolkit?
permify-toolkit is a small monorepo of three TypeScript packages that work together to make Permify feel native inside a NestJS project.
| Package | Purpose |
|---|---|
@permify-toolkit/core |
Schema DSL in TypeScript, typed client factory, shared config loader |
@permify-toolkit/nestjs |
NestJS module, guard, and @CheckPermission() decorator |
@permify-toolkit/cli |
CLI commands for schema push, relationship seeding, and more |
The key idea is one config file. You write a permify.config.ts once, and both your NestJS app and the CLI read from the same file.
No duplication. No drift.
GitHub: github.com/thisisnkc/permify-toolkit
Writing Your Schema in TypeScript, Not in a .perm File
This was the first thing I wanted to change. Permify schemas are written in a DSL called .perm. It works, but:
- It sits outside your TypeScript project
- It has no type checking
- It is one more thing to keep in sync
With permify-toolkit, you write your schema in TypeScript:
import { defineConfig, schema, entity, relation, permission } from "@permify-toolkit/core";
export default defineConfig({
tenant: "t1",
client: { endpoint: "localhost:3478", insecure: true },
schema: schema({
user: entity({}),
document: entity({
relations: {
owner: relation("user"),
viewer: relation("user"),
},
permissions: {
edit: permission("owner"),
view: permission("owner | viewer"),
},
}),
}),
});
This is your permify.config.ts. It is the single source of truth for your entire Permify setup, both for the CLI and for the NestJS module at runtime.
The schema DSL is fully type-safe. You get:
- Autocomplete on relation names, permission names, and entity references
- Compile-time errors if you rename an entity and forget to update a permission that references it
If something is wrong, TypeScript tells you before you push anything.
Pushing Your Schema with the CLI
Once your config is set up, pushing the schema to your Permify server is one command:
npx permify-toolkit schema push
No custom script. No manually calling the gRPC schema write endpoint.
The CLI reads your permify.config.ts, converts the TypeScript schema to the .perm format, and pushes it.
You can also seed relationships during development:
npx permify-toolkit relationships seed --file-path ./data/relationships.json
This is particularly useful in CI or local setup scripts where you want to provision a fresh Permify instance with test data before running integration tests.
Wiring It Into NestJS
This is where it starts to feel really clean. Add the module to your app:
import { PermifyModule } from "@permify-toolkit/nestjs";
@Module({
imports: [
PermifyModule.forRoot({
configFile: true,
resolvers: {
subject: (ctx) => ctx.switchToHttp().getRequest().user?.id,
},
}),
],
})
export class AppModule {}
That resolvers.subject function is how the module knows which user to check permissions for. You point it at your request object, return the user ID, and the guard takes care of the rest.
Then on any controller route, you add a decorator:
@Get(":id")
@CheckPermission({
resource: "document",
action: "view",
resourceId: (req) => req.params.id,
})
findOne(@Param("id") id: string) {
return this.documentsService.findOne(id);
}
That is the whole integration.
The guard:
- Intercepts the request
- Resolves the subject from your resolver
- Checks the permission against Permify
- Either lets the request through or throws a
403
Combining Permissions with AND / OR Logic
You can also combine permissions:
@CheckPermission([
{ resource: "document", action: "view", resourceId: (req) => req.params.id },
{ resource: "workspace", action: "member", resourceId: (req) => req.params.workspaceId },
])
By default, all conditions must pass (AND). You can switch to OR mode if any one permission should be enough to grant access.
Connecting to Permify
For connecting to the Permify server, the most flexible approach is environment variables. The clientOptionsFromEnv() helper reads them for you:
import { createPermifyClient, clientOptionsFromEnv } from "@permify-toolkit/core";
const client = createPermifyClient(clientOptionsFromEnv());
It picks up the following automatically:
PERMIFY_ENDPOINTPERMIFY_INSECUREPERMIFY_AUTH_TOKEN- TLS cert paths
If you use a custom env prefix for your app, you can pass it in:
clientOptionsFromEnv("MY_APP_")
This works well in environments where you are managing secrets through a platform like Kubernetes, Render, or Railway, and you do not want to hardcode connection details in your config file.
Why This Matters for Teams
The single-config design solves a real coordination problem.
In most codebases, you have three separate things:
- The Permify schema definition lives in one place
- The script that pushes it lives somewhere else
- The NestJS module config is a third thing
As the team grows and the schema evolves, these three things drift apart. You end up with:
- A production schema that does not quite match what the guard is checking
- A seed script that references entity types that were renamed three months ago
When everything reads from permify.config.ts, there is one place to update. The CLI, the NestJS module, and your tests all work from the same definitions.
Refactoring an entity name is a TypeScript rename, not a multi-file search-and-replace across config formats.
How to Get Started
Install the packages with npm:
npm install @permify-toolkit/core @permify-toolkit/nestjs
npm install @permify-toolkit/cli
Or with pnpm:
pnpm add @permify-toolkit/core @permify-toolkit/nestjs @permify-toolkit/cli
Then:
- Create your
permify.config.ts - Push your schema with the CLI
- Add the NestJS module
- Start decorating your routes
Resources
- Full documentation: thisisnkc.github.io/permify-toolkit
- GitHub repo: github.com/thisisnkc/permify-toolkit
If you run into issues or want to suggest something, the repo is the right place.
Closing Thoughts
Permify is a genuinely great solution for fine-grained authorization. The Google Zanzibar model is the right one for most modern SaaS applications where access control is relationship-based and cannot be expressed cleanly with simple role checks.
The goal of permify-toolkit is not to add features on top of Permify. It is to remove the friction of using Permify in a TypeScript and NestJS project:
- ✅ Less boilerplate
- ✅ One config
- ✅ Type-safe schemas
- ✅ A guard that just works
If it saves you an afternoon of setup, that is exactly what it was built for.
Links
- 🔗 GitHub: https://github.com/thisisnkc/permify-toolkit
- 📚 Docs: https://thisisnkc.github.io/permify-toolkit
If you find it useful, a ⭐ on the repo goes a long way in helping others discover it.
Top comments (0)