Multi-Tenancy Architecture
DELPHOS is a multi-tenant platform where each clinic or organization operates
in complete data isolation. A tenant is identified by an app_id (UUID) that is
resolved automatically from the API key sent in the x-api-key header — callers
never need to specify a tenant manually.
How Isolation Works
Every API request passes through the following pipeline before any data is read or written:
Request + x-api-key header | v1. Authentication ─── resolve app_id from key hash | v2. Tenant Middleware ─── SET LOCAL app.current_app_id = '{app_id}' | v3. PostgreSQL RLS ─── WHERE app_id = current_setting('app.current_app_id') | v4. Response ─── contains only this tenant's dataBecause the session variable is set with SET LOCAL, it is transaction-scoped
and automatically cleared when the transaction ends. There is no risk of a leaked
context carrying over to a subsequent request.
Tenant Model
Each tenant is represented by an App record with the following key fields:
| Field | Type | Description |
|---|---|---|
id | UUID (PK) | Unique tenant identifier (app_id) |
slug | text | URL-safe short name (e.g. clinica-saude) |
api_key_hash | text | SHA-256 hash of the master key |
rate_limits | JSONB | Per-tenant throttle config |
is_active | boolean | Disabled tenants are rejected at auth |
Default Rate Limits
{ "per_minute": 60, "per_hour": 1000, "per_day": 10000}Enterprise plans can override these values to higher or unlimited quotas.
API Key Model
Each App can have multiple API keys, each with its own scope and lifecycle:
| Field | Type | Description |
|---|---|---|
id | UUID (PK) | Key identifier |
app_id | UUID (FK) | Owning tenant |
key_prefix | char(16) | Visible prefix for identification |
environment | enum | production, staging, or development |
permissions | JSONB | Granular capability flags |
expires_at | timestamptz | Optional expiration |
revoked_at | timestamptz | Null until explicitly revoked |
last_used_at | timestamptz | Updated on each request |
usage_count | bigint | Lifetime request counter |
Permission Flags
The permissions JSONB object controls what the key can do:
{ "can_read": true, "can_write": true, "can_delete": false, "can_admin": false}A staging key might enable reads and writes but disable deletes and admin operations. A CI/CD key might be read-only. This lets you follow the principle of least privilege for every integration point.
Row-Level Security Policies
RLS is enabled on every tenant-scoped table. The policy pattern is consistent across the entire schema:
CREATE POLICY documents_isolation ON documents FOR ALL TO PUBLIC USING ( app_id = current_setting('app.current_app_id', TRUE)::UUID );The USING clause filters rows on read, update, and delete — a tenant can never
see or modify another tenant’s records.
CREATE POLICY documents_insert_isolation ON documents FOR INSERT TO PUBLIC WITH CHECK ( app_id = current_setting('app.current_app_id', TRUE)::UUID );The WITH CHECK clause prevents a tenant from inserting a row with a different
app_id, blocking cross-tenant writes at the database level.
Tables Covered
RLS policies are applied to all data tables, including:
appsandapi_keysdocumentsandknowledge_collectionspatientsandconsultationsprescriptionsusage_tracking- All future tenant-scoped tables follow the same pattern
Tenant Context Middleware
After authentication resolves the app_id, a dedicated middleware sets the
PostgreSQL session variables that RLS policies depend on:
TenantContextMiddleware | +-- SET LOCAL app.current_app_id = '{app_id}' +-- SET LOCAL app.current_tenant_slug = '{slug}' | vRequest proceeds with tenant context activeKey behaviors:
- Transaction-local scope —
SET LOCALis scoped to the current transaction and is automatically cleared on commit or rollback. - Cross-tenant detection — If a request somehow references a different
tenant’s
app_id, the event is logged to thesecurity_audit_logat CRITICAL severity. - Non-blocking audit — Security audit logging is asynchronous so it never adds latency to the request path.
Repository Pattern (Double Protection)
Every data repository accepts app_id in its constructor and calls
SET LOCAL before executing any query:
class DoctorRepository: def __init__(self, app_id: UUID): self.app_id = app_id
async def find_by_crm(self, crm: str) -> Doctor | None: async with self.pool.acquire() as conn: # app_id is a UUID validated at the authentication layer await conn.execute( f"SET LOCAL app.current_app_id = '{self.app_id}'" ) return await conn.fetchrow( "SELECT * FROM doctors WHERE crm = $1", crm )This means tenant isolation is enforced at two independent layers:
- Middleware — sets context before the handler runs
- Repository — re-sets context before each query executes
Even if a code path bypasses the middleware (e.g., a background job), the repository still enforces the correct tenant scope.
Tenant-Scoped Knowledge Bases
Each tenant has an independent set of knowledge base collections ( used by the DELPHOS intelligence layer):
- Collection names are derived from the tenant slug:
{slug}_medical_knowledge - A unique constraint
UNIQUE(app_id, name)prevents naming collisions across tenants. - Indices and search results are all scoped — a query from Tenant A never returns documents belonging to Tenant B.
Tenant "clinica-saude" └── clinica-saude_medical_knowledge ├── 12,400 embedded documents └── HNSW index (independent)
Tenant "hospital-central" └── hospital-central_medical_knowledge ├── 38,200 embedded documents └── HNSW index (independent)Quota and Usage Tracking
Rate limits are stored per tenant in the apps.rate_limits JSONB column and
enforced before the request reaches the handler:
| Window | Default Limit | Enterprise |
|---|---|---|
| Per minute | 60 | Custom |
| Per hour | 1,000 | Custom |
| Per day | 10,000 | Unlimited |
Usage is tracked per tenant, per endpoint, and per time window. The usage records themselves are protected by RLS — a tenant can query its own consumption but never see another tenant’s traffic patterns.
Cross-Tenant Protection
DELPHOS implements defense-in-depth against cross-tenant access:
| Layer | Mechanism |
|---|---|
| API key binding | Each key is cryptographically bound to one tenant. Presenting a key from Tenant A will always resolve to Tenant A’s app_id. |
| RLS policies | PostgreSQL enforces row filtering at the storage engine level, below application code. |
| Middleware context | SET LOCAL ensures the session variable cannot leak across requests. |
| Audit logging | Any CROSS_TENANT_ATTEMPT event is logged at CRITICAL severity with full request context. |
| Repository guards | Double-check layer that re-sets tenant context per query. |
Provisioning a New Tenant
The high-level flow for creating a new tenant:
1. Create App record (slug, rate_limits, is_active=true) | v2. Generate API key → link to App via app_id FK | v3. Return plaintext key to caller (returned exactly once) | v4. Store only the key hash — plaintext is never persistedExample: Using the API Key
Once provisioned, the tenant authenticates by passing the key in every request:
curl -X GET "https://your-instance.delphos.app/v1/patients" \ -H "x-api-key: delphos_clinica-saude_prod_a1b2c3d4e5f6g7h8" \ -H "Content-Type: application/json"import httpx
client = httpx.AsyncClient( base_url="https://your-instance.delphos.app/v1", headers={"x-api-key": "delphos_clinica-saude_prod_a1b2c3d4e5f6g7h8"},)
response = await client.get("/patients")# Returns only patients belonging to this tenantprint(response.json())The x-api-key header is the only credential required. DELPHOS resolves the
tenant, sets the RLS context, and enforces rate limits — all transparently.
Summary
| Concern | How DELPHOS Handles It |
|---|---|
| Identity | UUID app_id resolved from API key hash |
| Data isolation | PostgreSQL RLS on every tenant-scoped table |
| Context propagation | SET LOCAL session variable (transaction-scoped) |
| Write protection | WITH CHECK policies prevent cross-tenant inserts |
| Knowledge bases | Per-tenant collections with independent indices |
| Rate limiting | JSONB config per tenant with per-minute/hour/day windows |
| Audit trail | Cross-tenant attempts logged at CRITICAL severity |
| Key management | Multiple keys per tenant, each with scoped permissions |
Next Steps
- Authentication & API Keys — How API key authentication works
- Security & LGPD — Data protection and privacy compliance