tenantlayer.io

TenantLayer documentation

Multi-tenancy for Spring Boot and Postgres, where the database does the enforcing.

What it looks like

Add the dependency, tell it where the tenant comes from, and write your application as if it had one customer:

tenantlayer.resolvers=JWT
tenantlayer.strategy=ROW_LEVEL_SECURITY
@RestController
class OrderController {
 
    private final OrderRepository orders;
 
    @GetMapping("/orders")
    List<Order> list() {
        return orders.findAll();     // returns only the acting tenant's rows
    }
}

There is no where tenantId = ?, no filter to remember, and nothing in that controller that mentions tenancy. findAll() genuinely means all — and Postgres returns only the rows this connection is allowed to see, because the tenant was published onto it before your query ran.

The same is true of a native query, a JdbcTemplate call, a bulk update, or a psql session on the same credentials. That is the point of enforcing in the database rather than in the ORM.

Where to start

Setting up a new application — Getting started, then Row-level security before you deploy anything. Twenty minutes.

Building something specific — Recipes has complete, working answers to onboarding a tenant, suspending one, running a nightly job across all of them, serving an admin endpoint that spans tenants, and the rest.

Adding it to a running system — Adopting in an existing app. It is designed to go on gradually rather than as a flag day.

Evaluating whether to use it — Architecture for how it works and what is deliberately not in the path, including the ways isolation can still be bypassed, which are documented rather than hidden.

Something is not working — Troubleshooting. "It returns nothing" and "the policy is not applying" are the first two entries, and between them they cover most of it.

Every guide

GuideWhat it answers
Getting startedAdding the dependency and getting isolation in ten minutes
RecipesComplete solutions: onboarding, suspension, nightly jobs, admin endpoints, Kafka, migrations
Row-level securityHow isolation is actually enforced, the whole setup end to end, and the three mistakes that quietly break it
Isolation strategiesRow-level security, schema-per-tenant, database-per-tenant, and the discriminator column
Tenant resolutionHeaders, subdomains, paths, JWT claims, precedence, and writing your own
Securing resolutionWhy a header alone is not enough, and how to close that
Context propagationThe overview: every boundary the tenant has to cross
Async, threads and scheduling@Async, virtual threads, CompletableFuture, your own executors, @Scheduled, parallel streams
Outbound HTTPRestClient, RestTemplate, WebClient and Feign — and the builder mistake that silently drops the header
KafkaProduce, consume, batch listeners, and the retained tenant that causes cross-tenant writes
Onboarding a tenantProvisioning a new tenant at runtime, hooks, and what happens when one fails
Tenant endpointsDriving the registry over HTTP, and the two properties that turn it on
The tenant registryWho your tenants are, and running work for each of them
MigrationsRunning Flyway across tenants, and the Boot setting you must turn off first
CachingThe one hole row-level security cannot cover, and how it is closed
The isolation checkerThe start-up scan that tells you which tables are not actually protected
MetricsThe tenant tag on observations, and the cardinality cap that keeps it affordable
TestingFixtures, assertions, and how to tell a real isolation test from a vacuous one
Context storageThreadLocal today, ScopedValue later
Configuration referenceEvery property, and four complete configurations
ArchitectureHow the pieces fit together, and why the obvious alternatives are wrong
Adopting in an existing appGetting there from a running system, without a flag day
TroubleshootingIt returns nothing · the policy is not applying · the tenant is null

Three isolation strategies

Chosen by one property, with no application code change:

Tenants shareIsolated byUse when
ROW_LEVEL_SECURITYone schemaa Postgres policyThe default. Most SaaS.
SCHEMA_PER_TENANTone databasesearch_pathPer-tenant schema shape, or noisy-neighbour queries
DATABASE_PER_TENANTnothinga separate database and poolCompliance, or a tenant large enough to want its own

Switching is not behaviour-preserving on the no-tenant path — row-level security returns an empty result set, schema-per-tenant raises an unresolved relation, database-per-tenant throws. All three are safe; only the last is impossible to ignore. See isolation strategies.

The one-paragraph version

Postgres already has row-level security. TenantLayer does not reinvent it — the database does the enforcing, which is exactly why it is trustworthy. What TenantLayer does is the wiring around it: getting the tenant onto the connection at the right moment, guaranteeing it is cleared when the connection returns to the pool, carrying it across every thread and network boundary in between, failing closed when it is absent, and giving you a test that proves all of that rather than a README that claims it.

You can also remove it and keep your isolation: the policies are plain SQL you own.