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-nuggetsAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT Payload:{ "clientId": "org-abc123", "userId": "user-456", "scopes": ["read", "write"]}Backend filters all queries:
SELECT * FROM truth_nuggetsWHERE 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 policyCREATE POLICY client_isolation ON truth_nuggets USING (client_id = current_setting('app.current_client_id'));
-- Every query automatically filtered by client_idSELECT * FROM truth_nuggets; -- Only returns current client's rowsDouble 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└── certificatesBenefit: 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 datamy_data = client.truth_nuggets.list()
# Verify no one else can access it# Try different authentication tokenother_client = TruthVouch(api_key="different-api-key")try: other_data = other_client.truth_nuggets.list() # Should be empty or different tenant's dataexcept Unauthorized: # Correct: access deniedAudit Trail Isolation
Audit logs are global but client-filtered:
Global audit_logs table:id | client_id | event | timestamp | ...
Query: SELECT * FROM audit_logsResult: Only rows where client_id = current_clientYou 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_embeddingsWHERE client_id = $1 -- Filtered by tenantORDER BY embedding <-> query_vectorBackup Isolation
Backups are encrypted and tagged by client:
backup_2024_01_15_org_abc123.sql.encbackup_2024_01_15_org_xyz789.sql.enc
Restoration: Restore entire database with RLS policies re-appliedTenant 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 returnedPenetration 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