# Nearest region by default: plan summary for review

Plan: `2026-10-02-nearest-region-default.md` (rev 7). Spec:
`../specs/2026-10-01-nearest-region-default-design.md` (rev 6.2). Scope is
placement only (R8): no DNS writes, no steering, no latency routing.

## What ships, by release

| Release | Tasks | What changes for people |
| --- | --- | --- |
| Box release | 1, 1b | Nothing visible. Each box's edges file also says whether it takes ordinary instances (`serves_shared_pool`, `accepts_inbound`) and which secrets key it holds (`secrets_key_fp`). |
| Console release, switch off | 2 to 17, 23 | Apps gain a stored region (the home for now). `GET …/regions` gains place names and `app_default` (with `region_known`, `placement_known`, `instance_domain`). A new guess endpoint. An explicit create into a region whose boxes provably lack the secrets key is refused (C31). Nothing non-home is written. |
| Switch on (flagship) | none (config) | New apps get the region nearest the person (timezone, then IP, then home), shown in a picker when two or more regions are eligible. New instances default to their app's region. |
| CLI release | 18 to 22 | `--region` on `apps create`, `deploy` (new apps only) and `canary`; the CLI sends a timezone hint and prints the place. |
| Docs | 24 | `docs/site/cli.md` flags; README release checklist, floor version and "Turning app regions on". |
| Later, own repo | none here | titogo adopts the guess endpoint and place names. |

## Tasks (one line each)

0. Copy inventory and review; write the approved copy record. Nothing else starts before it.
1. Boxes publish shared-pool and inbound eligibility, from the reconciler's own predicates.
1b. Boxes publish their secrets-key fingerprint (R9).
2. `internal/geo`: points, distance, nearest pick.
3. `internal/geo`: embedded tz zone table and country points.
4. AWS region table and its 34 place names.
5. Optional GeoIP locator (MaxMind format, pinned test fixtures).
6. Console page copy.
7. Console API messages, including the empty-list variants.
8. Store: `apps.region` and `apps.region_source`, carried through every rebuild.
9. Console config: the switch, the named home, the expected secrets fingerprint, test seams.
10. Strict edges read, region candidates, one-box eligibility (incl. R9), app policy.
11. The guess, with its invariant property test.
12. Region resolution inside `claimApp`; the API create door takes region and timezone.
13. The new-app page picker and browser guess route; no-picker form byte-identical.
14. The instance create door: fill from the app region, eligibility, the R9 refusal, place names.
15. `app_default`, place names and `POST …/regions/guess`.
16. The Instances tab region lines.
17. The roll-forward discrepancy log.
18. CLI copy.
19. Timezone detection on the person's machine.
20. Create-app wire, `apps create --region`, the onboarding line.
21. `deploy --region` on an existing app, checked before anything ships.
22. `canary --region`, read-only checks before any write; instance output names the place.
23. The region service-gate script, proved against a real backhaul.
24. Docs and the release checklist.
25. Whole-branch gate (build, vet, gofmt, tests, five cross-compile targets, copy checks).

## The copy-review gate (Task 0)

Every string the plan writes is reviewed with the copy-review skill before
any product file holds it, and Task 25 proves the approved text landed byte
for byte. The inventory is **121 strings**:

- 44 user-facing messages and labels (C and R entries, including C16b to
  C19b and C30c added during review);
- 34 place names (C23);
- 10 doc passages (D1 to D10);
- 20 operator log lines (O1 to O20);
- 13 script, usage and fixture strings (S1 to S3, G1 to G6, U1 to U3, F1).

Two notes for the review: C25's draft says `carlos canary remove`, but the
verb is `carlos canary rm`; and C24 needs DB-IP's required attribution
wording confirmed.

## Rollout (plan "Rollout"; spec §17)

| # | Step | Needs Paul |
| --- | --- | --- |
| 1 | Audit manifests' `regions`; boxes publish their AWS region; set `CONSOLE_HOME_REGION` and `CARLOS_SECRETS_PUBKEY` on the console; name the floor version in README, release notes and the roll recipe. | approve the console config |
| 2 | Stage `CARLOS_SECRETS_KEY` on every box that should take placed instances. **Done for the flagship (2026-10-03).** | only for other deployments |
| 3 | Box release everywhere; check every `secrets_key_fp` equals the console's. | approve the production roll |
| 4 | Console release with the switch unset. | approve the production roll |
| 5 | Dev: switch on, browser and API creates, `/regions` diff, service gate. | no |
| 6 | Production service gate with the switch off: probe, deploy, gate from outside and through every edge. | no (report to Paul) |
| 7 | Flagship: switch on. The R7 rollback floor applies from here. | **yes: his call** |
| 8 | CLI release. | approve the release |
| 9 | titogo adopts the guess and names (its own repo). | yes |

## Risks

- **A placed instance on a box without the secrets key runs with no config,
  secrets or store** (R9, the kpmg outage). Automatic choices need the key on
  one box and explicit creates are refused when it is provably absent.
  Rollout step 3 checks the fingerprints. The flagship now has the key
  everywhere.
- **Rolling the console back below the floor** makes non-home apps create
  instances at home again. The rule is a runbook floor (R7), and the
  discrepancy log (Task 17) catches what slips through.
- **No latency routing yet** (R8). A non-home instance is reached through the
  home's DNS and the backhaul until the follow-on spec. The service gate
  (Task 23) proves reachability, not speed.
- **Mixed versions.** Every wire change is additive. An old CLI is treated as
  sending no hints. A new CLI against an old console skips deploy's check,
  and refuses `canary --region` rather than guess.
- **Guess quality.** Timezone first, then a GeoIP database if configured, then
  home. The tz table is a release-checklist refresh; a stale one only misses
  new zone names.
- **Local test environment.** On this machine the root package's full suite
  is blocked by the operator's `~/.carlos/config`, and a few `internal/edge`
  and `internal/activator` tests fail identically on the unmodified base. CI
  is the real gate (Task 25).
- **Review size.** 121 strings is a long copy review, and it gates every
  task after Task 0.
