tenantlayer.io

The tenant registry

What it is

One table listing the tenants that exist, their status, and where they live.

create table tenantlayer_tenants (
    tenant_id      varchar(64) primary key,
    status         varchar(16)  not null default 'ACTIVE',
    region         varchar(64),
    tenant_group   varchar(64),
    datasource_ref varchar(128),
    metadata       jsonb        not null default '{}'::jsonb
);

Create it through your own migration tool. TenantLayer does not issue DDL against your database; TenantRegistrySchema.DDL is the statement, for you to commit and review.

It has no row-level security, deliberately

The registry is shared infrastructure that gets consulted during tenant resolution, before any tenant is known. A policy on this table would hide it from the very code whose job is to read it, and the application would not start. Every other table in your schema should have one; this one must not.

Columns nothing reads yet

region and tenant_group are unused in v0.1 and present anyway. A registry is the hardest table in the system to change once it holds production rows — adding a column later means a migration on every deployment plus a backfill nobody has the data for. Both are nullable and cost a schema no one has to alter twice. datasource_ref is the same argument for schema- and database-per-tenant routing.

Running work for every tenant

This is the case a request-scoped tenancy layer cannot serve. Done wrong it is not an error: every query runs with no tenant, returns nothing, and the job reports success having done nothing at all.

@Scheduled(cron = "0 0 3 * * *")
void rebuildReports() {
    tenantTasks.forEachTenant(tenant -> reportService.rebuild());
}

Each iteration runs with that tenant bound, so reportService needs no tenant parameter and its queries scope themselves. Suspended tenants are skipped.

One tenant's failure does not cancel the rest. A nightly job that aborts on the first bad tenant leaves everyone after it in the list unprocessed, and which ones those are depends on alphabetical order. Every tenant is attempted; failures are collected and thrown at the end as a TenantIterationException that names them, in registry order, so the alert says which tenants failed rather than that something did.

Map<String, Long> counts = tenantTasks.mapEachTenant(tenant -> orders.count());
// {acme=1, globex=2}

For a fixed-tenant job:

tenantTasks.runAs("acme", () -> reportService.rebuild());

The scheduler thread is left exactly as it was found — schedulers pool their threads, and a job that leaves a tenant behind hands it to the next job on that thread.