DEV Community

Cover image for Fine-Grained Authorization in NestJS Without the Boilerplate (A TypeScript Toolkit for Permify)
Nikhil
Nikhil

Posted on

Fine-Grained Authorization in NestJS Without the Boilerplate (A TypeScript Toolkit for Permify)

Fine-Grained Authorization in NestJS Without the Boilerplate

A practical look at how permify-toolkit removes 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 .perm file 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:

  1. The NestJS module wiring.
  2. The schema push script.
  3. The seed script.
  4. 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"),
      },
    }),
  }),
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

That is the whole integration.

The guard:

  1. Intercepts the request
  2. Resolves the subject from your resolver
  3. Checks the permission against Permify
  4. 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 },
])
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

It picks up the following automatically:

  • PERMIFY_ENDPOINT
  • PERMIFY_INSECURE
  • PERMIFY_AUTH_TOKEN
  • TLS cert paths

If you use a custom env prefix for your app, you can pass it in:

clientOptionsFromEnv("MY_APP_")
Enter fullscreen mode Exit fullscreen mode

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:

  1. The Permify schema definition lives in one place
  2. The script that pushes it lives somewhere else
  3. 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
Enter fullscreen mode Exit fullscreen mode

Or with pnpm:

pnpm add @permify-toolkit/core @permify-toolkit/nestjs @permify-toolkit/cli
Enter fullscreen mode Exit fullscreen mode

Then:

  1. Create your permify.config.ts
  2. Push your schema with the CLI
  3. Add the NestJS module
  4. Start decorating your routes

Resources

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

If you find it useful, a ⭐ on the repo goes a long way in helping others discover it.

Top comments (0)