Cookbook — multi-tenant SaaS
Patterns for serving many customers from one bigRAG deployment — isolation, per-tenant API keys, usage metering, and cost controls.
This recipe turns bigRAG into a per-tenant RAG backbone for a typical SaaS app — one collection per customer (or one collection shared with tenant filtering), scoped API keys, usage dashboards, and cost caps. Python snippets assume the async SDK is running inside an async function with an initialized client.
Pattern A — collection per tenant
Simplest isolation model. Each tenant's data lives in its own collection; queries are physically scoped by the collection name.
Provisioning
await client.collections.create({
"name": f"tenant_{tenant_id}",
"embedding_provider": "openai",
"embedding_model": "text-embedding-3-small",
"dimension": 1536,
"metadata_schema": {
"type": "object",
"required": ["source"],
"properties": {
"source": {"type": "string", "enum": ["email", "docs", "chat"]},
"tier": {"type": "string", "enum": ["free", "pro", "enterprise"]},
},
},
})Per-tenant API key
Mint a scoped key so the tenant's browser/mobile app can query but can't touch other collections or admin APIs:
key = await client.admin.api_keys.create({
"name": f"tenant-{tenant_id}",
"scopes": ["query:read", "document:upload"],
"collection": f"tenant_{tenant_id}",
})
tenant_api_key = key["key"]The middleware enforces both the scopes and the collection pin, so a leaked tenant key can only reach its own collection.
Collections-per-tenant is simple but creates operational overhead as tenant count grows. Above small/medium tenant counts, move to Pattern B so one Turbopuffer namespace can use tenant metadata filters.
Pattern B — shared collection with tenant filters
One collection, tenant isolation via a required metadata filter. Setting tenant_field tells bigRAG to configure that field for backend filtering and reject uploads, raw vector upserts, queries, and chat calls that omit the tenant field.
await client.collections.create({
"name": "shared_tenants",
"tenant_field": "tenant_id",
})
await client.documents.upload(
"shared_tenants",
handle,
metadata={"tenant_id": "acme", "source": "docs"},
)
results = await client.queries.query("shared_tenants", {
"query": "quarterly revenue",
"filters": {"tenant_id": "acme"},
})For tenant-guarded collections, the tenant filter must identify one tenant. {"tenant_id": {"$eq": "acme"}} and {"tenant_id": {"$in": ["acme"]}} are valid; multi-value $in filters are rejected because they cross tenant boundaries.
You still need to pass the tenant filter on every tenant-scoped query and chat request. bigRAG rejects missing filters, but your application still owns mapping the authenticated tenant to the exact allowed tenant value.
Per-tenant usage + billing
usage = await client.get_usage(window_days=30)
# {
# "by_collection": [
# {"collection": "tenant_acme", "documents": 142, "chunks": 4321,
# "storage_bytes": 5120000, "embedding_tokens": 1500000,
# "embedding_cost_usd_estimate": 0.03, "queries": 842, ...},
# ...
# ]
# }Join this with your billing system's tenant_id ↔ collection_name map and you have a per-tenant invoice line. For Pattern B, the built-in usage endpoint is collection-level; keep per-tenant counters in your own application when multiple tenants share one collection.
Audit trail for compliance
Privileged actions such as key creation, webhook config, user updates, collection mutations, and document mutations are in audit_log. Surface them in internal tooling by filtering on actor_id, resource_type, or the relevant resource_id:
curl -b cookies.txt \
"http://localhost:4000/v1/admin/audit?resource_type=collection"Offboarding
When a tenant cancels, tear down their data:
# Pattern A — collection per tenant
await client.collections.delete(f"tenant_{tenant_id}")
# Pattern B — shared collection
await client.documents.batch_delete("shared_tenants", document_ids)For shared collections, keep a tenant-owned document ID index in your application so offboarding can delete the right documents. If you use raw vector upserts directly, keep vector IDs and call client.vectors.delete. Revoke the tenant's API keys via client.admin.api_keys.delete(key_id) and archive the relevant audit rows for your records.