carlos / platform Public

#162: spec revision 3 — custom domains are the half that can actually serve

Summary

the plan review found the account-host path unusable by its only
requester (latency steering is refused on a pooled record, and titogo is
pooled), while custom domains need no steering change at all because they
already CNAME to edge.<apps-domain>. Paul put custom domains back into the
first landing. Also: protectedFieldsEqual keeps Role and Home only (it
cannot reach an old binary, and Replicas there would refuse the new door's
own PATCH), the rotation's second key comes from the pass's own memory,
and a replica cannot live in the deployment's home region.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ed49ec3db921b4ab2444315ab2ac9417a3b03deb parent b67dbcb
1 file changed, +84 −15
  • docs/superpowers/specs/2026-09-21-replica-placements-design.md +84 −15
diff --git a/docs/superpowers/specs/2026-09-21-replica-placements-design.md b/docs/superpowers/specs/2026-09-21-replica-placements-design.md
index c859b4a5e..986ad702f 100644
--- a/docs/superpowers/specs/2026-09-21-replica-placements-design.md
+++ b/docs/superpowers/specs/2026-09-21-replica-placements-design.md
@@ -1,6 +1,6 @@
# Replica placements: an account served locally in a second placement
-Design record, 2026-09-21, revision 2. The platform half of amadan
+Design record, 2026-09-21, revision 3 (2026-09-22). The platform half of amadan
discussion carlos/platform #162, *A route can carry replica placements,
served locally instead of hairpinned*, filed by titogo for its edge-checkout
work (titogo `docs/superpowers/specs/2026-09-21-edge-checkout-design.md`,
@@ -92,6 +92,13 @@ the home region has `Region == ""` and a literal comparison never fires.
lives at the door, which knows `s.cfg.HomeRegion` and compares through
`homeRegionOut` (§6).
+One consequence of those two paid-for decisions, stated because it will
+otherwise be discovered at a door **(fable A18)**: a replica **cannot live
+in the deployment's own home region**. The console normalises that region
+to `""` on a record, and a replica record with an empty `Region` is
+refused by the rail above. The door says so in those words rather than
+answering "no region".
+
The replica's own record is an ordinary socket-backed instance record whose
`Region` is the replica's box, so the existing scope test mints it there
and nowhere else, with no change to `inScope` or `everywhereClass`
@@ -149,6 +156,14 @@ finds it `Adoptable` and restarts it under a name no process answers to
record-driven mint cannot produce "same socket, different host"
(`reconcile.go:348-372`).
+**Custom domains ride the same row.** For every claim whose `Route` is the
+home's host, the same pass derives `claim.Host -> replica.Host` on the
+replica's box. Today a custom-domain alias is derived only when its primary
+is a LOCAL route (`aliases.go:95-100`), which on the replica's box it is
+not — the replica's route is the local one, and the claim names the home.
+The replica derivation is therefore a second lookup in the same loop:
+claim's route is not local, but a local route names it as its `ReplicaOf`.
+
**The mark, and who writes the alias.** `route_aliases` is wholly owned by
`domains.ReconcileAliases`, which rebuilds the desired set from claims
every pass and deletes every alias it did not derive
@@ -246,8 +261,35 @@ So:
no buyer, and looks correct from every box.
- For an account reached through a **tenant-owned** domain or a custom
domain, the tenant's DNS must CNAME to `edge.<apps-domain>`. The platform
- cannot do this and does not pretend to; §12 keeps custom domains out of
- the first landing.
+ cannot do this and does not pretend to — but a custom domain that already
+ does it needs no per-host record at all, because `edge.<apps-domain>` is
+ itself minted per armed edge by `desiredState`. **That is the path the
+ first landing actually serves on** (Paul, 2026-09-22): see §5a.
+
+### 5a. Why custom domains are in the first landing
+
+Revision 2 cut custom domains and kept the account host. The plan review
+found that backwards for the deployment as it stands **(fable E2)**:
+
+- The account-host path needs `steering: "latency"` on the record, and
+ `handleAPIInstanceUpdate` refuses that on any record with a non-empty
+ `Pool` (`apiinstances.go:1208-1212`). titogo's instances live in pool
+ `tito`, so no titogo home can be steered, so no titogo home can opt into
+ a replica: the feature would land unusable by its only requester.
+- The custom-domain path needs neither. A live custom domain already
+ CNAMEs to `edge.<apps-domain>`, which already answers from every armed
+ edge, so buyers already arrive near — and public checkout, which is what
+ this whole feature exists for, lives on custom domains.
+
+So the derivation in §3 covers both: the home record's own host, **and**
+every custom-domain claim whose route is that host. The `steering:
+"latency"` rail stays as written, because the account-host half is real
+and silently serving nothing is the failure it prevents; it simply is not
+the half that carries titogo's traffic.
+
+The pool rail itself is not reopened here. Whether latency steering should
+be available to a pooled app is a separate question with its own
+rationale to recover, filed as its own discussion.
- **Deployment prerequisite:** `CARLOS_STEERING_ZONE_ID` is not armed by
any root module in `infra/` today (only `modules/region-host` declares
the variable, `variables.tf:153`). Until it is, replica placements are
@@ -339,9 +381,24 @@ predates these fields reads a record without them and writes it back
without them — `replicas` vanishes from a home (bypassing the refusal
above), `role`/`home` vanish from a replica, which clears `replica_of` and
sweeps the alias on the next pass, silently. Tito's console is pinned on an
-older build today. So `Role`, `Home` and `Replicas` join
-`protectedFieldsEqual`: an old writer's PATCH is refused with the
-maintenance-projection error rather than accepted **(fable B-7)**.
+older build today.
+
+Revision 2 answered this by adding all three fields to
+`protectedFieldsEqual`. The plan review showed that is the wrong seam
+**(fable A12)**: an old console runs its *own* `UpdateRecord` with its own
+copy of that function, which has never heard of the fields, so the guard
+cannot reach the writer it is for — and adding `Replicas` to it would
+refuse the new door's own `replicas` PATCH, since the value legitimately
+differs from the stored one. So:
+
+- `Role` and `Home` join `protectedFieldsEqual`. They are create-only by
+ §2 and never change, so plain equality is exactly right, and the guard
+ catches any writer in *this* binary that drops them.
+- `Replicas` does not. The PATCH door's own rails police it.
+- The rest is a **rollout rule, not code**: no replica record is created
+ for an app until every console that can PATCH that app's records runs a
+ build carrying the fields. That is a sequencing constraint on the
+ deployment, and it belongs in the branch handoff.
## 7. Sibling addressing (Ask 3): no new channel
@@ -422,10 +479,21 @@ the agent already holds the pair in memory (`homemap.go:675-683`).
**Rotation.** The two files sit on two boxes and are rewritten by two
passes, so a mesh rotation moves them at different moments; a key that is
current on one side and not yet on the other would fail every call in
-between **(fable B-11)**. So `keys` is a list, current first, previous
-second: the caller signs with the first and the receiver accepts any entry.
-The previous key is dropped on the pass after the rotation is complete. No
-`_NEXT` variable and no restart **(fable 13)**.
+between **(fable B-11)**. So `keys` is a list: the caller signs with the
+first and the receiver accepts any entry.
+
+Where the second key comes from needs saying, because the edge does not
+hold it **(fable A26)**. `homeMap.meshKeys()` returns `(current, next)` —
+the pair mirrored *during* a rotation — and after promotion it returns the
+new current and nothing else; the old key is gone. So the siblings pass
+keeps its own memory: it retains the derivative it last wrote for each
+pair and goes on publishing it for one further pass after it stops being
+derivable. During a rotation the file therefore carries the derivatives of
+both mesh keys, and after promotion it carries the new one plus the
+retired one for a pass. Since the two sides may sign with different firsts
+mid-rotation, the receiver must accept any entry in the list — which is
+what makes the order unimportant and the window survivable. No `_NEXT`
+variable and no restart **(fable 13)**.
titogo signs and shapes the lease and drain calls itself over that address
and those keys. The protocol stays entirely theirs. Two things the platform
@@ -528,11 +596,12 @@ edges-file mark; the explicit `foldHomes` case; the siblings-file pass and
its keys; the list/get/CLI surface (`role`, `home`, `replicas`, a role
column, and `docs/site/cli.md` for any new verb, per AGENTS.md).
-**Out, deliberately:** custom domains on the replica box — an alias for a
-custom domain is only derived when its primary is a local route
-(`aliases.go:95-100`), so it needs `ReconcileAliases` work *and* a tenant
-DNS change, and the account host is enough to prove the feature
-**(fable 13)**. Also out: a platform-side drain or mirror (titogo reuses
+**In, added in revision 3 (Paul, 2026-09-22):** custom domains on the
+replica box (§5a). Revision 2 cut them; the pool rail makes them the only
+half titogo can use, and they need no DNS change because a live custom
+domain already CNAMEs to `edge.<apps-domain>`.
+
+**Out, deliberately:** a platform-side drain or mirror (titogo reuses
its sandbox sync), an AU box, console cosmetics for replica aliases, and
any change to the lease protocol.