1# Deploying the daily23One app on Fly.io, one machine, and a nightly job on slate:45- **`app`** (`mosaic-daily`, dfw, shared-cpu-2x, 2 GB, the `/data`6 volume): serves the site, judges (milliseconds), runs the daily loop.7 It generates nothing except a small emergency field when a day has no8 pushed field at rollover (`MOSAIC_GENERATE=1`), which `/` announces.9 Measured on it: ring 1 of the daily took 40 min — far too slow for the10 nightly, fine for judging.11- **The nightly runs on slate** (the user's Woodpecker CI,12 `.woodpecker/nightly.yaml`): cron `nightly` at 05:00 UTC (and the13 manual event) builds the engine with `golang:1.27` and runs14 `deno task push-day --tomorrow --host https://mosaic-daily.fly.dev15 --rings all --jobs $(nproc) --band 24` with the `mosaic_push_token`16 secret; caches live under `/var/lib/mosaic-nightly` on the agent (repo17 is trusted for volumes). Woodpecker repo id 4, cron id 9. Measured:18 pipeline #5 (2026-08-27, `--jobs 16` on slate's 16 cores) built the19 engine in 17 s and generated the whole G=1000 field (197 tiles + 1220 rings, 22 MB) in 1098 s; the Fly performance-4x fallback took 3019 s21 for the same field. Jobs are now `cores/3` (5 on slate: a chunk uses22 1.5-2.6 cores, more parallel chunks only oversubscribe); #6 with jobs23 5 hit the agent's generator cache (0 s), so the first real comparison24 is the next cron run (2026-08-28 05:00 UTC, generating 2026-08-29) —25 read its `generated in` line in the push step's log. `/status`26 on the app reports whether tomorrow's field has arrived.27- **If slate is down**, push from any machine with the engine built:28 `MOSAIC_PUSH_TOKEN=... deno task push-day <date> --host29 https://mosaic-daily.fly.dev --rings all --jobs N --band 24` (a few30 minutes on the Mac). The paid alternative is a Fly scheduled machine31 with dedicated CPU running the same command from the app's image32 (`fly machine run registry.fly.io/<image> -a mosaic-daily --region dfw33 --vm-size performance-4x --schedule daily --restart no -e34 MOSAIC_PUSH_TOKEN=... --shell --command "cd /app/site && deno task35 push-day --tomorrow --host https://mosaic-daily.fly.dev --rings all36 --jobs 4 --band 24"`; ~50 min and ~$0.15 per run, measured 2026-08-27).3738Rollover uses the pushed field, else the emergency field. The token is39the `MOSAIC_PUSH_TOKEN` Fly secret; the same value is the Woodpecker40secret.4142## First time4344```45fly launch --no-deploy --copy-config --name mosaic-daily --region dfw46fly volumes create mosaic_data --region dfw --size 147fly deploy48```4950## Every time5152```53fly deploy # the app machine; the nightly builds its own engine on slate54```5556## Configuration (fly.toml [env])5758- `MOSAIC_DAILY=1` runs the daily loop in-process; `MOSAIC_DAILY_ROLLOVER_UTC`59 (default 0) is the hour the day changes; `MOSAIC_DAILY_GROW_MINUTES` (60)60 the growth cadence; `MOSAIC_DAILY_CHUNKS`/`CHUNK_G`/`G` (3 / 60 / 1000) the61 emergency field's shape (the pushed field carries its own);62 `MOSAIC_DAILY_NAMESPACE` changes every day's seed.63- `MOSAIC_PUBLIC=1` serves `/mcp` for any Host; `MOSAIC_PUBLIC_URL` is the64 origin agents are told to use.65- `MOSAIC_DATA=/data/` is the volume.6667## Checks6869- `fly logs` shows `{"daily": "started day-YYYY-MM-DD ..."}` on boot;70 `curl https://mosaic-daily.fly.dev/status` shows whether tomorrow's71 field has been pushed.72- `curl https://mosaic-daily.fly.dev/leaderboard/<date>` returns both boards.73- A restart (`fly machines restart`) resumes today's game from its log at74 the same revision (proven 2026-08-27: revision 179 before, 179 after,75 play continued).76- Live: https://mosaic-daily.fly.dev/ (dfw, shared-cpu-2x, 2 GB, 1 GB77 volume; about $11/month at Fly's list prices).7879## Playing against it8081Open `https://mosaic-daily.fly.dev/` and press "copy prompt", or:8283```84cd site && deno task solve -- --url https://mosaic-daily.fly.dev/mcp --join <game_id> --agent me --model my-model85```8687(`list_games` on the MCP endpoint names today's game: `day-<date>`.)8889## Machine identity and login9091Identity follows the tribes convention: `~/.config/mosaic/identity.json`92`{v:1, publicKey, privateKey (JWK Ed25519), createdAt, name?}`, mode 600;93machine id = `m-` + sha256(raw public key)[:16]. `POST /api/keys/auth94{publicKey, ts, sig}` (sig over `mosaic-key-auth:<host>:<ts>`) mints a95`msc_` bearer token (one per key; re-minting replaces it) and records the96key on first sight in `data/identity.sqlite`; the token goes on MCP97requests as `Authorization: Bearer`. The spec agents read is `/identity`98(`/identity.md` as text). `deno task identity` is one way to hold the file99(`--name`, `--token <host>`, `--link <host>`, `--sign <game_id>`).100101Humans log in with shoo.dev (`shoo.js`, `POST /api/session {id_token}`102verified against shoo's JWKS for `origin:<origin>`, HMAC cookie signed103with the `SESSION_SECRET` Fly secret; `GET /api/me`, `POST /api/logout`).104A machine is claimed by opening a single-use link (`POST105/api/keys/link-token` with the bearer, 10 minutes) while logged in:106`/link?t=...` binds the machine to the account.