Runtime contracts and validation boundaries
This page collects user-facing contracts that are enforced by the current runtime implementation and are useful when extending or integrating Quarkus Multitenancy.
Tenant resolution outcomes
A TenantResolver returns exactly one of three TenantResolution outcomes:
| Outcome | Contract |
|---|---|
|
A tenant identifier was produced. For HTTP resolution, the identifier is validated before it is published to |
|
The resolver had no applicable input. Dispatch continues to the next resolver or strategy. If the whole HTTP chain is not applicable, the configured |
|
The resolver processed applicable input and rejected it. HTTP dispatch stops with status |
Custom resolvers should therefore return Rejected for present-but-invalid security input instead of NotApplicable. Rejection reasons may appear in diagnostics, so they should be log-safe and should not embed unsanitized request-controlled values.
Resolver ordering
The HTTP dispatcher uses two stages:
-
User-defined
TenantResolverbeans run before built-in strategies. A resolver is considered custom when itsname()is blank,null, or does not match a built-in strategy name. -
Built-in strategies then run in the exact order configured by
quarkus.multi-tenant.http.strategy.
Evaluation stops at the first Resolved or Rejected outcome.
Multiple custom resolvers are ordered deterministically:
-
A resolver annotated with
jakarta.annotation.Priorityruns before a resolver with a lower numeric priority. -
A resolver without
@Priorityhas the default priority0. -
Resolvers with the same priority are ordered by fully qualified implementation class name.
-
Two beans with the same implementation type and priority are ambiguous and fail application startup with an actionable error. Assign distinct priorities to make the intent explicit.
For example:
@ApplicationScoped
@Priority(200)
public class ContractTenantResolver implements TenantResolver {
// ...
}
@ApplicationScoped
@Priority(100)
public class HeaderOverrideTenantResolver implements TenantResolver {
// ...
}
ContractTenantResolver runs first. When it returns NotApplicable, resolution continues with HeaderOverrideTenantResolver. The first Resolved or Rejected result stops evaluation.
The built-in HTTP resolvers explicitly use priority 0. Their relative order is still controlled by quarkus.multi-tenant.http.strategy, because configured strategy order takes precedence inside the built-in stage. Custom resolvers continue to run before that stage for backward compatibility.
Programmatic tenant binding is a trusted boundary
TenantContextRunner.runAsTenant(…) is intended for application-controlled, synchronous background work. It activates the CDI request context when necessary, restores any previous tenant after completion, and rejects deferred Uni or CompletionStage results.
The runner does not apply the HTTP or Kafka tenant-id validation policy to the supplied tenant identifier. If the tenant originates from untrusted input, validate or map it before calling runAsTenant.
This distinction is intentional: HTTP and Kafka are external trust boundaries and validate incoming tenant metadata, while programmatic background binding is local application code.
HTTP startup validation
Several HTTP configuration errors fail fast during application startup rather than changing behavior only after the first request:
-
unknown non-blank values in
quarkus.multi-tenant.http.strategyare rejected; strategy names are trimmed and case-insensitive; -
quarkus.multi-tenant.http.tenant-id.reject-statusmust be between400and499; -
default-tenantis validated at startup when tenant-id validation is enabled; -
enabling the
jwtstrategy requires a supported SmallRye JWT or Quarkus OIDC verification source unlessquarkus.multi-tenant.http.jwt.skip-startup-check=trueis explicitly set; -
the path strategy requires a valid regular expression and
path-groupmust select an existing capturing group from1to the pattern’s group count.
For path resolution, a request path that does not match the configured expression is NotApplicable, so dispatch continues to the next strategy and may eventually use the default tenant.
TenantContext lifecycle
TenantContext represents the tenant associated with the active request context:
-
getTenantId()returns the current tenant when one has been bound; -
setTenantId(…)replaces the current binding; -
clear()removes it.
It is not a global tenant store. Work that leaves the current request lifecycle must use a supported propagation mechanism instead of assuming the same TenantContext instance remains available.
ORM bridge contract
The ORM adapter reads the tenant from the shared TenantContext for Hibernate ORM access. If a request context exists but no tenant has been set before ORM access, resolution fails instead of silently selecting an application tenant.
The reserved __bootstrap identifier is used internally as Hibernate ORM’s default/bootstrap tenant when no request-scoped tenant context is available. Application-selected reserved tenant identifiers are rejected before normal ORM access.
The current adapter is registered with an unqualified @PersistenceUnitExtension, so the built-in bridge targets the default persistence unit. Applications using named persistence units need an application-specific integration until named-persistence-unit support is added.
Validation boundaries summary
| Boundary | Validation | Notes |
|---|---|---|
Incoming HTTP |
Common HTTP tenant-id policy |
Applies to built-in and custom HTTP resolvers before publishing a |
Incoming Kafka |
Kafka validator chain |
Always rejects blank/reserved identifiers; configurable Kafka policy can inherit corresponding HTTP tenant-id settings. |
Outgoing Kafka |
No incoming syntax/length policy |
Propagates trusted local |
|
No automatic tenant-id policy |
Intended for trusted application-controlled synchronous binding. |
ORM access |
Reserved-id guard |
Requires a tenant to have been bound for request-scoped ORM access; |