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:
| Capability | Option | What the extension does |
|---|---|---|
| Automatic permission checks | enableAutomaticChecks | Maps 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 filtering | enableDataFiltering | Adds an id filter to findMany queries, so the query returns only the records the active user can read. |
| Resource instance sync | enableResourceSync, enableAttributeSync | After 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.iosupports RBAC and ReBAC. ABAC policies need an Edge PDP, such as a PDP container athttp://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:
| Option | Type | Default | Description |
|---|---|---|---|
permitConfig.token | string | Required | Permit API key of the environment. |
permitConfig.pdp | string | Required | Address of the PDP. |
permitConfig.debug | boolean | false | Logs each intercepted operation, permission check, and decision. |
permitConfig.apiUrl | string | Node.js SDK default | Address of the Permit API. |
permitConfig.throwOnError | boolean | false | If true, a failed permission check request throws a PermitError with the cause. If false, a failed request counts as a denied check. |
enableAutomaticChecks | boolean | false | Checks permissions on every Prisma operation. Data filtering and resource sync run only when enableAutomaticChecks is true. |
enableDataFiltering | boolean | false | Filters findMany results to the records the active user can read. |
enableResourceSync | boolean | false | Syncs 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. |
enableAttributeSync | boolean | false | Runs the same instance sync as enableResourceSync. Turn on either option to sync instances. |
defaultTenant | string | "default" | Tenant key of synced resource instances. |
resourceTypeMapping | Record<string, string> | None | Maps Prisma model names to Permit resource keys, for example { BlogPost: "article" }. |
excludedModels | string[] | None | Prisma models that skip automatic checks. |
excludedOperations | string[] | None | Prisma 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 operations | Permit action |
|---|---|
findUnique, findUniqueOrThrow, findFirst, findFirstOrThrow, findMany | read |
create, createMany | create |
update, updateMany, upsert | update |
delete, deleteMany | delete |
Any other operation, such as count | The 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:
- Intercepts the operation.
- Maps the operation to a Permit action and the model to a resource.
- Calls
permit.check()for the active user, action, and resource. - Runs the operation if the PDP allows it, or throws a
PermitErrorif 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");
}
}
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
- Add
debug: truetopermitConfigand restart your application. - Call
setUser()with a user who lacks thecreatepermission on a resource, and run acreatequery on the matching model. The query throws aPermitErrorwith aPermission deniedmessage, and the log showsPermission result: DENIED. - Open the audit log in the Permit dashboard. The denied check appears with the user, action, and resource.