Multi-Tenancy
Isolate data across organizations, teams, and environments with two layers of separation.
How It Works
Nocturnus provides two levels of data isolation via HTTP headers:
| Header | Purpose | Default |
|---|---|---|
| X-Database | Selects the logical database — completely separate Hexastore instances with independent rules and facts | "default" |
| X-Tenant-ID | Selects the tenant within a database — data-level isolation sharing the same rules engine | required* |
ValidationException if missing). MCP routes are the exception — they default to "default" if not specified.
Creating a Database
Create a new logical database:
curl -X POST http://localhost:9300/databases \
-H "Content-Type: application/json" \
-d '{"name": "production"}' List all databases:
curl http://localhost:9300/databases Using Tenants
Pass the tenant and database headers with every request:
# Assert a fact for Acme Corp in the production database
curl -X POST http://localhost:9300/tell \
-H "Content-Type: application/json" \
-H "X-Database: production" \
-H "X-Tenant-ID: acme_corp" \
-d '{"predicate": "plan", "args": ["enterprise"]}'
# Query facts for a different tenant — sees only its own data
curl -X POST http://localhost:9300/ask \
-H "Content-Type: application/json" \
-H "X-Database: production" \
-H "X-Tenant-ID: globex_inc" \
-d '{"predicate": "plan", "args": ["?tier"]}'
# Returns empty — Globex has no facts yet
{ "results": [] } Context Optimization Isolation Pattern
For context-heavy agents, separate concerns like this:
- Database = environment boundary (dev/staging/prod)
- Tenant = customer or agent boundary
- Scope = per-conversation or per-document slice
- sessionId = diff state for
/context/optimizeand/context/diff
curl -X POST http://localhost:9300/context/optimize \
-H "Content-Type: application/json" \
-H "X-Database: production" \
-H "X-Tenant-ID: acme_corp" \
-d '{
"scope": "conversation_2026_04_03",
"sessionId": "acme-support-thread-42",
"maxFacts": 25
}' # Same tenant+scope, later turn: request only changes
curl -X POST http://localhost:9300/context/diff \
-H "Content-Type: application/json" \
-H "X-Database: production" \
-H "X-Tenant-ID: acme_corp" \
-d '{
"scope": "conversation_2026_04_03",
"sessionId": "acme-support-thread-42",
"maxFacts": 25
}'
# End-of-thread cleanup for this tenant/session
curl -X POST http://localhost:9300/context/session/clear \
-H "Content-Type: application/json" \
-H "X-Database: production" \
-H "X-Tenant-ID: acme_corp" \
-d '{"sessionId":"acme-support-thread-42"}' Common Patterns
SaaS Multi-Tenant
One database, one tenant per customer:
# Each customer gets isolated data
X-Database: production
X-Tenant-ID: customer_123 # Customer A
X-Tenant-ID: customer_456 # Customer B Environment Separation
Separate databases per environment:
# Completely independent knowledge bases
X-Database: development
X-Database: staging
X-Database: production Per-Agent Isolation
Each agent gets its own tenant so knowledge doesn't leak:
# Agent-specific knowledge stores
X-Database: agents
X-Tenant-ID: agent_support_bot
X-Tenant-ID: agent_sales_assistant Multi-Tenancy with MCP
When using MCP, the database and tenant can be configured via headers in the MCP client config:
{
"mcpServers": {
"nocturnus": {
"url": "http://localhost:9300/mcp/sse",
"transport": "sse",
"headers": {
"X-Database": "production",
"X-Tenant-ID": "acme_corp"
}
}
}
} Tenant Management API
Manage tenants programmatically via the admin API:
# List tenants in a database
curl http://localhost:9300/admin/databases/production/tenants \
-H "Authorization: Bearer $ADMIN_KEY"
# Create a tenant
curl -X POST http://localhost:9300/admin/databases/production/tenants \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "acme_corp"}'
# Delete a tenant and all its data
curl -X DELETE http://localhost:9300/admin/databases/production/tenants/acme_corp \
-H "Authorization: Bearer $ADMIN_KEY" Scope Management
Scopes are logical partitions within a tenant — they are NOT isolation boundaries (use tenants for that). Scopes enable Git-like knowledge branching for hypothetical reasoning, versioning, and A/B testing.
# Fork a scope
curl -X POST http://localhost:9300/scope/fork \
-H "Content-Type: application/json" \
-H "X-Database: production" \
-H "X-Tenant-ID: acme_corp" \
-d '{"source": "main", "target": "hypothesis-1"}'
# Diff two scopes
curl -X POST http://localhost:9300/scope/diff \
-H "Content-Type: application/json" \
-H "X-Database: production" \
-H "X-Tenant-ID: acme_corp" \
-d '{"source": "hypothesis-1", "target": "main"}'
# Merge scopes (strategies: SOURCE_WINS, TARGET_WINS, KEEP_BOTH, REJECT)
curl -X POST http://localhost:9300/scope/merge \
-H "Content-Type: application/json" \
-H "X-Database: production" \
-H "X-Tenant-ID: acme_corp" \
-d '{"source": "hypothesis-1", "target": "main", "strategy": "SOURCE_WINS"}'
# List all scopes
curl http://localhost:9300/scopes \
-H "X-Database: production" \
-H "X-Tenant-ID: acme_corp" scope=null matches facts from ALL scopes. Set an explicit scope to restrict results to that partition only.
Scoped API Keys
Combine multi-tenancy with Security & Auth to create API keys restricted to specific databases and tenants:
# Create a key that can only access Acme Corp's data
curl -X POST http://localhost:9300/auth/keys \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"role": "writer",
"databases": ["production"],
"tenants": ["acme_corp"],
"description": "Acme Corp writer key"
}'