This guide explains the tenant isolation strategy used by LATRACE across tenants, users, apps, and sessions.
It is written for self-hosted developers who call Memory directly and need to understand:
- what the current isolation boundary is
- which fields are required to avoid cross-contamination
- how to think about multi-tenant deployments at higher scale
LATRACE uses a layered isolation strategy.
Not every field has the same meaning:
- some fields define a hard storage boundary
- some fields define a namespace inside that boundary
- some fields only narrow a search or a session
The current default setup is:
flowchart TB
A[API Token / JWT] --> B[tenant_id]
B --> C[Shared modality collections]
C --> D[tenant_id filter]
D --> E[user_tokens]
E --> F[memory_domain]
F --> G[run_id]
G --> H[memory_scope]
In plain terms:
- authentication resolves the caller into a
tenant_id - data is written into shared collections per modality
- queries are isolated by
tenant_id user_tokens,memory_domain,run_id, andmemory_scopefurther narrow access and retrieval
tenant_id is the primary isolation key in LATRACE.
Use it to separate:
- customers
- environments
- organizations
- fully independent applications
This is the boundary that prevents one tenant from reading another tenant's memory.
If you operate a multi-tenant deployment, tenant_id should be treated as mandatory.
flowchart LR
T1[Tenant A] --> C[memory_text / memory_image / memory_audio]
T2[Tenant B] --> C
T3[Tenant C] --> C
C --> F[Queries always include tenant_id]
user_tokens are the strongest isolation key inside a single tenant.
Use them to separate:
- end users
- accounts
- personas
- application-owned principals
Examples:
["u:alice"]["u:bob"]["u:alice", "agent:planner"]
If two requests share the same tenant_id but use different user_tokens, retrieval and graph expansion can stay separated inside the tenant.
memory_domain is an application namespace such as:
dialogworksupportplanning
Use it when the same user or tenant should keep different memory surfaces separate.
This is the recommended way to prevent cross-recall between different product surfaces inside the same tenant.
run_id groups memory by session, conversation, or task execution.
Use it for:
- session-local retrieval
- session-local deduplication
- conversation replay and debugging
It is not a tenant boundary. It is a local scope boundary.
memory_scope is for highly localized isolation, for example:
- a single video
- a single imported asset
- a single ephemeral workspace
It is useful when you need a boundary narrower than run_id.
API keys authenticate the caller. They do not define the primary memory partition by themselves.
In the current public interface:
- API key or JWT identifies the caller
- the request resolves to a
tenant_id tenant_idbecomes the storage isolation boundary
If you need strict separation between multiple apps under one tenant, do not rely on API key identity alone. Use:
- distinct
memory_domainvalues - distinct
user_tokens - or separate
tenant_idvalues when you need a hard boundary
LATRACE currently uses shared collections per modality, not per tenant.
That means:
- text memories go into a text collection
- image memories go into an image collection
- audio memories go into an audio collection
Tenants are separated logically inside those collections.
flowchart TB
subgraph Collections[Shared collections]
C1[memory_text]
C2[memory_image]
C3[memory_audio]
end
C1 --> M1[metadata.tenant_id]
C1 --> M2[metadata.user_id]
C1 --> M3[metadata.memory_domain]
C1 --> M4[metadata.run_id]
C1 --> M5[metadata.memory_scope]
This layout is correct for:
- self-hosted single-tenant deployments
- small and medium multi-tenant deployments
- most direct Memory API use cases
For most developers, the isolation hierarchy should be understood like this:
flowchart LR
A[tenant_id] --> B[user_tokens]
B --> C[memory_domain]
C --> D[run_id]
D --> E[memory_scope]
You can read this as:
tenant_iddecides who owns the memory spaceuser_tokensdecide which principals inside that tenant may see itmemory_domaindecides which product namespace it belongs torun_iddecides which session or task it belongs tomemory_scopedecides the narrowest local boundary
| Need | Recommended field |
|---|---|
| Separate two customers | tenant_id |
| Separate two users inside one customer | user_tokens |
| Separate chat memory from planning memory | memory_domain |
| Restrict to one conversation or task | run_id |
| Restrict to one asset or local subspace | memory_scope |
Examples:
{
"tenant_id": "acme-prod",
"user_tokens": ["u:alice"],
"memory_domain": "dialog"
}{
"tenant_id": "acme-prod",
"user_tokens": ["u:alice"],
"memory_domain": "support"
}{
"tenant_id": "acme-prod",
"user_tokens": ["u:alice"],
"memory_domain": "planning"
}{
"tenant_id": "acme-prod",
"user_tokens": ["u:alice"],
"memory_domain": "dialog",
"run_id": "sess_2026_04_01_001"
}If you are self-hosted but only have one tenant, still use a stable value such as:
local-devprod-mainmy-app
This keeps data ownership explicit and makes future migration easier.
API keys are credentials. They are not your primary namespace contract.
If you need multiple isolated application surfaces, represent them explicitly with:
tenant_iduser_tokensmemory_domain
If your application serves multiple end users, do not rely on tenant-level isolation alone.
Always provide stable user tokens such as:
u:user_123u:customer_abc
If the same user has different memory surfaces, isolate them by domain:
dialognotescrmagent
If you want conversation- or task-local behavior, use run_id.
This is especially useful for:
- dialog retrieval
- temporary task context
- evaluation runs
The current default setup uses shared collections with logical tenant isolation.
At larger scale, a multi-tenant operator may want to reduce resource interference between tenants.
That is where shard-level isolation becomes useful.
It does not replace tenant_id.
Instead, it changes where the tenant boundary is enforced physically:
flowchart TB
A[API Token / JWT] --> B[tenant_id]
B --> C[Shard key = tenant_id]
C --> D[Target shard]
D --> E[tenant_id filter]
E --> F[user_tokens / memory_domain / run_id / memory_scope]
If you deploy advanced multi-tenant sharding, the recommended shard key is:
tenant_id
Not:
user_tokensrun_idmemory_scope
Why:
tenant_idis stable- it matches the hard ownership boundary
- it reduces cross-tenant resource contention
- it does not fragment memory into tiny physical partitions
Even with tenant sharding enabled, these should remain logical filters inside the tenant shard:
user_tokensmemory_domainrun_idmemory_scope
This gives you the right split:
- physical isolation at the tenant layer
- logical isolation inside the tenant
For most public users of LATRACE:
flowchart LR
A[Default deployment] --> B[tenant_id]
B --> C[user_tokens]
C --> D[memory_domain]
D --> E[run_id / memory_scope]
For advanced operators with heavy multi-tenant traffic:
flowchart LR
A[Advanced deployment] --> B[Shard by tenant_id]
B --> C[Filter by tenant_id]
C --> D[Filter by user_tokens]
D --> E[Filter by memory_domain]
E --> F[Filter by run_id / memory_scope]
In short:
- use
tenant_idas the hard ownership boundary - use
user_tokensas the principal boundary - use
memory_domainas the namespace boundary - use
run_idandmemory_scopeas local scope boundaries - shard by
tenant_idwhen you need stronger physical isolation at scale
LATRACE isolates memory in layers.
The most important rule is:
tenant_idowns the memory space. Everything else narrows scope inside that space.
That means:
tenant_idis the hard boundaryuser_tokensseparate principals inside a tenantmemory_domainseparates product namespacesrun_idandmemory_scopenarrow local context
This strategy keeps the public API simple while still supporting more advanced multi-tenant deployments later.