Three connected features for the deployment-strategy demo: 1. Visitor identity cookie (v3). A new visitors table backs a first-party gb_visitor cookie: middleware on /api checks the cookie's UUID against the database on every request and regenerates + persists it when the id is missing, malformed, or unknown (fresh browser, pruned identity, or a reset database). The identity remembers the name a visitor signed with (pre-filled next time, used for unsigned posts), replaces the standalone reaction cookie for reaction de-dupe, and feeds a live "N visiting" counter (identities seen in the last 5 minutes) shown on the wall, in /api/info, the admin overview, and the presence/tick SSE events. The admin console gains Visitors/Visiting tiles and a visitors table, plus a GET /api/me endpoint and GET /api/admin/visitors. 2. guestbook reset command. Wipes entries (including soft-deleted rows), reactions and visitor identities under the schema advisory lock, with --dry-run, --hard (also reset wall settings) and --reseed flags; meant to run as a one-off Miabi Job. After a reset every outstanding cookie is invalidated, so browsers get a fresh identity on their next request. 3. cleanup learns --prune-visitors (default 30 days): the cron job now also forgets idle identities together with their reactions. Docs (README, DEMO) updated: act 8 covers reset, the visitor-identity talking point is in act 3, and the API tables list the new endpoints. Akili-Task: tsk_ptdi5ufndrzwlljk4v5g Akili-Agent: goldfish
10 KiB
Guestbook demo runbook
A step-by-step script for showing Miabi with the guestbook. Each act builds on the previous one and takes 3–8 minutes. Skip acts freely: everything after act 2 only assumes the app is deployed.
Keep two windows open: the wall (https://<domain>/) and the admin
console (https://<domain>/admin). A third tab on the Miabi app page is
handy for logs, events and scaling.
| Act | Shows | Miabi features |
|---|---|---|
| 1 | Deploy from Git / image | pipeline, build, domain + TLS |
| 2 | Managed Postgres | databases, env injection |
| 3 | Secrets + admin console | secret env vars |
| 4 | Scale out → broken live updates | replicas |
| 5 | Attach Redis → fixed | managed Redis, env prefix |
| 6 | Break it on purpose (+ rolling wave) | restarts, health gating, alerts, logs, analytics |
| 7 | Canary v1 → v2 | canary weights, promote |
| 8 | Scheduled cleanup + reset | cron jobs, one-off jobs |
0. Prepare (before the session)
docker build --build-arg VERSION=1.0.0 -t <registry>/guestbook:1.0.0 .
docker build --build-arg VERSION=2.0.0 -t <registry>/guestbook:2.0.0 .
docker push <registry>/guestbook:1.0.0 && docker push <registry>/guestbook:2.0.0
- Pick an admin token:
openssl rand -hex 16. - Create an empty Postgres and a Redis in the workspace ahead of time, since provisioning is slower than talking.
- Open
scripts/hammer.shin a terminal (see act 7).
1. Deploy
- New application, from the Git repo (the
.miabi/pipeline.yamlbuilds, scans and deploys) or from imageguestbook:1.0.0. - Port
8080. Health check: HTTP, path/healthz. (The default is none, and without it act 6 loses its punch.) - Add a domain. TLS is issued automatically.
- Open the wall. It runs on SQLite, as the first seeded signature says.
Talking point: zero config. With no database attached the app falls back to a local SQLite file. Mount a volume at
/dataif you want it to survive redeploys.
2. Attach Postgres
- App → Databases → attach the Postgres, no prefix.
- Miabi injects
DATABASE_URL(as a secret reference) plusDB_*and flags the app for redeploy. - Redeploy. The app migrates the schema and seeds the wall on first boot. The welcome signature now says PostgreSQL.
3. Secrets and the admin console
- App → Environment → add
ADMIN_TOKEN= your token, marked secret. AddDEBUG_ENDPOINTS=truetoo (used in act 6). - Redeploy, open
/adminand sign in with the token. - Tour it:
- Overview: signatures per day, total reactions, replicas (with each one's request counter), and this replica's runtime (host, DB, uptime, memory).
- Moderation: each signature shows the signer's IP, forwarded by the gateway, plus its reaction tally. Click the IP to filter by it. Pin a signature. It jumps to the top of every open wall, live. Hide one and it disappears.
- Settings: set a banner ("Welcome to the Miabi training 👋"), pick a wall theme (force light or dark — every open tab flips instantly), then toggle Pause signing. The wall goes read-only instantly in every tab.
Talking point: the token never appears in the app's config in plain text. Rotating it (edit + redeploy) signs every admin out, because sessions are HMAC-signed with it.
Ask the audience to open the wall on their phones and sign it. The online counter climbs live. Have them tap the ❤️ / 🎉 reactions — the tallies move on every screen at once, with no refresh.
Talking point — visitor identity: each browser also gets a
gb_visitorcookie, and the wall greets it ("you are guest-… · N signatures"). The backend only accepts the cookie while its id exists in the database — in the admin console, the Visitors tile and table show every identity issued, and Visiting counts the ones seen in the last 5 minutes. Reactions are keyed on the same identity.
4. Scale out, and watch it break
- App → Replicas → scale to 3.
- In the admin Overview, the served by chip changes as you refresh, but the Replicas table only ever lists one host. The hint says in-process broker.
- Open the wall in two browsers (or phones) until their clock cards show different hosts. Sign in one: the other doesn't update. Each replica only pushes to its own SSE clients. Reactions are the same: a ❤️ on one host never shows on the other.
- Also note the
via replica-xtag on new signatures: each one records which replica accepted it. Andcurl -s https://<domain>/healthza few times — therequestscounter climbs on whichever replica answered, so you can watch the load-balancer spread traffic even though live updates stay broken.
Talking point: this is the classic "works on one box" bug. Stateless apps need shared state for fan-out. Scaling is a platform feature; being scalable is an app property.
5. Attach Redis, and it's fixed
- App → Databases → attach the Redis with prefix
REDIS. Miabi injectsREDIS_DATABASE_URL, which the app reads (so doesREDIS_URL). - Redeploy. Startup logs show
live updates ready broker=redis. - Repeat the two-browser test: signatures and reactions now appear everywhere. The Replicas table lists all 3 hosts with their client counts and request counters, and the wall header says 3 replicas.
- Chaos lab → Traffic probe → Run probe: the bars show requests spread across the three hosts. Compare with the per-replica
requestscounters in the table — they tell the same story server-side.
6. Break it on purpose
All buttons are in the admin Chaos lab. Each acts on the replica that receives the request, and the activity log shows which one.
| Button | What happens | Where to look in Miabi |
|---|---|---|
| 💥 Crash it | process exits 1; the restart policy restarts it; the page reports when it's back | app Events, Notifications (crash-loop if repeated) |
| 🩺 Unhealthy | /healthz → 503 on that replica |
app_unhealthy alert; a deploy made now fails the health gate |
| 🔥 CPU burn | all cores busy for N s | app metrics |
| 🧠 Hold memory | allocates N MiB. Set a Memory limit of 256 MB first, then hold 400 MiB → OOM kill → restart | app_oom alert |
| 📜 Log burst | N error lines | Logs tab (Live) |
Probe /api/debug/slow |
400 ms latency | Analytics → Performance |
Probe /api/debug/error |
5xx responses | Analytics → HTTP Traffic |
The same endpoints work from a shell:
T=<admin-token>; D=https://<domain>
curl -XPOST -H "Authorization: Bearer $T" $D/api/debug/crash
curl -XPOST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"healthy":false}' $D/api/debug/health
curl -XPOST -H "Authorization: Bearer $T" "$D/api/debug/memory?mb=400&seconds=30"
Remember to click Healthy afterwards, or redeploy.
The rolling-update wave
Still in the Chaos lab, Rolling-update wave → Bounce each replica, one at a time restarts the fleet exactly like a rolling update does, but driven by the crash endpoint. Keep the Replicas table in view: each replica disappears, comes back, and its request counter restarts from ~0 while the others keep climbing. The wall never goes down — which is the whole point of rolling updates, shown from the inside.
Talking point: recreate would drop every replica at once (all counters would reset together, with a gap of downtime). Rolling keeps capacity by replacing one at a time. Same mechanism, two strategies.
7. Canary v1 → v2
- In a terminal:
scripts/hammer.sh https://<domain>. It prints a live count and, on Ctrl-C, a tally ofstatus version host. - Set deploy strategy to canary (initial weight 10%), and deploy image
2.0.0. - On the wall, reload a few times: ~1 in 10 loads shows the v2 badge in a different colour and the new live server-time card. In the admin Traffic probe, the bars split by version; in the Replicas table, the two versions sit side by side with their own request counters.
- Promote now. The probe goes 100% v2, and
hammer.shshould show0 failed.
The v2 build is the same code with a different
VERSION. Nothing needs to differ for the demo to work — but the clock card (absent in v1) makes the split visible to the audience without a terminal.
8. Scheduled cleanup and reset
The binary has a cleanup subcommand that permanently removes deleted
signatures, optionally expires old ones, and prunes idle visitor identities:
guestbook cleanup --purge-after 168h # default: purge deletes older than 7 days
guestbook cleanup --max-age 720h --dry-run # preview expiring >30-day-old, unpinned entries
guestbook cleanup --prune-visitors 720h # forget identities idle for 30 days (and their reactions)
- Jobs → New cronjob, image blank (uses the app's image), command
/app/guestbook cleanup --purge-after 1h, schedule*/15 * * * *. - Delete a signature in Moderation, then Run now. The job log shows
cleanup finished purged=… visitors_pruned=… remaining=….
There is also a reset subcommand to clean the database between sessions
— entries, reactions and visitor identities go (soft-deleted rows are wiped
for good), wall settings are kept unless --hard:
guestbook reset --dry-run # report what would be removed
guestbook reset --reseed # wipe, then insert the sample signatures
guestbook reset --hard --reseed # also reset banner/pause/theme
- Jobs → New job, command
/app/guestbook reset --reseed. The wall empties immediately; every browser'sgb_visitorcookie stops matching a database row, so on the next request each visitor is handed a fresh identity — the "you are …" card on the wall changes, and the admin Visitors table starts refilling from zero.
guestbook migrate works the same way as a one-off Job, for teams that
want migrations as an explicit step rather than on boot.
Reset between sessions
- Admin → Settings: clear the banner, set the theme back to System, un-pause.
- Chaos lab: Healthy.
- To wipe the wall: run
/app/guestbook reset --reseedas a one-off Job (entries, reactions and visitor identities go; the wall is re-seeded), or detach/recreate the database. Browsers pick up a fresh identity cookie on their next request either way.