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. And an agent that connects is told how this organization keeps its index — a skill in Claude’s own folder format, for the organization and for each layer.

$ NACRE_SERVICE_KEY=nacre_sk_… npx @nacre.work/mcp

→ tools/list
  search · list_layers · get_document
  ingest_document · request_upload · upload_file
  ingest_status · delete_document
  list_skills · get_skill · update_skill

→ 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. For a source you would rather not script, there are reference connectors.

$ 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. On Kubernetes, the Helm chart is in the same open repository and released with it.

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.

86 releases so far, each one written up with what it cost to find — the release notes are their own page. The newest is 0.41.0: a budget per caller, client registration bounded, and an MCP client identified by the URL of its own metadata document. Before it, 0.40.0 linked people and groups to an identity provider without SQL, 0.39.1 fixed a memory leak and a slow search that only showed under load, 0.39.0 rebuilt a search index that survived a restore, 0.38.3 offered Delete only where you may delete, 0.38.1 and 0.38.2 fixed what writing the console's manual turned up, and 0.38.0 was a security release, fifteen findings from a full review, each reproduced before it was fixed. Before it, 0.37.1 showed each person the Connections screen for their role, 0.37.0 let an administrator change access from a panel beside the agent’s answer, 0.36.1 was a security fix in a dependency, 0.36.0 showed an administrator’s agent’s answers as panels, 0.35.0 let that agent send mail, and only to people in the organization, 0.34.0 let the agent propose a change that the person applies, 0.33.0 opened a skill as a panel inside the agent, 0.32.0 gave an organization administrator an agent that can read everything about the organization and change nothing, 0.31.1 stopped a connected application approving a wider connection for itself, 0.31.0 let an application be allowed to edit one layer’s skill and nothing else, 0.30.0 put skills on the console, 0.29.4 kept a re-sent document on the new model during a move, 0.29.3 put the Helm chart in the open repository and on every release, 0.29.2 showed a connected application only the tools it may use, 0.29.1 made the server’s own guide the first thing every agent is told, and 0.29.0 brought skills — an organization’s and each layer’s conventions in Claude’s own folder format, handed to an agent before its first call — and 0.28.0 put the MCP server on the reference SDK and let a file reach the index without passing through a model. The server runs on Node 24.

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, per-tenant quotas on documents and searches, and rate limits per tenant so no one organization takes the installation from the rest, set by the platform administrator on the tenant's screen. And a command that finishes an offboarding by erasing the rows and every collection the tenant owned. Past the size anybody pages through, people, groups, grants and a layer's documents are found by key, prefix or status.

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. An organization can also require a second factor before anybody signs in with a password.

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. A daily schedule, a run and a restore by name are on a screen, and nothing the screen returns decrypts anything; restoring a database that is already gone stays a command, because a route cannot restore what its own sign-in reads.

Plainly

All nine 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.