Query plan adapter for LangChain.js and ChromaDB

Published by Alex Olivier on February 16, 2026
Query plan adapter for LangChain.js and ChromaDB

Externalized authorization keeps access control logic out of your application code. You define policies centrally -- who can access what, under which conditions -- and a dedicated engine evaluates them at runtime. With Cerbos, those policies are written as code, stored in version control, and can express roles, attributes, hierarchies, and conditions without requiring application changes.

For traditional database queries, Cerbos offers the PlanResources API: rather than checking each resource individually, Cerbos partially evaluates your policies and returns a query plan -- an abstract syntax tree describing the conditions under which access is granted. Adapters translate that AST into native database filters, so authorization is enforced at the query layer. The database only returns rows the user is allowed to see, and your application never handles unauthorized data.

This same approach applies to vector databases and AI retrieval pipelines. Retrieval-augmented generation (RAG) systems fetch documents based on semantic similarity, but similarity is not authorization. Without filtering, a RAG pipeline can surface confidential data simply because it is semantically close to the question.

Today we are releasing @cerbos/langchain-chromadb, a query plan adapter that translates Cerbos query plans into ChromaDB Where filters. These filters can be passed directly to the LangChain.js Chroma vector store, so authorization is enforced at the retrieval layer -- before any document reaches the LLM.

 

Why this matters for AI applications

Most RAG implementations have a gap between "what is relevant" and "what the user is allowed to see." The typical workaround is to filter results after retrieval, but that wastes vector search budget on documents that will be thrown away, and risks leaking information through summaries or snippets generated before the filter runs.

By pushing authorization filters into ChromaDB's metadata query, the vector search itself only considers documents the user is permitted to access. No unauthorized document ever reaches the LLM.

 

How it works

queryPlanToChromaDB takes a Cerbos plan and a field name mapper, and returns a ChromaDB Where filter object.

import { GRPC as Cerbos } from "@cerbos/grpc";
import { Chroma } from "@langchain/community/vectorstores/chroma";
import { OpenAIEmbeddings } from "@langchain/openai";
import { queryPlanToChromaDB, PlanKind } from "@cerbos/langchain-chromadb";

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

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

const result = queryPlanToChromaDB({
  queryPlan: plan,
  fieldNameMapper: {
    "request.resource.attr.department": "department",
    "request.resource.attr.clearance": "clearance_level",
  },
});

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

const chroma = await Chroma.fromExistingCollection(new OpenAIEmbeddings(), {
  collectionName: "internal_docs",
});

const filters =
  result.kind === PlanKind.CONDITIONAL ? result.filters : undefined;

const matches = await chroma.similaritySearch("quarterly revenue", 10, filters);

The mapper translates Cerbos attribute paths (like request.resource.attr.department) to the metadata field names in your ChromaDB collection. It accepts either a plain object or a function for dynamic resolution.

 

Full example

Consider a policy that restricts document access by department and clearance level:

# policies/document.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  resource: document
  version: default
  rules:
    - actions: ["view"]
      effect: EFFECT_ALLOW
      roles: ["EMPLOYEE"]
      condition:
        match:
          all:
            of:
              - expr: request.resource.attr.department == request.principal.attr.department
              - expr: request.resource.attr.clearance_level <= request.principal.attr.clearance

When an employee searches for documents, the adapter converts the plan into a ChromaDB metadata filter so the vector search only returns documents the employee is cleared to see:

import { GRPC as Cerbos } from "@cerbos/grpc";
import { Chroma } from "@langchain/community/vectorstores/chroma";
import { OpenAIEmbeddings } from "@langchain/openai";
import { queryPlanToChromaDB, PlanKind } from "@cerbos/langchain-chromadb";

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

async function authorizedSearch(userId: string, question: string) {
  const plan = await cerbos.planResources({
    principal: {
      id: userId,
      roles: ["EMPLOYEE"],
      attr: { department: "engineering", clearance: 3 },
    },
    resource: { kind: "document" },
    action: "view",
  });

  const result = queryPlanToChromaDB({
    queryPlan: plan,
    fieldNameMapper: {
      "request.resource.attr.department": "department",
      "request.resource.attr.clearance_level": "clearance_level",
    },
  });

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

  const chroma = await Chroma.fromExistingCollection(new OpenAIEmbeddings(), {
    collectionName: "internal_docs",
  });

  const filters =
    result.kind === PlanKind.CONDITIONAL ? result.filters : undefined;

  return await chroma.similaritySearch(question, 10, filters);
}

The filters object passed to similaritySearch might look like:

{
  "$and": [
    { "department": { "$eq": "engineering" } },
    { "clearance_level": { "$lte": 3 } }
  ]
}

ChromaDB applies this filter during the vector search itself, so unauthorized documents are never retrieved or sent to the LLM.

 

Supported operators

Category Cerbos operators ChromaDB output
Logical and, or $and, $or
Negation not Operator inversion via De Morgan's law
Comparison eq, ne, lt, le, gt, ge $eq, $ne, $lt, $lte, $gt, $gte
Membership in $in

ChromaDB does not natively support $not, so the adapter inverts operators directly: not(eq) becomes $ne, not(and(A, B)) becomes $or[not(A), not(B)] using De Morgan's law, and double negations are eliminated.

ChromaDB stores flat scalar metadata, so string helpers (contains, startsWith, endsWith) and collection operators (exists, all, hasIntersection) are not supported. If a policy emits these operators, the adapter throws a descriptive error.

 

Get started

npm install @cerbos/langchain-chromadb

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

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.