--- date: 2026-03-04T19:37:59.203261+00:00 git_commit: cb0bcad145b03fec63be0ee3c1fca46ee545329e branch: master topic: "T-4: Development Infrastructure Setup" tags: [research, codebase, t4, database, liquibase, testcontainers, router, test-infrastructure, docker-compose] status: complete --- # Research: T-4 Development Infrastructure Setup ## Research Question What is the current state of the codebase relative to T-4's acceptance criteria? What already exists, what is missing, and what are the technical considerations for each criterion? ## Summary T-4 is the final infrastructure task before user story implementation can begin. It bridges the gap between the existing project scaffolds (T-1, T-2, T-3, T-5 — all complete) and actual feature development with TDD. The task covers six areas: database migration framework, database connectivity, environment variable configuration, SPA router, backend test infrastructure, frontend test infrastructure, docker-compose documentation, and container verification with PostgreSQL. The codebase already has partial coverage: Vue Router exists with placeholder routes, frontend test infrastructure (Vitest + @vue/test-utils) is operational, and backend test infrastructure (JUnit 5 + Spring Boot Test + MockMvc) is partially in place. What's missing: JPA/Liquibase, Testcontainers, environment variable wiring, and docker-compose documentation. ## Detailed Findings ### AC 1: Database Migration Framework (Liquibase) **Current state:** Not present. No Liquibase or Liquibase dependency in `pom.xml`. No migration files anywhere in the project. The CLAUDE.md explicitly states "No JPA until T-4." **What's needed:** - Add `spring-boot-starter-data-jpa` dependency to `backend/pom.xml` - Add `liquibase-core` dependency (Spring Boot manages the version) - Create changelog directory at `backend/src/main/resources/db/changelog/` - Create master changelog: `db.changelog-master.xml` that includes individual changesets - Create first empty/baseline changeset to prove the tooling works - Spring Boot auto-configures Liquibase when it's on the classpath and a datasource is available — no explicit `@Bean` config needed **Spring Boot + Liquibase conventions:** - Default changelog location: `classpath:db/changelog/db.changelog-master.xml` - Format: XML (chosen for schema validation and explicitness) - Changelogs are DB-agnostic — Liquibase generates dialect-specific SQL at runtime - Spring Boot 3.5.x ships Liquibase via its dependency management - Liquibase runs automatically on startup before JPA entity validation **Architectural note:** The hexagonal architecture has an existing `adapter.out.persistence` package (currently empty, with `package-info.java`). JPA repositories and entity classes will go here. Domain model classes remain in `domain.model` without JPA annotations — the persistence adapter maps between them. The existing ArchUnit tests already enforce this boundary. ### AC 2: Database Connectivity via Environment Variables **Current state:** `application.properties` has no datasource configuration. Only `spring.application.name=fete` and actuator settings. **What's needed:** - Configure Spring datasource properties to read from environment variables via profile-based separation **Chosen approach: Profile-based separation with generic env vars.** The properties are split across three files with clear responsibilities: **`application.properties`** — environment-independent, always active: ```properties spring.application.name=fete spring.jpa.hibernate.ddl-auto=validate spring.jpa.open-in-view=false management.endpoints.web.exposure.include=health management.endpoint.health.show-details=never ``` **`application-prod.properties`** — committed, production profile, activated in Docker via `ENV SPRING_PROFILES_ACTIVE=prod`: ```properties spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USERNAME} spring.datasource.password=${DATABASE_PASSWORD} ``` **`application-local.properties`** — gitignored, developer creates from `.example` template: ```properties spring.datasource.url=jdbc:postgresql://localhost:5432/fete spring.datasource.username=fete spring.datasource.password=fete ``` **`application-local.properties.example`** — committed as template, never directly used. **Dockerfile:** ```dockerfile ENV SPRING_PROFILES_ACTIVE=prod ``` Key points: - No datasource defaults in `application.properties` — if neither profile nor env vars are set, the app fails to start (intentional: no silent fallback to a nonexistent DB) - Generic env var names (`DATABASE_URL`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`) — the container user never sees Spring property names - `ddl-auto=validate` ensures Hibernate validates entities against the Liquibase-managed schema but never modifies it - `open-in-view=false` prevents the anti-pattern of lazy-loading in views (also avoids Spring Boot's startup warning) - PostgreSQL JDBC driver (`org.postgresql:postgresql`) is needed — Spring Boot manages the version - Tests use `@ServiceConnection` (Testcontainers) which auto-configures the datasource — no profile or env vars needed for tests ### AC 3: All Runtime Configuration via Environment Variables **Current state:** No environment-variable-driven configuration exists beyond Spring Boot defaults. **What's needed beyond database:** - Unsplash API key: optional, used by US-16 - Max active events: optional, used by US-13 **Implementation pattern:** `@ConfigurationProperties(prefix = "fete")` class (`FeteProperties`) in `de.fete.config`. Type-safe, validatable, testable. These properties also go in `application-prod.properties` with generic env var mapping: ```properties fete.unsplash.api-key=${UNSPLASH_API_KEY:} fete.max-active-events=${MAX_ACTIVE_EVENTS:0} ``` Empty `UNSPLASH_API_KEY` = feature disabled. `MAX_ACTIVE_EVENTS=0` = unlimited. **Note:** These properties are only scaffolded in T-4 (the `FeteProperties` class with fields and defaults). The business logic using them comes with US-13/US-16. ### AC 4: SPA Router Configuration **Current state:** Vue Router IS configured and operational. **File:** `frontend/src/router/index.ts` - Uses `createWebHistory` (HTML5 History API — clean URLs, no hash) - Two routes defined: `/` (HomeView, eager) and `/about` (AboutView, lazy-loaded) - Router is registered in `main.ts` via `app.use(router)` **Backend SPA support:** Already implemented in `WebConfig.java`: - `PathResourceResolver` falls back to `index.html` for any path not matching a static file - This enables client-side routing — the backend serves `index.html` for all non-API, non-static paths **Assessment:** This AC is effectively already met. The router exists, uses history mode, and the backend supports it. What will change during user stories: routes will be added (e.g., `/event/:token`, `/event/:token/edit`), but the infrastructure is in place. ### AC 5: Backend Test Infrastructure **Current state:** Partially in place. **What exists:** - JUnit 5 (via `spring-boot-starter-test`) — operational - Spring Boot Test with `@SpringBootTest` — operational - MockMvc for REST endpoint testing — operational (`FeteApplicationTest.java`, `WebConfigTest.java`) - ArchUnit for architecture validation — operational (`HexagonalArchitectureTest.java`) - Surefire configured with fail-fast (`skipAfterFailureCount=1`) - Test logging configured (`logback-test.xml` at WARN level) **What's missing:** - **Testcontainers** — not in `pom.xml`, no test configuration for it - **Integration test support with real PostgreSQL** — currently no database tests exist (because no database exists yet) **What's needed:** - Add `org.testcontainers:postgresql` dependency (test scope) - Add `org.testcontainers:junit-jupiter` dependency (test scope) — JUnit 5 integration - Add `spring-boot-testcontainers` dependency (test scope) — Spring Boot 3.1+ Testcontainers integration - Create a test configuration class or use `@ServiceConnection` annotation (Spring Boot 3.1+) for automatic datasource wiring in tests **Spring Boot 3.5 + Testcontainers pattern (TestApplication):** A `TestFeteApplication.java` in `src/test/` registers Testcontainers beans. All `@SpringBootTest` tests automatically get a PostgreSQL instance — no per-test wiring needed. Existing tests (`FeteApplicationTest`, `WebConfigTest`) continue to work without modification. ```java // src/test/java/de/fete/TestFeteApplication.java @TestConfiguration(proxyBeanMethods = false) public class TestcontainersConfig { @Bean @ServiceConnection PostgreSQLContainer postgresContainer() { return new PostgreSQLContainer<>("postgres:17-alpine"); } } ``` With `@ServiceConnection`, Spring Boot auto-configures the datasource to point at the Testcontainers-managed PostgreSQL — no manual URL/username/password wiring needed. Testcontainers starts one shared container per test suite run, not per test class. **Important:** Once JPA is on the classpath, every `@SpringBootTest` needs a datasource. The TestApplication pattern ensures this globally. Without it, all existing `@SpringBootTest` tests would break immediately. **Test categories after T-4:** - Unit tests: no Spring context, no database — fast, test domain logic - Integration tests: `@SpringBootTest` + Testcontainers — test full stack including database - Architecture tests: ArchUnit — already in place ### AC 6: Frontend Test Infrastructure **Current state:** Already in place and operational. **What exists:** - Vitest configured (`vitest.config.ts`): jsdom environment, bail=1, e2e excluded - `@vue/test-utils` v2.4.6 installed — Vue component mounting and assertions - TypeScript test config (`tsconfig.vitest.json`) with jsdom types - Sample test exists: `components/__tests__/HelloWorld.spec.ts` — mounts component, asserts text - Test command: `npm run test:unit` (runs Vitest in watch mode) / `npm run test:unit -- --run` (single run) **Assessment:** This AC is already met. The test infrastructure is functional with a passing sample test. ### AC 7: Both Test Suites Executable **Current state:** Both work. - Backend: `cd backend && ./mvnw test` — runs JUnit 5 tests (3 tests in 3 classes) - Frontend: `cd frontend && npm run test:unit -- --run` — runs Vitest (1 test in 1 file) - CI pipeline (`ci.yaml`) already runs both in parallel **Assessment:** Already met. Will remain met after adding Testcontainers (new tests use the same `./mvnw test` command). ### AC 8: README Docker-Compose Documentation **Current state:** No docker-compose file or documentation exists. The README covers development setup and code quality but has no deployment section. **What's needed:** - A `docker-compose.yml` example (either in-repo or documented in README) - Must include: app service (the fete container) + postgres service - Must document required environment variables: `DATABASE_URL`, `DATABASE_USERNAME`, `DATABASE_PASSWORD` - Must document optional environment variables: `UNSPLASH_API_KEY`, `MAX_ACTIVE_EVENTS` - Per CLAUDE.md: "A docker-compose example in the README is sufficient" — no separate file in repo **Example structure:** ```yaml services: db: image: postgres:17-alpine environment: POSTGRES_DB: fete POSTGRES_USER: fete POSTGRES_PASSWORD: changeme volumes: - fete-db:/var/lib/postgresql/data app: image: gitea.example.com/user/fete:latest ports: - "8080:8080" environment: DATABASE_URL: jdbc:postgresql://db:5432/fete DATABASE_USERNAME: fete DATABASE_PASSWORD: changeme # MAX_ACTIVE_EVENTS: 100 # optional # UNSPLASH_API_KEY: abc123 # optional depends_on: db: condition: service_healthy volumes: fete-db: ``` ### AC 9: Container Health Check with PostgreSQL **Current state:** The Dockerfile has a HEALTHCHECK directive that queries `/actuator/health`. Currently the app starts without a database and the health check passes. **After T-4:** With JPA and Liquibase on the classpath, Spring Boot will: - Fail to start if no database is reachable (datasource auto-configuration fails) - Include database health in `/actuator/health` automatically (via `DataSourceHealthIndicator`) - Run Liquibase migrations on startup — if migrations fail, the app won't start **What's needed for verification:** - Start the app with docker-compose (app + postgres) - Verify `/actuator/health` returns `{"status":"UP"}` (which now includes DB connectivity) - Verify Liquibase ran the baseline migration (check `flyway_schema_history` table or app logs) ## Code References ### Existing Files (will be modified) - `backend/pom.xml:1-170` — Add JPA, Liquibase, PostgreSQL driver, Testcontainers dependencies - `backend/src/main/resources/application.properties:1-4` — Add datasource, JPA, Liquibase, app-specific config - `README.md:1-134` — Add deployment section with docker-compose example ### Existing Files (relevant context, likely untouched) - `backend/src/main/java/de/fete/config/WebConfig.java:1-40` — SPA routing already configured - `backend/src/main/java/de/fete/FeteApplication.java` — Entry point, no changes needed - `frontend/src/router/index.ts:1-23` — Router already configured - `frontend/vitest.config.ts:1-15` — Test infra already configured - `frontend/package.json:1-52` — Test dependencies already present - `.gitea/workflows/ci.yaml` — CI pipeline, may need Testcontainers Docker access for backend tests ### New Files (to be created) - `backend/src/main/resources/db/changelog/db.changelog-master.xml` — Liquibase master changelog - `backend/src/main/resources/application-prod.properties` — Production profile with env var placeholders - `backend/src/main/resources/application-local.properties.example` — Template for local development - `backend/src/test/java/de/fete/TestFeteApplication.java` (or similar) — Testcontainers PostgreSQL bean via TestApplication pattern - `de/fete/config/FeteProperties.java` — `@ConfigurationProperties` class for app-specific settings - README deployment section — docker-compose example inline (no standalone file) - `backend/src/main/resources/application-prod.properties` — Production profile with env var placeholders - `backend/src/main/resources/application-local.properties.example` — Template for local development ### Package Structure (existing, will gain content) - `de.fete.adapter.out.persistence` — JPA entities and Spring Data repositories (empty now) - `de.fete.domain.model` — Domain entities (empty now, no JPA annotations here) - `de.fete.config` — App configuration (WebConfig exists, may add `@ConfigurationProperties` class) ## Architecture Documentation ### Hexagonal Architecture and JPA The existing ArchUnit tests (`HexagonalArchitectureTest.java`) enforce: - Domain layer must not depend on Spring, adapters, application, or config - Ports (in/out) must be interfaces - Web adapter and persistence adapter must not cross-depend This means JPA integration must follow the pattern: 1. Domain entities in `domain.model` — plain Java, no JPA annotations 2. JPA entities in `adapter.out.persistence` — annotated with `@Entity`, `@Table`, etc. 3. Mapping between domain and JPA entities in the persistence adapter 4. Repository interfaces (Spring Data) in `adapter.out.persistence` 5. Port interfaces in `domain.port.out` — define what the domain needs from persistence 6. Service implementations in `application.service` — use port interfaces, not repositories directly This is a well-established hexagonal pattern. The ArchUnit tests will automatically validate any new code follows these boundaries. ### Test Architecture After T-4 ``` Test Type | Context | Database | Speed | Purpose -------------------|---------------|-----------------|---------|--------------------------- Unit tests | None | None | Fast | Domain logic, services Integration tests | SpringBoot | Testcontainers | Medium | Full stack, DB queries Architecture tests | None (static) | None | Fast | Structural validation ``` All test types run via `./mvnw test`. Testcontainers starts/stops PostgreSQL containers automatically — no external setup needed. The CI pipeline already has Docker available (runs on `ubuntu-latest` with Docker socket). ### CI Pipeline Considerations The current CI pipeline runs `./mvnw -B verify` for backend tests. Testcontainers requires Docker socket access. On Gitea Actions with `ubuntu-latest` runners, Docker is typically available. If the runner uses a Docker-in-Docker setup, the Testcontainers `DOCKER_HOST` environment variable may need configuration — but this is a runtime concern, not a code concern. ### Spring Boot Profiles Currently no profiles are configured. For T-4, a `test` profile may be useful to separate test-specific configuration (e.g., Testcontainers datasource) from production defaults. Spring Boot's `@ActiveProfiles("test")` on test classes or `application-test.properties` can handle this. However, with `@ServiceConnection`, Testcontainers auto-configures the datasource without profile-specific properties. ## Acceptance Criteria Status Matrix | # | Criterion | Current Status | Work Required | |---|-----------|----------------|---------------| | 1 | Liquibase configured with first changelog | Not started | Add `liquibase-core`, create changelog dir and master XML | | 2 | External PostgreSQL via env var | Not started | Add datasource properties with env var placeholders | | 3 | All runtime config via env vars | Not started | Add datasource + app-specific properties | | 4 | SPA router configured | **Done** | Vue Router with history mode already works | | 5 | Backend test infra (Testcontainers) | Partial | JUnit 5 + MockMvc exist; add Testcontainers | | 6 | Frontend test infra | **Done** | Vitest + @vue/test-utils operational | | 7 | Both test suites executable | **Done** | Both `./mvnw test` and `npm run test:unit` work | | 8 | README docker-compose documentation | Not started | Add deployment section with example | | 9 | Container health with PostgreSQL | Not started | Verify after JPA/Liquibase are added | ## Resolved Decisions 1. **Liquibase** for database migrations, **XML** format. DB-agnostic changelogs — Liquibase generates dialect-specific SQL at runtime. XML chosen over YAML for schema validation and explicitness. The project intentionally avoids PostgreSQL-specific features in migrations to keep the database layer portable. 2. **Profile-based properties separation** with generic environment variable names. Three files: `application.properties` (environment-independent, always active), `application-prod.properties` (committed, maps `${DATABASE_URL}` etc. to Spring properties, activated in Docker via `ENV SPRING_PROFILES_ACTIVE=prod`), `application-local.properties` (gitignored, concrete local values, activated via `-Dspring-boot.run.profiles=local`). A committed `.example` template guides developers. The container user sets `DATABASE_URL`, `DATABASE_USERNAME`, `DATABASE_PASSWORD` — never sees Spring internals. 3. **`@ConfigurationProperties`** for app-specific settings (`FeteProperties` class). Type-safe, validatable, testable. Properties: `fete.unsplash.api-key` (from `UNSPLASH_API_KEY`) and `fete.max-active-events` (from `MAX_ACTIVE_EVENTS`). Both are only scaffolded in T-4; business logic using them comes with US-13/US-16. 4. **docker-compose example in README only** — no standalone `docker-compose.yml` in the repo. Per CLAUDE.md: "A docker-compose example in the README is sufficient." A local docker-compose for development may be added later separately. 5. **TestApplication pattern** for Testcontainers integration. A `TestFeteApplication.java` in `src/test/` registers a `@ServiceConnection` PostgreSQL container. All `@SpringBootTest` tests automatically get a database — existing tests continue to work without modification. 6. **README erweitern** with local development setup documentation (how to copy `application-local.properties.example`, start with profile, PostgreSQL prerequisites). ## Open Questions 1. **Testcontainers in CI:** The Gitea Actions runner needs Docker available for Testcontainers. This works out-of-the-box on `ubuntu-latest` but should be verified after implementation. 2. **Baseline changelog content:** The first Liquibase changeset should be a minimal, empty changeset that proves the tooling works. No schema needed yet — US-1 will create the first real table.