--- date: 2026-03-04T20:09:31.044992+00:00 git_commit: cb0bcad145b03fec63be0ee3c1fca46ee545329e branch: master topic: "T-4: Development Infrastructure Setup" tags: [plan, database, liquibase, testcontainers, configuration, docker-compose] status: draft --- # T-4: Development Infrastructure Setup — Implementation Plan ## Overview Set up the remaining development infrastructure needed before the first user story (US-1) can be implemented with TDD. This adds JPA + Liquibase for database migrations, PostgreSQL connectivity via environment variables, Testcontainers for integration tests, app-specific configuration properties, and README deployment documentation with a docker-compose example. ## Current State Analysis **Already complete (no work needed):** - SPA router: Vue Router with `createWebHistory`, backend SPA fallback in `WebConfig.java` - Frontend test infrastructure: Vitest + `@vue/test-utils`, sample test passing - Both test suites executable: `./mvnw test` (3 tests) and `npm run test:unit` (1 test) **Missing (all work in this plan):** - JPA, Liquibase, PostgreSQL driver — no database dependencies in `pom.xml` - Testcontainers — not configured - Database connectivity — no datasource properties - App-specific config — no `@ConfigurationProperties` - Profile separation — no `application-prod.properties` - Deployment docs — no docker-compose in README ### Key Discoveries: - `backend/pom.xml:1-170` — Spring Boot 3.5.11, no DB dependencies - `backend/src/main/resources/application.properties:1-4` — Only app name + actuator - `HexagonalArchitectureTest.java:22` — `config` is already an adapter in ArchUnit rules - `FeteApplicationTest.java` — Uses `@SpringBootTest` + MockMvc; will need datasource after JPA is added - `Dockerfile:26` — No `SPRING_PROFILES_ACTIVE` set - `.gitignore:47-51` — `.env*` patterns exist but no `application-local.properties` ## Desired End State After this plan is complete: - `./mvnw test` runs all backend tests (including new Testcontainers-backed integration tests) against a real PostgreSQL without external setup - `./mvnw spring-boot:run -Dspring-boot.run.profiles=local` starts the app against a local PostgreSQL - Docker container starts with `DATABASE_URL`/`DATABASE_USERNAME`/`DATABASE_PASSWORD` env vars, runs Liquibase migrations, and responds to health checks - README contains a copy-paste-ready docker-compose example for deployment - `FeteProperties` scaffolds `fete.unsplash.api-key` and `fete.max-active-events` (no business logic yet) ### Verification: - `cd backend && ./mvnw verify` — all tests green, checkstyle + spotbugs pass - `cd frontend && npm run test:unit -- --run` — unchanged, still green - `docker build .` — succeeds - docker-compose (app + postgres) — container starts, `/actuator/health` returns `{"status":"UP"}` ## What We're NOT Doing - No JPA entities or repositories — those come with US-1 - No domain model classes — those come with US-1 - No business logic for `FeteProperties` (Unsplash, max events) — US-13/US-16 - No standalone `docker-compose.yml` file in repo — inline in README per CLAUDE.md - No `application-local.properties` committed — only the `.example` template - No changes to frontend code — AC 4/6/7 are already met --- ## Phase 1: JPA + Liquibase + PostgreSQL Dependencies ### Overview Add all database-related dependencies to `pom.xml`, create the Liquibase changelog structure with an empty baseline changeset, and update `application.properties` with JPA and Liquibase settings. ### Changes Required: #### [x] 1. Add database dependencies to `pom.xml` **File**: `backend/pom.xml` **Changes**: Add four dependencies after the existing `spring-boot-starter-validation` block. ```xml org.springframework.boot spring-boot-starter-data-jpa org.liquibase liquibase-core org.postgresql postgresql runtime ``` Add Testcontainers dependencies in test scope (after `archunit-junit5`): ```xml org.springframework.boot spring-boot-testcontainers test org.testcontainers postgresql test org.testcontainers junit-jupiter test ``` Spring Boot's dependency management handles versions for all of these — no explicit version tags needed (except `archunit-junit5` which is already versioned). #### [x] 2. Create Liquibase master changelog **File**: `backend/src/main/resources/db/changelog/db.changelog-master.xml` (new) **Changes**: Create the master changelog that includes individual changesets. ```xml ``` #### [x] 3. Create empty baseline changeset **File**: `backend/src/main/resources/db/changelog/000-baseline.xml` (new) **Changes**: Empty changeset that proves the tooling works. Liquibase creates its tracking tables (`databasechangelog`, `databasechangeloglock`) automatically. ```xml Baseline changeset — Liquibase tooling verification ``` #### [x] 4. Update application.properties with JPA and Liquibase settings **File**: `backend/src/main/resources/application.properties` **Changes**: Add JPA and Liquibase configuration (environment-independent, always active). ```properties spring.application.name=fete # JPA spring.jpa.hibernate.ddl-auto=validate spring.jpa.open-in-view=false # Liquibase spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml # Actuator management.endpoints.web.exposure.include=health management.endpoint.health.show-details=never ``` ### Success Criteria: #### Automated Verification: - [ ] `cd backend && ./mvnw compile` succeeds (dependencies resolve, checkstyle passes) - [ ] Changelog XML files are well-formed (Maven compile does not fail on resource processing) #### Manual Verification: - [ ] Verify `pom.xml` has all six new dependencies with correct scopes - [ ] Verify changelog directory structure: `db/changelog/db.changelog-master.xml` includes `000-baseline.xml` **Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase. --- ## Phase 2: Profile-Based Configuration and App Properties ### Overview Create the profile-based property files for production and local development, add the `FeteProperties` configuration class, update `.gitignore`, and set the production profile in the Dockerfile. ### Changes Required: #### [x] 1. Create production properties file **File**: `backend/src/main/resources/application-prod.properties` (new) **Changes**: Production profile with environment variable placeholders. Activated in Docker via `SPRING_PROFILES_ACTIVE=prod`. ```properties # Database (required) spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USERNAME} spring.datasource.password=${DATABASE_PASSWORD} # App-specific (optional) fete.unsplash.api-key=${UNSPLASH_API_KEY:} fete.max-active-events=${MAX_ACTIVE_EVENTS:0} ``` #### [x] 2. Create local development properties template **File**: `backend/src/main/resources/application-local.properties.example` (new) **Changes**: Template that developers copy to `application-local.properties` (which is gitignored). ```properties # Local development database # Copy this file to application-local.properties and adjust as needed. # Start with: ./mvnw spring-boot:run -Dspring-boot.run.profiles=local spring.datasource.url=jdbc:postgresql://localhost:5432/fete spring.datasource.username=fete spring.datasource.password=fete ``` #### [x] 3. Add `application-local.properties` to `.gitignore` **File**: `.gitignore` **Changes**: Add the gitignore entry for the local properties file (under the Environment section). ``` # Spring Boot local profile (developer-specific, not committed) backend/src/main/resources/application-local.properties ``` #### ~~4. Create `FeteProperties` configuration properties class~~ (deferred) **File**: `backend/src/main/java/de/fete/config/FeteProperties.java` (new) **Changes**: Type-safe configuration for app-specific settings. Both properties are only scaffolded — business logic comes with US-13/US-16. ```java package de.fete.config; import org.springframework.boot.context.properties.ConfigurationProperties; /** * Application-specific configuration properties. * *

Mapped from {@code fete.*} properties. Both properties are optional: *

*/ @ConfigurationProperties(prefix = "fete") public class FeteProperties { private final Unsplash unsplash; private final int maxActiveEvents; /** Creates FeteProperties with the given values. */ public FeteProperties(Unsplash unsplash, int maxActiveEvents) { this.unsplash = unsplash != null ? unsplash : new Unsplash(""); this.maxActiveEvents = maxActiveEvents; } /** Returns the Unsplash configuration. */ public Unsplash getUnsplash() { return unsplash; } /** Returns the maximum number of active events (0 = unlimited). */ public int getMaxActiveEvents() { return maxActiveEvents; } /** Unsplash-related configuration. */ public record Unsplash(String apiKey) { /** Creates Unsplash config with the given API key. */ public Unsplash { if (apiKey == null) { apiKey = ""; } } /** Returns true if an API key is configured. */ public boolean isEnabled() { return !apiKey.isBlank(); } } } ``` #### ~~5. Create `FetePropertiesConfig` configuration class~~ (deferred) **File**: `backend/src/main/java/de/fete/config/FetePropertiesConfig.java` (new) **Changes**: Separate `@Configuration` that enables `FeteProperties`. ```java package de.fete.config; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; /** Activates {@link FeteProperties} binding. */ @Configuration @EnableConfigurationProperties(FeteProperties.class) public class FetePropertiesConfig { } ``` #### [x] 6. Set production profile in Dockerfile **File**: `Dockerfile` **Changes**: Add `ENV SPRING_PROFILES_ACTIVE=prod` in the runtime stage, before `ENTRYPOINT`. ```dockerfile # Stage 3: Runtime FROM eclipse-temurin:25-jre-alpine WORKDIR /app COPY --from=backend-build /app/backend/target/*.jar app.jar EXPOSE 8080 ENV SPRING_PROFILES_ACTIVE=prod HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD wget -qO- http://localhost:8080/actuator/health || exit 1 ENTRYPOINT ["java", "-jar", "app.jar"] ``` ### Success Criteria: #### Automated Verification: - [ ] `cd backend && ./mvnw compile` succeeds (FeteProperties compiles, checkstyle passes) - [ ] `docker build .` succeeds #### Manual Verification: - [ ] `application-prod.properties` contains all five env-var placeholders - [ ] `application-local.properties.example` is committed; `application-local.properties` is gitignored - [ ] `FeteProperties` fields: `unsplash.apiKey` (String), `maxActiveEvents` (int) - [ ] Dockerfile has `ENV SPRING_PROFILES_ACTIVE=prod` before `ENTRYPOINT` **Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase. --- ## Phase 3: Testcontainers Integration ### Overview Set up the TestApplication pattern so that all `@SpringBootTest` tests automatically get a Testcontainers-managed PostgreSQL instance. This is critical: once JPA is on the classpath, every `@SpringBootTest` needs a datasource. Without this, all three existing `@SpringBootTest` tests break. ### Changes Required: #### [x] 1. Create Testcontainers configuration **File**: `backend/src/test/java/de/fete/TestcontainersConfig.java` (new) **Changes**: Registers a PostgreSQL Testcontainer with `@ServiceConnection` for automatic datasource wiring. ```java package de.fete; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Bean; import org.testcontainers.containers.PostgreSQLContainer; /** Provides a Testcontainers PostgreSQL instance for integration tests. */ @TestConfiguration(proxyBeanMethods = false) public class TestcontainersConfig { @Bean @ServiceConnection PostgreSQLContainer postgresContainer() { return new PostgreSQLContainer<>("postgres:17-alpine"); } } ``` #### [x] 2. Create TestFeteApplication for `spring-boot:test-run` **File**: `backend/src/test/java/de/fete/TestFeteApplication.java` (new) **Changes**: Entry point that imports `TestcontainersConfig`. Enables `./mvnw spring-boot:test-run` for local development with Testcontainers (no external PostgreSQL needed). ```java package de.fete; import org.springframework.boot.SpringApplication; /** Test entry point — starts the app with Testcontainers PostgreSQL. */ public class TestFeteApplication { public static void main(String[] args) { SpringApplication.from(FeteApplication::main) .with(TestcontainersConfig.class) .run(args); } } ``` #### [x] 3. Import TestcontainersConfig in existing `@SpringBootTest` tests **File**: `backend/src/test/java/de/fete/FeteApplicationTest.java` **Changes**: Add `@Import(TestcontainersConfig.class)` so the test gets a datasource. ```java @SpringBootTest @AutoConfigureMockMvc @Import(TestcontainersConfig.class) class FeteApplicationTest { // ... existing tests unchanged } ``` **File**: `backend/src/test/java/de/fete/config/WebConfigTest.java` **Changes**: Same — add `@Import(TestcontainersConfig.class)`. Note: `HexagonalArchitectureTest` uses `@AnalyzeClasses` (ArchUnit), not `@SpringBootTest` — it needs no changes. #### [x] 4. Add SpotBugs exclusion for Testcontainers resource management **File**: `backend/spotbugs-exclude.xml` **Changes**: Testcontainers `PostgreSQLContainer` bean intentionally has container lifecycle managed by Spring, not try-with-resources. SpotBugs may flag this. Add exclusion if needed — check after running `./mvnw verify`. ### Success Criteria: #### Automated Verification: - [ ] `cd backend && ./mvnw test` — all existing tests pass (context loads, health endpoint, ArchUnit) - [ ] `cd backend && ./mvnw verify` — full verify including SpotBugs passes - [ ] Testcontainers starts a PostgreSQL container during test execution (visible in test output) - [ ] Liquibase baseline migration runs against Testcontainers PostgreSQL #### Manual Verification: - [ ] `./mvnw spring-boot:test-run` starts the app with a Testcontainers PostgreSQL (for local dev without external DB) **Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase. --- ## Phase 4: README Deployment Documentation ### Overview Add a deployment section to the README with a docker-compose example, environment variable documentation, and local development setup instructions. ### Changes Required: #### [x] 1. Add deployment section to README **File**: `README.md` **Changes**: Add a `## Deployment` section after the existing `## Code quality` section and before `## License`. Contains the docker-compose example, environment variable table, and notes. ```markdown ## Deployment ### Docker Compose The app ships as a single Docker image. It requires an external PostgreSQL database. ```yaml services: db: image: postgres:17-alpine environment: POSTGRES_DB: fete POSTGRES_USER: fete POSTGRES_PASSWORD: changeme volumes: - fete-db:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U fete"] interval: 5s timeout: 3s retries: 5 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 # UNSPLASH_API_KEY: your-key-here depends_on: db: condition: service_healthy volumes: fete-db: ``` ### Environment variables | Variable | Required | Default | Description | |----------------------|----------|-----------|------------------------------------------------| | `DATABASE_URL` | Yes | — | JDBC connection string for PostgreSQL | | `DATABASE_USERNAME` | Yes | — | Database username | | `DATABASE_PASSWORD` | Yes | — | Database password | | `MAX_ACTIVE_EVENTS` | No | Unlimited | Maximum number of simultaneously active events | | `UNSPLASH_API_KEY` | No | — | Unsplash API key for header image search | ``` #### [x] 2. Add local development setup section to README **File**: `README.md` **Changes**: Extend the `## Development` section with database setup instructions. ```markdown ### Local database setup **Option A: Testcontainers (no external PostgreSQL needed)** ```bash cd backend && ./mvnw spring-boot:test-run ``` This starts the app with a Testcontainers-managed PostgreSQL that is created and destroyed automatically. **Option B: External PostgreSQL** ```bash cd backend cp src/main/resources/application-local.properties.example \ src/main/resources/application-local.properties # Edit application-local.properties if your PostgreSQL uses different credentials ./mvnw spring-boot:run -Dspring-boot.run.profiles=local ``` ``` ### Success Criteria: #### Automated Verification: - [ ] `cd frontend && npm run test:unit -- --run` — frontend tests still pass (no regression) #### Manual Verification: - [ ] README docker-compose example is syntactically correct YAML - [ ] Environment variable table lists all five variables with correct Required/Default values - [ ] Local development section documents both Testcontainers and external PostgreSQL options - [ ] docker-compose startup: `docker compose up` starts app + postgres, `/actuator/health` returns `{"status":"UP"}` **Implementation Note**: After completing this phase, all T-4 acceptance criteria should be met. Run the full verification checklist below. --- ## Testing Strategy ### Unit Tests: - `FeteProperties` — verify defaults (empty API key = disabled, maxActiveEvents=0 = unlimited) - No other new unit tests in T-4 — the infrastructure is verified by integration tests ### Integration Tests: - Existing `FeteApplicationTest.contextLoads()` — validates that Spring context starts with JPA + Liquibase + Testcontainers - Existing `FeteApplicationTest.healthEndpointReturns200()` — validates health check includes DB health - Existing `WebConfigTest` — validates SPA routing still works with JPA on classpath - ArchUnit rules — validate `FeteProperties`/`FetePropertiesConfig` in `config` adapter is properly isolated ### Manual Testing Steps: 1. `cd backend && ./mvnw verify` — full backend pipeline green 2. `cd frontend && npm run test:unit -- --run` — frontend unchanged 3. `docker build .` — image builds successfully 4. docker-compose (app + postgres) — start, wait for health, verify `/actuator/health` returns UP ## Performance Considerations - Testcontainers PostgreSQL startup adds ~3-5 seconds to backend test execution. This is acceptable for integration tests. - Testcontainers reuses the container across all `@SpringBootTest` classes in a single Maven run (Spring's test context caching). - The empty baseline changeset adds negligible startup time. ## Migration Notes - **Existing tests**: `FeteApplicationTest` and `WebConfigTest` need `@Import(TestcontainersConfig.class)` — without it, they fail because JPA requires a datasource. - **CI pipeline**: `./mvnw -B verify` now requires Docker for Testcontainers. Gitea Actions `ubuntu-latest` runners have Docker available. If the runner uses Docker-in-Docker, `DOCKER_HOST` may need configuration — verify after implementation. - **Local development**: Developers now need either Docker (for Testcontainers via `./mvnw spring-boot:test-run`) or a local PostgreSQL (with `application-local.properties`). ## References - Research: `docs/agents/research/2026-03-04-t4-development-infrastructure.md` - T-4 spec: `spec/setup-tasks.md` (lines 79-98) - Spring Boot Testcontainers: `@ServiceConnection` pattern (Spring Boot 3.1+) - Liquibase Spring Boot integration: auto-configured when `liquibase-core` is on classpath