Nacre

Self-hosted · Apache 2.0 core

Strength from layers.

A self-hosted index for AI agents. Each agent and each person finds only what they are cleared for — enforced inside the search, revoked on the next request.

  • Apache 2.0 core
  • 86 releases
  • 44 permission cases on every build

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 tour · two minutes

Watch one index answer three people differently.

The same question from an administrator, an engineer and a contractor; a grant given and taken back; an agent connected with the person approving each layer; and the access log that records all of it. Recorded on the open core, running locally.

Nacre 0.41.0 · the community console and its REST and MCP surfaces · captions in English.

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.

Filter after the search 6 of 10 returned

Nacre: filter inside the index 10 of 10 returned

Both asked for ten on behalf of a reader who may not see four of the best matches. Filtering afterwards hands back six and says nothing; filtering inside the traversal walks further into the index and hands back ten they may read.
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.

Ask for ten results.
Get ten you are cleared to read.


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. The permission model has every rule and every case.

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" }

$ curl localhost:8080/v1/jobs/$JOB_ID \
    -H "Authorization: Bearer $NACRE_TOKEN"

  ← 200 { "status": "indexed" }

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

A small model on a CPU is enough to start, and the demo profile brings one. Each layer names its own model: a local embedder, any OpenAI-compatible endpoint, or Cloudflare, Google and Voyage through an adapter. Moving a layer to another model keeps search answering 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.

Around that loop: sign-in with a second factor — an authenticator app, or a security key you cannot be phished out of — hybrid search with optional reranking, moving a layer to another embedding model with recall checked before it switches, an access log readable as JSON, JSONL or CSV, skills that tell a connecting agent how the organization keeps its index, and an administrator’s agent whose changes a person applies.

Every release is 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. 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.


Before you install it

Questions people ask first.

Can I try it without installing anything?

Yes. demo.nacre.work runs the released images on a real index, with the logins of two people whose access differs printed on the page — ask both the same question and compare. You can also create an organization of your own there, which is erased after a day.

Do I need a GPU?

No. The demo profile runs a small English model on a CPU, and that is enough to see the whole loop. A larger multilingual model such as bge-m3 is too slow on a laptop CPU: give it a GPU, or point a layer at a hosted embedding API instead, which is a choice per layer.

Does anything leave my network?

Nothing phones home. Document text leaves only if you configure a hosted embedder or reranker, and then it goes to that vendor — the configuration reference says so in those words. The air-gapped profile ships without the adapter that could reach one, rather than with it switched off.

Which agents can connect?

Anything that speaks MCP over Streamable HTTP or STDIO. A client finds the authorization server from the 401 it gets, the person approves the connection on a consent screen, and the client then acts as that person, bounded per layer. An agent that runs on its own uses a service account key instead.

How is this different from filtering results afterwards?

A post-filter asks the index for the top ten and drops what the caller may not see, so a restricted reader gets fewer and worse answers and nobody is told. Here the permission filter is part of the index traversal: ten means ten permitted results.

What can I put in it?

Text and Markdown, PDF, Word, PowerPoint and Excel files, their OpenDocument equivalents, EPUB and RTF. A file’s bytes are kept in S3-compatible object storage — MinIO in the full profile — so uploading files needs a bucket; text needs nothing extra. The legacy .doc, .xls and .ppt formats are not accepted.

How do documents stay in step with where they live?

Ingest is idempotent on the layer and your own identifier, so sending a document again replaces it. Six reference connectors — git, S3, SQL, Google Drive, MongoDB and IMAP — add, change and remove documents from a source on an interval, one container per source.

Does it run on Kubernetes?

Yes. The Helm chart is in the open repository under the same licence, versioned with the product and attached to every release. It brings no database of its own: you point it at your PostgreSQL, Qdrant and Redis.

What is open, and what do I pay for?

Everything one developer on a laptop needs is Apache 2.0, including the permission model and every test that guards it. Multi-tenancy, OIDC and SCIM, issuing document-level rules, SIEM forwarding and encrypted backups are commercial modules, and licensing them is a conversation rather than a price page.

Two ways in

Ask it a question as three different people.

Two minutes with Docker Compose on your own machine, or none at all on the public stand. Apache 2.0, and no account to create for either.