carlos / platform Public

plan: woodstar โ†’ flagship (recon complete, execution deferred behind multi-account)

Agent-authored recon + plan. Woodstar is the cleanest fit yet (same account,
arm64-native, no secrets, no IMDS need, no canonical-host pinning) but needs
W0: the edge routes by Host only and woodstar's client is same-origin across
four path-prefixed services. Execution waits for multi-account so woodstar is
born into its own account rather than migrated twice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 3481167cc13e967d748830058a36169adcf1c6c8 parent 1e1f048
1 file changed, +381 −0
  • docs/superpowers/plans/2026-08-02-plan-woodstar-flagship.md +381 −0
diff --git a/docs/superpowers/plans/2026-08-02-plan-woodstar-flagship.md b/docs/superpowers/plans/2026-08-02-plan-woodstar-flagship.md
new file mode 100644
index 000000000..d7adfe2a9
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-02-plan-woodstar-flagship.md
@@ -0,0 +1,381 @@
+# ๐Ÿค– Plan: Woodstar โ†’ CARLOS flagship migration
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+> ๐Ÿค– Written by an LLM (Claude), per factor X.
+
+**Goal:** Woodstar's production instance (`woodstar.app` + `status.woodstar.app`)
+serving from the CARLOS flagship box (`i-092c0c1eea75723cb` / 99.81.104.219),
+riding carlos release channels, hostname unchanged โ€” passkeys and the iOS
+app's AASA binding are domain-bound, so `woodstar.app` must survive
+byte-identical. The **devbox stays put** (it is the dev/canary workflow, and
+it depends on Caddy wildcard DNS-01 certs the carlos edge cannot mint).
+
+**Playbook:** the kass + keymail migrations (plans B and
+2026-08-02-plan-keymail-flagship.md): ship `-target linux-arm64`,
+route-before-adopt, per-host live link, `/data` 0711 + `/data/<app>` 0700,
+rehearse on a COPY, never fork a litestream lineage, DNS flip HELD for Paul's
+explicit go, no full `carlos bootstrap` on the live box ever.
+
+## Where Woodstar lives today (recon 2026-08-02)
+
+- Repo: `/home/paulca/github.com/woodstarapp/woodstar` (origin/main
+ `dfe406a`). Pure Go 1.25, **modernc.org/sqlite (no CGO)** โ€” cross-compiles
+ to linux-arm64 with zero friction; both current boxes are already arm64
+ (t4g). The 16 MB `relay` file checked into the repo root is a stray
+ **macOS** arm64 build, not a deploy artifact โ€” deploys always cross-compile
+ (`scripts/deploy.sh`). No non-Go native deps anywhere in the serve path.
+- **Four services, four separate binaries** (`cmd/relay|media|indexer|notify`):
+ - `relay` :7071 โ€” SQLite `relay.db`, WebSocket hub (`GET /subscribe`),
+ embedded web client (go:embed), serves `/.well-known/woodstar.json` and
+ `/.well-known/apple-app-site-association`.
+ - `media` :7072 โ€” content-addressed blob store on disk (`-dir`), no DB.
+ - `indexer` :7073 โ€” SQLite `indexer.db` (FTS), tails the relay firehose
+ over HTTP (`-relay http://127.0.0.1:7071`).
+ - `notify` :7074 โ€” SSE (`text/event-stream`), stateless, tails the relay.
+- **Boxes โ€” verified live, SAME account as the flagship (271376211898,
+ eu-west-1, profile `keymail`; profile `woodstar` also reaches it):**
+ - prod: `i-09e55d1b6c9598dff` (t4g.nano, EIP **52.30.232.124**, tag name
+ `woodstar-canary` โ€” legacy name, it IS prod), hand-built:
+ Caddy (route53 DNS-01, wildcard certs) fronts the four services on
+ loopback with **path routing**: `/media*`โ†’7072, `/api/*`โ†’7073,
+ `/stream/*`โ†’7074, everything elseโ†’7071; also serves
+ `status.woodstar.app` as static files from `/opt/woodstar/status`.
+ The `34.241.196.33` in `infra/prod-Caddyfile`'s comment is a stale IP
+ from a previous box.
+ - devbox: `i-0c388ec47cf98067a` (t4g.micro, EIP 34.253.20.182) โ€”
+ `dev.woodstar.app` + per-branch canaries, fully scripted
+ (`infra/devbox-init.sh`), disposable. **Not migrating.**
+ - Units (from devbox-init.sh; prod mirrors it hand-built):
+ `woodstar-relay|media|indexer|notify.service`, `User=woodstar` (static
+ system user), loopback addrs, `WorkingDirectory` = the data dir.
+ SSH `ec2-user@<ip>` with `~/.ssh/woodstar-canary.pem`; **no SSM agent
+ registered** on either box (instance role is route53+S3 only) โ€” box-side
+ work at cutover is over SSH, and SSH to these boxes is currently
+ classifier-blocked for agents (Paul: allow rule or run those steps).
+- **Data:** SQLite ร—2 (`relay.db`, `indexer.db`) + media dir. Litestream
+ 0.3.13 sidecar (root, `/etc/litestream.yml`) streams both DBs to
+ `s3://woodstar-backups-271376211898/prod/{relay,indexer}`; media backed up
+ hourly by a root oneshot timer (`aws s3 sync`, additive, no --delete) to
+ `.../prod/media`. Restore = manual `litestream restore` + `aws s3 sync`
+ back (runbook in `scripts/litestream-setup.sh`) โ€” **no restore-on-boot**
+ embedded in the app (unlike keymail), so the F4 silently-empty failure
+ mode does not exist here; restore is explicit or it visibly didn't happen.
+ Sizes (bucket, 2026-08-02): relay 188 KB, indexer 103 KB, media 6.5 MB.
+ Tiny.
+- **Secrets: none.** The only runtime env is `WOODSTAR_APPLE_APP_ID`
+ (verified live: prod serves AASA `J5NDF5PCD8.app.woodstar.ios`) โ€” an
+ identifier, not a secret. No SES, no OAuth, no tokens, no SMTP. Nothing
+ to stage in SSM.
+- **DNS:** Route53 zone `woodstar.app` (`Z07702842ZC4MUKZR07JB`) **in the
+ same account**. Records: `woodstar.app` A + `*.woodstar.app` A โ†’
+ 52.30.232.124 (TTL 300); `dev.woodstar.app` + `*.dev.woodstar.app` โ†’
+ devbox. `status.woodstar.app` has NO explicit record โ€” it rides the
+ wildcard. Registrar is Key-Systems LLC (not DNSimple 285 โ€” checked; NS
+ delegated to Route53), irrelevant to cutover: the flip is two A-record
+ changes in Route53, same account, no DNSimple involvement.
+- Infra: OpenTofu `infra/main.tf`, state
+ `s3://woodstar-tfstate-271376211898/infra/terraform.tfstate`. Will drift
+ at cutover (records changed outside its state) โ€” expected, same as kass
+ and keymail.
+
+## CARLOS fit assessment
+
+- **Arch: clean.** Pure Go, CGO-free, deploys already build
+ `GOOS=linux GOARCH=arm64`; prod runs on arm64 today. No x86-only
+ artifacts, no native deps (the repo-root `relay` Mach-O stray is inert).
+- **IMDS: no allowance needed.** Zero AWS SDK / SigV4 / SES in the Go
+ services (grepped clean). AWS credentials are used only by the litestream
+ sidecar and the media-sync timer, both of which run as **root** on the
+ flagship pattern (root is already through the uid-gate). The
+ keymail-style `allow-imds-user.sh` step is NOT required. Corollary: no
+ silently-empty-restore trap.
+- **Canonical-host pinning: none server-side.** The relay never derives or
+ emits its own origin โ€” no `-host`/`-tls` flags, no absolute URLs minted.
+ CORS is `*`, WS `CheckOrigin` always true. Passkeys are client-side only
+ (`web/public/lib/passkey.js`: `rp: {name: "Woodstar"}`, no `id` โ†’ RP ID
+ defaults to the page's domain); the server stores wrapped-key blobs and
+ never verifies WebAuthn origins. Hostname stays `woodstar.app` โ†’ passkeys
+ and the iOS AASA binding survive. **No K0-equivalent flag needed.**
+- **โš  The one real gap (W0): path fan-out.** The deployed client is
+ same-origin for all four services (`lib/api.js` DEFAULT_SERVICES); today
+ Caddy path-routes one hostname to four backends. The carlos edge routes
+ **by Host only** (`internal/registry` Route has no path;
+ kinds instance|service|static). Without a front, `woodstar.app` โ†’ relay
+ alone serves a degraded app (no media, no search, no live notifications).
+ Two options:
+ - **W0a (recommended): teach the relay to front its satellites.** Small
+ Go change: optional flags `-media-upstream`, `-indexer-upstream`,
+ `-notify-upstream`; when set the relay reverse-proxies `/media`,
+ `/media/*`, `/api/*`, `/stream/*` (exactly Caddy's `prod_routes`
+ block). httputil.ReverseProxy passes WS upgrades and flushes
+ `text/event-stream` immediately, so `/stream/*` SSE survives the extra
+ hop. Unset = today's behavior byte-identical. Also improves the
+ self-host story ("bin/relay alone serves a complete instance").
+ - W0b: teach the carlos edge path-prefix routing. Platform feature,
+ bigger blast radius, benefits future multi-service tenants. Not needed
+ for the timeline if W0a lands.
+- **Process/tenancy shape: static user `woodstar`, four bespoke hardened
+ units** (the keymail pattern). Not `carlos-app@` DynamicUser: that
+ template is per-route (`ExecStart=live/<host> serve`) and only the relay
+ has a public route โ€” media/indexer/notify are route-less satellites, and
+ the four units share loopback TCP + a data tree, which wants one stable
+ uid. Units: `woodstar-{relay,media,indexer,notify}.service`, User=woodstar,
+ harden-host.sh sandbox drop-ins, data under `/data/woodstar` (0700,
+ /data 0711).
+- **App packaging vs the release model:** `carlos ship` is one-binary-per-app
+ keyed by GOOS-GOARCH (multi-file ship = multi-TARGET of one binary;
+ basename-keyed artifacts are invisible to adopt โ€” the kass lesson). Four
+ binaries โ‡’ **four carlos apps**: `woodstar-relay`, `woodstar-media`,
+ `woodstar-indexer`, `woodstar-notify` (+ `woodstar-status`, kind static).
+ Alternative: merge into one `woodstar` binary with verbs (titogo/keymail
+ shape) โ€” cleaner platform-side, but contradicts the repo's stated "one
+ small self-hostable binary per service" design value. Paul's call (open
+ question 2); the four-app shipment already staged is non-binding either
+ way.
+- **Privileged ports: none.** No :25, no SMTP, nothing below 1024. Edge
+ owns :80/:443; all four services go loopback (proposed 8451โ€“8454).
+- **Streaming through the edge:** two proxied hops post-W0a
+ (edge โ†’ relay โ†’ notify) for SSE, one for WS (`/subscribe` terminates at
+ the relay). Both supported by Go's ReverseProxy; verify live in W3 โ€”
+ an explicit rehearsal checkbox, not an assumption.
+- **Wildcard loss (decision needed):** Caddy serves `*.woodstar.app` today
+ with a DNS-01 wildcard cert; the carlos edge is per-host HTTP-01 with a
+ registry-backed HostPolicy (wildcard.go deliberately not ported yet;
+ titogo hit the same wall). Nothing live depends on prod subdomains today
+ except `status.woodstar.app` (which gets its own route + cert) โ€” the
+ `you.woodstar.app` per-user idea in `docs/` is aspirational (DESIGN ยง11).
+ After cutover, random `*.woodstar.app` hosts will fail TLS instead of
+ serving the app. Recommend: accept the loss, keep the wildcard A record
+ pointed at the flagship (the :80 fallback redirect still catches typos),
+ add explicit routes if a subdomain ever matters.
+
+## Box headroom verdict
+
+**Fits. No instance bump needed.** The ENTIRE prod stack (4 Go services +
+Caddy + litestream + AL2023) runs inside a **t4g.nano (512 MB)** today โ€”
+that is a hard upper bound on its footprint, and its data is ~7 MB total.
+Flagship measured 2026-08-02 ~09:35 UTC (SSM, read-only) with kass +
+keymail (relay + 18 instances) + console + edge + status loop all resident:
+**1179 MB available of 1858** (679 used). Expect Woodstar to add roughly
+100โ€“200 MB across the four services โ€” comfortably inside headroom, with
+keymail's mailbox-fleet growth remaining the number to watch on this box.
+(Exact per-process RSS on the woodstar prod box was NOT measured โ€” no SSM
+agent there and agent SSH is classifier-blocked; the nano bound makes the
+verdict safe regardless.)
+
+## Global constraints
+
+- NO DNS changes, no touching the running woodstar services, no production
+ data writes until the explicitly gated steps; the cutover flip is **HELD
+ for Paul's explicit go**.
+- Rehearsals must never point a litestream at the live
+ `woodstar-backups-โ€ฆ/prod/` prefixes (lineage fork). Rehearse from COPIES
+ under `carlos-data-ie-271376211898`.
+- Never run old-box and flagship woodstar stacks against the same
+ replication prefixes at once.
+- host.env / apps-env values with spaces double-quoted; SSM adopt (once SSM
+ matters โ€” flagship has it) needs `set -a; . /etc/carlos/host.env; set +a`
+ first; full `carlos bootstrap` never re-runs on the live flagship.
+- Woodstar's `infra/` tofu stack stays untouched until post-cutover
+ retirement (it will drift when we flip records + stop units โ€” expected).
+
+---
+
+### Task W0: relay front-proxy (the one code change)
+
+- [ ] In a **woodstar worktree** (its CLAUDE.md: never build in the main
+ checkout; multiple sessions active): add optional
+ `-media-upstream`, `-indexer-upstream`, `-notify-upstream` flags to
+ `cmd/relay`. When set, the relay's mux reverse-proxies
+ `/media` + `/media/{hash...}` โ†’ media, `/api/` โ†’ indexer,
+ `/stream/` โ†’ notify (mirror Caddy's `prod_routes` exactly, including
+ `POST /media` uploads). Unset = today's behavior byte-identical.
+ Tests: proxy wiring, SSE pass-through flush, WS `/subscribe`
+ untouched. TDD per superpowers.
+- [ ] `make test` green; run the repo's harness if practical
+ (`make e2e`; the browser drives need Playwright).
+- [ ] PR into woodstar main; note in the PR that dev/devbox behavior is
+ unchanged (Caddy keeps doing the fan-out there).
+- [ ] After merge: rebuild all four binaries at the merged sha
+ (`GOOS=linux GOARCH=arm64 CGO_ENABLED=0 -trimpath`) and re-ship
+ (`carlos ship -app woodstar-<svc> -kind binary -version <sha>
+ -target linux-arm64 <bin>`). The dfe406a shipment is
+ pipeline-validation only; the cutover binaries are the W0 sha.
+
+### Task W1: flagship-side layout decision + prep (no tofu needed by default)
+
+- [ ] **Backup home decision (open question 3):** recommended โ€” replicate
+ to the platform data bucket `carlos-data-ie-271376211898` under
+ `woodstar/{relay,indexer,media}` (kass precedent; host role already
+ has RW; **zero IAM/tofu change**). Alternative: keep
+ `woodstar-backups-โ€ฆ` as the target, which needs a host-role IAM
+ policy in `platform-infrastructure/flagship` (a small `woodstar.tf`
+ mirroring `keymail.tf`'s bucket-RW block).
+- [ ] Extend the flagship box litestream config (hand-written, root โ€” the
+ generator still doesn't do nested layouts) with
+ `/data/woodstar/relay.db` and `/data/woodstar/indexer.db` โ†’
+ chosen bucket. DO NOT enable until cutover (lineage rule).
+- [ ] Media backup timer: `woodstar-media-backup.{service,timer}` (root,
+ hourly, additive `aws s3 sync /data/woodstar/media s3://<chosen>/โ€ฆ`)
+ โ€” port of `scripts/litestream-setup.sh`'s unit, path-adjusted.
+- [ ] Units staged (not started): `woodstar-{relay,media,indexer,notify}.service`
+ โ€” static user `woodstar`, harden drop-ins, `/data/woodstar` (0700),
+ env file `/etc/carlos/apps/woodstar.env` (0600 root) carrying
+ `WOODSTAR_APPLE_APP_ID=J5NDF5PCD8.app.woodstar.ios`. Loopback ports
+ 8451 relay / 8452 media / 8453 indexer / 8454 notify;
+ relay flags: `-addr 127.0.0.1:8451 -db /data/woodstar/relay.db
+ -name woodstar -media-upstream http://127.0.0.1:8452
+ -indexer-upstream http://127.0.0.1:8453
+ -notify-upstream http://127.0.0.1:8454`;
+ media `-dir /data/woodstar/media`; indexer/notify
+ `-relay http://127.0.0.1:8451`.
+- [ ] No SSM secrets to stage (Woodstar has none). No IMDS allowance
+ needed (no in-process AWS).
+
+### Task W2: verify prod box actuals (operator or SSH-allowed session)
+
+The prod box is hand-built and SSH-only (no SSM). Before rehearsing,
+confirm against assumptions (read-only):
+- [ ] `systemctl cat woodstar-relay` on 52.30.232.124 โ€” exact flags/env
+ (esp. `WOODSTAR_APPLE_APP_ID`, `-name`, any surprise env).
+- [ ] `ps`/`free` โ€” real RSS per service (refines the headroom estimate).
+- [ ] `ls /opt/woodstar/data` โ€” confirm nothing beyond
+ relay.db/indexer.db/media (e.g. no third DB the litestream.yml
+ doesn't cover).
+
+### Task W3: flagship restore rehearsal (no prod impact, copies only)
+
+- [ ] Copy backups (server-side CopyObject, read-only on source):
+ `s3://woodstar-backups-271376211898/prod/{relay,indexer,media}` โ†’
+ `s3://carlos-data-ie-271376211898/woodstar-rehearsal/โ€ฆ`.
+- [ ] On the flagship (SSM): restore both DBs
+ (`litestream restore -o /data/woodstar-rehearsal/relay.db โ€ฆ`) +
+ `aws s3 sync` the media copy; `PRAGMA integrity_check` on both.
+- [ ] Transient units on rehearsal ports, W0 binaries, data dir
+ `/data/woodstar-rehearsal`; route-before-adopt:
+ `carlos add -app woodstar-relay -kind instance
+ -addr 127.0.0.1:8451 -channel canary/woodstar-rehearsal
+ woodstar-rehearsal.99.81.104.219.sslip.io` โ†’ adopt โ†’ LE cert on
+ first hit.
+- [ ] Verify through the edge: client loads clean (WebKit
+ `console-check.mjs` against the rehearsal URL โ€” the repo's own
+ post-deploy law), feed renders restored posts, **media images load
+ (`/media/*`), search returns (`/api/search`), SSE connects and
+ streams (`/stream/*`), WS `/subscribe` connects** โ€” the four-hop
+ checks are the point of the rehearsal.
+- [ ] `woodstar-status`: promote to a rehearsal channel, route
+ `status-woodstar-rehearsal.โ€ฆ.sslip.io` kind=static, verify 200.
+- [ ] Record freeze-to-serving time; tear down units + `/data/woodstar-rehearsal`
+ + rehearsal routes; optionally delete the copied prefix (re-copy is
+ pennies at 7 MB).
+
+### Task W4: cutover (HELD โ€” Paul's explicit go required)
+
+Pre-flight: W0 merged + shipped + promoted (`canary/woodstar` at most until
+bake), W1 staged, W2 checked, W3 green, `/data/woodstar` empty, TTLs
+already 300.
+
+1. Routes exist + adopted on flagship: `woodstar.app` โ†’ 127.0.0.1:8451
+ (kind instance, app woodstar-relay), `status.woodstar.app` (kind
+ static, app woodstar-status). Certs mint on first post-flip hit.
+2. **Stop** on old prod (SSH): the four `woodstar-*` units, then
+ `woodstar-media-backup.timer`, run one final
+ `woodstar-media-backup.service` pass, wait for litestream's final WAL
+ frames (~30 s), then stop `litestream.service`. Verify newest LTX
+ timestamps under `prod/{relay,indexer}` are post-stop. (Old litestream
+ MUST be stopped before the flagship one starts if the same prefixes are
+ reused; with the recommended fresh `carlos-data-ie` prefixes, do a final
+ `litestream restore` on the flagship from `woodstar-backups` AFTER the
+ old one stops โ€” order is stop-old โ†’ restore-new โ†’ start-new.)
+3. **Restore + start** on flagship: litestream restore both DBs into
+ `/data/woodstar`, media sync, chown woodstar, integrity_check, start
+ the four units + enable the flagship litestream config + media timer.
+4. Smoke pre-flip: loopback `/info` 200 (`{"name":"woodstar",โ€ฆ}`),
+ `/.well-known/apple-app-site-association` correct, feed query via
+ `curl --resolve woodstar.app:443:99.81.104.219`.
+5. **Flip Route53** (zone Z07702842ZC4MUKZR07JB, same account):
+ `woodstar.app` A and `*.woodstar.app` A โ†’ 99.81.104.219. Watch the LE
+ certs issue for woodstar.app + status.woodstar.app.
+6. Verify (Paul, browser): sign-in with an existing passkey at
+ https://woodstar.app, post, image upload + display, search, live
+ notification, DM; iOS app still validates AASA; status page 200.
+7. Old prod box: units left stopped, disk intact โ€” rollback body; freeze
+ 30 days like kass/keymail.
+
+**Rollback** (before/at 6): stop flagship units + its litestream, restart
+the old box's units + litestream, flip the two A records back. Writes
+accepted by the flagship during the window live only in its lineage โ€” keep
+the window short, quiet hour.
+
+### Task W5: post-cutover
+
+- [ ] Promote W0 sha to stable after the 72 h supply-chain bake
+ (canary channels bypass by design; stable does not).
+- [ ] Repoint `scripts/status-publish.sh` (or replace it with
+ `carlos ship -kind static` + promote โ€” statuses become releases).
+- [ ] Note drift in woodstar `infra/` (records + stopped units outside its
+ state); plan prod-instance retirement after the freeze. Keep zone,
+ bucket, devbox, IAM as-is โ€” devbox workflow is untouched.
+- [ ] Decide `-status-named` opt-in for the fleet status page.
+- [ ] Update woodstar CLAUDE.md "The boxes" section post-retirement.
+
+## Special risks
+
+- **Degraded-not-broken failure shape:** if W0 fan-out is misconfigured the
+ site still "works" (relay serves the client; media/search/notify
+ silently 404/fail). The W3 four-path checks and the WebKit console check
+ exist precisely to catch this โ€” do not skip them at cutover (step 4/6).
+- **SSE/WS through two proxies:** verified in rehearsal, not assumed.
+- **Wildcard subdomains lose TLS** post-cutover (edge is per-host
+ HTTP-01). Accepted per assessment; revisit if `you.woodstar.app`-style
+ features ship (that's the edge wildcard.go port, a platform feature).
+- **iOS binding:** `WOODSTAR_APPLE_APP_ID` must be in the flagship env
+ BEFORE the flip โ€” a missing AASA breaks iOS passkey assertions
+ (`J5NDF5PCD8.app.woodstar.ios`, verified live on prod).
+- **No restore-on-boot:** restore is explicit (step 3). The empty-DB trap
+ is a no-show here, but the inverse risk exists: starting units before
+ restoring gives the relay a FRESH founder-less DB and the first
+ registrant becomes founder โ€” never start units on an empty
+ `/data/woodstar` connected to the real hostname.
+
+## Prep already completed (2026-08-02, this session)
+
+- Recon above, all verified live read-only (EC2/Route53/S3 describes via
+ profile `keymail`; SSM free/ps on the flagship; HTTPS GETs of
+ /info + AASA on prod). No box, DNS, or service touched.
+- origin/main `dfe406a` cloned fresh and cross-built
+ (`GOOS=linux GOARCH=arm64 CGO_ENABLED=0 -trimpath`): four static ELF
+ aarch64 binaries.
+- **Shipped to `carlos-flagship-271376211898` (NOT promoted โ€” no channel,
+ no route, no box touched):**
+ - `apps/woodstar-relay/releases/dfe406a/linux-arm64` (+manifest)
+ - `apps/woodstar-media/releases/dfe406a/linux-arm64`
+ - `apps/woodstar-indexer/releases/dfe406a/linux-arm64`
+ - `apps/woodstar-notify/releases/dfe406a/linux-arm64`
+ - `apps/woodstar-status/releases/dfe406a/site.tar.gz` (kind static)
+ These validate the pipeline; W0's merged sha supersedes them for cutover.
+
+## Open questions for Paul
+
+1. **W0 shape:** relay-side fan-out flags (W0a, recommended, small) or
+ edge path routing (W0b, platform feature)? W0a can land this week.
+2. **Packaging:** keep four carlos apps (matches the repo's
+ one-binary-per-service design) or merge to one `woodstar` binary with
+ verbs (matches titogo/keymail platform shape)? Shipment staged as four;
+ non-binding.
+3. **Backup home:** move replication to `carlos-data-ie-271376211898/woodstar/`
+ (zero IAM change, kass precedent) or keep `woodstar-backups-โ€ฆ`
+ (needs a small host-role IAM addition)? Either way the old bucket keeps
+ the pre-migration history.
+4. **Wildcard `*.woodstar.app`:** accept TLS loss for arbitrary subdomains
+ (recommended; nothing live uses them) or prioritize the edge
+ wildcard/DNS-01 port first?
+5. **Sequencing:** kass stable flip is 2026-08-05 02:30 UTC and titogo's
+ TTL raise + keymail post-cutover tests are still open โ€” slot Woodstar
+ after those settle, or interleave (it's small)?
+6. **Devbox:** confirmed staying as-is? (Canary workflow needs Caddy
+ wildcard DNS-01; nothing on the flagship replaces that today.)
+7. **SSH access:** W2/W4 need commands on the prod box (no SSM). Add an
+ allow rule for `ssh ec2-user@52.30.232.124` for the executing session,
+ or Paul runs those steps by hand?