Externalized authorization separates access control logic from application code. Instead of scattering if statements and role checks across your codebase, you define policies centrally and query a decision engine at runtime. Cerbos is purpose-built for this: your policies live as code in version control, and applications call Cerbos to find out whether a given principal can perform a given action on a given resource.
This works well for individual access checks, but most applications also need to answer a different question: "which resources can this user see?" The naive approach -- fetch everything, then call checkResources on each row -- does not scale. You end up reading data the user will never be allowed to see, wasting database I/O and application memory.
Cerbos solves this with the PlanResources API. Instead of evaluating a specific resource instance, PlanResources uses partial evaluation to analyze your policies and return a query plan -- an abstract syntax tree (AST) that describes the conditions under which access is granted. The plan contains the same operators and attribute references your policies use, but structured as a tree that can be mechanically translated into any query language. The result is one of three outcomes:
The conditional case is where query plan adapters come in. They walk the AST, map Cerbos attribute paths to your schema's column names, and produce a native filter that your ORM or database client can execute directly. Authorization logic stays in your policies, and the database applies it at the query layer -- aligned with indexes and query optimization, not burning cycles in application code.
Today we are releasing @cerbos/orm-drizzle, a query plan adapter for Drizzle ORM.
Drizzle has become one of the most popular TypeScript ORMs. It is lightweight, SQL-first, and gives developers direct control over the queries that hit the database. Its type-safe query builder maps closely to SQL, which makes it a natural target for translating Cerbos query plans into efficient database filters.
The adapter works with every SQL dialect Drizzle supports -- SQLite, PostgreSQL, MySQL, and PlanetScale.
queryPlanToDrizzle takes a Cerbos PlanResourcesResponse and a mapper that associates Cerbos attribute paths with Drizzle columns. It walks the expression tree and returns a Drizzle SQL fragment that slots straight into a .where() clause.
import { queryPlanToDrizzle, PlanKind } from "@cerbos/orm-drizzle";
import { resources } from "./schema";
const plan = await cerbos.planResources({
principal: { id: "user1", roles: ["USER"] },
resource: { kind: "document" },
action: "view",
});
const result = queryPlanToDrizzle({
queryPlan: plan,
mapper: {
"request.resource.attr.status": resources.status,
"request.resource.attr.owner": resources.ownerId,
},
});
switch (result.kind) {
case PlanKind.ALWAYS_ALLOWED:
return await db.select().from(resources);
case PlanKind.ALWAYS_DENIED:
return [];
case PlanKind.CONDITIONAL:
return await db.select().from(resources).where(result.filter);
}
Because the adapter produces a standard Drizzle SQL fragment, you can compose it with your own conditions using and() or or() like any other filter.
Real-world authorization policies rarely check flat columns alone. A policy might grant access to documents owned by a specific department, where the department is a row in another table. The Drizzle adapter handles this with relation mappings that generate EXISTS subqueries, including support for nested relations and many-to-many joins.
const result = queryPlanToDrizzle({
queryPlan: plan,
mapper: {
"request.resource.attr.tags": {
relation: {
type: "many",
table: resourceTags,
sourceColumn: resources.id,
targetColumn: resourceTags.resourceId,
fields: {
name: {
relation: {
type: "one",
table: tags,
sourceColumn: resourceTags.tagId,
targetColumn: tags.id,
field: tags.name,
},
},
},
},
},
},
});
Consider a policy that allows users to view published documents, or any document they own:
# policies/document.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: document
version: default
rules:
- actions: ["view"]
effect: EFFECT_ALLOW
roles: ["USER"]
condition:
match:
any:
of:
- expr: request.resource.attr.status == "published"
- expr: request.resource.attr.ownerId == request.principal.id
With Drizzle, the adapter turns this into a composable SQL filter:
import { GRPC as Cerbos } from "@cerbos/grpc";
import { queryPlanToDrizzle, PlanKind } from "@cerbos/orm-drizzle";
import { eq, and } from "drizzle-orm";
import { db } from "./db";
import { documents } from "./schema";
const cerbos = new Cerbos("localhost:3592", { tls: false });
async function listDocuments(userId: string) {
const plan = await cerbos.planResources({
principal: { id: userId, roles: ["USER"] },
resource: { kind: "document" },
action: "view",
});
const result = queryPlanToDrizzle({
queryPlan: plan,
mapper: {
"request.resource.attr.status": documents.status,
"request.resource.attr.ownerId": documents.ownerId,
},
});
switch (result.kind) {
case PlanKind.ALWAYS_ALLOWED:
return await db.select().from(documents);
case PlanKind.ALWAYS_DENIED:
return [];
case PlanKind.CONDITIONAL:
return await db
.select()
.from(documents)
.where(and(eq(documents.deleted, false), result.filter));
}
}
The adapter produces a standard Drizzle SQL fragment, so you can compose it with your own conditions using and() or or() like any other filter.
The adapter covers the full range of operators that Cerbos can emit in a query plan:
and, or, noteq, ne, lt, gt, le, ge, incontains, startsWith, endsWithisSethasIntersection, exists, exists_one, all
npm install @cerbos/orm-drizzle
The full documentation and source are available on GitHub. If you have questions or run into issues, join the Cerbos community Slack.
Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team




Join thousands of developers | Features and updates | 1x per month | No spam, just goodies.
What is Cerbos?
Cerbos is an end-to-end enterprise authorization software for Zero Trust environments and AI-powered systems. It enforces fine-grained, contextual, and continuous authorization across apps, APIs, AI agents, MCP servers, services, and workloads.
Cerbos consists of an open-source Policy Decision Point, Enforcement Point integrations, and a centrally managed Policy Administration Plane (Cerbos Hub) that coordinates unified policy-based authorization across your architecture. Enforce least privilege & maintain full visibility into access decisions with Cerbos authorization.