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
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.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