# PLAN-M2c-dev: Infra for development (D-001) ## 1. Goal Give development a local server on the owner's M4 Mac: a Docker compose stack with the `server` (built from `server/Dockerfile`), optional Caddy, and an on-demand backup job, reachable by debug builds on the owner's phone over the home Wi-Fi. Set up the Firebase project and apps now, because push is needed during development. ## 2. Read first 1. SPEC §10 Deployment & ops: the intro paragraph "Development first (D-001)", §10.2 Compose stack (service shapes, limits, logging), §10.3 Images, registry and build (the `server` and `backup` images), §10.4 Server configuration and CLI, §10.7 Firebase setup, §10.9 Backups and restore (backup steps 1 to 5). 2. SPEC §4 AC-2.7 and AC-2.8 (debug builds accept `http://` to private LAN addresses), FR-20 Health. 3. SPEC §8.7 Privacy and logging, §8.9 Server hardening (container user, read-only root, pragmas). 4. SPEC §11.2 (row M2c-dev), §11.3 (note M2c-dev / M2c-pi), §12 (OQ-1 answered, OQ-3 open for M2c-pi only), §14.7. 5. `docs/decisions.md` D-001, D-002. 6. Files: `server/Dockerfile` (from M2a, if landed), `.gitignore`, `CLAUDE.md` (BUILD AND VERIFY: Cargo and Docker builds; EXECUTION PROTOCOLS: Stay inside the repository, Stop and ask; THE OWNER). ## 3. Already established - During development the server runs on the Mac, via `infra/dev/compose.yaml` or plain `cargo run`; debug builds pair against it (D-001, §10 intro). - The dev stack has `server`, an optional `caddy` with a self-signed certificate or no TLS for local use, and `backup` runnable on demand without the Drive upload (D-001, §10 intro, §11.2). - `backup.sh` steps: `.backup` snapshot, `PRAGMA integrity_check` must print `ok`, gzip and pictures tar with a UTC stamp, rclone copy only, `last-success` marker; when `RCLONE_FOLDER_ID` is empty step 4 is skipped with a log line and local files are kept (§10.9, D-001). On the Pi, local files are deleted as soon as confirmed on Drive (D-002; built by M2c-pi). - `backup` image: `alpine` with `sqlite`, `gzip`, `tar`, `rclone`, `infra/backup/backup.sh` run by `crond` (§10.3). - Server config is only the env variables of §10.4; `KVITT_FCM_CREDENTIALS` default `/secrets/fcm-service-account.json`, push disabled with a warning when missing; `KVITT_FCM_PROJECT_ID` `kvitt-1337` (§10.4). - Firebase: project `kvitt-1337`; Android apps `se.hagstedt.kvitt` ("Kvitt") and `se.hagstedt.kvitt.debug` ("Kvitt debug"); one merged `android/app/google-services.json` (git-ignored); service account `kvitt-fcm` with `roles/firebasecloudmessaging.admin`, key at `infra/secrets/fcm-service-account.json`; stop if an organisation policy blocks key creation; free tier, no billing (§10.7). - The registry is Gitea at `http://gitea.lan`; not used by this plan (D-001, OQ-1). - Secrets live in `infra/secrets/` (git-ignored) and are never committed (§10 intro, `.gitignore`). - Never build Docker images while a Gradle build runs (CLAUDE.md). ## 4. Owned paths From SPEC §14.7, verbatim: `infra/**` (owner "M2c-dev, then M2c-pi (D-001)"; after landing: "M10 updates runbooks"). Also, from §11.2 (row M2c-dev): the git-ignored `android/app/google-services.json` and `infra/secrets/` contents, written in the main checkout `/Users/jhag/projects/kvitt` (where every worktree copies them from, CLAUDE.md), only with the start prompt's allowance. Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator. ## 5. Deliverables - `infra/dev/compose.yaml`: `server` (image built from `server/Dockerfile`, data in a named volume, FCM key mounted read-only from `infra/secrets/`, `KVITT_PUBLIC_URL` set to the Mac's LAN URL, user 10001, read-only root, no capabilities, `no-new-privileges`), optional `caddy`, `backup` (on demand), logging and `TZ=Europe/Stockholm` as §10.2. - `infra/dev/Caddyfile`: only if the stack includes Caddy (self-signed via `tls internal`, or plain HTTP). - `infra/dev/README.md`: start, stop, logs, `bootstrap` and `pair-device` commands, finding the Mac's LAN address, pairing a debug build over the home Wi-Fi, running a backup, resetting the dev data. - `infra/backup/Dockerfile`, `infra/backup/backup.sh`: the backup image and script of §10.9 with the empty-`RCLONE_FOLDER_ID` local mode. - `infra/env.example`: the `.env` keys of §10.2 with dev-friendly comments (no secret values). - Firebase project `kvitt-1337` with both Android apps (cloud state, no repository file). - `infra/secrets/fcm-service-account.json` (git-ignored, main checkout). - `android/app/google-services.json` with both clients (git-ignored, main checkout). ## 6. Steps 1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m2c-dev -b agent-m2c-dev main`, then in `.claude/worktrees/agent-m2c-dev`: `git reset --hard main`. Copy from the main checkout whatever already exists of `infra/secrets/` (`mkdir -p infra/secrets && cp -R /Users/jhag/projects/kvitt/infra/secrets/. infra/secrets/` if present). Run `tools/install-git-hooks.sh`. 2. Read §2 in order. Check the tools: `command -v docker gcloud firebase shellcheck rclone`, `docker buildx version`, `docker compose version`. If `docker`, `gcloud` or `firebase` is missing, stop and ask the owner (do not install). 3. Versions: resolve from official sources and record: the `alpine` image tag and digest (Docker Hub official image), `rclone` package version in that Alpine release, the `caddy` image version and digest if Caddy is used (Docker Hub official image). Pin by version and digest. 4. Write `infra/backup/backup.sh` and `infra/backup/Dockerfile`; run `shellcheck` on the script. 5. Write `infra/dev/compose.yaml` (and `infra/dev/Caddyfile` if used), `infra/env.example`, `infra/dev/README.md`. Record which TLS option (self-signed or none) was chosen and why. 6. Run `docker compose -f infra/dev/compose.yaml config`. 7. Firebase (needs HUMAN step H-1, §8). After the owner signed in, run §10.7 steps 2 to 6 exactly: `gcloud config set project kvitt-1337`; `gcloud services enable fcm.googleapis.com firebase.googleapis.com`; `firebase projects:addfirebase kvitt-1337` (skip if already a Firebase project); `firebase apps:create ANDROID "Kvitt" --package-name se.hagstedt.kvitt`; `firebase apps:create ANDROID "Kvitt debug" --package-name se.hagstedt.kvitt.debug`; `firebase apps:sdkconfig ANDROID ` for each, merged into one `android/app/google-services.json` in the main checkout; `gcloud iam service-accounts create kvitt-fcm --display-name "Kvitt FCM sender"`; grant `roles/firebasecloudmessaging.admin` on the project; `gcloud iam service-accounts keys create /Users/jhag/projects/kvitt/infra/secrets/fcm-service-account.json --iam-account kvitt-fcm@kvitt-1337.iam.gserviceaccount.com`. If an organisation policy blocks key creation, stop and report; change no policy. Copying the key to the Pi (§10.7 step 7) belongs to M2c-pi. 8. Health check (needs M2a on `main`; if not landed, report this part as outstanding): build the server image locally (no Gradle build running), `docker compose -f infra/dev/compose.yaml up -d`, `curl -fsS http://localhost:/health` on the Mac; then from the owner's phone on the home Wi-Fi (see §8 H-3). 9. Backup check: run the backup job once on demand (`docker compose -f infra/dev/compose.yaml run --rm backup /backup.sh` or as written in the README), then `sqlite3 "PRAGMA integrity_check"` prints `ok`. 10. Run `shellcheck infra/**/*.sh` (in `zsh` or `bash` with `shopt -s globstar`, so `**` recurses) if shellcheck is installed. 11. Run the gate (§7). Commit on `agent-m2c-dev` (never a secret), `git rebase main`, rerun the gate. Never merge, push or remove the worktree. Leave the dev stack running or stopped as the start prompt says, and say which in the report. ## 7. Gate From SPEC §11.2, verbatim: "`docker compose -f infra/dev/compose.yaml config` succeeds; `shellcheck infra/**/*.sh` clean if shellcheck is installed; on the Mac (with the M2a image): `GET /health` answers 200 from the Mac and from the owner's phone on the home Wi-Fi; one local backup run produces a snapshot that passes `PRAGMA integrity_check`". Commands: `docker compose -f infra/dev/compose.yaml config`; `shellcheck infra/**/*.sh` (globstar on); `curl -fsS http://localhost:/health`; the phone check of H-3; `sqlite3 "PRAGMA integrity_check"`. Green means: `config` exits 0; shellcheck prints nothing and exits 0 (or "not installed" stated); `/health` returns 200 with `"status":"ok"` from the Mac and the phone; integrity check prints `ok`. ## 8. HUMAN steps From SPEC §11.2, verbatim: "`gcloud auth login`, `firebase login`". No open question blocks this plan (OQ-1 answered; OQ-3 only blocks M2c-pi). - H-1, before step 7, on the owner's Mac: `gcloud auth login` (sign in with the owner's Google account in the browser that opens), then `firebase login` (same account). Expected result: both commands end with a "logged in as" line. - H-2 (only if step 2 finds a tool missing; not a §11.2 step): the owner decides whether and how to install it. - H-3, in step 8 (needed by the gate; not listed in §11.2): if the agent cannot query the server from the phone over USB itself, the owner opens `http://:/health` in Chrome on the phone on the home Wi-Fi and answers yes/no: the page shows `"status":"ok"`. If macOS asks whether Docker may accept incoming connections, the owner taps Allow. ## 9. Out of scope - Everything that concerns the Pi (M2c-pi): `infra/compose.yaml`, `infra/Caddyfile`, cert-sync image, `infra/build-images.sh`, `infra/deploy.sh`, `infra/publish-apk.sh`, `infra/README.md`, `infra/RESTORE.md`, the Gitea registry and Gitea Actions, router forwarding, the Drive upload and `rclone authorize`, copying secrets to the Pi. - Server code (M2a, M3, M4); Android code and Gradle files (M2b and later). - Changing org policies or billing in Google Cloud. ## 10. Report - Branch `agent-m2c-dev`, head commit hash after rebasing on `main`. - Gate output lines: `config` exit status, shellcheck result, `/health` responses (Mac, phone), integrity check output. - Versions chosen (images with digests, rclone) with sources; TLS option chosen. - Firebase: app ids created (ids are not secrets), service account email, whether `google-services.json` holds both clients; any policy block. - Where the secrets were written (paths only, never contents). - Whether the dev stack is left running, and its URL. - Numbering used or `TODO(spec)` occurrences. - HUMAN steps done and outstanding, worded for relay. - Contract requests, open questions, spec gaps (with section numbers). ## 11. Numbering Decision ids, spec version and schema version are assigned by the orchestrator in the start prompt; use `TODO(spec)` if none was given.