Namespaces vs metadata filters for multi-tenant search
With a tenant filter, every query has to remember the filter. With a namespace per tenant and a key limited to it, the server refuses everything else. Pick the first when tenants are searched together; pick the second when an agent should only ever see one customer.
The difference in one table
Both designs can be correct. They differ in who has to get it right on every request.
| One index, tenant filter | Namespace per tenant, scoped key | |
|---|---|---|
| Who enforces the boundary | Every query your code sends must carry the right tenant filter | The server: a key limited to a namespace gets 404 for every other one |
| What one bug costs | A query without the filter searches every tenant | A request for another namespace fails; the key can’t widen itself |
| What the agent holds | A key that can read every tenant, plus your promise to filter | A key that can read one tenant |
| Cross-tenant queries | One query, by leaving the filter off | SQL joins across namespaces with a key that may read them |
| Deleting a customer | Delete by filter and check nothing was missed | Delete the namespace |
| Many tiny tenants | One index to run | A namespace each: unlimited on pro, an idle one costs only its bytes |
| Still your job | Derive the tenant from the session, on every access path | Map each session to the right key on a trusted backend; keep the admin key out of the agent |
What one missing filter returns
Two fictional help centers, Birch and Maple, with different refund policies, in one shared namespace. The filtered search is right. The same search without the filter, the bug a new code path or a prompt-injected agent tool call makes, answers with Maple’s policy to a Birch customer.
B=https://api.skimmerdb.com/v1 H=(-H "Authorization: Bearer $ADMIN_KEY" -H "Content-Type: application/json") # One shared namespace, a tenant field on every document curl -fsS "$B/namespaces/kb-shared/docs" "${H[@]}" -d '{"wait":true,"docs":[ {"id":"birch-1","text":"Refunds: Birch issues refunds within 14 days.","meta":{"tenant":"birch"}}, {"id":"maple-1","text":"Refunds: Maple gives store credit only, never cash refunds.","meta":{"tenant":"maple"}}]}' | jq -c '{written}' # {"written":2} # the filter keeps Birch's answer to Birch's documents curl -fsS "$B/namespaces/kb-shared/search" "${H[@]}" \ -d '{"q":"refunds","filter":{"tenant":"birch"}}' | jq -c '[.hits[].id]' # ["birch-1"] # forget the filter once and both customers' documents come back curl -fsS "$B/namespaces/kb-shared/search" "${H[@]}" \ -d '{"q":"refunds"}' | jq -c '[.hits[].id]' # ["maple-1","birch-1"]
The same mistake against a namespace
Now each help center gets its own namespace, and Birch’s assistant gets a read-only key limited to kb-birch. It finds Birch’s policy. Asking for Maple’s namespace, or for the shared one, answers 404 just like a name nobody has, and listing namespaces shows only its own. SQL that names kb-maple from Birch’s namespace is refused too.
# A namespace per customer, and a key that reaches only Birch's for T in birch maple; do curl -fsS "$B/namespaces/kb-$T/docs" "${H[@]}" -d "$(jq -n --arg t "$T" \ '{wait:true,docs:[{id:"\($t)-1",text:"Refunds policy for \($t)"}]}')" >/dev/null done BIRCH_KEY=$(curl -fsS "$B/keys" "${H[@]}" \ -d '{"name":"birch reader","scope":"read","namespaces":["kb-birch"]}' | jq -r .key) K=(-H "Authorization: Bearer $BIRCH_KEY" -H "Content-Type: application/json") curl -fsS "$B/namespaces/kb-birch/search" "${K[@]}" -d '{"q":"refunds"}' | jq -c '[.hits[].id]' # ["birch-1"] # the same query without a filter can't reach Maple: the server refuses curl -sS -w ' HTTP %{http_code}\n' "$B/namespaces/kb-maple/search" "${K[@]}" -d '{"q":"refunds"}' # {"error":"no such namespace","code":"not_found"} HTTP 404 curl -sS -w ' HTTP %{http_code}\n' "$B/namespaces/kb-shared/search" "${K[@]}" -d '{"q":"refunds"}' # {"error":"no such namespace","code":"not_found"} HTTP 404 curl -fsS "$B/namespaces" "${K[@]}" | jq -c '[.namespaces[].name]' # ["kb-birch"]
Run on a fresh local server on October 22, 2026; outputs as printed. The multi-tenant RAG guide adds result checks, a denied write (403), key revocation and cleanup.
When a metadata filter is the better choice
- Tenants searched together. Shared catalogs, or an internal team that searches every customer at once.
- A trusted layer adds the filter. A service the agent can’t bypass, or row-level security in Postgres, enforces it on every path.
- Narrowing, not isolating. Inside one tenant’s namespace, filter by source, owner, date or status as much as you like.
When a namespace per tenant is
- An agent holds the key. A copilot, support bot or MCP client for one customer gets a key that can’t reach the others, whatever it’s told.
- Customers leave. Deleting the namespace deletes their documents; copies in your own app are still yours to remove.
- A long tail of small tenants. An idle namespace runs no compute and costs only its bytes.
What other databases recommend
From each vendor’s documentation, read October 22, 2026.
| System | What the docs say |
|---|---|
| Pinecone | Recommends “one namespace per tenant”; puts all tenants in one namespace with metadata filters only “when tenant isolation isn’t a strict requirement, or when you need to query across multiple tenants simultaneously”, and notes such queries scan the whole namespace. |
| Qdrant | Recommends keeping tenants in one collection, partitioned by a payload field, and separate collections only for “a limited number of tenants that need strict isolation”. Its granular API keys are scoped per collection; the docs describe no scoping by payload value. |
| turbopuffer | Advises namespaces “as small as they can be without having to routinely query more than one at a time”. |
| PostgreSQL | Row-level security policies make the database add the tenant condition itself, so one table can enforce the boundary server-side. |
What this doesn’t cover
A scoped key keeps one tenant’s requests out of another’s namespaces. It doesn’t check that your backend handed the right key to the right user, and it doesn’t cover copies of the data in your logs, caches or other services. A key made without a namespace list, or with a broad prefix like ws-*, reaches everything it matches. Test the whole path, and repeat the negative checks when you change how keys are issued.
Questions
Should I use a namespace per tenant or a metadata filter?
Use a namespace per tenant when an agent or service that touches one customer should be unable to read the others: the key, not the query, sets the boundary. Use one index with a tenant filter when tenants must often be searched together, or when a trusted service or database policy adds the filter on every path.
Is a metadata filter a security boundary?
Only if something you trust adds it to every query. In the example on this page, the same key that runs the filtered search also runs the unfiltered one, and that returns both tenants’ documents. A key can be limited to namespaces, not to a filter value.
Does a namespace alone keep tenants apart?
No. A key made without a namespace list can reach every namespace in the account, and a broad prefix such as ws-* reaches every match. The boundary is a key limited to one namespace (or a narrow prefix), handed out by your backend.
What does a key limited to one namespace see of the others?
Nothing: searching or reading another namespace answers 404, exactly like a name nobody has, and listing namespaces shows only its own. SQL that names another namespace is refused as well.
Can I still filter inside a namespace?
Yes. Filters on metadata (source, owner, dates, status) work inside each namespace, alone or with text, regex and vector search. Use them to narrow results, and the namespace for the tenant.
How many namespaces can I have?
Unlimited on pro and 1,000 on the free plan. A namespace is made by its first write, and an idle one runs no compute; it costs its storage, at $0.10 per GB-month on pro.
Try it with your own documents
Free up to 1 GB stored and 100k queries a month. No card to start.