MATIH Platform is in active MVP development. Documentation reflects current implementation status.
3. Security & Multi-Tenancy
multi-tenancy
Database Isolation

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 block

Security Properties

PropertyGuarantee
Schema boundaryA SQL query in one schema cannot reference tables in another schema
No missing filterTenantContext.requireCurrentTenantId() throws if context is absent
Connection-level scopingsearch_path is set per connection before each query
Context cleanupTenantContextHolder.clear() called in filter chain finally block

Schema Provisioning

When the Tenant Service provisions a new tenant:

StepSQL Operation
1CREATE SCHEMA IF NOT EXISTS {tenant_slug}
2Run Flyway/Alembic migrations against the new schema
3Seed initial data (default configuration, templates)
4Verify table structure matches the system schema

Schema Decommissioning

When a tenant is deleted:

StepAction
1Verify tenant is suspended (no active workloads)
2Export schema for archival (if required by retention policy)
3DROP SCHEMA {tenant_slug} CASCADE
4Publish 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:

AdvantageDescription
Resource efficiencyNo per-tenant connection pool overhead
Operational simplicitySingle pool to monitor and tune
Fast tenant switchingSET 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