# infra: Kvitt infrastructure This file covers the **development stack on the owner's Mac** (`infra/dev/`, milestone M2c-dev, D-001) and the **Firebase setup** (SPEC §10.7). The Pi stack (`infra/compose.yaml`, Caddy with the Let's Encrypt certificate, cert-sync, the Gitea registry, Drive backups, deploy and APK publishing) is built by M2c-pi, which adds its own sections here. ## Layout | Path | What | |---|---| | `dev/compose.yaml` | Dev stack: `server` (image `kvitt-server:dev`) and the on-demand `backup` job | | `dev/.env.example` | Dev settings, no secret values; copy to `dev/.env` (git-ignored) to override | | `dev/build.sh` | Builds `kvitt-server:dev` from `server/Dockerfile` and `kvitt-backup:dev` from `backup/Dockerfile` | | `dev/up.sh` | Starts the server, waits for `/health`, prints the Mac and phone URLs; `--down` stops the stack | | `dev/logs.sh` | Follows the logs (`dev/logs.sh --tail 100 server`) | | `dev/backup.sh` | Runs the backup job once and checks the snapshot with `PRAGMA integrity_check` | | `dev/copy-google-services.sh` | Copies `secrets/google-services.json` to `android/app/google-services.json` | | `dev/lib.sh` | Helpers sourced by the scripts | | `backup/Dockerfile`, `backup/backup.sh` | Backup image and job of SPEC §10.9 (shared with the Pi stack) | | `secrets/` | Git-ignored: `fcm-service-account.json`, `google-services.json`. Never committed | All commands below run from the repository root. ## Development stack **Choices.** No Caddy and no TLS: debug builds accept `http://` to private LAN addresses (AC-2.8), so the server publishes its own port (default 8080) on all of the Mac's interfaces. The server keeps the hardening of SPEC §8.9 (uid 10001, read-only root, no capabilities, `no-new-privileges`), the 160 MiB limit, `json-file` logging (10 MB x 3) and `TZ=Europe/Stockholm`. Data lives in the bind-mounted folder `infra/dev/data/` (git-ignored) so it is easy to inspect and reset. The secrets folder is mounted read-only at `/secrets`; if `fcm-service-account.json` is missing the server runs with push disabled (§10.4). On this Mac `docker` is Podman (`docker compose` runs `podman-compose`); the files work with Docker Desktop as well. ### Build ```bash infra/dev/build.sh # both images infra/dev/build.sh server # only kvitt-server:dev; needs server/Dockerfile (milestone M2a) infra/dev/build.sh backup # only kvitt-backup:dev ``` The server build is `docker build --platform linux/arm64 -f server/Dockerfile -t kvitt-server:dev server` (override the context with `KVITT_SERVER_BUILD_CONTEXT`). The script refuses to run while a Gradle build holds `/tmp/kvitt-gradle.lock` (CLAUDE.md). ### Start, stop, logs ```bash infra/dev/up.sh # start; prints health, the Mac URL and the phone URL infra/dev/logs.sh # follow logs (Ctrl-C to stop following) infra/dev/up.sh --down # stop and remove the containers (data stays in infra/dev/data/) ``` `KVITT_PUBLIC_URL` (used in invite links) is left empty in `.env.example`; `up.sh` sets it to `http://:`. Pin it in `infra/dev/.env` if the Mac's address changes often. ### Finding the Mac's LAN address `up.sh` prints it. By hand: `ipconfig getifaddr en0` (Wi-Fi on most Macs), or the interface of the default route: `route -n get default | grep interface`, then `ipconfig getifaddr `. It must be a private address (`192.168.*`, `10.*`, `172.16.*` to `172.31.*`) for a debug build to accept `http://` (AC-2.8). The first time a phone connects, macOS may ask whether the container runtime (Podman's `gvproxy`, or Docker) may accept incoming network connections: answer Allow. ### Bootstrap and pairing a debug build The `bootstrap` and `pair-device` subcommands (SPEC §10.4, AC-1.1, AC-1.6) arrive with the server milestones. ```bash C="docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example" # or --env-file infra/dev/.env $C exec server kvitt-server bootstrap # first admin invite: code, link, expiry $C exec server kvitt-server pair-device --user # extra device invite for an existing user $C run --rm server migrate # when the server is stopped (entrypoint is kvitt-server) ``` On the phone (home Wi-Fi, debug build `se.hagstedt.kvitt.debug`): open the pairing screen, enter the server URL `http://:8080` and the printed code (AC-2.7). Check reachability first by opening `http://:8080/health` in Chrome on the phone: it shows `"status":"ok"`. ### Backup (local, no Drive upload) ```bash infra/dev/backup.sh ``` Runs `infra/backup/backup.sh` once in the `backup` image (compose profile `backup`, so `up` never starts it) with `RCLONE_FOLDER_ID` empty: `.backup` snapshot, `PRAGMA integrity_check` must print `ok`, then `infra/dev/backups/kvitt-.sqlite.gz`, `pictures-.tar.gz` and `last-success`; the Drive upload is skipped with a log line and the files are kept (SPEC §10.9, D-001). The wrapper then checks the new snapshot again. To check by hand: `gunzip -c infra/dev/backups/kvitt-.sqlite.gz > /tmp/k.sqlite && sqlite3 /tmp/k.sqlite "PRAGMA integrity_check"`. ### Resetting the dev data ```bash infra/dev/up.sh --down rm -rf infra/dev/data # the dev database, pictures and APKs; never the phones' data infra/dev/up.sh # fresh database; run bootstrap again ``` Paired debug builds then hold a token the new database does not know: re-pair them (clear the debug app's data on the phone, or reinstall the debug build). ### Worktrees Agents work in `.claude/worktrees/agent-`, which has no `infra/secrets/` unless copied (CLAUDE.md). Either copy it from the main checkout, or set `KVITT_SECRETS_DIR=/Users/jhag/projects/kvitt/infra/secrets` in `infra/dev/.env`. `infra/dev/copy-google-services.sh /Users/jhag/projects/kvitt` copies the Firebase config from the main checkout into the worktree's `android/app/`. ## Firebase (SPEC §10.7) Project `kvitt-1337` (Firebase added to the existing Google Cloud project). Free tier only; nothing billable is created by this setup. | Resource | Name | |---|---| | APIs enabled | `firebase.googleapis.com`, `fcm.googleapis.com`, `cloudresourcemanager.googleapis.com`, `iam.googleapis.com` | | Android app | "Kvitt", package `se.hagstedt.kvitt` | | Android app | "Kvitt debug", package `se.hagstedt.kvitt.debug` | | Service account | `kvitt-fcm@kvitt-1337.iam.gserviceaccount.com` ("Kvitt FCM sender"), role `roles/firebasecloudmessaging.admin` (the narrowest predefined role holding `cloudmessaging.messages.create`) | | Key | `infra/secrets/fcm-service-account.json` (git-ignored, mode 600) | | Config | `infra/secrets/google-services.json`, one file with both Android clients (git-ignored) | Put the config where Gradle reads it: `infra/dev/copy-google-services.sh`. The setup used only `gcloud` and the Firebase Management REST API. To repeat or inspect it: ```bash T=$(gcloud auth print-access-token) H=(-H "Authorization: Bearer $T" -H "x-goog-user-project: kvitt-1337") API=https://firebase.googleapis.com/v1beta1/projects/kvitt-1337 curl -sS "${H[@]}" "$API/androidApps" # list the apps (ids, packages) curl -sS "${H[@]}" "$API/androidApps//config" # configFileContents is base64 google-services.json gcloud iam service-accounts keys list --iam-account kvitt-fcm@kvitt-1337.iam.gserviceaccount.com --project kvitt-1337 ``` Rotating the key: create a new one with `gcloud iam service-accounts keys create infra/secrets/fcm-service-account.json --iam-account kvitt-fcm@kvitt-1337.iam.gserviceaccount.com --project kvitt-1337`, restart the server, then delete the old key id with `gcloud iam service-accounts keys delete ...`. Copying the key to the Pi is M2c-pi (§10.7 step 7).