Self-hosted · Apache 2.0 core
Strength from layers.
Permission-aware context layer for agents. Your index, your access rules, your perimeter.
One index, one query
Who is asking?
3 layers indexed
The difference that matters
The filter runs inside the index, not after it.
Most retrieval stacks fetch the top k results and then remove the ones the caller may not see. Ask for ten and you get ten minus whatever was stripped — silently, and worse the more restricted the reader is. Nacre puts the access filter inside the traversal, so top_k returns k permitted results.
The query builder takes a resolved access plan and has no parameter for a raw filter. There is no method a caller can reach that skips the permission model.
A document you may not see and a document that does not exist return the same status, the same body, and the same wording. Otherwise identifiers can be enumerated.
If permissions cannot be evaluated, access is refused. There is no path that shrugs and lets a request through because a cache was cold.
The question a security engineer actually asks
What happens when you revoke someone's access?
Not "eventually". Here is the sequence, and where the boundary of "eventually" actually sits.
t + 0
The grant is deletedA database trigger increments the organization's permission version in the same transaction. Not the application — one writer, so a version bumped by whichever service remembered cannot lie.
the very next request
The revoked grant can no longer be servedThe resolver's cache is keyed on that version, so the next request composes a different key and misses. The stale entry is never invalidated — it is simply never asked for again. The cache TTL is a memory bound, not the correctness mechanism.
within the SLA
The denormalized tags catch upEach chunk in the vector index carries a cached copy of who may read it. A
background pass rewrites them, and until it finishes the layer bound from Postgres
applies too — both filters, every query. The window is bounded by
NACRE_ACL_PROPAGATION_SLA.
continuously
You can watch itnacre_acl_propagation_lag_seconds reports the age of the oldest
document whose tags predate the current version, per organization. Alert on
max(). It is the external evidence that the promise above still holds —
an unmeasured backlog is indistinguishable from no backlog.
Six rules · breaking one is an incident, not a bug
The invariants, in the open.
They are numbered because the code and the tests refer to them by number. Each one has cases in a suite that runs on its own CI job, against a real database and a real vector store.
Two surfaces, one authorization service
Agents over MCP. Applications over REST.
The MCP server is not a wrapper with its own rules — it resolves permission through the same code the REST API uses, per call. Two implementations would be two places for the rules to drift.
MCP · Streamable HTTP and STDIO
For an agent
The search tool's description is generated from the layers that
caller can actually see, so the model knows what is worth asking for.
$ NACRE_SERVICE_KEY=nacre_sk_… npx @nacre.work/mcp
→ tools/list
search · list_layers · get_document
ingest_document · delete_document
→ tools/call search
"when do new hires get access"
← 1 result · layer=handbook
REST · OpenAPI
For an application
Ingest is idempotent on the layer and your own identifier, so the retry every client makes after a timeout costs nothing.
$ curl -X POST localhost:8080/v1/documents \
-H "Authorization: Bearer $NACRE_TOKEN" \
-d '{"layer":"handbook",
"external_id":"onboarding-2026",
"content":"New engineers get…"}'
← 202 { "job_id": "…", "status": "queued" }
Where it runs
Inside your network, with no phone-home.
PostgreSQL, Qdrant, a worker, the API, the MCP server and a parser sidecar. Three profiles: minimal, full, and air-gapped.
Any OpenAI-compatible endpoint, bound per layer, swappable with a reindex that keeps search available throughout.
The core is open, including the permission model and the whole test suite that guards it. Commercial modules are a separate repository.
State, plainly
Early, and it runs. The whole loop has been driven against a real PostgreSQL and a real Qdrant: create an organization, set up access, ingest, and search — over REST and over both MCP transports. Revoking a grant drops a document from results while its vectors are still in the index.
Since written: email and password sign-in with rotating refresh tokens, cross-encoder reranking, garbage collection for tombstoned vectors, rate limiting, idempotency keys, cursor pagination, and the TypeScript SDK and admin UI.
Not built yet: reindexing a layer onto a different model, reading the
audit log back over the API, OAuth discovery and dynamic client registration, and
multipart upload on ingest. Commercial modules — multi-tenancy, SSO, document-level
deny rules — are planned and none is written; there is nothing to buy today.
The specifications in docs/ are normative and in places still ahead of
the code, which is the honest way round.