Chroma#
pip install "needtoknow[chroma]"
The permission filter becomes Chroma’s where clause, so Chroma filters inside the search.
Set up#
import chromadb
from needtoknow.backends.chroma import ChromaBackend
client = chromadb.PersistentClient(path="./chroma")
collection = client.get_or_create_collection("docs", configuration={"hnsw": {"space": "cosine"}})
backend = ChromaBackend(collection) # uses the collection's embedding function
Pass embed= to embed queries and documents with your own model instead of the collection’s
embedding function. Scores are 1 - distance, so create the collection with cosine distance for
scores between -1 and 1. add(documents, embeddings=None) upserts by id and delete(ids) removes
chunks.
How metadata is stored#
needtoknow does not use Chroma’s array metadata, and the reason is a real leak. In chromadb 1.5.x
(local mode), deleting a collection leaves its array metadata behind, and because SQLite reuses
record ids, new records in other collections come back with those arrays and are matched by
where filters (chroma-core/chroma#7594).
For permission data, that means a chunk can appear to belong to groups it was never shared with.
So the backend stores metadata in a form that bug cannot affect:
Your metadata |
Stored in Chroma |
|---|---|
|
|
|
One boolean field per value: |
Everything |
An exact JSON copy under |
Search results are rebuilt from the JSON copy, never from the fields Chroma returns, so the retriever’s re-check always sees the true permissions. Records written by something other than needtoknow have no copy and are treated as having no permissions: the re-check denies them.
Field names may not contain ::, and needtoknow_metadata is reserved. Values that are not
strings or integers are kept in the JSON copy only, which matches the reference semantics: no filter
matches them.
How the filter becomes where#
In(field, values) becomes an $or of $in on the scalar field (one per value type, since
Chroma’s $in lists hold a single type) and an $eq: true test on each value’s list field.
And and Or become $and and $or. A filter that can match nothing, such as In(field, []),
skips the query entirely.
Performance#
In the benchmarks (57,600 chunks, local Chroma 1.5.9), pre-filtered queries kept recall at 0.990 or better, but took 200 to 900 ms against about 18 ms unfiltered, rising with the number of groups in the filter. So:
When users may read a small part of the collection, keep the default (
auto, which pre-filters): it is the only strategy that finds their documents.When users may read most of it,
strategy="postfilter"returns the same results far faster (20 ms instead of 876 ms when everything was readable).Grant access through a few broad groups (team, department, division) rather than hundreds of individual groups, which keeps the
wheredocument small.
Chroma’s own tenants and access control#
needtoknow applies permissions itself, as a filter inside each query and a check of each result, so it never relies on Chroma’s tenants, databases or authorization providers. Do not rely on them to separate your tenants either: as of October 2026, Chroma 1.5.9 (the latest release) has unfixed advisories in which an authenticated user can reach any tenant’s collections (CVE-2026-45830, CVE-2026-45831), and two that let a request run code on a Chroma server (CVE-2026-45829, CVE-2026-45833). If you run a Chroma server, keep it on a private network, reachable only by your application.