Database Isolation
Database isolation is one of the four isolation layers that protect tenant data in the MATIH Platform. This page documents how the platform uses Hibernate's schema-based multi-tenancy to ensure that each tenant's data is stored in a separate PostgreSQL schema and that database queries can never access another tenant's data.
Schema-Per-Tenant Model
Each tenant's data resides in a dedicated PostgreSQL schema within a shared database instance:
PostgreSQL Database: matih_ai
+-- Schema: system (platform metadata, migrations)
+-- Schema: acme_corp (Tenant: ACME Corporation)
+-- Schema: globex (Tenant: Globex Inc.)
+-- Schema: initech (Tenant: Initech LLC)Every service database follows this pattern. When a query executes, Hibernate sets the PostgreSQL search_path to the current tenant's schema before running any SQL.
How It Works
TenantIdentifierResolver
The TenantIdentifierResolver bridges the application tenant context to Hibernate's multi-tenancy system:
@Component
public class TenantIdentifierResolver
implements CurrentTenantIdentifierResolver<String> {
@Override
public String resolveCurrentTenantIdentifier() {
return TenantContext.getCurrentTenantIdOrDefault("system");
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}Query Execution Flow
1. Request arrives with JWT containing tenant_id = "acme-corp"
2. TenantContextHolder.setTenantId("acme-corp")
3. Repository method called: dashboardRepository.findAll()
4. TenantIdentifierResolver.resolveCurrentTenantIdentifier()
--> returns "acme_corp"
5. Hibernate sets: SET search_path TO 'acme_corp';
6. Query executes: SELECT * FROM dashboards;
--> Only returns data from acme_corp schema
7. TenantContextHolder.clear() in finally blockSecurity Properties
| Property | Guarantee |
|---|---|
| Schema boundary | A SQL query in one schema cannot reference tables in another schema |
| No missing filter | TenantContext.requireCurrentTenantId() throws if context is absent |
| Connection-level scoping | search_path is set per connection before each query |
| Context cleanup | TenantContextHolder.clear() called in filter chain finally block |
Schema Provisioning
When the Tenant Service provisions a new tenant:
| Step | SQL Operation |
|---|---|
| 1 | CREATE SCHEMA IF NOT EXISTS {tenant_slug} |
| 2 | Run Flyway/Alembic migrations against the new schema |
| 3 | Seed initial data (default configuration, templates) |
| 4 | Verify table structure matches the system schema |
Schema Decommissioning
When a tenant is deleted:
| Step | Action |
|---|---|
| 1 | Verify tenant is suspended (no active workloads) |
| 2 | Export schema for archival (if required by retention policy) |
| 3 | DROP SCHEMA {tenant_slug} CASCADE |
| 4 | Publish TENANT_DECOMMISSIONED event |
Connection Pool Sharing
All tenants share a single HikariCP connection pool per service. The TenantIdentifierResolver sets the correct schema on each connection before use:
| Advantage | Description |
|---|---|
| Resource efficiency | No per-tenant connection pool overhead |
| Operational simplicity | Single pool to monitor and tune |
| Fast tenant switching | SET search_path is a lightweight operation |
Integration Testing
Tenant isolation is verified through integration tests:
@Test
void shouldNotAccessOtherTenantData() {
// Create data as Tenant A
TenantContext.withTenant("tenant-a", () -> {
dashboardService.create(new Dashboard("My Dashboard"));
});
// Attempt to access as Tenant B
TenantContext.withTenant("tenant-b", () -> {
List<Dashboard> dashboards = dashboardService.findAll();
assertThat(dashboards).isEmpty();
});
}Related Pages
- Namespace Isolation -- Kubernetes namespace boundaries
- Resource Isolation -- CPU/memory quotas
- Tenant Context Propagation -- How tenant context drives database scoping
- Architecture: Database Isolation -- Architecture perspective