Convex adapter :: Cerbos Authorization Management Platform // Documentation
Convex adapter
The @cerbos/orm-convex package converts a Cerbos PlanResources response into a Convex filter function. Authorization conditions are split between a database-level filter and an optional JavaScript post-filter for operators that Convex cannot express natively.
Requirements
- Cerbos >= v0.16
@cerbos/httpor@cerbos/grpcclient- Convex 1.x
- Node.js >= 20.0.0
Installation
npm install @cerbos/orm-convex
Supported operators
Database-level operators
| Category | Operators |
|---|---|
| Logical | and, or, not — q.and(…), q.or(…), q.not(…) |
| Comparison | eq, ne, lt, le, gt, ge — q.eq, q.neq, q.lt, q.lte, q.gt, q.gte |
| Membership | in — composed as q.or(q.eq(field, v1), q.eq(field, v2), …) |
| Existence | isSet — q.neq(field, undefined) or q.eq(field, undefined) |
Post-filter operators
The following operators cannot be expressed as Convex database filters. When encountered, the adapter returns a postFilter function that evaluates them in JavaScript:
| Category | Operators |
|---|---|
| String | contains, startsWith, endsWith |
| Collection | hasIntersection, exists, exists_one, all, filter, map, lambda |
For and(…) expressions with mixed operator types, the adapter splits the tree: database-pushable children go to filter, the rest go to postFilter. For or(…) with any unsupported child, the entire expression goes to postFilter to avoid missing results.
allowPostFilter opt-in
By default, queryPlanToConvex throws when the query plan requires a postFilter. This is because post-filter operators cause data to be fetched before authorization filtering is fully applied. To opt in:
const { kind, filter, postFilter } = queryPlanToConvex({
queryPlan,
mapper,
allowPostFilter: true,
});
If your policies only use operators that Convex supports natively, filter alone enforces the full policy at the database level and this flag is not needed.
Usage
import { queryPlanToConvex, PlanKind } from "@cerbos/orm-convex";
const queryPlan = await cerbos.planResources({
principal: { id: "user1", roles: ["USER"] },
resource: { kind: "document" },
action: "view",
});
const { kind, filter, postFilter } = queryPlanToConvex({
queryPlan,
mapper,
allowPostFilter: true,
});
if (kind === PlanKind.ALWAYS_DENIED) return [];
if (kind === PlanKind.ALWAYS_ALLOWED && !postFilter) {
return await ctx.db.query("documents").collect();
}
let query = ctx.db.query("documents");
if (filter) query = query.filter(filter);
let results = await query.collect();
if (postFilter) results = results.filter(postFilter);
return results;
Field mapper
const mapper = {
"request.resource.attr.title": { field: "title" },
"request.resource.attr.status": { field: "status" },
};
// Or as a function:
const mapper = (path) => ({
field: path.replace("request.resource.attr.", ""),
});
The field property rewrites a Cerbos path to a Convex document field. Dot notation is supported for nested fields. If the mapper is omitted, the adapter uses query plan paths as-is.
Limitations
- String and collection operators are evaluated as a JavaScript
postFilterafter the database query returns. These conditions do not reduce the number of documents read from the database. - For
or(…)expressions where any child uses an unsupported operator, the entire OR is evaluated viapostFilter. - The
inoperator is composed as multipleeqcomparisons joined withor, which may be less efficient for large value lists.