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.1.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.1.0Planned
Tenant resolution
  • 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
Authorisation
  • Membership verification — a claimed tenant is checked against the caller
Schema & policies
  • Entity scanning
  • One-shot RLS policy generation
Tenant registry
  • Table-backed registry with status, region, group and metadata
Migrations
Caching
Observability
  • MDC log enrichment
Testing
  • @WithTenant
  • assertTenantCannotSee
  • Testcontainers fixtures
Dashboard

Every planned item is an open issue. 26 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.1.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.1.0 is a first release. The hard part — isolation, propagation, authorisation, the testing kit — is shipped and tested. Migrations, caching and the alternative isolation strategies are not.

0.x means the API may break between minor versions; every break gets a changelog entry with a migration note. It stabilises at 1.0, after all three isolation strategies exist and have stress-tested it between them.

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