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

All caching layers are namespaced by tenant, ensuring complete isolation. Each client’s cached data is only accessible within their own tenant context — there is no cross-tenant cache contamination.

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