NEWPromptev MCP Server: one URL for every tool, every agent and your team’s knowledge. Connect Claude Code, Cursor, or any MCP client.→

Permission-aware retrieval

Retrieval that knows who is asking.

Every point above is a chunk of somebody’s corpus, clustered by the team that owns it. Ask a question and the search returns the nearest ones. Then your ACL deletes most of what came back. Nothing in the stack reports the loss. The answer just arrives thinner.

See the measurement

Python 3.12+  ·  Node 22+  ·  one Postgres schema, either client

Recall@10 · permissions inside the query · caller sees 1%
1.000Agreement with a corpus holding only the caller’s documents, not relevance. Exact at or below 50,000 visible chunks.
Searches returned empty · filter after the search · caller sees 1%
99.0%Same search, same limit, permissions applied afterwards.
Recall@10 · filter after, even fetching 50x more · caller sees 1%
0.083Hybrid search, 100,000 documents, synthetic team-shaped permissions.

The mechanism

The search stops before it reaches you.

A search that does not know who is asking returns the nearest rows in the whole corpus, and the permission filter runs on what came back. When a team's documents sit together in the embedding space, that limit goes almost entirely on rows the caller may not read. Fetching more helps less than it should, for the same reason. Below: one query, how far the search reached, and who is inside it.

  • Query
  • Returned
  • Permitted, withheld
  • Other principals
Measured recall@10 against a visible-only corpus · caller sees 10%
0.084
The caller’s top results returned · measured recall drawn on 20 slots

2 of 20 permitted rows returned · 18 withheld Measured recall 0.084.

Search first, filter afterwards. The search does not know who is asking, so it spends its limit on the nearest rows in the whole corpus. Those belong overwhelmingly to other groups, and the filter removes them. Measured: recall 0.084, and 63.5% of searches came back with nothing at all. No error is raised.

Measured on 100,000 documents from BEIR HotpotQA (100,646 chunks) with hybrid search at its shipped defaults, 200 searches, and synthetic team-shaped permissions under which the caller sees 10% of the corpus. Recall is agreement with a corpus that holds only the caller’s documents, not relevance, and the vector leg is exact here because the caller sees fewer than 50,000 chunks. Drop visibility to 1% and filtering after the search returns nothing at all for 99.0% of searches: not a degraded answer, an empty one. The plot illustrates a single query on a toy corpus. The recall readout is the measured figure, and the result slots are that same figure drawn on 20 slots. Every table, the conditions and what was not measured are on the benchmarks page.

Don't take our number

Measure your own corpus, offline.

A benchmark on someone else's data proves nothing about yours: the loss depends entirely on the shape of your permissions. So the measurement ships as a command. It samples your real ACL values, replays query vectors against your own index, and never sends a byte anywhere.

check-acl-exposure
$ context-engine check-acl-exposure --database-url $DATABASE_URL --samples 200

The report lists your corpus size and, for each ACL value that occurs in your data, its share of visible rows, the query plan Postgres chose, the recall at the top k (10 by default) of a bare scoped query (plain) and of this library's shipped path on the same rows (engine), the share of queries that came back empty, and the median latency. Both recall columns are scored against an exact scan over the eligible rows. It ends with a verdict of ok, degraded or exposed for the worst scope, and exits non-zero only when the shipped path itself under-returns, so it can gate a deployment.

No sample figures shown. Your numbers depend on the shape of your permissions

What it is

A library over the Postgres you already run.

Full-text, trigram, vector and graph legs fuse through Reciprocal Rank Fusion, and all four compile through one scope predicate. There is no permissive default and no way to call retrieval without saying who is asking.

Enforced inside the plan

The ACL is a SQL predicate every retrieval leg shares, so a document you may not see is never fetched, never ranked, and never reaches a reranker.

Redaction before embedding

A pure transform: no model, no network. Mask at ingest and the secret never enters the index; mask at output and it never leaves. ACL decides who, redaction decides what.

Your Postgres, your keys

BYO database, embedding provider and LLM. The package never calls a hosted service you didn't configure: no control plane, no egress, no vendor holding your corpus.

Python and TypeScript

Two clients, one Postgres schema: a corpus ingested by either is searchable by the other. Serve it on FastAPI, Flask, Django, Hono, Express, Fastify, or over MCP.

Requirements are a database, not a stack: PostgreSQL 14+ with vector (pgvector 0.8+) for the semantic leg, pg_trgm for fuzzy and partial matching, and unaccent so café and cafe match. All three are stock PostgreSQL and the engine creates them itself on the first migration: you install nothing by hand.

Start here

Find the loss before an auditor does.

Install it, point it at the database you already have, and read the verdict it prints. If the loss isn't there, you've spent four minutes.

Read the write-up

Apache-2.0  ·  Python 3.12+  ·  Node 22+  ·  PostgreSQL 14+ with vector (pgvector ≥ 0.8), pg_trgm and unaccent

1,000 free credits for a 30-day trial · No card required · No subscription1,000 free credits · 30-day trial · No card