Prisma
permdock/prisma compiles portable conditions to Prisma where inputs with toWhere, fails closed with an empty OR when nothing is granted, and targets Prisma 8 native policy blocks for RLS generation.
Status: planned Phase: 3
permdock/prisma turns permdock.where(permission) into a Prisma WhereInput for the matching model. When no grant applies it returns { OR: [] }, the fail-closed shape CASL established, and it ships a client extension that keeps that shape effective across all query types. For RLS, permdock rls generate --target prisma emits Prisma 8 policy_* blocks so the same conditions are enforced in the database.
Purpose
Prisma has no server-side row filter unless the database enforces one, so list queries must carry the permission filter in where. @casl/prisma showed the working pattern and its pitfalls: accessibleBy returns { OR: [] } when nothing is allowed, but Prisma did not reliably treat an empty OR as always-false (prisma#17367), so createCaslExtension rewrites any where containing it into { ...where, OR: [], AND: [where] } (CASL research). PermDock adopts the same fail-closed behaviour and adds the Prisma 8 native RLS target introduced in July 2026 (changelog).
API
import { toWhere, permdockExtension } from 'permdock/prisma'
const prisma = new PrismaClient().$extends(permdockExtension())
const posts = await prisma.post.findMany({
where: {
AND: [toWhere(permdock.where(permissions.post.read)), { published: true }],
},
})
// typed to the model's WhereInput when the resource schema was generated from Prisma types
toWhere<Prisma.PostWhereInput>(permdock.where(permissions.post.read))toWhere(condition, options?)maps fields to model fields by name (oroptions.fields), and operators to Prisma filter operators:eqtoequals,netonot,in/notIn,gt/gte/lt/lte,isNulltoequals: null,containstohasfor scalar lists andcontainsfor strings,and/or/nottoAND/OR/NOT.- No grant compiles to
{ OR: [] }. permdockExtension()is a Prisma client extension that detectsOR: []anywhere in awhereand rewrites the query so it returns no records forfindMany,findFirst,count,aggregate,updateManyanddeleteMany, closing the gap in prisma#17367.- Prisma 7 custom-output generators are supported through a runtime entry that does not import
@prisma/client; types are passed by the caller as in the example.
RLS generation:
// emitted by: permdock rls generate --target prisma --dialect supabase
model Post {
id String @id
authorId String
published Boolean
@@rls
}
policy_select post_read_member {
target = Post
roles = [authenticated]
using = "(select auth.uid()) = \"authorId\""
}
policy_update post_update_member {
target = Post
roles = [authenticated]
using = "(select auth.uid()) = \"authorId\""
withCheck = "(select auth.uid()) = \"authorId\""
}@@rls enables RLS fail-closed on the model; roles reference Prisma 8 role declarations (@prisma/orm-extension-supabase supplies anon, authenticated, service_role); prisma migration plan emits the ENABLE ROW LEVEL SECURITY and CREATE POLICY statements and prisma db verify fails on drift.
Request lifecycle
- The request-scoped
PermDockis created. permdock.where(permission)flattens grants (each allow ANDed with higher-priority deny negations, ORed together) into a portable tree; unconditional allow yieldstrue, no grant yieldsfalse.toWherecompiles the tree into aWhereInput;truebecomes{}andfalsebecomes{ OR: [] }. Subject values are inlined as literal filter values (Prisma parameterises them).- The app composes the input with its own filters. With
permdockExtensioninstalled, anOR: []anywhere guarantees zero rows for every operation type. on('decision')fires with the granted-or-not outcome for observability.
What it validates
- Condition fields must exist on the model when the resource schema was generated from Prisma types; otherwise the mismatch surfaces as a Prisma validation error at query time and
toWherecannot help, which is why generated schemas are recommended. - Closure grants:
permdock.wheremarks them{ portable: false }andtoWherethrowsPermDockValidationErrornaming the grant. - The extension validates nothing about rows; it only rewrites
where. - Rows returned by Prisma are trusted server data and are not validated (
validate: 'boundary').
How denials surface
- Not granted: an empty result set. As with Drizzle, run
assert(permissions.post.list)first when the API should answer403instead of an empty list;{ OR: [] }alone cannot distinguish forbidden from not found (CASL #794, #404). - Writes:
updateMany/deleteManywith{ OR: [] }affect zero rows; single-recordupdate/deleteshould be preceded byassert(permissions.post.update, post)because Prisma's unique-where does not accept the filter. - Non-portable grant:
PermDockValidationErrorat compile time. - In RLS mode: policies filter (
USING) or raise42501(WITH CHECK), which Prisma surfaces as a known request error;permdock rls verifymaps both tofiltered/rejected.
Example app
apps/examples/prisma: a Next.js route handler API with toWhere on list routes, permdockExtension installed, a Prisma 8 schema with generated policy_* blocks, and a parity test running filter in-process against findMany with toWhere and against RLS via set local role.
Related standards
- Postgres RLS: policy semantics behind the
policy_*blocks. - Conditions: the operator set and the flattening rule.
- RLS adapter:
generate --target prisma,import,verify. - Research: CASL v7:
accessibleBy,{ OR: [] },createCaslExtension.
Open questions
- Whether
toWhereshould generate relation filters (some/every/none) for membership conditions or require a subquery-free formulation. - How to type
toWherewithout importing@prisma/clientin the adapter, given Prisma 7 and 8 custom output paths. - Whether the extension should also guard
findUniqueby throwing when{ OR: [] }would be the only filter (unique lookups ignore it). - Prisma 8 policy syntax is new; the exact quoting and
rolesshape will be pinned once the target ships in Phase 3.
Drizzle
permdock/drizzle compiles portable conditions to Drizzle where clauses with toWhere, generates pgPolicy entries for RLS through drizzle-orm/supabase helpers, and reuses drizzle-zod schemas for generated definitions.
Kysely
permdock/kysely compiles portable conditions into Kysely expression-builder callbacks with toWhere, fails closed when nothing is granted, and documents the Kysera @kysera/rls dual-mode prior art.