npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cerbos/orm-convex

v0.1.0

Published

An adapter library that takes a [Cerbos](https://cerbos.dev) Query Plan ([PlanResources API](https://docs.cerbos.dev/cerbos/latest/api/index.html#resources-query-plan)) response and converts it into a [Convex](https://convex.dev/) filter function. It is d

Downloads

12

Readme

Cerbos + Convex Adapter

An adapter library that takes a Cerbos Query Plan (PlanResources API) response and converts it into a Convex filter function. It is designed to run alongside a project that is already using the Cerbos JavaScript SDK to fetch query plans so that authorization logic can be pushed down to Convex queries.

How it works

  1. Use a Cerbos client (@cerbos/http or @cerbos/grpc) to call planResources and obtain a PlanResourcesResponse.
  2. Provide queryPlanToConvex with that plan and an optional mapper that describes how Cerbos attribute paths relate to your Convex document fields.
  3. The adapter walks the Cerbos expression tree and returns { kind, filter?, postFilter? }:
    • filter is a Convex-native filter function (q) => Expression<boolean> pushed to the DB.
    • postFilter is a JS predicate (doc) => boolean for operators Convex can't express natively (string ops, collection ops). Requires allowPostFilter: true — see below.
  4. Inspect result.kind:
    • ALWAYS_ALLOWED: the caller can query without any additional filters.
    • ALWAYS_DENIED: short-circuit and return an empty result set.
    • CONDITIONAL: apply result.filter server-side and result.postFilter client-side (see usage example below).

Supported operators

| Category | Operators | Behavior | | --- | --- | --- | | Logical | and, or, not | Builds q.and(...), q.or(...), q.not(...) groups. | | Comparisons | eq, ne, lt, le, gt, ge | Emits q.eq, q.neq, q.lt, q.lte, q.gt, q.gte against the mapped field. | | Membership | in | Composed as q.or(q.eq(field, v1), q.eq(field, v2), ...). | | Existence | isSet | Uses q.neq(field, undefined) for set, q.eq(field, undefined) for unset. |

Post-filter operators

The following operators cannot be expressed as Convex DB filters. When the adapter encounters them, it returns a postFilter function that evaluates them in JavaScript against each document:

| Category | Operators | JS Behavior | | --- | --- | --- | | String | contains, startsWith, endsWith | String.prototype.includes / startsWith / endsWith | | Collection | hasIntersection | a.some(v => b.includes(v)) | | Quantifiers | exists, exists_one, all | Array.prototype.some / filter-count / every with lambda | | Higher-order | filter, map, lambda | Used internally by quantifier operators |

For mixed expressions (e.g. and(eq(...), contains(...))), the adapter splits the tree: DB-pushable children go to filter, the rest go to postFilter. For or(...) with any unsupported child, the entire expression goes to postFilter (partial OR push-down would miss results).

allowPostFilter opt-in

By default, queryPlanToConvex throws an error when the query plan requires a postFilter. This is because post-filter operators cause data to be fetched from the database before authorization filtering is applied — the DB-level filter alone may not fully enforce the authorization policy.

To enable post-filtering, pass allowPostFilter: true:

const { kind, filter, postFilter } = queryPlanToConvex({
  queryPlan,
  mapper,
  allowPostFilter: true,
});

If your Cerbos policies only use operators that Convex supports natively (comparisons, in, isSet, logical combinators), you don't need this flag — filter alone will enforce the full policy at the DB level.

Requirements

  • Cerbos > v0.16 plus either the @cerbos/http or @cerbos/grpc client

System Requirements

  • Node.js >= 20.0.0
  • Convex 1.x

Installation

npm install @cerbos/orm-convex

API

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

const { kind, filter, postFilter } = queryPlanToConvex({
  queryPlan, // PlanResourcesResponse from Cerbos
  mapper, // optional Mapper - see below
  allowPostFilter: true, // opt in to client-side filtering (see note below)
});

if (kind === PlanKind.ALWAYS_DENIED) return [];
if (kind === PlanKind.ALWAYS_ALLOWED && !postFilter) {
  return await ctx.db.query("myTable").collect();
}

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

PlanKind is re-exported from @cerbos/core:

export enum PlanKind {
  ALWAYS_ALLOWED = "KIND_ALWAYS_ALLOWED",
  ALWAYS_DENIED = "KIND_ALWAYS_DENIED",
  CONDITIONAL = "KIND_CONDITIONAL",
}

Mapper configuration

The Cerbos query plan references fields using paths such as request.resource.attr.title. Use a mapper to translate those names to the field names in your Convex documents.

export type MapperConfig = {
  field?: string;
};

export type Mapper =
  | Record<string, MapperConfig>
  | ((key: string) => MapperConfig);
  • field rewrites a single Cerbos path to a different field name in your Convex document. Dot-notation is supported for nested fields.

If you omit the mapper the adapter will use the query plan paths verbatim.

Direct fields

const mapper: Mapper = {
  "request.resource.attr.aBool": { field: "aBool" },
  "request.resource.attr.title": { field: "title" },
  "request.resource.attr.nested.value": { field: "metadata.value" },
};

Mapper functions

You can also supply a function if your mappings follow a predictable pattern:

const mapper: Mapper = (path) => ({
  field: path.replace("request.resource.attr.", ""),
});

Usage example

import { GRPC as Cerbos } from "@cerbos/grpc";
import {
  queryPlanToConvex,
  PlanKind,
  type Mapper,
} from "@cerbos/orm-convex";

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

const mapper: Mapper = {
  "request.resource.attr.title": { field: "title" },
  "request.resource.attr.status": { field: "status" },
  "request.resource.attr.priority": { field: "priority" },
};

// Inside a Convex query function:
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;

Error handling

queryPlanToConvex throws descriptive errors in the following scenarios:

  • The plan kind is not one of the Cerbos PlanKind values (Invalid query plan.).
  • A conditional plan omits the operator/operands structure (Invalid Cerbos expression structure).
  • An operator listed in the plan is not implemented by this adapter (Unsupported operator for Convex: <name> or Unsupported operator: <name>).
  • The in operator is given a non-array value.
  • The query plan requires client-side filtering and allowPostFilter is not set to true.

Limitations

  • String and collection operators (contains, startsWith, endsWith, hasIntersection, exists, all, etc.) are evaluated as a JavaScript postFilter after the DB query returns. This means 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 client-side via postFilter. Only and(...) expressions can be split between DB filter and post-filter.
  • The in operator is composed as multiple eq comparisons joined with or, which may be less efficient for large value lists.