Query plan adapter for Convex

Published by Alex Olivier on February 09, 2026
Query plan adapter for Convex

Externalized authorization moves access control out of your application code and into a dedicated policy engine. With Cerbos, you define who can do what in policy files that live alongside your code in version control. At runtime, your application asks Cerbos for a decision rather than implementing the logic itself. Policies can express roles, attributes, conditions, and relationships - and they can change without redeploying your application.

Individual access checks are straightforward: pass a principal, resource, and action to checkResources and get a permit or deny. But listing resources - "show me everything this user can see" - is a harder problem. Checking every row one by one means fetching data the user may never be allowed to access. That is wasteful at best and a scaling bottleneck at worst.

The PlanResources API takes a different approach. Instead of evaluating specific resource instances, Cerbos partially evaluates your policies and returns a query plan: an abstract syntax tree that describes the conditions under which access is granted. This AST can be translated into any query language, pushing authorization filtering down to the data layer where it belongs. The database applies the filter using its own indexes and query engine, rather than your application processing rows it will discard.

Convex is a reactive backend platform where your database, server functions, and real-time sync all live in one place. Today we are releasing @cerbos/orm-convex, a query plan adapter that translates Cerbos query plans into Convex filter functions.

 

The challenge with Convex

Convex queries use a functional filter API - q.eq, q.lt, q.and, and so on - rather than SQL. This means a straightforward SQL translation will not work. The adapter needs to produce composable filter functions that Convex's query engine understands natively.

There is also a gap in what Convex can express at the database level. String operations like contains and collection operators like exists have no Convex equivalent. The adapter addresses this with a two-tier approach: operations that Convex supports natively become database-level filters, and everything else is evaluated as a post-filter in JavaScript.

 

How it works

queryPlanToConvex takes a Cerbos plan and returns up to two functions: a filter for the database and an optional postFilter for client-side evaluation.

import { queryPlanToConvex, PlanKind } from "@cerbos/orm-convex";

const plan = await cerbos.planResources({
  principal: { id: "user1", roles: ["USER"] },
  resource: { kind: "task" },
  action: "view",
});

const { kind, filter, postFilter } = queryPlanToConvex({
  queryPlan: plan,
  mapper: {
    "request.resource.attr.status": { field: "status" },
    "request.resource.attr.priority": { field: "priority" },
  },
  allowPostFilter: true,
});

if (kind === PlanKind.ALWAYS_DENIED) return [];

let query = ctx.db.query("tasks");
if (filter) query = query.filter(filter);
let results = await query.collect();
if (postFilter) results = results.filter(postFilter);
return results;

DB-level vs. post-filter operators

Pushed to Convex DB Evaluated in JavaScript (post-filter)
eq, ne, lt, le, gt, ge contains, startsWith, endsWith
and, or, not hasIntersection
in, isSet exists, exists_one, all

For and expressions that mix both tiers, the adapter splits the tree: DB-pushable children go to filter, the rest go to postFilter. For or expressions with any unsupported child, the entire expression is evaluated client-side to avoid returning false positives.

 

Full example

Consider a policy that allows users to view tasks assigned to them, or tasks with a priority above a threshold:

# policies/task.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  resource: task
  version: default
  rules:
    - actions: ["view"]
      effect: EFFECT_ALLOW
      roles: ["USER"]
      condition:
        match:
          any:
            of:
              - expr: request.resource.attr.assignee == request.principal.id
              - expr: request.resource.attr.priority >= 3

Inside a Convex query function:

import { GRPC as Cerbos } from "@cerbos/grpc";
import { queryPlanToConvex, PlanKind } from "@cerbos/orm-convex";
import { query } from "./_generated/server";

const cerbos = new Cerbos("localhost:3592", { tls: false });

export const listTasks = query({
  handler: async (ctx) => {
    const plan = await cerbos.planResources({
      principal: { id: "user1", roles: ["USER"] },
      resource: { kind: "task" },
      action: "view",
    });

    const { kind, filter, postFilter } = queryPlanToConvex({
      queryPlan: plan,
      mapper: {
        "request.resource.attr.assignee": { field: "assignee" },
        "request.resource.attr.priority": { field: "priority" },
      },
    });

    if (kind === PlanKind.ALWAYS_DENIED) return [];

    let q = ctx.db.query("tasks");
    if (filter) q = q.filter(filter);
    let results = await q.collect();
    if (postFilter) results = results.filter(postFilter);
    return results;
  },
});

Because this policy only uses comparison operators, the adapter pushes the entire condition to the Convex database layer - no post-filter is needed and allowPostFilter is not required.

Opting into post-filtering

By default, queryPlanToConvex throws if the plan requires a post-filter. This is a deliberate safety choice - post-filtering means documents are read from the database before the full authorization condition is applied. Pass allowPostFilter: true to enable it when your policies need string or collection operators.

If your policies only use comparisons, in, isSet, and logical combinators, you do not need the flag. The database filter alone will enforce the complete policy.

 

Get started

npm install @cerbos/orm-convex

The full documentation and source are available on GitHub. If you have questions, join the Cerbos community Slack.

Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team

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.