Retrieval that knows who’s asking#

needtoknow puts your permission system inside RAG retrieval. Every user gets only the chunks they are allowed to read, whatever vector store, permission system or framework you use.

pip install needtoknow
The needtoknow logo: a document whose readable lines are highlighted and one line is redacted

Built for production RAG#

A RAG assistant over company documents is only as safe as its retrieval step. If the search ignores who is asking, an engineer can ask about salaries and get HR documents summarised back. needtoknow sits between your application and your vector store, and closes that gap.

Works with what you have

pgvector, Qdrant, Milvus, MongoDB, Weaviate, Redis, LanceDB, Elasticsearch, OpenSearch, Chroma or any LangChain or LlamaIndex store; OpenFGA, SpiceDB, Cerbos, Open Policy Agent or your own rules. The core has no dependencies.

Exact where it matters

With the filter inside the search, results are the true top-k of what the user may read, not the top-k of everything with the forbidden ones removed.

Fails closed

Errors, timeouts and malformed answers from the permission system raise an error. They never return unfiltered results.

Defence in depth

Pre-filtered results are checked again, so a backend that ignores the filter cannot leak, and each result counts the violations it caught.

Observable and auditable

Every retrieval emits an audit event and, optionally, an OpenTelemetry span, without the query or chunk text.

Sync and async

retrieve and aretrieve share one implementation of the security logic, with native async clients where the store has them.

How it works#

1. Ask the permission system

The authorizer turns the user into a filter (OpenFGA, SpiceDB, Cerbos, Open Policy Agent, or permissions stored on the chunks), or checks candidates one by one.

2. Filter inside the search

The filter runs inside the vector store’s own query, so the nearest chunks the user may read come back, however few of the corpus that is.

3. Check, then return

Every hit is checked again before it reaches the prompt. Anything that cannot be decided raises an error instead.

Your stack, unchanged#

from needtoknow import Document, InMemoryBackend, MetadataACLAuthorizer, PermissionedRetriever, Subject

backend = InMemoryBackend(
    [
        Document("handbook", "Annual leave is 25 days.", {"allowed_groups": ["*"]}),
        Document("salaries", "Salary bands for 2026 ...", {"allowed_groups": ["hr"]}),
    ]
)
retriever = PermissionedRetriever(backend, MetadataACLAuthorizer())

result = retriever.retrieve("leave salary bands", Subject("bob", groups=["engineering"]), k=3)
print([hit.id for hit in result.hits])  # ['handbook']
from psycopg_pool import ConnectionPool

from needtoknow import PermissionedRetriever, Subject
from needtoknow.authorizers.openfga import OpenFGAAuthorizer
from needtoknow.backends.pgvector import PgVectorBackend

backend = PgVectorBackend(ConnectionPool("postgresql://localhost/app"), embed=embed)
authorizer = OpenFGAAuthorizer("http://localhost:8080", store_id, resource_field="doc_id")
retriever = PermissionedRetriever(backend, authorizer)

result = retriever.retrieve("What are the salary bands?", Subject("alice"))
from needtoknow import MetadataACLAuthorizer, PermissionedRetriever, Subject
from needtoknow.langchain import NeedToKnowRetriever

retriever = NeedToKnowRetriever(retriever=PermissionedRetriever(backend, MetadataACLAuthorizer()), k=4)

docs = retriever.invoke("What changed in Q3 pricing?", subject=Subject("bob", ["sales"]))
from llama_index.core.query_engine import RetrieverQueryEngine

from needtoknow import MetadataACLAuthorizer, PermissionedRetriever, Subject
from needtoknow.llamaindex import NeedToKnowRetriever

permissioned = PermissionedRetriever(backend, MetadataACLAuthorizer())
retriever = NeedToKnowRetriever(permissioned, Subject("bob", ["engineering"]), k=4)

engine = RetrieverQueryEngine.from_args(retriever)  # uses your configured LLM

Integrations#

Permission systems
Frameworks and operations

Measured, not claimed#

0 forbidden chunks returned in 21,000 benchmark retrievals

0.88–0.97 recall@10 at a million chunks, pre-filtered on pgvector

129/s retrievals on 4 CPUs, a million chunks in PostgreSQL

8–10% cost of a remote permission service, all of it recovered by caching

Every number comes from a reproducible run, with the method and full tables on the Benchmarks page.

Secure by default#

No silent fallback

An unavailable or misbehaving permission system stops retrieval; it never widens it. The security model states what is guaranteed and what is assumed.

Verifiable releases

Every release carries Sigstore signatures, a CycloneDX SBOM and PyPI attestations, published by GitHub Actions without stored credentials.

Hardened supply chain

Actions pinned to commits, CodeQL on the code and the workflows, and a published OpenSSF Scorecard.