Nacre

Self-hosted · Apache 2.0 core

Strength from layers.

Permission-aware context layer for agents. Your index, your access rules, your perimeter.

Nacre shimmers, but you cannot see through it. Its light comes from interference between layers — and it stays opaque. Every agent and every user sees the cross-section they are cleared for, and not one line more.

One index, one query

Who is asking?

3 layers indexed

POST /v1/search"when do new hires get access"
Bearer token

Simulated here. The same three-answers-to-one-question demo runs on a real index at demo.nacre.work, with logins published — or take an organization of your own for a day.

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.

Pre-filter, structurally

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.

Invisible is not forbidden

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.

Failure denies

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" — the next request, and there is no window after it. Here is the sequence.

t + 0

The grant is deleted

A 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 served

The 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.

there is no step after this

Nothing propagates, because nothing was copied

The permitted set is computed from grants on every request. No denormalized copy of who-may-read trails behind in the vector index, so there is no background pass to finish, no backlog to watch, and no SLA to bound. That is why the guarantee is a property of the code and not of a clock: the request after the revoke already composes the new answer, and there is nothing left to catch up.


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.

I1
The organization comes from the token Never from a body, a path, or a header — not even as an admin override.
I2
Filtering is a pre-filter, never a post-filter The filter goes inside the index traversal, so k means k permitted.
I3
A failure to evaluate denies There is no "couldn't compute it, let it through" path.
I4
No permission and no such object are the same answer 404, never 403 — including the wording of the message.
I5
A deleted document is never returned Including before garbage collection has taken its vectors.
I6
Write does not imply read Admin implies both. The opposite of most systems, and deliberate.

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. Queries run dense and BM25 together, because an agent asks for SQLSTATE 23505 as often as it asks a question.

$ 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.

Docker Compose

PostgreSQL, Qdrant, a worker, the API, the MCP server, a parser sidecar — and the admin UI, served on its own origin. Three profiles: minimal, full, and air-gapped. Built for amd64 and arm64, so an Apple Silicon Mac runs it natively rather than emulated.

Your embeddings

Any OpenAI-compatible endpoint, bound per layer, swappable with a reindex that keeps search available throughout.

Apache 2.0

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.

Since then: a layer can be moved onto a different embedding model with search answering throughout, and the migration is scored against a query set you supply before it switches over — one that lost recall stops instead of going live. The access log reads back over the API as JSON, JSONL or CSV. A document can be uploaded as a form. Tokens can be signed with an Ed25519 key, with the public half published for anything that only needs to verify. OAuth discovery is served; client registration is the authorization server's business, and this is a resource server.

And since then: an account is no longer one secret. A second factor — an authenticator app, or a security key you cannot be phished out of — a password you can change yourself, and password recovery by email where the installation names a relay.

49 releases so far, each one written up with what it cost to find — the release notes are their own page. The newest is 0.24.0: a second full audit of the tree. Twenty concurrent session refreshes could stop the whole API — one pool connection held while awaiting a second, no acquisition timeout, so everything waited forever. A document deleted and re-ingested unchanged stayed invisible while reporting indexed. The collection copy a model migration runs had no claim, so scaled-out workers destroyed each other’s work. And ping was answered on one MCP transport and 404’d on the other for the whole life of both, under a parity guard that compared its table against a literal. Every fix was measured red with the defect restored before it was believed.

Your server, their agent

A remote MCP server you host yourself.

The MCP surface is Streamable HTTP, so it is a remote server: it runs inside your network and a client reaches it over a URL. The agent is somebody else's product; the index, the documents and the permission rules stay yours.

A URL, not a pasted token

An unauthenticated call answers 401 naming its discovery document (RFC 9728); the API is the authorization server, with RFC 8414 metadata and RFC 7591 client registration; the person approves the connection on a consent screen and the client gets its own token. That whole chain was walked with the reference MCP SDK against a running server, which is how two defects in it were found.

It acts as the person who approved it

A connection carries the authority of whoever consented and never more — bounded per layer, so “read the handbook, write to scratch” is a thing somebody can actually say. Connecting an agent hands it a slice of one person's access, not the organization's.

Revoking is not waiting for a token to expire

The token carries who it acts for, never what they may see: the permitted set is computed per request. Disable the person and the connection stops answering on its next call.

Which clients

This side speaks the specification, and that is the whole of what it promises. Anything that speaks MCP over Streamable HTTP can connect — and clients that do include Claude, ChatGPT, Cursor, VS Code and GitHub Copilot, Gemini CLI, Windsurf, Zed and Goose, with more arriving than a list like this can keep up with.

What any of them allows — whether remote servers are available on your plan, which of them an administrator may add — is that client's decision and changes without us. So this page will not tell you it is “supported” anywhere: point a client at your installation and it either speaks the protocol or it does not. The MCP document has the transport, the authorization chain and the tool catalog.

For an organization

The half a security team pays for.

The line is not which features are good. It is who needs them: if one developer on a laptop needs it, it is in the Apache 2.0 core. What is commercial is what an organization needs once more than one team, more than one tenant and an auditor are involved.

Multi-tenancy

Many organizations in one installation, each with its own vector collection. Suspension, offboarding and per-tenant quotas on documents and searches, and a command that finishes an offboarding by erasing the rows and every collection the tenant owned.

Directory sign-in

OIDC, and SCIM group sync. Access follows group membership, so joining a team is a membership change rather than a grant somebody has to remember to revoke — and leaving takes effect on the next request.

Document-level rules

Issuing per-document permissions and deny rules. Evaluation of both is in the open core and stays there: a deny beats an allow above it, and the filter is inside the index traversal either way. What is commercial is issuing and administering them.

Evidence

The access log — who read which document, when, over which surface — is the core's, readable over the API as JSON, JSONL or CSV. Forwarding it to a SIEM is commercial, as is a cross-organization administrator who sees administrative actions and never document reads.

Agent authorization

Enterprise-Managed Authorization and ID-JAG, so an agent acting for a person carries that person's authority and no more — bounded per layer, and revoked the moment they are disabled rather than when a token expires.

Recovery

An encrypted backup of the database and the stored documents, and a restore from it. Vectors are deliberately not in it: they are derived, and a stale copy of a derived store answers where an absent one gets rebuilt. It is a command rather than a route — something that reads every row and every object is better placed behind the database credentials than behind an HTTP check.

Plainly

All seven modules are written and ship as versioned artifacts from a private repository, with an image that carries them. Licensing is not self-serve — there is no price page today, and saying so is more useful than a form that pretends otherwise.

The open core is complete without any of it. Nothing is held back to force an upgrade: the permission model, the six invariants and the whole suite that guards them are Apache 2.0, and the specifications in docs/ stay normative for both halves. That is the honest way round, and it is also the only arrangement in which a self-hoster can verify the claim this product makes.