Files
fete/specs/004-dev-infrastructure/plan.md
nitrix 6aeb4b8bca Migrate project artifacts to spec-kit format
- Move cross-cutting docs (personas, design system, implementation phases,
  Ideen.md) to .specify/memory/
- Move cross-cutting research and plans to .specify/memory/research/ and
  .specify/memory/plans/
- Extract 5 setup tasks from spec/setup-tasks.md into individual
  specs/001-005/spec.md files with spec-kit template format
- Extract 20 user stories from spec/userstories.md into individual
  specs/006-026/spec.md files with spec-kit template format
- Relocate feature-specific research and plan docs into specs/[feature]/
- Add spec-kit constitution, templates, scripts, and slash commands
- Slim down CLAUDE.md to Claude-Code-specific config, delegate principles
  to .specify/memory/constitution.md
- Update ralph.sh with stream-json output and per-iteration logging
- Delete old spec/ and docs/agents/ directories
- Gitignore Ralph iteration JSONL logs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:19:41 +01:00

22 KiB

date, git_commit, branch, topic, tags, status
date git_commit branch topic tags status
2026-03-04T20:09:31.044992+00:00 cb0bcad145 master T-4: Development Infrastructure Setup
plan
database
liquibase
testcontainers
configuration
docker-compose
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:22config 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.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
</dependency>

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

Add Testcontainers dependencies in test scope (after archunit-junit5):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

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 version="1.0" encoding="UTF-8"?>
<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
        http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <include file="db/changelog/000-baseline.xml"/>

</databaseChangeLog>

[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 version="1.0" encoding="UTF-8"?>
<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
        http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <!-- T-4: Baseline changeset. Proves Liquibase tooling works.
         First real schema comes with US-1. -->
    <changeSet id="000-baseline" author="fete">
        <comment>Baseline changeset — Liquibase tooling verification</comment>
    </changeSet>

</databaseChangeLog>

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

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.

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

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

package de.fete.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * Application-specific configuration properties.
 *
 * <p>Mapped from {@code fete.*} properties. Both properties are optional:
 * <ul>
 *   <li>{@code fete.unsplash.api-key} — Unsplash API key (empty = feature disabled)
 *   <li>{@code fete.max-active-events} — Maximum active events (0 = unlimited)
 * </ul>
 */
@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.

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.

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

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

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.

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

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

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