Prisma Row-Level Security Extension
prisma-rls is a Prisma client extension designed to implement row-level security (RLS) on any database, including those without native RLS support (e.g., MySQL). It achieves this by automatically injecting 'where' clauses into all Prisma model queries, effectively filtering data based on defined permissions. The current stable version is `0.5.6`. The package shows active development with frequent minor releases and bug fixes, indicated by the recent changelog entries. A key differentiator is its database-agnostic approach, allowing RLS where it wouldn't natively exist, and its integration directly into the Prisma client query pipeline. It ships with TypeScript types, providing a type-safe way to define permissions configurations and contexts. It's crucial to note that this extension does not apply to raw database queries, which require manual handling.
Common errors
-
TypeError: Cannot read properties of undefined (reading 'id')
cause The `PermissionsContext` object or a property within it (e.g., `user` or `user.id`) is `null` or `undefined` when a permission function tries to access it.fixEnsure that your `PermissionsContext` object is always fully populated with expected values, or add null-checking/optional chaining within your permission functions, for example: `permissionContext.user?.id`. -
PrismaClientKnownRequestError: Argument 'where' must not be empty.
cause A permission function evaluated to an empty `where` object when a non-empty `where` clause was expected, potentially due to a context issue or a misconfigured permission that disallows access entirely (e.g., `false`).fixReview the permission definition for the specific model and operation. Ensure the permission function returns a valid `where` object or `true` for allowed access, or `false` for explicit denial, rather than an implicitly empty object.
Warnings
- breaking Version `0.5.0` introduced major internal type safety improvements and refactored error handling to use dedicated error classes (`AuthorizationError`, `ReferentialIntegrityError`). Existing error handling logic might need updates.
- gotcha The `prisma-rls` extension does not apply to raw Prisma queries (e.g., `prisma.$queryRaw`, `prisma.$executeRaw`). RLS for raw queries must be handled manually at the database level or through application-side validation.
- gotcha When dealing with mandatory 'belongs-to' relations (where the foreign key owner side is required), Prisma typically does not generate filters to prevent referential integrity violations. This can lead to unexpected behavior where RLS might not apply as expected.
Install
-
npm install prisma-rls -
yarn add prisma-rls -
pnpm add prisma-rls
Imports
- createRlsExtension
const { createRlsExtension } = require('prisma-rls');import { createRlsExtension } from 'prisma-rls'; - PermissionsConfig
import type { PermissionsConfig } from 'prisma-rls';import { PermissionsConfig } from 'prisma-rls'; - ExtensionOptions
import { ExtensionOptions } from 'prisma-rls';import type { ExtensionOptions } from 'prisma-rls';
Quickstart
import { Prisma, PrismaClient } from "@prisma/client";
import Fastify, { FastifyRequest } from "fastify";
import { createRlsExtension, PermissionsConfig } from "prisma-rls";
// Define shared types for roles and permissions context
export type Role = "User" | "Guest";
export type PermissionsContext = { userId: string | null };
export type RolePermissions = PermissionsConfig<Prisma.TypeMap, PermissionsContext>;
export type PermissionsRegistry = Record<Role, RolePermissions>;
// Define user permissions
const userPermissions: RolePermissions = {
Post: {
read: { published: { equals: true } },
create: true,
update: (ctx) => ({ authorId: { equals: ctx.userId } }),
delete: (ctx) => ({ authorId: { equals: ctx.userId } })
},
User: {
read: (ctx) => ({ id: { equals: ctx.userId } }),
create: false,
update: (ctx) => ({ id: { equals: ctx.userId } }),
delete: false
}
};
// Define guest permissions
const guestPermissions: RolePermissions = {
Post: {
read: { published: { equals: true } },
create: false,
update: false,
delete: false
},
User: {
read: false,
create: false,
update: false,
delete: false
}
};
// Combine permissions into a registry
export const permissionsRegistry = {
User: userPermissions,
Guest: guestPermissions
} satisfies PermissionsRegistry;
(async () => {
const prisma = new PrismaClient();
const server = Fastify();
// Dummy function to resolve user from auth header
const resolveUser = async (authorizationHeader?: string | string[] | undefined) => {
if (authorizationHeader === 'Bearer user-token') {
return { id: 'user-123', role: 'User' };
}
return null;
};
server.decorateRequest('db', null);
server.addHook('onRequest', async (request: any, reply) => {
const user = await resolveUser(request.headers.authorization);
const userRole: Role = user ? user.role : "Guest";
const permissionsContext: PermissionsContext = { userId: user?.id ?? null };
const rlsExtension = createRlsExtension({
dmmf: Prisma.dmmf,
permissionsConfig: permissionsRegistry[userRole],
context: permissionsContext,
});
request.db = prisma.$extends(rlsExtension);
});
server.get("/posts", async function handler(request: any, reply) {
// Assuming a user with 'user-token' can only see their own posts
// and public posts. Guests can only see public posts.
return await request.db.post.findMany();
});
server.get("/profile", async function handler(request: any, reply) {
// A user can only see their own profile, guests see nothing.
return await request.db.user.findMany(); // Will apply RLS based on `userId`
});
await server.listen({ port: 8080, host: "0.0.0.0" });
console.log('Server listening on http://0.0.0.0:8080');
})();