Skip to content

Multi-Tenant Architecture

TruthVouch is a multi-tenant SaaS platform where many organizations share infrastructure while maintaining complete data isolation. This guide explains isolation mechanisms.

Isolation Layers

Layer 1: JWT-Based Tenant Filtering

Every API request includes JWT token with ClientId:

POST /api/v1/truth-nuggets
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT Payload:
{
"clientId": "org-abc123",
"userId": "user-456",
"scopes": ["read", "write"]
}

Backend filters all queries:

SELECT * FROM truth_nuggets
WHERE client_id = $1 AND text ILIKE $2;
-- $1 = JWT.clientId (enforced)

Layer 2: Row-Level Security (RLS)

PostgreSQL RLS enforces isolation at database level:

-- Create RLS policy
CREATE POLICY client_isolation ON truth_nuggets
USING (client_id = current_setting('app.current_client_id'));
-- Every query automatically filtered by client_id
SELECT * FROM truth_nuggets; -- Only returns current client's rows

Double Protection: Even if application bug bypasses JWT check, database RLS prevents data leakage.

Layer 3: Separate Schemas

Each tenant gets separate PostgreSQL schema (optional for Enterprise):

public.
├── users (shared)
├── clients (shared metadata only)
└── audit_logs (global, client-filtered)
client_abc123.
├── truth_nuggets
├── verification_logs
└── certificates
client_xyz789.
├── truth_nuggets
├── verification_logs
└── certificates

Benefit: Complete isolation, different retention policies per tenant.

Layer 4: Application-Level Scoping

Every service explicitly scopes to tenant:

public class TruthNuggetService {
public async Task<List<TruthNugget>> ListNuggets(ClientId clientId) {
// Explicitly pass clientId
return await _repository.Where(n => n.ClientId == clientId);
}
}

Data Isolation Verification

Verify your data is isolated:

# Query your data
my_data = client.truth_nuggets.list()
# Verify no one else can access it
# Try different authentication token
other_client = TruthVouch(api_key="different-api-key")
try:
other_data = other_client.truth_nuggets.list()
# Should be empty or different tenant's data
except Unauthorized:
# Correct: access denied

Audit Trail Isolation

Audit logs are global but client-filtered:

Global audit_logs table:
id | client_id | event | timestamp | ...
Query: SELECT * FROM audit_logs
Result: Only rows where client_id = current_client

You see your audit trail, nothing else.

Cache Isolation

Neural Cache (Redis + pgvector) is keyed by client:

Redis key: "cache:org-abc123:query-xyz"
└─ client_id explicitly in key
pgvector query:
SELECT * FROM cached_embeddings
WHERE client_id = $1 -- Filtered by tenant
ORDER BY embedding <-> query_vector

Backup Isolation

Backups are encrypted and tagged by client:

backup_2024_01_15_org_abc123.sql.enc
backup_2024_01_15_org_xyz789.sql.enc
Restoration: Restore entire database with RLS policies re-applied

Tenant Context Propagation

Context flows through entire system:

API Request (JWT)
Controller: Extract ClientId from JWT
Service Layer: Pass ClientId to data layer
Repository: Apply WHERE client_id = @ClientId
Database: RLS policy enforces client_id match
Response: Only this client's data returned

Penetration Testing

Third-party pentesters verify isolation:

  • Attempt cross-tenant data access
  • Try JWT tampering
  • Test SQL injection
  • Verify RLS enforcement
  • Check cache isolation

Results: Zero cross-tenant data access in annual pentest.

Compliance

  • GDPR: Data strictly isolated per controller
  • SOC 2: Multi-tenancy tested in Type II audit
  • HIPAA: RLS enforces HIPAA-required access controls
  • ISO 42001: Isolation reviewed in AI governance audit

Next Steps

  • Data Handling: Encryption and key management
  • Security Overview: Full security posture
  • GDPR: Data subject rights and DPA