v1.34 · Prisma 6 & 7 · Zod v4

Your schema is
the security policy.

prisma-guard turns your Prisma schema into a runtime data boundary: input validation, query shape enforcement, and tenant isolation — declared in one .guard(shape) chain instead of scattered middleware.

716+ unit tests Prisma 6 | 7 CI matrix Node 20 / 22 MIT license
projects.router.ts
// one chain: validate + enforce shape + tenant filter
await prisma.project
  .guard((ctx) => ({
    where: { tenantId: { equals: force(ctx.Tenant) } },
    select: { id: true, title: true },
    take: { max: 50, default: 20 },
  }))
  .findMany(req.body)
Why this exists

Three mistakes ship to production every week.

prisma-guard prevents all three classes at the data layer — not per route, not per developer's discipline.

RISK / 01

Invalid input reaches Prisma

The client controls the entire payload. Unknown fields, wrong types, mass assignment.

prisma.user.create({ data: req.body })
RISK / 02

Dangerous query shapes

A client-controlled include chain traverses relations and selects sensitive fields from unrelated models.

include: { author: { select: { passwordHash: true } } }
RISK / 03

Missing tenant filters

If projectId belongs to another tenant, data leaks across the boundary you never enforced.

where: { id: { equals: projectId } }
What you get

A boundary layer, generated from your schema.

{}

Input validation

Zod schemas generated from Prisma types. @zod directives in the schema are validated at generate-time and applied automatically.

Query shape enforcement

Whitelist where filters, orderBy, include trees and take limits. Everything else is rejected with a typed ShapeError.

Tenant isolation

Tenant filters declared in context-dependent shapes as primary enforcement. Automatic scope injection runs underneath as a top-level backstop.

@

Caller routing

Named shapes route requests by caller with parameterized patterns. Fail-closed: unmatched callers throw unless a default exists.

Projection control

Read projections auto-apply as defaults and whitelists. Forced where conditions reach nested to-many includes.

Fail closed

Ambiguous scope, vacuous filters, empty combinators, unconstrained bulk deletes — rejected at construction or runtime. Never silently ignored.

How it works

One chain between the request and the database.

Client requestreq.body
.guard(shape)validate + enforce
tenant filtersfrom the shape
scope backstopextension, top-level
The API

Shapes read like security policy.

true means client-controlled. Literals and force() mean server-enforced. Functions refine the Zod type. That is the whole language.

TypeScript
import { force } from 'prisma-guard'

await prisma.project
  .guard({
    data: {
      title: (base) => base.min(1, 'Title required').max(200),
      status: true,              // client-controlled, @zod chains apply
      createdBy: currentUserId,   // forced server value
      isActive: force(true),      // forced boolean
    },
  })
  .create({ data: req.body })
await prisma.project
  .guard({
    where: { title: { contains: true } },  // only allowed filter
    orderBy: { createdAt: true, title: true },
    take: { max: 100, default: 25 },
    select: {
      id: true, title: true,
      tasks: { select: { id: true }, take: { max: 10 } },
    },
  })
  .findMany(req.body)
// client omits select? the shape's projection is applied automatically
// primary enforcement: declare tenants in the shape
const prisma = new PrismaClient().$extends(
  guard.extension(() => ({ Tenant: store.getStore()?.tenantId }))
)

await prisma.project
  .guard((ctx) => ({
    where: { tenantId: { equals: force(ctx.Tenant) } },
    include: {
      tasks: { where: { tenantId: { equals: force(ctx.Tenant) } } },
    },
  }))
  .findMany(req.body)
// scope extension still runs as a top-level backstop
await prisma.post
  .guard({
    data: { title: true },
    where: {
      id: true,                    // unique selector from client
      authorId: force(userId),     // permission check merged into where
    },
  })
  .update({
    data: req.body.data,
    where: { id: req.params.id },
  })
// final query is constrained by both id and authorId
Comparison

Raw Prisma gives you everything. Including the footguns.

Featureprisma-guardraw Prisma
Input validationyesno
Query shape enforcementyesno
Tenant filters via context-dependent shapesyesmanual
Nested read tenant filtering (to-many, via shapes)yesmanual
Automatic scope injection backstop (top-level)yesno
Bulk mutation safetyrequired wherenot handled
Vacuous combinator rejectionyesnot handled
Create completeness validationyesno
Nested write scope enforcementdeclare tenant filters in shapesno
Get started

Stop trusting
the request.

Install, add one generator block, run prisma generate. Your boundary layer compiles with your types.

Quick start GitHub