- 01-02: wave 1→2, depends_on [01]; drop unused androidLibrary classpath entry; guard Kotlin compilerOptions with plugins.withId listeners - 01-05: remove misleading 'gradle exit' echo from verify block - 01-06: harden credential check on docker-compose.yml alone - 01-07: drop hardcoded /Users/rwilk/dev/repo/recipe cd prefix - 01-RESEARCH: rename Open Questions → (RESOLVED); replace 'Recommendation:' with 'RESOLVED:' per gsd Dimension 11 convention
309 lines
16 KiB
Markdown
309 lines
16 KiB
Markdown
---
|
|
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>
|