---
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.
