tenantlayer.io

Everything multi-tenant SaaS needs from Spring Boot and Postgres

Isolation the database enforces. Context that survives every thread, job and message. A test that proves it, not a README that claims it.

@Test
@WithTenant("acme")
void oneTenantCannotSeeAnother() {
    assertThat(orders.findAll()).isNotEmpty();  // acme sees its own
    assertTenantCannotSee("globex");            // and nothing of globex's
}

0.4.0 · Apache 2.0 · on Maven Central · Java 17+ · Spring Boot 3.3+

The bug this exists for

Your connection pool reuses sessions. Set the tenant on one, miss the reset, and the next request inherits it — reading another customer’s data with no error, no exception, and nothing in your logs.

Your tests will not catch this. Tests don’t contend for pooled connections.

It is one of about six mistakes every hand-rolled multi-tenancy implementation makes, and they are all silent. TenantLayer is those six mistakes, already made and already fixed.

“Why not just add WHERE tenant_id = ?”

Because you will forget once, in four years, in a report query written by someone who has since left — and nothing will tell you.

And because the ORM is not the only thing that talks to your database. A native query, a JdbcTemplate call, a bulk update, a psql session on the same credentials: none of them go through your filter. All of them go through a row-level security policy.

The predicate belongs where it cannot be forgotten.

The whole surface

Multi-tenancy is not one feature. Here is every concern it involves, and exactly where we are on each — including the rows that are still empty.

ConcernShipped in 0.4.0Planned
Tenant resolution
  • Auth0 and Keycloak organisation presets
  • Header
  • Subdomain
  • Path segment
  • JWT claim
  • Pluggable SPI
  • Ordered chain
  • Strict fail-closed
—
Context & propagation
  • @Async
  • CompletableFuture
  • Virtual threads
  • @Scheduled
  • Outbound HTTP
  • Kafka produce & consume
  • MDC logging
Isolation strategies
  • Row-level security
  • Hibernate @TenantId discriminator
  • Schema-per-tenant
  • Database-per-tenant
  • Strategy switch by configuration
Authorisation
  • Membership verification — a claimed tenant is checked against the caller
—
Schema & policies
  • Start-up isolation checker
  • Entity scanning
  • One-shot RLS policy generation
—
Tenant registry
  • Tenant onboarding, in one call
  • Provisioning hooks
  • Tenant CRUD over an actuator endpoint
  • Table-backed registry with status, region, group and metadata
Migrations
  • Flyway per-tenant runner
  • Per-database migration under database-per-tenant
Caching
  • Tenant-scoped cache keys
  • Per-tenant eviction
  • Shared caches opted out by name
—
Observability
  • Per-tenant metric tag, with a cardinality cap
  • MDC log enrichment
Testing
  • @WithTenant
  • assertTenantCannotSee
  • Testcontainers fixtures
Dashboard—

Every planned item is an open issue. 15 of them are unclaimed.

Proof, not claims

  • Every isolation claim is mutation-tested

    After a test passes, we break the implementation and confirm it fails. It has caught something real every time.

  • 101 tests, on real infrastructure

    Java 17 and 21, Hibernate 6.6 and 7.0, against a real Postgres and a real Kafka broker. Not mocks.

  • A consumer builds against the published artifact

    On every commit. It caught a startup crash the library's own tests structurally could not find.

  • The bugs are written up, not buried

    Enabling virtual threads silently removed tenant isolation from every @Async method. That is on the blog, not in a changelog footnote.

Get started

Three lines of dependency, two of configuration, one policy per table.

<dependency>
  <groupId>io.tenantlayer</groupId>
  <artifactId>tenantlayer-spring-boot-starter</artifactId>
  <version>0.4.0</version>
</dependency>

Read this before you deploy

Isolation depends on your application connecting as a role that is neither a superuser nor the table owner. Both bypass row-level security by design, and a test suite written against a superuser connection passes whether or not your policies work — including after someone deletes them. Row-level security →

Where this is

0.4.0 is the third release. All three isolation strategies now exist — row-level security, schema-per-tenant and database-per-tenant — alongside propagation, authorisation, caching, migrations and the testing kit. Reactive support, Liquibase and the dashboard do not.

0.x means the API may break between minor versions; every break gets a changelog entry with a migration note. Having all three strategies has already stress-tested the seam between them — building database-per-tenant found a migration bug that would have left tenants silently on an old schema — which is the kind of thing that has to happen before 1.0 means anything.

If that is too early for you, watch the repository and come back at 0.3.