A modular multitenancy extension for Quarkus providing a shared tenant context, pluggable HTTP resolution, Kafka tenant propagation, and Hibernate ORM integration.
Quarkus Multitenancy provides a generic tenant-resolution contract and a request-scoped TenantContext that can be reused across application and integration boundaries.
The extension focuses on tenant identification and propagation. It does not create database, schema, cache, or authorization isolation by itself; those remain application and framework configuration concerns.
Quarkus already provides powerful building blocks such as OIDC multitenancy and Hibernate ORM multitenancy. Applications still frequently need a common tenant abstraction that can be resolved once and consumed consistently across HTTP, persistence, messaging, background work, and custom integrations.
This extension provides that reusable layer while keeping the individual integrations independent.
| Module | Responsibility |
|---|---|
quarkus-multitenancy-core-runtime |
TenantContext, TenantResolver, resolution outcomes, and synchronous tenant binding |
quarkus-multitenancy-core-deployment |
Core Quarkus build-time integration |
quarkus-multitenancy-http-runtime |
HTTP tenant resolution from header, cookie, JWT claim, path, or custom resolvers |
quarkus-multitenancy-http-deployment |
HTTP integration registration |
quarkus-multitenancy-messaging-kafka-runtime |
Incoming and outgoing Kafka tenant propagation |
quarkus-multitenancy-messaging-kafka-deployment |
Kafka integration registration |
quarkus-multitenancy-orm-runtime |
Bridges the shared TenantContext into Hibernate ORM tenant resolution |
quarkus-multitenancy-orm-deployment |
ORM integration registration |
quarkus-multitenancy-demo |
PostgreSQL multi-tenant demo application |
TenantContext is request scoped and exposes the active tenant to downstream code:
@Inject
TenantContext tenantContext;
String tenant = tenantContext.getTenantId().orElseThrow();Tenant resolvers return one of three explicit outcomes:
| Outcome | Meaning |
|---|---|
Resolved |
A tenant identifier was resolved successfully |
NotApplicable |
The resolver had no applicable input, so resolution may continue |
Rejected |
Input was present but invalid or untrusted; the request must not silently fall back |
For HTTP requests, a Rejected outcome aborts the request with HTTP 401. If every resolver returns NotApplicable, the configured default tenant is used.
Add the HTTP extension:
<dependency>
<groupId>io.quarkiverse.multitenancy</groupId>
<artifactId>quarkus-multitenancy-http</artifactId>
<version>${quarkus-multitenancy.version}</version>
</dependency>The built-in HTTP strategies are:
headerβ tenant from a configurable HTTP header, defaultX-Tenantcookieβ tenant from a configurable cookie, defaulttenant_cookiejwtβ tenant from a verified JWT claim, defaulttenantpathβ tenant from a configurable request-path regular expression
The default built-in strategy chain is:
quarkus.multi-tenant.http.strategy=header,cookieA complete example:
quarkus.multi-tenant.http.enabled=true
quarkus.multi-tenant.http.strategy=header,jwt,cookie,path
quarkus.multi-tenant.http.header-name=X-Tenant
quarkus.multi-tenant.http.cookie-name=tenant_cookie
quarkus.multi-tenant.http.jwt-claim-name=tenant
quarkus.multi-tenant.http.default-tenant=public
quarkus.multi-tenant.http.path-pattern=^/t/([^/]+)(?:/|$)
quarkus.multi-tenant.http.path-group=1Custom CDI beans implementing TenantResolver run before the configured built-in chain. Annotate custom resolvers with Jakarta @Priority when more than one may handle the same request; higher values run first, the default is 0, and equal priorities are ordered by implementation class name. Built-in resolvers run in the order declared by quarkus.multi-tenant.http.strategy.
Resolved HTTP tenant identifiers are validated before they are published to TenantContext:
quarkus.multi-tenant.http.tenant-id.validation-enabled=true
quarkus.multi-tenant.http.tenant-id.max-length=64
quarkus.multi-tenant.http.tenant-id.pattern=[A-Za-z0-9_-]+
quarkus.multi-tenant.http.tenant-id.reject-status=400The default rejection status for an invalid resolved identifier is HTTP 400. This is intentionally different from a resolver-level Rejected outcome, which represents an authentication/trust failure and produces HTTP 401.
Invalid strategy names, invalid tenant-validation configuration, invalid path regular expressions, invalid path capture groups, and incompatible JWT setup fail fast during startup where applicable.
The jwt strategy is opt-in and expects a verified identity supplied by SmallRye JWT, Quarkus OIDC, or an application-provided JsonWebToken bean.
Example with SmallRye JWT:
quarkus.multi-tenant.http.strategy=jwt
quarkus.multi-tenant.http.jwt-claim-name=tenant
mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.publickey.algorithm=RS256
mp.jwt.verify.issuer=https://issuer.example.comIf a bearer token is present but cannot be trusted, or the configured tenant claim is invalid, the request is rejected and does not fall back to the default tenant.
Applications that intentionally provide their own authenticated JsonWebToken bean can opt out of the built-in startup check:
quarkus.multi-tenant.http.jwt.skip-startup-check=trueFor scheduled jobs, startup observers, maintenance callbacks, or other synchronous work outside the HTTP pipeline, use TenantContextRunner:
@Inject
TenantContextRunner tenantRunner;
void refreshTenant() {
tenantRunner.runAsTenant("acme", () -> {
// TenantContext contains "acme" here.
});
}TenantContextRunner restores the previous tenant after the callback completes or throws and can temporarily activate the CDI request context when one is not already active.
It is intentionally synchronous only. Do not use it to return a CompletionStage, Mutiny Uni, or other asynchronous result that may outlive the callback.
TenantContextRunner is a trusted programmatic boundary and does not apply the HTTP or Kafka tenant-id validation policy automatically. Validate or map externally controlled tenant identifiers before binding them.
Reactive does not automatically mean that tenant propagation must be handled manually. While work remains inside the same Quarkus REST request, the request-scoped TenantContext is preserved by Quarkus across supported context-aware boundaries.
This includes Mutiny Uni pipelines, @Blocking/worker-thread dispatch, and CompletionStage work executed through a MicroProfile ManagedExecutor. A REST endpoint using those mechanisms does not need TenantContextRunner just because execution is asynchronous.
This behavior comes from Quarkus REST, Vert.x duplicated context, and SmallRye Context Propagation rather than from a custom propagation mechanism in this extension. Work submitted directly to a raw JDK executor is different and does not automatically inherit the request context.
See docs/modules/ROOT/pages/index.adoc for the detailed reactive boundary table and docs/modules/ROOT/pages/context-propagation.adoc for the cross-boundary propagation guide.
Add the optional Kafka module:
<dependency>
<groupId>io.quarkiverse.multitenancy</groupId>
<artifactId>quarkus-multitenancy-messaging-kafka</artifactId>
<version>${quarkus-multitenancy.version}</version>
</dependency>The Kafka integration applies only to SmallRye Reactive Messaging channels backed by the Kafka connector. It does not modify AMQP, in-memory, or custom connectors.
Incoming propagation requires Quarkus Messaging request scope:
quarkus.messaging.request-scoped.enabled=trueBasic configuration:
quarkus.multi-tenant.messaging.kafka.enabled=true
quarkus.multi-tenant.messaging.kafka.header-name=X-Tenant
quarkus.multi-tenant.messaging.kafka.fail-on-missing-incoming-tenant=false
quarkus.multi-tenant.messaging.kafka.fail-on-missing-outgoing-tenant=falseOutgoing messages inherit the current tenant through Kafka record metadata unless application code already supplied the configured tenant header explicitly.
Incoming Kafka tenant identifiers are treated as untrusted external input and are validated before binding:
quarkus.multi-tenant.messaging.kafka.tenant-id.validation-enabled=true
quarkus.multi-tenant.messaging.kafka.tenant-id.max-length=64
quarkus.multi-tenant.messaging.kafka.tenant-id.pattern=[A-Za-z0-9_-]+Applications can also provide CDI beans implementing KafkaTenantValidator for domain-specific checks.
Add the ORM extension:
<dependency>
<groupId>io.quarkiverse.multitenancy</groupId>
<artifactId>quarkus-multitenancy-orm</artifactId>
<version>${quarkus-multitenancy.version}</version>
</dependency>OrmTenantResolverAdapter bridges the request-scoped TenantContext into Hibernate ORM's tenant resolver SPI.
During ORM bootstrap, the adapter uses an internal reserved bootstrap tenant. During application ORM access, a tenant must already be present in TenantContext; otherwise access fails instead of silently selecting another tenant.
The ORM module also contains a legacy X-Tenant request filter, enabled by default:
quarkus.multi-tenant.orm.header-filter.enabled=trueWhen HTTP tenant resolution is handled by quarkus-multitenancy-http β especially when using jwt, cookie, or path β disable the ORM header filter so both filters do not compete:
quarkus.multi-tenant.orm.header-filter.enabled=falseThe default Hibernate ORM persistence unit is integrated automatically. To use the same TenantContext with named multitenant persistence units, select them explicitly at build time:
quarkus.multi-tenant.orm.named-persistence-units=users,inventory
quarkus.hibernate-orm."users".multitenant=DATABASE
quarkus.hibernate-orm."inventory".multitenant=SCHEMAEvery selected name must identify an existing Hibernate ORM persistence unit with multitenancy enabled. A non-selected, non-multitenant unit receives no adapter. If the application provides its own TenantResolver for a persistence unit, that resolver overrides the built-in adapter without creating a CDI ambiguity.
| Boundary | Mechanism |
|---|---|
| Incoming HTTP request | HTTP resolver chain |
| Reactive/worker work inside the same HTTP request | Automatic through supported Quarkus REST / Vert.x / SmallRye context propagation (Uni, @Blocking, ManagedExecutor) |
| Synchronous scheduled/background callback | TenantContextRunner.runAsTenant(...) |
| Async work after leaving the request or temporary background binding | Use a context-aware mechanism for that framework, or pass the tenant id explicitly |
| Raw executor submitted to directly | Request context is not propagated automatically |
| Incoming Kafka message | Kafka record header β validated TenantContext |
| Outgoing Kafka message | Current TenantContext β Kafka record header |
| Hibernate ORM access | TenantContext β ORM tenant resolver adapter |
TenantContext should not be treated as global or as a general-purpose thread-local value. Supported reactive work within an active Quarkus REST request keeps the request context automatically; explicit propagation is needed when work leaves that context or crosses into another integration boundary.
mvn clean install
cd quarkus-multitenancy-demo
mvn quarkus:devThe Quarkiverse documentation contains the detailed runtime contracts, configuration guidance, and tenant-context propagation behavior:
https://docs.quarkiverse.io/quarkus-multitenancy/dev/
Relevant source pages in this repository include:
docs/modules/ROOT/pages/index.adocdocs/modules/ROOT/pages/context-propagation.adocdocs/modules/ROOT/pages/runtime-contracts.adoc
This project is licensed under the Apache License 2.0.