The tenancy enforcement batch took longer than the individual issue sizes suggested. Four issues on one branch: a migration (#411), a CDI qualifier pattern (#405), DB-level Row Level Security (#406), and a registry hashCode bug (#410). The migration was mechanical. The other three had opinions.

The qualifier pattern

Engine#405 asked for @CrossTenant as an access-control gate on cross-tenant SPIs. The qualifier itself was two annotations and a producer — straightforward. The wrinkle: recovery services need cross-tenant access but run outside any request context. Production CurrentPrincipal is @RequestScoped. Calling it from an @ApplicationScoped recovery service outside a request throws ContextNotActiveException.

The spec called for a system-actor principal that always returns isCrossTenantAdmin() = true. Platform doesn’t have one yet, so I added SystemCurrentPrincipal to the engine — @ApplicationScoped @EngineSystem, not @DefaultBean, selected only via the @EngineSystem qualifier in the producer. When platform ships the real thing, one injection point changes and this class disappears.

RLS: two surprises and a redesign

Engine#406 started with a CDI interceptor approach — annotate repository classes with @TenantBound, intercept calls, inject SET LOCAL "casehub.tenancy_id". Clean on paper.

First surprise: PostgreSQL SET LOCAL doesn’t accept JDBC bind parameters. session.createNativeQuery("SET LOCAL ... = :tid").setParameter("tid", value) compiles fine and fails at runtime. PostgreSQL treats configuration statements differently from DML — no prepared statement protocol, no $1. String interpolation with a sanitization guard is the only option.

Second surprise: CDI interceptors don’t work for reactive code. An @AroundInvoke interceptor runs synchronously and completes before the Uni pipeline starts executing. By the time Hibernate Reactive opens a connection and begins the reactive session, the interceptor is long gone — and so is any SET LOCAL it tried to issue. The SET goes to a different connection entirely.

I brought Claude in to rethink the approach. We landed on a TenantAwareRepository base class that wraps Panache.withTransaction() and injects SET LOCAL inside the reactive pipeline, before the work runs. Reads also go through withTenantTransaction() rather than withSession()SET LOCAL only applies within an explicit transaction, not in autocommit mode.

Cross-tenant access uses SET LOCAL ROLE casehub_crosstenancy (a BYPASSRLS PostgreSQL role) rather than a second policy branch. Simpler policy, auditable in pg_roles.

Two bugs surfaced during implementation. The TABLES list in RlsPolicyApplicator had sub_case_group — the entity’s @Table annotation says subcase_group. And the BYPASSRLS role was granted correctly but never got SELECT/INSERT/UPDATE/DELETE on the engine tables, so every cross-tenant query failed with permission denied. Both caught before any commit reached main.

One limitation the integration test exposed: Quarkus Dev Services creates a PostgreSQL superuser, and superusers bypass FORCE ROW LEVEL SECURITY unconditionally. The test confirms the BYPASSRLS path and application-level filtering work — kernel-level RLS enforcement against the policy requires a non-superuser DB user to test.

The registry fix

Engine#410 was a mutable hashCode key in a ConcurrentHashMap<CaseMetaModel, CaseDefinition>. CaseMetaModel has public setters on namespace, name, version — the three fields used in hashCode(). If any of those change after a put(), Map.get() looks in the wrong bucket. There was a defensive linear scan already in place as a guard; the fix was to replace the map key with an immutable CaseKey record. Two maps became one Map<CaseKey, RegistryEntry> — one atomic put(), no consistency window.


<
Previous Post
eidos gets a memory
>
Next Post
Layer 5: The Gate That Works