Multi-Tenancy & Scoping
How to keep each end-user's memory isolated in a SaaS app. The collection is the tenancy boundary — pass collection_id everywhere and one user's data never leaks into another's.
The isolation model
Collection = boundary
Create one collection per end-user (or per tenant). Everything you store and retrieve is scoped to a collection.
Scope every call
Pass collection_id on writes and reads. A scoped read returns only that collection's data — strict isolation, no cross-leak.
Account-wide = opt-in
Omit collection_id for legacy account-wide behavior. Data with no collection is "global" and excluded from scoped reads by default.
Where collection_id applies
| Surface | Scoping |
|---|---|
POST /v1/memories, /v1/memories/process, /v1/memories/raw | Stores into the given collection |
POST /v1/search (and /search/reason, /search/similar) | Returns only that collection's memories |
POST /v1/chat/completions | memory_context + profile facts scoped to the collection |
GET /v1/profile, GET /v1/profile/facts, PATCH /v1/profile, POST /v1/profile/facts | Per-collection profile facts (no cross-leak) |
GET /v1/corrections/relevant, GET /v1/corrections | Strict collection scope (+ include_global) |
POST /v1/decisions, GET /v1/decisions/similar | Strict collection scope (+ include_global) |
POST /v1/knowledge-graph/entities + /relationships, /query | Entities/edges tagged + filtered by collection |
Note on globals: items written without a collection_id are account-wide. Scoped reads exclude them by default; corrections, decisions, and profile reads accept include_global=true to also surface account-wide entries alongside the collection's own.
End-to-end: isolate two users
import os, requests
BASE = "https://api.hebbrix.com/v1"
H = {"Authorization": f"Bearer {os.environ['HEBBRIX_API_KEY']}"}
# One collection per end-user
alice = requests.post(f"{BASE}/collections", headers=H,
json={"name": "user-alice"}).json()["id"]
bob = requests.post(f"{BASE}/collections", headers=H,
json={"name": "user-bob"}).json()["id"]
# Store a fact for Alice only
requests.post(f"{BASE}/memories", headers=H,
json={"content": "Allergic to shellfish.", "collection_id": alice})
# A scoped search for Bob can NEVER see Alice's fact
r = requests.post(f"{BASE}/search", headers=H,
json={"query": "food allergies", "collection_id": bob, "limit": 5}).json()
assert all("shellfish" not in m["content"].lower() for m in r["results"]) # isolated
# Scope the profile too (so a global preference never leaks across users)
requests.patch(f"{BASE}/profile", headers=H,
json={"diet": "pescatarian", "collection_id": alice})
prof_bob = requests.get(f"{BASE}/profile", headers=H,
params={"collection_id": bob}).json()
# prof_bob does not contain Alice's "diet" factBest practices
Always pass collection_id on both writes and reads in a multi-tenant app. Treating it as optional is how cross-user leaks happen.
Use include_global sparingly. Reserve account-wide (no-collection) entries for genuinely global policy — not per-user data.
The API key is the account; the collection is the tenant. Keys scope to your account; collections scope to each of your end-users within it.
