Aller au contenu

Story TENANT-001: Foundation — dev environment, multi-datasource resolver & Flyway multi-profile wiring

Module: tenant-admin Slice: S0 (Vertical Slice Decomposition §11) — V0 anchor for prd-v0 §5.1 (D1/D2) + §5.2 (D9) Side: [BACKEND] Version target: [V0.0.0] Priority: 1 Depends on: None Can develop concurrently with: None (serial — must merge first; every other module's S0 depends on this infra) Merge order: Before all other TENANT stories Estimated complexity: M PRD User Stories: N/A - Technical Story Wireframe: N/A — backend only


Objective

This is the bootstrap slice for the entire platform. It stands up the local dev/test infrastructure (two Postgres logical DBs, Keycloak, Redis, MinIO via docker-compose, plus a Testcontainers harness) and wires the two cross-cutting Day-1 architectural anchors that make multi-profile (SHARED → later BYO) support non-disruptive: the tenant-aware datasource resolver (D1/D2) and the Flyway multi-profile migration runner (D9). In V0.0.0 the resolver only ever returns the SHARED datasource and Flyway has a single tenant target — but the wiring is in place so that V0.0.4 (BYO-DB) is purely additive. Without this slice nothing else compiles.


Backend Scope

Entities

None in this slice (schema is introduced in TENANT-002).

Migrations

None (the Flyway folder layout + runner are created here, but the first migration files land in TENANT-002). Create the empty folder structure:

backend/src/main/resources/db/migration/
├── platform/      ← applied once to the platform DB at app start
└── tenant/        ← applied to the shared tenant DB at app start; later (V0.0.4) also to each BYO DB during provisioning

Service Layer

  • TenantContext (request-scoped holder): carries tenantId, agencyId? (used from V0.0.2), countryCode, and a platformAdmin() sentinel factory. No ThreadLocal that leaks across async pools — async listeners receive tenantId in the event payload (prd-v0 §5.3).
  • TenantAwareDataSourceResolver / TenantDataSourceRouter: implements the routing interface getConnection(tenantId). In V0.0.0 it returns the shared tenant datasource unconditionally. The branch on a byo_db flag is a TODO comment referencing TENANT-007; do NOT implement it here, but the method signature must already accept a tenant id so V0.0.4 is non-breaking.
  • Two logical datasources wired as Spring beans: @Qualifier("platformJdbcClient") JdbcClient over the platform DB, and the tenant datasource behind the resolver. Platform-DB access MUST use the qualified JdbcClient, NEVER CrudRepository (Spring Data JDBC @Primary routing would silently hit the tenant pool — see Technical Risk R1).
  • FlywayMultiProfileRunner: configures two Flyway instances — one against the platform DB (location db/migration/platform), one against the shared tenant DB (location db/migration/tenant). Both run at application start. The runner is written so a third invocation (against a supplied BYO connection) can be added in V0.0.4 with no refactor.

API Endpoints

None.

Validation Rules

N/A.

Multi-Tenant Considerations

This slice establishes the isolation backbone. The resolver is on the data-access path of every tenant-scoped repository from Day 1, even though it only returns one datasource in V0.0.0. The tenant and country_profile tables (TENANT-002) live in the platform DB; all tenant-scoped tables live behind the resolver. Both V0 DB profiles (SHARED now, BYO in V0.0.4) must work without any code path diverging past TenantAwareDataSource (NFR-08). Country-specific behavior is resolved from the tenant's country_code, never per agency.

Audit & Logging

  • MDC must carry tenant_id, agency_id, country_code on every log line (blueprint §7.5). NEVER log byo_db_password, JDBC URLs containing password=, or raw Authorization headers. Add a Logback masking converter that redacts password=... in JDBC URLs (Technical Risk R2).
  • No audit events are emitted by this slice (no business action yet).

Frontend Scope

N/A — backend only.


Acceptance Criteria

AC-T001.1: Dev stack boots
Given a clean checkout and `docker compose up -d`
When the stack starts
Then postgres-platform (5433), postgres-tenant (5434), keycloak (8081), redis (6379) and minio (9000/9001) are all healthy
And `./gradlew :backend:bootRun` starts the application with both Flyway instances reporting "no migrations to apply" (folders empty)
AC-T001.2: Resolver returns the shared datasource and is on the data path
Given the application is running with the SHARED profile only
When any tenant-scoped repository requests a connection via TenantDataSourceRouter.getConnection(tenantId)
Then the shared tenant datasource is returned
And no code branches on a byo_db flag yet (a single integration test asserts the router has exactly one resolvable datasource in V0.0.0)
AC-T001.3: Platform DB is never reached via CrudRepository
Given the module under test
When the architecture/modulith verification test runs
Then it asserts that no interface extending CrudRepository is parameterised on a platform entity (Tenant/CountryProfile)
And platform-DB access compiles only through the @Qualifier("platformJdbcClient") JdbcClient
AC-T001.4: Secrets never leak to logs
Given a log statement that includes a JDBC URL containing "password=secret"
When it is emitted
Then the Logback masking converter redacts the password segment
And an integration test attempts to log a credential-bearing string and asserts the secret does not appear in captured output

Compliance Rules

None directly. This slice creates the substrate on which compliance-bearing slices run. No INCERTAIN/PRÉLIMINAIRE obligation is touched.


Standards & Conventions

  • docs/standards/tech-stack.md — locked versions (Java 26, Spring Boot 4.0.5, PostgreSQL, Keycloak ≥ 26.6, Redis, MinIO).
  • docs/standards/multi-datasource-patterns.md — Pattern 1 (platform JdbcClient), resolver wiring.
  • docs/standards/multi-tenant-model.md — DB profile taxonomy (SHARED/BYO/SCHEMA/DATABASE), resolver contract.
  • docs/standards/database-guidelines.md — Flyway layout, forward-only + hand-written undo for dev.
  • docs/standards/java-spring-guidelines.md — Spring Data JDBC (not JPA), package root com.altarys.papillon.pcs.controlplane.tenant.
  • docs/standards/critical-rules.md — rule #1 (tenant isolation), secrets handling.
  • docs/standards/git-workflow.md.

Testing Requirements

Unit Tests

  • TenantDataSourceRouter returns shared datasource for any tenant id (V0.0.0).
  • Logback masking converter redacts password= segments.

Integration Tests

  • @Testcontainers harness: PostgreSQLContainer:16 (platform) + a second for shared tenant, KeycloakContainer:26 (or dasniko/testcontainers-keycloak), Redis, MinIO container with a pre-created papillon bucket. Reused across the suite via @ServiceConnection.
  • Application context boots with both Flyway instances configured.
  • Modulith verification test: no CrudRepository on platform entities.

What QA Will Validate

  • docker compose up -d then ./gradlew :backend:bootRun succeeds on a clean machine; smoke per arch §14.3.
  • Logs carry MDC tenant fields and never a raw password.

Out of Scope

  • Any tenant/country_profile/agency schema or seed (TENANT-002).
  • BYO-DB resolver branch + BYO Flyway target (TENANT-007 / V0.0.4).
  • SCHEMA / DATABASE profiles (V3).
  • Keycloak realm/Organization provisioning logic (TENANT-004).
  • Production secret sourcing (Coolify/SOPS) — dev uses .env.

Definition of Done

  • [ ] docker-compose stack (2 Postgres, Keycloak, Redis, MinIO) boots healthy
  • [ ] App starts with both Flyway instances wired (platform + shared tenant)
  • [ ] TenantContext + TenantDataSourceRouter on the data path, returns SHARED only
  • [ ] Platform DB reached only via @Qualifier("platformJdbcClient") JdbcClient; modulith test guards against CrudRepository on platform entities
  • [ ] Logback masking converter redacts JDBC passwords; test proves no secret in logs
  • [ ] Testcontainers harness reusable across the suite
  • [ ] Unit + integration tests passing
  • [ ] No silent failures on infra unavailability (clear startup error)