Skip to main content

Enforce Permit.io permissions in Prisma queries

The @permitio/permit-prisma package is a Prisma Client extension that checks each Prisma operation against your Permit.io policy before the operation reaches the database. This page is for Node.js developers who use Prisma ORM and want role-based access control (RBAC), attribute-based access control (ABAC), or relationship-based access control (ReBAC) enforced at the data layer.

The extension does three things, each turned on by its own option:

CapabilityOptionWhat the extension does
Automatic permission checksenableAutomaticChecksMaps each Prisma operation to a Permit action and calls permit.check() for the active user. Throws a PermitError when the policy decision point (PDP) denies the operation.
Data filteringenableDataFilteringAdds an id filter to findMany queries, so the query returns only the records the active user can read.
Resource instance syncenableResourceSync, enableAttributeSyncAfter a create, update, or delete operation, creates, updates, or deletes the matching resource instance in Permit.

Prerequisites

  • A Permit.io account and an API key for the environment. See Get your API key.
  • A PDP. The Cloud PDP at https://cloudpdp.api.permit.io supports RBAC and ReBAC. ABAC policies need an Edge PDP, such as a PDP container at http://localhost:7766. See Run the PDP and Cloud PDP capabilities.
  • Resources, actions, roles, and policies in Permit that match your Prisma models. See Build an RBAC policy.
  • A Node.js project that uses Prisma Client.

Install the Permit Prisma extension

Install the extension and the Prisma Client package from npm:

npm install @permitio/permit-prisma @prisma/client

Add the extension to your Prisma client

Extend your PrismaClient with createPermitClientExtension(). Replace <YOUR_PERMIT_API_KEY> with your API key and set pdp to the address of your PDP.

import { PrismaClient } from "@prisma/client";
import { createPermitClientExtension } from "@permitio/permit-prisma";

const prisma = new PrismaClient().$extends(
createPermitClientExtension({
permitConfig: {
token: "<YOUR_PERMIT_API_KEY>", // Permit API Key (required)
pdp: "http://localhost:7766", // PDP address (required)
},
enableAutomaticChecks: true, // Enable automatic permission checks
enableResourceSync: true, // Sync resource instances with Permit.io
enableAttributeSync: true, // Sync resource attributes with Permit.io
enableDataFiltering: true, // Enable automatic query filtering by permissions
})
);

Configuration options

createPermitClientExtension() takes one object with these fields:

OptionTypeDefaultDescription
permitConfig.tokenstringRequiredPermit API key of the environment.
permitConfig.pdpstringRequiredAddress of the PDP.
permitConfig.debugbooleanfalseLogs each intercepted operation, permission check, and decision.
permitConfig.apiUrlstringNode.js SDK defaultAddress of the Permit API.
permitConfig.throwOnErrorbooleanfalseIf true, a failed permission check request throws a PermitError with the cause. If false, a failed request counts as a denied check.
enableAutomaticChecksbooleanfalseChecks permissions on every Prisma operation. Data filtering and resource sync run only when enableAutomaticChecks is true.
enableDataFilteringbooleanfalseFilters findMany results to the records the active user can read.
enableResourceSyncbooleanfalseSyncs resource instances to Permit after create, update, and delete operations. The instance key is the id of the returned record, and the instance attributes are the attributes field of the returned record, if the model has one.
enableAttributeSyncbooleanfalseRuns the same instance sync as enableResourceSync. Turn on either option to sync instances.
defaultTenantstring"default"Tenant key of synced resource instances.
resourceTypeMappingRecord<string, string>NoneMaps Prisma model names to Permit resource keys, for example { BlogPost: "article" }.
excludedModelsstring[]NonePrisma models that skip automatic checks.
excludedOperationsstring[]NonePrisma operations that skip automatic checks, for example ["count"].

How Prisma operations map to Permit actions

The extension converts the Prisma model name to a resource key in snake_case, for example MedicalRecord becomes medical_record, unless resourceTypeMapping sets a different key. The extension maps each Prisma operation to an action:

Prisma operationsPermit action
findUnique, findUniqueOrThrow, findFirst, findFirstOrThrow, findManyread
create, createManycreate
update, updateMany, upsertupdate
delete, deleteManydelete
Any other operation, such as countThe operation name

The resource in each check has the resource key as type, the id from the where clause as key, and the fields of data as attributes. A query without data sends the fields of where as attributes.

Enforce permissions on Prisma queries

With enableAutomaticChecks: true, call prisma.$permit.setUser() with the key of the signed-in user before you run queries. Pass a user key string, or an object with key and attributes for ABAC policies. The user key must match the key of a user synced to Permit.

Permission check flow

For each Prisma operation, the extension:

  1. Intercepts the operation.
  2. Maps the operation to a Permit action and the model to a resource.
  3. Calls permit.check() for the active user, action, and resource.
  4. Runs the operation if the PDP allows it, or throws a PermitError if the PDP denies it.
import { PermitError } from "@permitio/permit-prisma";

// Set the active user for permission checks
prisma.$permit.setUser("john@example.com");

// This will be checked against Permit.io policies automatically
try {
const document = await prisma.document.create({
data: {
title: "New Document",
content: "Document content",
}
});
console.log("Document created successfully");
} catch (error) {
if (error instanceof PermitError) {
console.error("Permission denied");
}
}
Set the user before every query

If no user is set, the extension skips the permission check and runs the operation. setUser() stores one user on the extended client, and every later query on that client runs as that user until the next setUser() call. Call setUser() at the start of each request, before the first query.

Configure the extension for your policy model

Turn on the options that your policy model needs. Permit allows an operation if any policy model grants the permission.

RBAC configuration

RBAC grants permissions on a resource type through roles, for example an admin role that can read every document. Automatic checks are enough for RBAC.

// Configure for RBAC
const prisma = new PrismaClient().$extends(
createPermitClientExtension({
permitConfig: { token: "YOUR_API_KEY", pdp: "http://localhost:7766" },
enableAutomaticChecks: true
})
);

// Simple role-based check
prisma.$permit.setUser("admin@example.com");
const documents = await prisma.document.findMany(); // Will succeed if admin role has read permission

ABAC configuration

ABAC decides access from attributes of the user and the resource, for example allow access if the user department matches the record department. Set the user attributes in setUser(). The extension sends the fields of the query as resource attributes. When you update a record, include every attribute that the policy evaluates in data. ABAC checks need an Edge PDP.

// Configure for ABAC
const prisma = new PrismaClient().$extends(
createPermitClientExtension({
permitConfig: { token: "YOUR_API_KEY", pdp: "http://localhost:7766" },
enableAutomaticChecks: true,
enableAttributeSync: true
})
);

// Set user with attributes
prisma.$permit.setUser({
key: "doctor@hospital.com",
attributes: { department: "cardiology" }
});

// Will succeed only if user department matches record department based on policy
const records = await prisma.medicalRecord.findMany({
where: { department: "cardiology" }
});

ReBAC configuration

ReBAC grants roles on specific resource instances, for example owner of one file and viewer of another. Turn on enableResourceSync so that each created record becomes a resource instance in Permit, with the record id as the instance key. Turn on enableDataFiltering so that findMany returns only the instances on which the user has the read permission. If the user can't read any instance, findMany returns an empty array.

// Configure for ReBAC
const prisma = new PrismaClient().$extends(
createPermitClientExtension({
permitConfig: { token: "YOUR_API_KEY", pdp: "http://localhost:7766" },
enableAutomaticChecks: true,
enableResourceSync: true,
enableDataFiltering: true
})
);

// Set user for instance-level permissions
prisma.$permit.setUser("owner@example.com");

// Will only succeed if the user has permission on this specific file instance
const file = await prisma.file.findUnique({
where: { id: "file-123" }
});

The extension syncs resource instances only. Create role assignments and relationship tuples on the instances with the Permit API or an SDK. See Build ReBAC policies.

Combined model configuration

// Configure for combined models (RBAC + ABAC + ReBAC)
const prisma = new PrismaClient().$extends(
createPermitClientExtension({
permitConfig: { token: "YOUR_API_KEY", pdp: "http://localhost:7766" },
enableAutomaticChecks: true,
enableResourceSync: true,
enableAttributeSync: true,
enableDataFiltering: true
})
);

Check permissions manually

prisma.$permit.check(user, action, resource) returns true or false. prisma.$permit.enforceCheck() takes the same arguments and throws a PermitError if the PDP denies the check. Both methods work without enableAutomaticChecks and don't use the user from setUser().

// Check permission manually
const canUpdateDocument = await prisma.$permit.check(
"john@example.com", // user
"update", // action
"document" // resource
);

if (canUpdateDocument) {
await prisma.document.update({
where: { id: "doc-123" },
data: { title: "Updated Title" }
});
}

// Or enforce check (throws if denied)
await prisma.$permit.enforceCheck(
"john@example.com",
"delete",
{ type: "document", key: "doc-123" }
);
await prisma.document.delete({ where: { id: "doc-123" } });

Verify that the extension enforces permissions

  1. Add debug: true to permitConfig and restart your application.
  2. Call setUser() with a user who lacks the create permission on a resource, and run a create query on the matching model. The query throws a PermitError with a Permission denied message, and the log shows Permission result: DENIED.
  3. Open the audit log in the Permit dashboard. The denied check appears with the user, action, and resource.

Next steps