Skip to content

epic: @TenantId Hibernate native multi-tenancy + workspace_id denormalization #1294

Description

@FelixTJDietrich

Context: #1290 (epic #1097 server foundations) delivered SQL-layer multi-tenancy via a regex-based WorkspaceStatementInspector running over every emitted statement. This is a runtime guardrail, not a structural fix.

The structural fix is Hibernate's native multi-tenancy via @TenantId on every workspace-scoped entity, with workspace_id denormalized onto each table so the predicate doesn't need a join to reach.

ADR 0004 documents why this was cut from the foundations epic: 20+ entities need workspace_id denormalized (some across multi-hop FK chains), each one a non-trivial migration with backfill semantics. 6–12 week effort.

Why eventually

The current inspector approach has documented holes:

  • 4 regex carve-outs (INSERT, PK-only DML, PK-anchored SELECT, workspace_id word-boundary) admit edge cases that depend on the surrogate-key invariant holding.
  • ~71 repositories are marked @WorkspaceAgnostic for FK-chain scoping. Each one is a defect surface — drop the annotation and queries pass the inspector with the wrong scope.
  • Inspector runs in log mode in prod today; the flip to throw is a separate decision (#TBD — file when ready).

Native @TenantId is a single declarative invariant per entity, enforced at the Hibernate level rather than at SQL-shape level.

What's in scope when this lands

  1. Add workspace_id NOT NULL column to every workspace-scoped table that doesn't already have one. Backfill from the existing FK chain.
  2. Annotate every workspace-scoped @Entity with @TenantId.
  3. Register a CurrentTenantIdentifierResolver that reads from the existing WorkspaceContext.
  4. Remove WorkspaceStatementInspector and @WorkspaceAgnostic (or repurpose @WorkspaceAgnostic to mean "intentionally cross-tenant query" with a registered tenant override).
  5. Migrate all repositories that today scope via FK chain or via @Where/@Filter to plain repositories — @TenantId makes the predicate implicit.

Why not now

Acceptance (epic-level)

  • Every workspace-scoped entity declares @TenantId with a workspace_id column
  • CurrentTenantIdentifierResolver reads from WorkspaceContext
  • WorkspaceStatementInspector removed (or demoted to assertion-only behind a debug flag)
  • All @WorkspaceAgnostic annotations either deleted or converted to tenant overrides
  • Backfill migrations validated against a prod-clone DB
  • No regression in CrossWorkspaceIsolationTest (feat(server): CrossWorkspaceIsolationTest reflection-walks every @WorkspaceScopedController #1292)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    application-serverSpring Boot server: APIs, business logic, databaseepicLarge feature or initiative with multiple sub-issuesfeatureNew feature or enhancementrefactorCode restructuring without changing behavior

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions