Multi-tenancy

A single GraphANN™ deployment hosts many isolated tenants. Each tenant owns its own indexes; selected indexes can be shared across an organisation. Roles control what each user inside a tenant may do.

Tenant isolation

Every tenant has a unique id prefixed t_. Indexes belong to exactly one tenant and inherit its isolation boundary. The server rejects any request whose path tenant does not match the index's owning tenant. No implicit cross-tenant reads.

  • Document text and metadata never leave the owning tenant unless an index is explicitly marked shared.
  • Quotas are enforced per tenant (max documents per index, ingest rate).
  • Tenant ids are validated against ^[a-zA-Z0-9_-]{1,128}$ to block path-traversal attacks via crafted ids.
# Tenant A creates an index
curl -X POST http://localhost:38888/v1/tenants/t_acme/indexes \
-d '{"id":"i_docs","name":"product-docs"}' \
-H 'content-type: application/json'

# Tenant B asks for it → 403 Forbidden
curl http://localhost:38888/v1/tenants/t_other/indexes/i_docs
# {"error":"Index does not belong to this tenant","code":"forbidden"}

Shared indexes

An index can be marked shared at the organisation level. Shared indexes remain owned by a tenant for write authority but are queryable by every user in the org. Use this for company-wide knowledge bases, public product docs, or shared reference data.

# Mark an index as shared at org-level (sync ingest path)
curl -X POST http://localhost:38888/v1/orgs/org_acme/documents \
-H 'content-type: application/json' \
-d '{
"shared": true,
"index_name": "company-handbook",
"documents": [
{"id":"hb-001","text":"Vacation policy ..."}
]
}'

Combined search

The org-level multi-index search endpoint runs a single query across a user's accessible content: their personal indexes plus any shared indexes their org grants. Results are merged and re-ranked.

# Search across a user's accessible content
# (their personal indexes + any shared org indexes)
curl -X POST \
http://localhost:38888/v1/orgs/org_acme/users/u_jane/search \
-H 'content-type: application/json' \
-d '{"query":"vacation policy","k":10}'

# 200 OK — results carry their source index id so you can show
# "from: company-handbook" or "from: my-notes" in the UI.

Roles

Each user inside a tenant has one of three roles. Roles gate write and admin actions; reads are allowed for any role inside the owning tenant.

RoleReadWrite (ingest, delete)Admin (tenant + index lifecycle)
adminyesyesyes
editoryesyesno
vieweryesnono

The role system is enforced server-side at the tenant boundary. Authentication wires the calling user's role into every request, and role checks gate every privileged operation before it touches the index.

Per-tenant rate limits

Quotas live on the tenant. Today they cover documents-per-index and a coarse ingest cap. Per-tenant request-per-second limiting is configured at the proxy layer (Traefik/nginx) in production deployments.

Design notes

  • One process, many tenants. No per-tenant subprocesses or sidecars. The server multiplexes everything in-memory with a per-index lock.
  • Storage layout. Each index gets its own directory under data/. Deleting a tenant deletes only that tenant's directories.
  • Idempotent creates. Pass a custom id on tenant or index creation to make the operation safe to replay (e.g. from a sync job that may run twice).
  • External ids. Use repo_id + file_path + commit_sha on documents to enable repo-scoped search filters. The same fields drive shared-index access checks at query time.