Multi-Tenancy¶
Pali is built to serve multiple tenants from one deployment, but the isolation model is intentionally simple:
- the REST API is tenant-scoped
- JWT auth, when enabled, is single-tenant per token
- MCP resolves a tenant for each tool call
- the dashboard is an operator surface, not a tenant-facing authenticated console
REST API Isolation¶
When auth.enabled: true, all /v1 routes require Authorization: Bearer <jwt>.
The JWT must include:
tenant_id
Current API behavior:
- the token tenant is loaded into request context by auth middleware
- tenant-scoped handlers compare the JWT tenant with the tenant in the body, query, or path
- mismatches return
403 - missing or invalid JWTs return
401
This means one token acts as one tenant. Pali does not currently expose an admin token that can operate across many tenants through the normal API handlers.
What A Tenant-Scoped Token Can Do¶
A token for tenant_a can:
- create
tenant_a - store memory in
tenant_a - ingest memory in
tenant_a - search memory in
tenant_a - list jobs for
tenant_a - delete memory in
tenant_a - read stats for
tenant_a
The same token cannot operate on tenant_b.
MCP Tenant Resolution¶
MCP uses the same underlying tenant and memory services, but tenant resolution is broader because MCP hosts differ in what metadata they send.
Resolution order:
- explicit
tenant_idin tool input - JWT tenant claim, when auth is enabled and the host forwards it
- MCP session default tenant
default_tenant_idfrom config- otherwise the tool returns an error
This is useful for agent hosts, but it is different from the HTTP API's strict bearer-token model. If you need hard tenant-bound auth, test the REST API path directly.
Dashboard Behavior¶
The dashboard is useful for operators, not end users.
Today:
- tenant and memory listings come from the SQLite-backed repository layer
- search and retrieval-backed views still pass through the core memory service
- configured vector and graph backends influence recall and ranking, but they are not the dashboard's source of truth for persisted memory rows
- dashboard routes are not currently protected by the same JWT middleware as
/v1
That means dashboard listing still works even when you enable Qdrant or Neo4j. Those systems extend retrieval behavior; they do not replace the repository used to render persisted memories.
Config Knobs That Matter¶
Relevant settings in configuration.md and pali.yaml.example (GitHub):
auth.enabledauth.jwt_secretauth.issuerdefault_tenant_idvector_backendentity_fact_backendretrieval.multi_hop.*
Recommended Pre-Deploy Checks¶
Before calling a deployment multi-tenant ready:
- enable
auth.enabled: true - mint two JWTs with different
tenant_idclaims - create and write memory for tenant A
- confirm tenant A token cannot read or write tenant B
- test MCP with explicit
tenant_idand with session/default tenant fallback - verify your reverse proxy or network layer protects the dashboard if you expose it outside a trusted environment
Example Dev Flow¶
Mint a token:
Create a tenant:
curl -X POST http://127.0.0.1:8080/v1/tenants \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{"id":"tenant_a","name":"Tenant A"}'
Store a memory:
curl -X POST http://127.0.0.1:8080/v1/memory \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{"tenant_id":"tenant_a","content":"User likes jasmine tea."}'
Test mismatch rejection:
curl -X POST http://127.0.0.1:8080/v1/memory/search \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{"tenant_id":"tenant_b","query":"tea"}'
Expected result: 403.