Skip to content

Latest commit

Β 

History

306 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🧩 Quarkus Multitenancy Extension

Build Documentation License Java Quarkus Status

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.

Why this exists

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.

Modules

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

Core model

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.

HTTP tenant resolution

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, default X-Tenant
  • cookie β€” tenant from a configurable cookie, default tenant_cookie
  • jwt β€” tenant from a verified JWT claim, default tenant
  • path β€” tenant from a configurable request-path regular expression

The default built-in strategy chain is:

quarkus.multi-tenant.http.strategy=header,cookie

A 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=1

Custom 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.

HTTP tenant-id validation

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=400

The 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.

JWT tenant resolution

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.com

If 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=true

Synchronous background work

For 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 work within an HTTP request

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.

Kafka tenant propagation

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=true

Basic 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=false

Outgoing 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.

Hibernate ORM integration

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=true

When 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=false

The 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=SCHEMA

Every 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.

Propagation boundaries

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.

Quick start

mvn clean install
cd quarkus-multitenancy-demo
mvn quarkus:dev

Documentation

The 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.adoc
  • docs/modules/ROOT/pages/context-propagation.adoc
  • docs/modules/ROOT/pages/runtime-contracts.adoc

License

This project is licensed under the Apache License 2.0.

About

A fully decoupled Quarkus extension providing a generic tenant resolution API and request-scoped TenantContext, with pluggable resolvers for HTTP, JWT, cookies

Topics

Resources

Contributing

Stars

10 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages