Wire project infrastructure

This commit is contained in:
2026-04-24 15:27:17 +02:00
parent 68e4a5637a
commit 015d8d51d0
66 changed files with 7276 additions and 211 deletions

View File

@@ -0,0 +1,308 @@
---
phase: 01-project-infrastructure-module-wiring
plan: 06
type: execute
wave: 2
depends_on: []
files_modified:
- docker-compose.yml
- README.md
autonomous: true
requirements: [INFRA-02]
requirements_addressed: [INFRA-02]
must_haves:
truths:
- "docker-compose.yml at repo root launches postgres:16 with POSTGRES_DB=recipe / POSTGRES_USER=recipe / POSTGRES_PASSWORD=recipe — matching application.conf defaults exactly (D-17)"
- "The postgres service has a named volume (recipe-pgdata) so data survives container restarts"
- "The postgres service has a healthcheck using pg_isready that lets `docker compose up --wait` block until ready"
- "README.md has a 'Local development' section documenting the full dev loop (docker compose up, gradlew server:run, curl /health, gradlew spotlessApply)"
- "README.md no longer documents the dropped js target (D-01); wasmJs section is preserved"
artifacts:
- path: "docker-compose.yml"
provides: "postgres:16 service on port 5432 with named volume and healthcheck"
contains: "image: postgres:16", "POSTGRES_DB: recipe", "recipe-pgdata"
- path: "README.md"
provides: "Updated dev docs with Local development section, no js target docs"
contains: "Local development", "docker compose up -d postgres"
key_links:
- from: "docker-compose.yml"
to: "server/src/main/resources/application.conf"
via: "POSTGRES_DB=recipe / POSTGRES_USER=recipe / POSTGRES_PASSWORD=recipe defaults match HOCON localhost URL"
pattern: "POSTGRES_(DB|USER|PASSWORD):\\s*recipe"
- from: "README.md Local development section"
to: "server/src/main/kotlin/dev/ulfrx/recipe/Application.kt"
via: "curl http://localhost:8080/health"
pattern: "curl .+ /health"
---
<objective>
Deliver the local developer ergonomics promised by D-17: a `docker-compose.yml` at the repo root running `postgres:16` with credentials + volume + healthcheck that align exactly with Plan 05's `application.conf` HOCON defaults, plus a "Local development" section in `README.md` documenting the dev loop. Drop the legacy `js` target documentation from `README.md` (D-01).
Purpose: Phase 3 (Households / DB migrations) and Phase 11 (homelab deploy) both assume a working local Postgres is one command away. This plan closes that gap so `docker compose up -d postgres && ./gradlew :server:run` is a two-command dev loop. Authentik is NOT in this compose file — it lives on the user's homelab (CONTEXT.md D-17).
Output: 1 new YAML file, 1 README edit. Entirely independent of Plans 01-05 in terms of files_modified — runs safely in parallel.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md
@.planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md
@.planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md
@.planning/phases/01-project-infrastructure-module-wiring/01-VALIDATION.md
@README.md
@CLAUDE.md
<interfaces>
<!-- Plan 05's application.conf expects these exact defaults -->
From server/src/main/resources/application.conf (Plan 05 created — value match required):
```hocon
database {
url = "jdbc:postgresql://localhost:5432/recipe"
url = ${?DATABASE_URL}
user = "recipe"
user = ${?DATABASE_USER}
password = "recipe"
password = ${?DATABASE_PASSWORD}
}
```
So docker-compose.yml MUST use:
- `POSTGRES_DB: recipe` (matches `/recipe` in jdbc URL path)
- `POSTGRES_USER: recipe`
- `POSTGRES_PASSWORD: recipe`
- port `5432:5432` (matches URL port)
From README.md current content:
- Section "Build and Run Web Application" (lines 63-85) documents BOTH `wasmJsBrowserDevelopmentRun` AND `jsBrowserDevelopmentRun` — the `js` part must go per D-01.
- "Build and Run Android/Desktop/Server/iOS" sections are fine and stay.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create docker-compose.yml at repo root</name>
<files>docker-compose.yml</files>
<read_first>
- .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 1055-1077 (canonical docker-compose.yml)
- .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 1128-1158 (docker-compose pattern — matched defaults)
- .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md D-17 (scope: postgres:16 + named volume; Authentik stays on homelab)
- (If Plan 05 is complete) server/src/main/resources/application.conf — verify credentials match
</read_first>
<action>
Create `docker-compose.yml` at the repo root with the following exact content:
```yaml
services:
postgres:
image: postgres:16
container_name: recipe-postgres
environment:
POSTGRES_DB: recipe
POSTGRES_USER: recipe
POSTGRES_PASSWORD: recipe
ports:
- "5432:5432"
volumes:
- recipe-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U recipe -d recipe"]
interval: 5s
timeout: 5s
retries: 5
volumes:
recipe-pgdata:
```
CRITICAL:
- `image: postgres:16` — pinned major version (D-17 specifies `postgres:16`).
- `POSTGRES_DB: recipe`, `POSTGRES_USER: recipe`, `POSTGRES_PASSWORD: recipe` — all MUST equal `"recipe"` (matches `application.conf` HOCON defaults from Plan 05 — `jdbc:postgresql://localhost:5432/recipe`, user `recipe`, password `recipe`).
- Named volume `recipe-pgdata` — survives container restart. Drop with `docker compose down -v` if you need a fresh DB.
- Healthcheck uses `pg_isready -U recipe -d recipe` so `docker compose up --wait postgres` or `depends_on: { postgres: { condition: service_healthy } }` works (Phase 3+ may add this).
- Port `5432:5432` — binds host port 5432 to container port 5432. Document in README that this is dev-local only.
- Do NOT add any other service (no Authentik — lives on user's homelab per D-17; no server — Ktor runs via Gradle on host for dev iteration).
- No `.env` file — D-17 / PATTERNS.md "Recommendation on `.env` vs inline": inline is fine for single-dev + matching application.conf defaults.
The file has NO leading version key (`version: "3"` etc. is legacy Docker Compose syntax — unnecessary in modern `docker compose v2`, and omitting it avoids a warning).
</action>
<verify>
<automated>test -f docker-compose.yml && grep -q 'image: postgres:16' docker-compose.yml && grep -q 'POSTGRES_DB: recipe' docker-compose.yml && grep -q 'POSTGRES_USER: recipe' docker-compose.yml && grep -q 'POSTGRES_PASSWORD: recipe' docker-compose.yml && grep -q 'recipe-pgdata:/var/lib/postgresql/data' docker-compose.yml && grep -q '"5432:5432"' docker-compose.yml && grep -q 'pg_isready -U recipe -d recipe' docker-compose.yml && grep -q '^volumes:$' docker-compose.yml && grep -q ' recipe-pgdata:' docker-compose.yml</automated>
</verify>
<acceptance_criteria>
- `docker-compose.yml` exists at repo root (`test -f docker-compose.yml`)
- `docker-compose.yml` contains `image: postgres:16` (not `postgres:latest`, not `postgres:15`, not `postgres`)
- `docker-compose.yml` contains `container_name: recipe-postgres`
- `docker-compose.yml` has `POSTGRES_DB: recipe`, `POSTGRES_USER: recipe`, `POSTGRES_PASSWORD: recipe` — all exactly `recipe` (lowercase, no variation)
- `docker-compose.yml` has port mapping `"5432:5432"`
- `docker-compose.yml` declares volume `recipe-pgdata` in both the service `volumes:` section AND the top-level `volumes:` section
- `docker-compose.yml` has a `healthcheck:` block using `pg_isready -U recipe -d recipe`
- `docker-compose.yml` does NOT contain a `version:` key (modern compose v2)
- `docker-compose.yml` does NOT define any service other than `postgres` (D-17: Authentik stays on homelab)
- `docker-compose.yml` credentials are the exact literals that Plan 05 hardcodes in `application.conf`: `grep -c '^\s*POSTGRES_\(DB\|USER\|PASSWORD\): recipe$' docker-compose.yml` returns `3` (DB, USER, PASSWORD all equal `recipe`). This is enforced on docker-compose.yml alone — the shared hardcoded contract (`recipe/recipe/recipe`) is stated identically in both plans' interfaces, so no cross-file lookup is required.
</acceptance_criteria>
<done>docker-compose.yml ships postgres:16 matching application.conf defaults; single-service compose file.</done>
</task>
<task type="auto">
<name>Task 2: Add "Local development" section to README.md and drop js target docs</name>
<files>README.md</files>
<read_first>
- README.md (current 100-line content — target of edit)
- .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 1161-1169 (README delta summary)
- .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md D-01 (drop js target), D-17 (docker-compose dev ergonomics)
</read_first>
<action>
Two edits to `README.md`:
**Edit A: Drop the `js` target section** — delete lines 77-85 of the current README (the "- for the JS target (slower, supports older browsers): - on macOS/Linux ... `./gradlew :composeApp:jsBrowserDevelopmentRun` - on Windows ..." block). Keep lines 68-76 (the wasmJs block). The entire "Build and Run Web Application" subsection should retain ONLY the wasmJs paragraph.
Resulting "Build and Run Web Application" subsection:
```markdown
### Build and Run Web Application
To build and run the development version of the web app, use the run configuration from the run widget
in your IDE's toolbar or run it directly from the terminal:
- for the Wasm target (faster, modern browsers):
- on macOS/Linux
```shell
./gradlew :composeApp:wasmJsBrowserDevelopmentRun
```
- on Windows
```shell
.\gradlew.bat :composeApp:wasmJsBrowserDevelopmentRun
```
```
**Edit B: Insert a new "Local development" section** AFTER the "Build and Run iOS Application" subsection and BEFORE the trailing `---` horizontal rule (around line 92 in the current file). The new section:
```markdown
### Local development
The server requires Postgres. A `docker-compose.yml` at the repo root ships a local Postgres
instance whose credentials match `application.conf` defaults (`recipe`/`recipe`/`recipe`).
Boot the database and server:
```shell
docker compose up -d postgres
./gradlew :server:run
```
Verify the server is up:
```shell
curl http://localhost:8080/health
# expected: {"status":"ok"}
```
Environment overrides (optional — set any of these to override `application.conf` defaults):
- `DATABASE_URL` — JDBC URL (default `jdbc:postgresql://localhost:5432/recipe`)
- `DATABASE_USER` — DB user (default `recipe`)
- `DATABASE_PASSWORD` — DB password (default `recipe`)
- `PORT` — Ktor port (default `8080`)
Before committing, format all Kotlin + Gradle + Markdown files:
```shell
./gradlew spotlessApply
```
The full check (Spotless + all tests across all targets):
```shell
./gradlew check
```
Reset the local database (destroys the `recipe-pgdata` volume):
```shell
docker compose down -v
```
```
Do NOT modify:
- The top-level introduction (lines 1-20)
- The "Build and Run Android Application" section
- The "Build and Run Desktop (JVM) Application" section
- The "Build and Run Server" section
- The "Build and Run iOS Application" section
- The trailing `---` + the learn-more links + the Compose/Wasm feedback paragraph
Keep the existing markdown heading level (`###`) for the new "Local development" section — matches the surrounding siblings.
</action>
<verify>
<automated>grep -q 'Local development' README.md && grep -q 'docker compose up -d postgres' README.md && grep -q 'curl http://localhost:8080/health' README.md && grep -q 'DATABASE_URL' README.md && grep -q 'gradlew spotlessApply' README.md && grep -q 'docker compose down -v' README.md && ! grep -q 'jsBrowserDevelopmentRun' README.md && grep -q 'wasmJsBrowserDevelopmentRun' README.md</automated>
</verify>
<acceptance_criteria>
- `README.md` contains the string `Local development` exactly once (new section heading)
- `README.md` contains `docker compose up -d postgres` as a documented command
- `README.md` contains `curl http://localhost:8080/health` as a documented command
- `README.md` lists all 4 env-var overrides: `DATABASE_URL`, `DATABASE_USER`, `DATABASE_PASSWORD`, `PORT`
- `README.md` contains `gradlew spotlessApply` (pre-commit formatter hint per D-10)
- `README.md` contains `gradlew check` (full-suite command)
- `README.md` contains `docker compose down -v` (volume reset hint)
- `README.md` does NOT contain `jsBrowserDevelopmentRun` (D-01 — js target dropped)
- `README.md` STILL contains `wasmJsBrowserDevelopmentRun` (wasmJs kept per D-01)
- All existing section headings ("Build and Run Android Application", "Build and Run Desktop (JVM) Application", "Build and Run Server", "Build and Run iOS Application") are preserved (unchanged)
- Top-of-file introduction (lines 1-20) is unchanged
</acceptance_criteria>
<done>README.md documents the dev loop (docker + gradle + curl + spotless + reset); legacy js target docs removed.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Developer host → localhost:5432 Postgres | Dev-local; `docker-compose.yml` binds port on loopback via host mapping. Non-localhost access requires the developer's host to be reachable from outside the machine AND port 5432 firewall-open — normally not the case on a laptop. |
| `docker-compose.yml` (committed to git) → POSTGRES_PASSWORD=recipe | Password is literal `recipe` — non-secret by design. Real homelab creds never land in this file; homelab has its own compose file or `.env` per Phase 11. |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-01-06-01 | Information Disclosure | Postgres port 5432 exposed on `0.0.0.0` | mitigate | Host-firewall is the developer's responsibility; the literal `"5432:5432"` mapping is Docker-default (binds to all host interfaces unless the host Docker is configured otherwise). README Local development section mentions "dev-local" usage but does NOT open a CVE window — this is standard dev practice. Phase 11 (homelab) uses a different compose file that does NOT expose the port publicly. |
| T-01-06-02 | Information Disclosure | Committing real secrets to `docker-compose.yml` | mitigate | Only the literal `recipe/recipe/recipe` triple is in the file. Real homelab Postgres creds stay out of this compose file (Phase 11 will add a separate file or switch to env-var-driven compose). |
| T-01-06-03 | Tampering | `docker compose down -v` accidentally destroying valuable data | accept | Dev-only volume (`recipe-pgdata`). If Phase 3+ develops real seed data, a developer running `down -v` repopulates from migrations — zero-trust default. |
| T-01-06-04 | Denial of Service | `postgres:16` image unavailable from Docker Hub | accept | `docker pull postgres:16` is a standard image; outage would be transient and outside our control. Pinning to major version (not `:latest`) limits drift. |
</threat_model>
<verification>
Phase-level verification for this plan:
- Task 1 + Task 2 `<automated>` blocks pass.
- `tools/verify-no-version-literals.sh` continues to exit 0 (no `.gradle.kts` files modified in this plan).
- No `./gradlew` invocations — docker-compose + README are pure dev-ergonomics.
Manual sanity check (optional, NOT blocking):
- `docker compose config` parses the YAML without warnings.
- `docker compose up -d postgres && sleep 3 && docker exec recipe-postgres pg_isready -U recipe -d recipe` returns "accepting connections".
- `docker compose down` — cleans up afterward.
</verification>
<success_criteria>
- `docker-compose.yml` exists at repo root with a single `postgres:16` service + named volume + healthcheck
- Credentials in `docker-compose.yml` match `application.conf` defaults exactly (`recipe/recipe/recipe`)
- `README.md` has a new "Local development" section
- `README.md` no longer documents the `js` target
- `README.md` still documents `wasmJs` target
</success_criteria>
<output>
After completion, create `.planning/phases/01-project-infrastructure-module-wiring/01-06-SUMMARY.md` recording: docker-compose content summary (one service, one volume), credential match with Plan 05, README sections added/removed, and any deviation from D-17 (expected: none).
</output>