1 · Start here
How diene runs
A team makes a repo and publishes a version. Machines do the rest: deploy, DNS, sign-in and failover.
- ArgoCD draws what should run. Kargo moves one version pointer per stage. DECIDED · owner D3
- Each landscape has one name with a list of cluster addresses; a 5 s check keeps it current. DECIDED · ARCH:220, 295
- 3 gaps to settle, and 5 design changes the real test found: see "Must settle". OPEN · GAPS G1, G2, G3 TESTED · poc/REPORT.md
The whole system in one picture
Follow one release from repo to user
- Person People add landscapes and clusters to
fleetby reviewed PR. Teams make one repo per platform (nitroso.) and one per service (carbon nitroso.). Making the repo is the registration. DECIDED · owner D3; fleet.md:125tin - CI CI in the service repo publishes an image, an app chart and a primordial chart, all at one version, e.g.
1.. DECIDED · lens-repo-cicd.md:610. 1 - Kargo Kargo finds the release and writes the version into one small row file per stage, starting with pichu. TESTED · spike A:111
- ArgoCD ArgoCD, on Primordial, makes every cluster match git. Kargo moves to the next stage only when the last one is Healthy. TESTED · spike A:112-125
- T3 T3 builds clusters, databases and DNS. Every 5 s it checks each cluster and writes the address list of each landscape name. DECIDED · ARCH:295
- DNS Route 53 holds the main answers. Cloudflare holds a copy and the parent zone
atomi.. DECIDED · ARCH:227cloud - Client Apps hold one name, their home landscape's, and sign in at that landscape's Logto. On a network error they retry once. DECIDED · ARCH:240-243; lib-contracts:79
- If Primordial is down, deploys stop, but traffic and sign-in keep working. DECIDED · ARCH:49-50
What is decided, tested, proposed, open
owner rulings or the design papers
a test ran it
a suggestion; whose is named
you decide
Counted from the tags in the five chapter files, written before the whole-system test. Also 28 marked MISSING (not written anywhere) and 31 NOT TESTED or design only.
Since then: the whole-system test ran on real clusters. TESTED · poc/REPORT.md
- 10 of 12 drills passed; 2 were cut on purpose (Vercel standby, mercury).
- jaeda's positions: 3 of 6 held as written, the rest with a caveat.
- 5 design changes the test says are a must.
Read in this order
- 2DeploymentRepos, Kargo, ArgoCD, order
- 3Cluster failoverAddress lists, the 5 s loop, swaps
- 4DNS failoverThe chain, and what survives
- 5Client-side routerRetry, rescue router, address books
- 6Frontend failoverWorkers, the Vercel standby
- 7Sign-in (Logto)Deploy, tokens, many domains
- 8GlobalRegions, home landscape, data
- 9Mercury (webhooks)One intake, the 421 rule, 72 h
- 10Must settle3 gaps, 5 must-change, 48 decisions
- 11Whole-system testReal clusters, drill by drill
How to read this site
- Tags: every statement carries a small chip. Tap it to see its source. DECIDED · example: owner D3
- DECIDED = an owner ruling or the design papers. TESTED = a test ran it. PROPOSED = a suggestion; whose is named. OPEN = you decide.
- MISSING = not written anywhere yet. NOT TESTED = designed, never run.
- Words: a dotted underline means a tool or a word we made up. Tap it for a one-line meaning.
- Steppers: every flow is a picture you step through: Next, Back, or Play.
- Colours: one colour per actor, the same everywhere. The name is always written too.
Actors
- Person
- CI
- Kargo
- ArgoCD
- T3
- DNS
- Client
- Vendor
Tags
- DECIDED
- TESTED
- PROPOSED
- OPEN
- MISSING
- NOT TESTED
The running example
- Platform nitroso, service tin.
- Landscapes pichu (dev) → pikachu (staging) → raichu (prod, Singapore) + ampharos (prod, US).
- Clusters raichu-opal and raichu-ruby. Virtual landscape mew = raichu + ampharos.
How the chapters connect
- Deployment puts things on clusters. Cluster failover sends traffic to them.
- Frontend and sign-in are where a dead DNS name hurts most: a browser's first request, and signing in.
- Global is cluster failover plus sign-in across regions, plus mercury.
- The DNS proposal and the Logto test (pass 2) feed the DNS and sign-in screens.
2 · Deployment
How a version reaches users
A team makes a repo and publishes a version. Everything after that is automatic.
Who edits what
fleetis one public repo with three folders. DECIDED · owner D2; T3:450registry/: landscapes and clusters. People, by reviewed PR.machinery/: the deploy rules and the compiler chart. Nobody, day to day.platforms/: row files. Machines only: T3 writes empty rows, Kargo writes versions.
nitroso.(one per platform):carbon platform.and shared charts. The nitroso team. DECIDED · carbon.md:168yaml nitroso.(one per service): code, app chart, primordial chart. The tin team. DECIDED · T3:1003tin - Creating the repo is the registration. No fleet edit. DECIDED · fleet.md:125; T3:1003
- Carbon and service repos hold no fleet knowledge and no fleet credentials. DECIDED · T3:1063
- GitHub guard:
mainneeds a reviewed PR; Kargo's bot may bypass; CODEOWNERS onregistry/. DECIDED · fleet.md:609** - Open: machine files move to their own repo,
fleet-state, so people and machines never push to each other's repo. OPEN · picks #4, default yes, untested
The two charts every service ships: DECIDED · owner D3 #5
- App chart = its pods. Goes to every cluster of the landscape.
- Primordial chart = what it needs from outside, such as a database. Goes once to Primordial.
One-time setup
One-time setup, in order
- Person By hand, once: make the Primordial machine (single-node k3s, secrets via SOPS at boot), install ArgoCD and Infisical, plant the root key. DECIDED · ARCH:893; seed-cluster.md:33
- Person Also by hand, outside Primordial so they survive its death: the Cloudflare zone, the Route 53 zones and the R2 "ledger" bucket where T3 keeps its memory. DECIDED · ARCH:125-127; T3:153
- Person Apply one file by hand:
machinery/. Later registry edits needed no hand apply. DECIDED · fleet.md:48 TESTED · spike A:49fleet-root. yaml - ArgoCD ArgoCD syncs
registry/andmachinery/and installs T3 first; its chart carries the record types. The sync-wave numbers are not written yet. DECIDED · ARCH:893-906 MISSING · sync-wave numbers - T3 T3's cluster controller builds each registered cluster, gets its LB address and registers it in ArgoCD. DECIDED · ARCH:893-906
- ArgoCD ArgoCD puts the base charts on each cluster (gateway, secrets sync and more). cert-manager gets TLS certificates. DECIDED · ARCH:893-906
- T3 T3's traffic controller fills the DNS address lists once clusters are healthy. DECIDED · ARCH:893-906
- T3 First platform: Platform record, then an Infisical project, then one Logto per landscape. DECIDED · ARCH:893-906
- ArgoCD Then the other services, frontends, mercury and the edge address books. Then the first user finds a landscape, signs in and gets a home. DECIDED · ARCH:893-906
- Records park and wait on a status until T3 is up; nobody orders them by hand. DECIDED · T3:15-16
- T3's controllers turn on in this order: cluster → traffic → platform → dependency → webhook → cf-deploy. DECIDED · T3:3516
Register a landscape and a cluster
Register a landscape, then a cluster
- Person A PR adds
fleet/. DECIDED · ARCH:912registry/ landscapes/ ampharos. yaml - Its region is an airport code from an allow-list, e.g.
IAD. A missing (code, provider) pair is refused:RegionUnsupported, nothing built. DECIDED · owner D1; T3:437-444, 874 - Person Each platform opts in by adding
ampharosto its ownplatform.. DECIDED · ARCH:912yaml - Person A PR adds
fleet/: landscape, mark, provider, cloud account,registry/ clusters/ raichu-ruby. yaml traffic. DECIDED · fleet.md:211 PROPOSED · field names, MASTER:283 - T3 T3 builds it through the cloud's own API (no Crossplane) and gets a fixed LB address. DECIDED · T3:813-816
- T3 T3 writes an ArgoCD cluster Secret with labels
landscape=raichu,mark=ruby: the only link between the registry and ArgoCD. It also plants one bootstrap secret per platform. DECIDED · T3:816 TESTED · spike A:38 OPEN · O7, default adopt - ArgoCD ArgoCD deploys every service at its current version. Measured: 12 s after the Secret. TESTED · spike A:130
- Where a cluster's cloud account lives is open. Default:
fleet/. OPEN · O6, T3:601registry/ accounts/ <vendor>-<name>. yaml
Add a platform
Add a platform: just create the repo
- Person The team creates
AtomiCloud/from the template. That is all. DECIDED · fleet.md:125nitroso. carbon - It holds
platform., e.g.yaml landscapes: [pichu, pikachu, raichu, ampharos]andstages: [pichu, pikachu, [raichu, ampharos]]. DECIDED · carbon.md:168 - ArgoCD An AppSet that scans for
*.repos finds it and makescarbon platform-nitroso. Measured: 13 s. DECIDED · fleet.md:125 TESTED · spike A:56 - ArgoCD That deploy has two sources: the compiler chart from
fleet@machinery-stableandplatform.. It renders the Platform record, one Kargo Project (a Warehouse and Stages per service) and the AppSets that turn row files into deploys. DECIDED · ARCH:695-704; T3:976 OPEN · O1 defaultyaml - T3 T3 makes one Infisical project for nitroso, one environment per landscape. Carbon's own charts hold what the platform shares. DECIDED · T3:939; owner D9
- Names shorter than 5 letters are refused. DECIDED · T3:988
In the whole-system test: new platform = create a repo, new service = create a repo: 0 fleet edits by people. A new cluster = one registry file; all apps Healthy on it ~90 s later. TESTED · drills 1, 2, poc/REPORT.md
Add a service
Add a service: just create the repo
- Person The team creates
nitroso.with two charts. No fleet edit. DECIDED · T3:1003tin - T3 T3's materializer sees the
nitroso.prefix and writes one empty row per landscape. DECIDED · T3:1009 - The empty row has fixed keys, because Kargo can only change keys that exist:
pin: {chart: "", image: ""}. DECIDED · T3:1018-1025 - ArgoCD ArgoCD makes tin's Kargo pipeline. Measured: 10 s. TESTED · spike A:166
- Nothing deploys yet: an empty pin is quiet, with no errors. TESTED · spike A:106
- Row layout is open. Tested:
platforms/. jaeda's pick:nitroso/ landscapes/ pichu/ tin. yaml platforms/, one folder per service. OPEN · picks #3, default bnitroso/ tin/ pichu. yaml - The service list file
services.is dropped by default; the list comes from the row names. The old design was refused by ArgoCD. OPEN · O1, default dropped TESTED · spike A:61-73, 157-169yaml
Release a version
Release tin 1.0.1, stage by stage
- CI CI publishes the image, app chart and primordial chart, all
1.. DECIDED · lens-repo-cicd.md:610. 1 - CI CI pings Kargo to look now; otherwise Kargo looks every 5 min. With a ping, Freight came in 3 s. PROPOSED · spike A:213; not yet in T3 TESTED · spike A:173-190
- Kargo The Warehouse makes Freight only when all three are
1.. Half a release moves nothing. DECIDED · T3:981 TESTED · spike A:1210. 1 - Kargo Kargo writes
1.into pichu's row: clone, edit, commit, push. TESTED · spike A:1110. 1 - ArgoCD ArgoCD sends the primordial chart to Primordial (once) and the app chart to every pichu cluster. T3 builds or updates tin's database from the primordial chart. TESTED · spike A:112 DECIDED · ARCH:747-751
- Kargo Kargo waits until every pichu deploy (clusters and primordial) is synced and Healthy. TESTED · spike A:125
- Kargo Kargo writes
1.into pikachu's row. Only tin's rows change; other services are untouched. TESTED · spike A:113 DECIDED · ARCH:728-7350. 1 - Kargo Then raichu and ampharos together. Parallel stages were only render-checked. NOT TESTED · render-checked only, spike A:88-91
- Kargo pichu never turns Healthy (the test used a missing image). Kargo stops at this stage. TESTED · spike A:118-120
- pikachu, raichu and ampharos keep the old version. Nobody has to act fast. TESTED · spike A:118-120
- Each stage step can be a bare name (automatic, health-gated) or
{landscape, gate: manual, soak: 1h, verification}. Only the bare form ran live. DECIDED · fleet.md:410-430 TESTED · spike A:91 - Frontends ride the same pointer; their primordial chart carries a
CloudflareDeployinstead (see Frontend). DECIDED · ARCH:737-746 - No git webhooks were wired in the test. Production should use them; otherwise budget 30 s per git hop and up to 5 min for Freight. PROPOSED · spike A:189-190
In the whole-system test: v1 → v2 everywhere in 112 s. A broken v3 stopped at pichu with 0 user errors; the fix rolled out in 112 s with no human step. TESTED · drill 3, poc/REPORT.md
How long each hop took (measured)
| Hop | Measured | What sets it |
|---|---|---|
| fleet-root re-applied → platform deploy exists | 13 s | scan every 30 s |
| T3 empty rows pushed → Kargo pipeline | 10 s (rows), 25–28 s (old list file) | ArgoCD git check every 30 s |
| registry PR merged → record on Primordial | 65 s | fleet-root re-check |
| cluster Secret → service Ready on new cluster | 12 s | ArgoCD reacts at once |
| new artifact → Freight | 98 s to ~5 min; 3 s with a ping | Kargo minimum 5 min |
| Freight → pin committed | ~1 s | Kargo |
| pin committed → deploys at new version | 2–27 s | ArgoCD git check |
| pichu Healthy → pikachu promotion | ~3 s | Kargo |
person's values: edit → live | 16 s | ArgoCD git check |
| publish → last stage Healthy (2 stages) | 63 s | all of the above, with a ping |
- No git webhooks were wired in the test. Production should use them; otherwise budget 30 s per git hop and up to 5 min for Freight. PROPOSED · spike A:189-190 DECIDED · direction for ArgoCD: fleet.md:441
The order of the two charts
Nothing makes the app wait for its database today. One Kargo pin moves the primordial and app deploys in the same promotion. TESTED · spike A:112
tin's very first release: database vs app (gap G3)
- Kargo Kargo writes
1.into pichu's row. TESTED · spike A:1120. 1 - ArgoCD ArgoCD sends the primordial chart (asks T3 for a Postgres) and the app chart at the same time. Nothing orders them. TESTED · spike A:112
- ArgoCD db-init runs before the pods update. It cannot reach the database, or its secret does not exist yet: red. DECIDED · rules:665-670; goals/dotnet-api.md:289
- What happens next is not written: does the job retry? does ArgoCD retry the sync? MISSING · no syncPolicy.retry in T3, fleet.md or ARCH
- T3 Meanwhile T3 builds the database and writes the secret; the secret sync copies it, about 1.5 min on a fresh cluster. TESTED · spike B:61, 132
- Kargo The gate could pass on app health alone: ArgoCD has no health rule for the five dependency records. MISSING · only CloudflareDeploy health is defined, T3:3352
What exists instead:
- Every app chart has a
db-initjob that runs before the pods update: it checks the database, key-value store and file store, runs schema migrations, seeds data. DECIDED · rules:665-670 - Pod health checks never depend on the database. DECIDED · rules:689
- During a database move, schema migrations are refused. DECIDED · T3:2075
jaeda's position on G3 app waits for its database PROPOSED · jaeda, POSITIONS.md
(a) ArgoCD custom health for Database / KeyValue / Cache / Store / Email (Healthy only when the record is Ready and its secret is written); (b) the primordial app syncs before the app chart (sync wave or Kargo step order), and the Kargo stage gate needs BOTH healthy; (c) the app pod waits for its secret (it cannot start without it) and db-init retries with backoff. No human step.
In the whole-system test: the first release of a new service with a database was up 98 s after the tag, with 0 human steps. Pods waited in ContainerCreating until the secret came. TESTED · drill G3
Settings, three layers (last wins)
- Chart defaults in the service repo (
values.). DECIDED · goals/dotnet-api.md:171yaml - Per-landscape file in the chart, e.g.
values.:raichu. yaml type: planetscalein raichu,type: neonin pichu. DECIDED · dotnet-api:171 OPEN · picks #7, default a - Emergency override in the row file's
values:block. Short-lived; a check flags any override older than 7 days; Kargo never touches it. Measured: live in 16 s. DECIDED · fleet.md:364-367 TESTED · spike A:114-116 - Not tested: how the AppSet picks
values.out of the published chart. MISSINGraichu. yaml
Rollback
- Service rollback: promote an older Freight in Kargo. Written for Workers; for normal services it is implied, not written. DECIDED · ARCH:744 MISSING · for normal services
- Database schema after rollback: not written; a forward migration stays applied. MISSING
- Proposed: migrations stay backward-compatible for one release ("expand, then contract"). PROPOSED · this chapter, decision 12
- Deploy rules (compiler chart): the
machinery-stabletag never moves back; revert in a new commit and move the tag forward. DECIDED · fleet.md:376-393 - Break-glass: a person may commit straight to a row file, skipping Kargo, until the next promotion. DECIDED · fleet.md:964; T3:1076
Remove
Remove a service
- Person The team deletes or archives
nitroso.. DECIDED · T3:1035-1036tin - T3 T3 marks
OrphanedSourceon the Platform and alerts. Rows, deploys and databases stay. DECIDED · T3:1035-1036 - Person A person adds a
Decommissionrecord naming tin, typing the name again to confirm. DECIDED · T3:172, 1039 - T3 T3 checks nothing still uses it, takes a final backup, deletes the outside resources, removes the rows. Which controller removes the rows is not named. DECIDED · T3:172 MISSING
- Why so careful: in the test, deleting rows deleted the pods on every cluster and the primordial records. Fix:
preserveResourcesOnDeletion: trueon every row AppSet. TESTED · spike A:148-153 DECIDED · T3:1049-1050
- A cluster: traffic off → drain → Decommission. Removing its registry file deletes its ArgoCD entries but keeps its pods. DECIDED · T3:824 TESTED · spike A:136-137
- A landscape: re-home its users first; platforms drop it from
platform.; refused while anything uses it. What enforces that is not written. DECIDED · ARCH:914; fleet.md:162 MISSINGyaml
In the whole-system test: removing a service by Decommission: gone everywhere in 1 m 58 s, vendor databases included, after 2 fixes (a label sweep for orphans; Decommission must be a lock every controller checks). TESTED · drill 11
Secrets at deploy time
- T3 writes app secrets to Infisical at
project = nitroso,env = raichu, folder/. DECIDED · T3:227database/ maindb - Each cluster reads them through a chain that narrows at every step: Primordial store → platform store → service. DECIDED · T3:254
- Infisical down: running apps keep working; a brand-new service or cluster cannot get secrets until it is back. TESTED · spike B:99
Everything else in this chapter
The 13 holes the test found, and their fixes
| # | Hole (spike A:192-228) | Fix | In T3 goal? |
|---|---|---|---|
| 1 | ArgoCD refuses one repo at two versions in one deploy | drop services. TESTED | yes, as O1 default (T3:579) |
| 2 | That failure is silent (looks green) | alert on ComparisonError | no – unmerged fleet edit, not proven |
| 3 | Deleting rows deletes pods and databases | preserveResourcesOnDeletion TESTED + never delete rows | yes (T3:1050) |
| 4 | First promotion of a new row fails | retry on the ArgoCD step TESTED | no – unmerged fleet edit |
| 5 | Landscape with zero clusters blocks releases forever | refuse it up front | no – not proven; who checks is OPEN |
| 6 | New cluster not health-checked until next release | T3 re-runs current release on it | yes, fix not proven (T3:900, 932) |
| 7 | Kargo looks for versions only every 5 min | CI ping or webhook (3 s, TESTED) | no – unmerged fleet edit |
| 8 | Freight with mixed versions | require all three equal TESTED | yes (T3:981-983) |
| 9 | Shallow merge of values: drops keys | deep merge TESTED | no – unmerged fleet edit |
| 10 | Empty row must carry the pin keys | fixed row shape TESTED | yes (T3:1018) |
| 11 | Removing a service prunes its Kargo history | removal only by Decommission | yes (T3:1055) |
| 12 | Carbon has no image; Warehouse needs one | chart-only Warehouse | no – not proven |
| 13 | Kargo objects could race their namespace | namespace at an earlier sync-wave TESTED | no – unmerged fleet edit |
- Source for "unmerged fleet edit":
spikes/. jaeda's note says "all in the T3 goal" (lock/ PR-BODY. final. md:76-85 dm/); for 7 of 13 that is not so. see the gapsa/ flow. md:34
What can go wrong, and what happens then
| What goes wrong | What happens | Who notices | Source |
|---|---|---|---|
| Half a release published | no Freight; nothing moves | Warehouse status | TESTED |
| Broken version | stops at its stage; next stage keeps old version | Kargo stage stays Progressing | TESTED |
| Platform deploy breaks quietly | old render stays, shows green, then ComparisonError | nobody, until hole 2's alert exists | TESTED |
| Database not ready on first release | db-init red; retry not written | not defined | MISSING |
| Row file deleted by mistake | pods + databases deleted, unless the AppSet flag is set | — | TESTED · + fix |
| New cluster on a bad version | not gated until next release | — | TESTED · gap, fix not proven |
Landscape with no clusters in stages: | release stuck forever; git says the new version | — | TESTED · gap |
| Infisical down | running apps fine; new services/clusters cannot start | alert | TESTED |
| Primordial down | no deploys; traffic and sign-in keep working | alert | DECIDED · ARCH:49-50 |
| Emergency override forgotten | flagged after 7 days | probe | DECIDED |
Tested and not tested
| Item | State | Source |
|---|---|---|
| fleet-root, carbon discovery, row → deploys, empty rows quiet | TESTED · Gitea stand-in for GitHub | spike A claims 1-6 |
| Release 1.0.0 → 1.0.4 with health gate, both ways | TESTED | spike A claim 7 |
| Add a cluster, add a second service | TESTED | spike A claims 8, 9 |
Drop services. | TESTED | spike A claim 11 |
| Manual gate, soak, verification, parallel prod stages | render-checked only | spike A:91 |
| Real GitHub org + GitHub App, webhooks, private registry | NOT TESTED | spike A:232-235 |
| Real T3 (scripts played it), Decommission, OrphanedSource | NOT TESTED | spike A:237 |
Row layout by service (tin/), fleet-state repo | NOT TESTED | picks #3, #4 |
| DB-not-ready ordering on first release | NOT TESTED · not designed | — |
| Scale (one platform, two services, 4 clusters only) | NOT TESTED · beyond that | spike A:238 |
Open decisions in this chapter
| # | Question | Default | Whose |
|---|---|---|---|
| O1 | Drop services. | yes, list from row names | T3 default (tested) |
| O6 | Where a cluster's cloud account is defined | fleet/ | T3 default = jaeda pick 5 |
| O7 | One secret store per platform per cluster | adopt | T3 default (security fix) |
| O8 | Which Infisical is the real one | leave as a setting | T3 default |
| P3 | Row layout | by service: platforms/ (test first) | jaeda |
| P4 | Machine files in their own fleet-state repo | yes (test first) | jaeda |
| P6 | Where young / mature / scaled lives | on the platform (platform.), with a check | jaeda |
| P7 | Per-landscape settings | in the service's chart | jaeda |
| D-1 | Who refuses a landscape with zero clusters (hole 5) | the compiler chart | MASTER:524 |
| D-2 | Database-not-ready rule on first release | db-init retries 10 min + ArgoCD health for dependency records | PROPOSED · this chapter |
| D-3 | Schema rollback | expand-then-contract, one release backward-compatible | PROPOSED · this chapter |
| D-4 | Do people keep the two fleet edits (emergency values:, break-glass)? | yes, as written in fleet.md | PROPOSED · this chapter |
3 · Cluster failover and cluster moves
When a cluster dies or moves
Each landscape has one name with a list of addresses. A check every 5 s drops a dead cluster; apps retry once on another address.
- Measured: a dead cluster leaves DNS in 13–19 s. TESTED · spike G:141, spike D:109
- Apps that retry once on another address: 0 failed requests. TESTED · spike D:111
- Moving to a new cluster = add its address, remove the old one. No name changes. DECIDED · ARCH:317-318
The address list per landscape
| Name | Answers with | Source |
|---|---|---|
raichu. | the LB address of every raichu cluster that is traffic: true and healthy | DECIDED · ARCH:220 |
*. | an alias of the line above, so every service rides it | DECIDED · ARCH:222 |
*. (ops name) | one cluster only, for testing before users arrive | DECIDED · ARCH:223 |
<service>. | the nearest healthy landscape's addresses | DECIDED · ARCH:224, 232 |
- Every service name is
<module>.. DECIDED · ARCH:57-59<service>. <platform>. <landscape>. cluster. atomi. cloud - Example:
api..tin. nitroso. raichu. cluster. atomi. cloud - Warning: Another paper writes
api.instead. The two naming rules clash: gap G1 on Must settle. OPEN · GAPS G1zinc. nitroso. serving. cluster. atomi. cloud
- Example:
- Route 53 holds the main answers. Cloudflare holds a copy and the parent zone
atomi.. DECIDED · ARCH:227cloud - The controller writes all three: Route 53, the S8 spare, the Cloudflare copy. DECIDED · ARCH:304-306
- Cache time (TTL): real Cloudflare refused 30 and 59; 60 is the lowest. Route 53 allows 30 s, but that is not tested. TESTED · spike D:103 PROPOSED · jaeda, how:124
Try it: kill a cluster
Tap a cluster to stop it. Watch the loop, the DNS lists and three kinds of client. Time runs 10× faster.
ampharos's two clusters are an example (the proposed minimum is 2 per prod landscape, decision 18).
Clients
What the loop did
- –Nothing yet. Tap a cluster above.
What happens when raichu-opal dies:
| Time | What happens | Source |
|---|---|---|
| 0 s | raichu-opal stops answering. | example |
| 5, 10, 15 s | Probe 1, 2, 3 fails. | DECIDED · T3:2937 |
| 15 s | 3 failed in a row: T3 removes opal's address from the raichu name. | DECIDED · T3:2937 |
| about 19 s | Every Cloudflare nameserver serves the new list (p50 3.4 s). Measured end to end: 13–19 s. | TESTED · spike D:87; spike G:141 |
| up to 60 s more | Devices that cached the old list forget it (TTL 60). | TESTED · spike D:103 |
- Our app (retries once): one slow request, then another address: 0 failed. TESTED · spike D:111
- Browser: tries the next address by itself. DECIDED · ARCH:292 NOT TESTED · real browsers, how:240
- Outside caller (no retry): on real Cloudflare (TTL 60), errors for 104 s, peak 39.5 %; 6.85 % of requests failed. TESTED · spike D:109-111
In the whole-system test (drill 4): T3 decided "down" at +14 s; Cloudflare +19 s, Route 53 +23 s, DigitalOcean +75 s. Users never noticed: clients tried the next address. 3,000 requests, 0 errors. TESTED · drill 4, poc/REPORT.md
The slowest host decides: up to 75 s + TTL 60 s (position G11 held only for the fast hosts). TESTED · poc/REPORT.md
One tick of the loop
One tick of the 5-second loop
- T3 The traffic controller probes each cluster's gateway health path once, via its LB address, and stamps
status.. DECIDED · T3:2851acceptingTraffic - T3 If more than half of all clusters look down at once, it assumes its own network broke: it writes nothing and pages. A person can override. DECIDED · T3:2922-2926 TESTED · spike D:69-71
- T3 Otherwise it recomputes every wanted record (landscape lists and every mew-style name) and compares with its last write. DECIDED · ARCH:297-301
- T3 Remove after 3 failed probes in a row (15 s); add back after 3 passes plus a hold (60 s, doubling, max 2 min). Never remove the last address of a name. DECIDED · T3:2919, 2937
- DNS It writes only the difference, to Route 53, the S8 spare and the Cloudflare copy. One writer at a time (a 10 s lease). DECIDED · ARCH:304-306; T3:3005
- A person's change (traffic flip, new cluster) skips the wait and lands within one tick. DECIDED · T3:2943
- Memory in four layers: process memory → a local Redis/memcached → a snapshot in S3 → live DNS as the truth. On start it reads DNS before it writes. DECIDED · owner D7; T3:2894-2907
- The loop watches itself: a "last good tick" metric pages when stale. DECIDED · T3:3060
- Primordial down: DNS freezes at its last state and keeps serving. DECIDED · ARCH:308-309
- Two controllers at once: the lease let one write; the other wrote 0 times. TESTED · spike D:167-176
The traffic switch
traffic: true / falseis a field in the cluster's registry file,fleet/. DECIDED · ARCH:310registry/ clusters/ raichu-ruby. yaml - A person changes it by PR; the traffic controller acts within one tick. DECIDED · MASTER:287
false= leave the address list. The cluster keeps running, so old connections finish. DECIDED · ARCH:311-312true= join the list. This is the switch for a canary and a cutover. DECIDED · ARCH:312-313- Destroying a cluster is a separate act (a
Decommissionrecord), never implied byfalse. DECIDED · ARCH:315-316
Swap a cluster (blue/green)
Swap raichu-opal for a new raichu-ruby
- Person A PR adds
fleet/withregistry/ clusters/ raichu-ruby. yaml traffic: false. DECIDED · ARCH:913 - T3 T3 builds the cluster through the cloud's own API, gets a fixed LB address, registers it in ArgoCD, plants the bootstrap secret. DECIDED · T3:813
- ArgoCD ArgoCD deploys the base charts and every service at its current version (12 s after the cluster secret). T3 asks Kargo to re-check the current version on it. TESTED · spike A:130 DECIDED · fix not proven, T3:897
- Person Dark-verify: test through the ops name
*., with no users yet. Who does it and what it checks is not stated. DECIDED · ARCH:314 OPEN · C-5ruby. raichu… - Person A PR sets raichu-ruby
traffic: true: its address is added. Old and new both serve. DECIDED · ARCH:317 - Person A PR sets raichu-opal
traffic: false: its address is removed. The cluster keeps running, so open connections finish. DECIDED · ARCH:311-318 - Everyone waits: about 2 h on the same provider, 24–48 h when moving to another provider. DECIDED · ARCH:319-320
- T3 The decommission gate refuses unless traffic is under 0.1 % for 1 h and no mew-style name would be left empty. DECIDED · T3:824
- Person A
Decommissionrecord (name typed again) makes T3 destroy raichu-opal. Same database throughout; the cache is not moved. DECIDED · T3:171; ARCH:99; owner D16
- Each address change was one all-or-nothing call; real Cloudflare served it in about 3.4 s. TESTED · spike D:87
- Users see nothing: at every moment at least one address is alive. PROPOSED · jaeda, how:176
- The whole swap was run in the whole-system test: blue/green to DOKS + AWS under load, 21,600 requests, 0 errors; old clusters switched off. TESTED · drill 5
Machine, cluster or region?
| What dies | What happens | How long | Source |
|---|---|---|---|
| One machine (node) | the cloud LB stops sending to it; DNS untouched | LB health time | DECIDED · ARCH:291 |
| One cluster | the loop removes its address; browsers try the next; apps retry once | 13–19 s to leave DNS, then devices forget within the TTL | TESTED · spike G:141, spike D:109 |
| A whole region or its database | restore from backup, move the record to a new place | hours | DECIDED · ARCH:293 |
- An older paper says "~5–10 s". That cannot happen with 3 failed probes 5 s apart; jaeda proposes saying "13 to 19 s". PROPOSED · jaeda, G11
- No global load balancer: the 30–60 s DNS lag is accepted. DECIDED · owner D17
What our apps do on normal days
- The app holds one name: its home landscape's. DECIDED · ARCH:240-241
- On a network error it retries exactly once, over a fresh connection. Never on an HTTP error reply. DECIDED · lib-contracts:79
- Browsers try the next address of a name by themselves. DECIDED · ARCH:292 NOT TESTED · real browsers, how:240
- The rare case, where the name itself is dead, is the rescue router.
Outside callers
- Callers we do not control (another company's server, a script) have no rescue router. They rely on the short TTL and the 5 s loop: a stale window of up to about a minute. Fine if they retry. DECIDED · ARCH:42-47 PROPOSED · jaeda, how:238
- Nothing tells partners to retry. MISSING · GAPS G20
Everything else in this chapter
What can go wrong, and what happens then
| What goes wrong | What happens | Who notices | Source |
|---|---|---|---|
| A cluster stops answering | removed after 3 failed probes; apps retry on another address | traffic controller | DECIDED TESTED |
| A cluster goes silent (no refusal) | apps wait for their connect time limit first; no limit is set anywhere | nobody yet | OPEN · gap |
| The controller's own network breaks | more than half look down → freeze, write nothing, page | on-call person | DECIDED · (spike D:69-71) TESTED · (spike D:69-71) |
| Most clusters really die | freeze stays on; a person overrides | on-call person | DECIDED · T3:2926 |
| Two controllers run at once | lease lets one write; the other wrote 0 times | — | TESTED · spike D:167-176 |
| Primordial dies | DNS freezes at last state, keeps serving; edge files freeze | alert | DECIDED · ARCH:308 |
| Only one cluster in a landscape dies | no second address; rescue router has nowhere to go in that landscape | users see errors | PROPOSED · rule: keep 2+ clusters (jaeda, how:281) |
| Old cluster destroyed too early | gate refuses: < 0.1 % traffic for 1 h AND no empty mew name | T3 | DECIDED · T3:824 |
| Route 53 down | main and S8 spare both dead; the Cloudflare copy is likely never asked, because the delegation points devices to Route 53 | users | PROPOSED · reading (jaeda, how:195 was unsure) OPEN · (see 2.8) |
| A bad address book is published | allowlist limits harm to our own hosts; issuer never from the book | — | DECIDED · ARCH:249-253 |
Tested and not tested
| Item | State | Source |
|---|---|---|
| Health loop rules (freeze, last address, lease, hold) | TESTED · locally and on real Cloudflare DNS, pretend clusters | spike G, spike D |
| Retry once on another address = 0 failed | TESTED · with 200 pretend users | spike D:111, spike G:37 |
| Cloudflare write → all nameservers: p50 3.4 s, max 8.3 s | TESTED | spike D:87 |
| Route 53 writes, TTL 30, geoproximity | NOT TESTED · no AWS token | how:127 |
| Rescue router, Docs A/B/C | NOT BUILT · NOT TESTED | how:132 |
| Real browsers trying the next address | NOT TESTED | how:240 |
| A silent (black-hole) cluster | NOT TESTED | how:240 |
| A full blue/green swap on real clusters | NOT TESTED · design | MASTER:503 |
| A real Cloudflare rate-limit (429), least-privilege token, IPv6 | NOT TESTED | T3:3036 |
| Two DNS hosts for one zone (local lab) | IN PROGRESS · by Brief 2 | model/ |
Open decisions in this chapter
| # | Question | Default | Whose default |
|---|---|---|---|
| C-1 | The DNS chain fix (spare name on the same host as main) | see PROPOSAL-DNS. | Brief 2 |
| C-2 | TTL: 60 s everywhere, or 30 s on Route 53 and 60 s on Cloudflare? | 60 s everywhere until Route 53 30 s is tested | PROPOSED · this chapter |
| C-3 | Client connect time limit (for a silent cluster) | 3 s in every client library | PROPOSED · jaeda (how:235: 2–3 s) |
| C-4 | Minimum clusters per production landscape | 2 | PROPOSED · jaeda (how:281) |
| C-5 | Who does dark-verify, and what it checks | automated smoke test of every service on the ops name, then a person flips traffic | PROPOSED · this chapter |
| C-6 | Where the "< 0.1 % for 1 h" number is measured | gateway request metric per cluster in Grafana Cloud | PROPOSED · this chapter |
| C-7 | Cloudflare in front for faster failover than ~104 s without retry | no (owner D17: DNS movement) | DECIDED · direction; pricing OPEN |
| C-8 | Freeze rule: fleet-wide "more than half" or per landscape | fleet-wide, as written | DECIDED · text (T3:2922); scope OPEN for small fleets |
4 · DNS failover
When a DNS name itself dies
Cloudflare and Route 53 are a chain, not two spares. Today the spare name sits on Route 53 too, so a Route 53 outage kills both.
- Cloudflare holds
atomi.and pointscloud cluster.to Route 53. DECIDED · owner D36atomi. cloud - The spare base name (S8) is also on Route 53. DECIDED · ARCH:353-358 PROPOSED · finding: jaeda, how:195
- The proposal below recommends option 4: the main and the spare name each served by Route 53 + Google together. PROPOSED · levi, PROPOSAL-DNS.md OPEN · owner D36
The chain
- Route 53 down: no answers for the main name. DECIDED · owner D36
- Cloudflare zone down: the pointer to Route 53 is lost once devices forget it, usually a day or two, so a short Cloudflare outage does not hurt. PROPOSED · jaeda, how.md
- The design's spare (S8) also lives on Route 53, so a Route 53 outage kills the main name and the spare together. PROPOSED · jaeda, how:195
- The controller also writes a copy into Cloudflare, but devices are likely never sent there, because the pointer sends them to Route 53. PROPOSED · reading, jaeda; OPEN
- The domain itself breaking, or a big mistake by us in the records, can also kill the name. Our own mistake is the likeliest; the loop's guards (never remove the last address, freeze) are tested. PROPOSED · jaeda, how.md TESTED · spike D:69-71
Try it: pick what fails
What fails?
One cluster (raichu-opal) is down. For each kind of user:
Our apps (after first load)
WorksRetry once on another address: 0 failed (measured). TESTED · spike D:111
Browser, first visit
WorksTries the next address by itself; the dead one leaves DNS in 13–19 s. DECIDED · ARCH:292 TESTED · spike G:141
Outside callers
PartlyA stale window of up to about a minute; fine if they retry. PROPOSED · jaeda, how:238
Webhook providers
WorksThe intake name drops the dead cluster like any other name. DECIDED · ARCH:297-301
Sign-in
WorksSame name, other cluster, same database. PROPOSED · jaeda, how:205-206
Route 53 is down. For each kind of user:
Our apps (after first load)
LostThe main name and the S8 spare are both on Route 53. The rescue router has no live name to try. PROPOSED · jaeda, how:195
Browser, first visit
LostNo answers for the main name. DECIDED · owner D36
Outside callers
LostNo answers for the main name. DECIDED · owner D36
Webhook providers
PartlyThe intake name has no answers; providers retry (often for days). DECIDED · ARCH:342-348
Sign-in
PartlySigned-in users keep working up to 10 min, then wait; nobody new signs in. PROPOSED · jaeda, how:208-220
This is gap G2. The DNS proposal (pass 2) is meant to fix it.
Cloudflare zone (atomi.cloud) is down. For each kind of user:
Our apps (after first load)
PartlyFine while devices remember the pointer to Route 53. After that the rescue router moves API calls to the spare base name. DECIDED · ARCH:353-360 PROPOSED · jaeda, how.md
Browser, first visit
LostLost once the pointer is forgotten. Accepted. DECIDED · ARCH:332-334
Outside callers
LostLost once the pointer is forgotten; no rescue router. PROPOSED · reading, jaeda, how.md
Webhook providers
PartlyProviders retry (often for days). DECIDED · ARCH:342-348
Sign-in
PartlyAPI calls move to the spare name, but sign-in cannot follow: the issuer is baked. DECIDED · lib-contracts:150; ARCH:252 PROPOSED · reading
All of Cloudflare is down. For each kind of user:
Our apps (after first load)
PartlyThe rescue router reads the AWS copy of the address book and moves to the spare base name. DECIDED · ARCH:336-339, 350-352
Browser, first visit
LostWeb runs on Workers: lost, and the error portal too. Accepted. DECIDED · ARCH:336-339
Outside callers
LostLost once the pointer is forgotten. PROPOSED · reading, jaeda, how.md
Webhook providers
PartlyProviders retry (often for days). DECIDED · ARCH:342-348
Sign-in
PartlySigned-in users up to 10 min, then wait; nobody new signs in. PROPOSED · jaeda, how:208-220
Cloudflare Workers only is down. For each kind of user:
Our apps (after first load)
WorksAPI calls fine: the API is not on Workers. DECIDED · ARCH:329-332
Browser, first visit
PartlyDown until someone repoints the CNAME to Vercel. Who does it is not written. DECIDED · ARCH:329-332 MISSING
Outside callers
WorksThey call the API, which is not on Workers. DECIDED · ARCH:329-332
Primordial (control cluster) is down. For each kind of user:
Our apps (after first load)
WorksDNS freezes at its last state and keeps serving. DECIDED · ARCH:308-309
Browser, first visit
WorksTraffic keeps working; only deploys stop. DECIDED · ARCH:49-50
Outside callers
WorksTraffic keeps working. DECIDED · ARCH:49-50
Webhook providers
WorksMercury's config is frozen; intake keeps working. DECIDED · T3:3107
Sign-in
WorksSign-in keeps working. DECIDED · ARCH:49-50
The whole raichu region is down. For each kind of user:
Our apps (after first load)
Lostraichu users wait for a restore (hours). The rescue router does not help: it changes addresses, not landscapes. DECIDED · ARCH:254, 293
Browser, first visit
Lostraichu users wait for a restore. DECIDED · ARCH:293
Outside callers
LostCalls to raichu wait for a restore. DECIDED · ARCH:293
Webhook providers
PartlyProviders land in ampharos. Events already queued in raichu wait; no takeover rule. DECIDED · ARCH:302-304 MISSING
Sign-in
Lostraichu users wait for a restore; nobody new signs in there. DECIDED · ARCH:291-294
In the whole-system test (drill 6): Route 53 blocked, then DigitalOcean, then both: 6,600 requests, 0 errors (849 via the spare name). One host of the pair down = 0 failed lookups. TESTED · drill 6, poc/REPORT.md
jaeda's position until the proposal lands
jaeda's position on G2 DNS chain PROPOSED · jaeda, POSITIONS.md
follow model/proposal/PROPOSAL-DNS.md when it lands. Until then: the traffic controller writes EVERY host in one loop (primary, mirror, spare), the spare base must not share a DNS host with the primary, TTL 60 s on Cloudflare and 30 s where the host allows.
The proposal
Copied from model/proposal/PROPOSAL-DNS.md as it stood at 2026-11-12 07:52 UTC; a rebuild picks up later changes.
Answer first
- jaeda is right. Today's design is a chain, not two spares. Route 53 down = the main name AND the spare name die. (Details in part 1.)
- The "Cloudflare copy" in the design cannot answer. While Cloudflare points
cluster.at Route 53, Cloudflare's own copy of those records is hidden. It only helps if someone removes the pointer, and caches then take hours. PROPOSED · reading; see 1.4.atomi. cloud - Big site practice: github.com and airbnb.com run Route 53 and NS1 at the same time. linkedin.com runs NS1 + Azure DNS. TESTED · :
dig NS, 2026-11-12. - We cannot do that at the top (atomi.cloud). It is registered at Cloudflare, and Cloudflare-registered names "must remain on Cloudflare nameservers". TESTED · RDAP + DOCS.
- We can do it one level down. Cloudflare's pointer for
cluster.can list two hosts. No registrar change.atomi. cloud - Recommendation PROPOSED · levi: option 4.
cluster.is served by Route 53 + Google Cloud DNS together.atomi. cloud - The spare name moves to its own domain, on another TLD, at a registrar that is not Cloudflare, also served by Route 53 + Google.
- The traffic controller writes both hosts every tick.
- Cost (DOCS): about USD 1 to 2 a month more, plus the spare domain (about USD 15 a year, C/concepts/ARCHITECTURE.md:933). Queries are split between hosts, so the query bill barely moves.
- What it fixes: Route 53 down, AWS down, Google down: users notice nothing except a little extra lookup time.
- Lab proof TESTED · part 5: one host stopped = 60 % of lookups failed. Two hosts, either stopped = 0 of 3600 failed, slowest 0.7 s.
- New risk it brings: if the controller cannot write one host, about half the users get a stale address until it is fixed (lab: 16 of 30). Client retry-once covers it.
- What it does not fix: Cloudflare's zone, the .cloud TLD, or the Cloudflare account dying. Then the main name dies once caches expire. Apps switch to the spare name; browsers on the website cannot. Sign-in renewals and new sign-ins stop (the issuer lives under atomi.cloud), unless Logto also gets a spare address: TESTED, that works with the issuer name unchanged (LOGTO-MULTIDOMAIN.md).
- Our own bad write hits every host at once. Two hosts never help there; the controller's brakes do.
The options side by side
Hosts compared in part 2.6. "Spare" = the S8 spare base name.
Option 1: keep as is, rely on the rescue router
- Survives: one cluster dying; Cloudflare DNS for a while (apps use the spare).
- Does not survive: Route 53 answers, or all of AWS. Main and spare both die.
- Cost: 0.
- T3 work: none new.
- Risk: the one host that holds both names is a single point of failure. The issuer dies too.
Option 2: spare base on another host, registrar and TLD
- Example: spare
atomi-spare.at a non-Cloudflare registrar, served by Google Cloud DNS. Main stays Route 53 only.net - Survives: Route 53 down (apps switch to the spare), AWS down, Cloudflare zone, .cloud TLD, Cloudflare account.
- Does not survive for browsers, new sign-ins, outside callers: they only know the main name.
- Cost: Google zone USD 0.20 + queries USD 0.40 per million (DOCS https://cloud.google.com/dns/pricing). Spare domain ~USD 15/yr.
- T3 work: a Google writer for the spare zone.
- Risk: every Route 53 outage becomes a "rescue" event: slower, and sign-in renewals stop after 10 minutes.
Option 3: two hosts for cluster.atomi.cloud (spare unchanged)
- How: the Cloudflare pointer lists 4 Route 53 servers + 4 Google servers. Both hold the same records. Resolvers pick one and try the other if it fails.
- Survives: Route 53 down, Google down, AWS down: for everyone, issuer included.
- Does not survive: Cloudflare zone / .cloud / Cloudflare account (pointer lost after caches). Spare is still on Route 53 only.
- Cost: ~USD 0.20 + share of queries.
- T3 work: write N hosts per tick (part 4).
- Risk: two hosts can differ for a moment (partial write, part 5 exp. 5). Geo answers differ a little between hosts (2.5).
Option 4: option 3 + spare on its own domain, also on two hosts (RECOMMENDED, PROPOSED by levi)
- Main
cluster.: Route 53 + Google.atomi. cloud - Spare
cluster.: other TLD (<spare> .ornet .), registrar not Cloudflare, nameservers Route 53 + Google.com - Survives: any one DNS host, AWS, Google, Cloudflare zone (apps), .cloud TLD (apps), Cloudflare account (apps).
- Does not survive: Route 53 + Google at once; our own bad write; for browsers and new sign-ins, the Cloudflare/.cloud/registrar cases.
- Cost: ~USD 0.40 zones + query share + domain ~USD 15/yr.
- T3 work: as option 3, with 2 zones per host.
Option 5: a third host
- Example: spare on Google + Bunny (or NS1), main on Route 53 + Google.
- Gain over option 4: survives Route 53 + Google together. Very rare.
- Cost: a third API adapter, third geo model, more drift to watch.
- PROPOSED: not now.
Option 6: two hosts at the top (atomi.cloud itself)
- Blocked: Cloudflare Registrar names "must remain on Cloudflare nameservers" (DOCS https://developers.cloudflare.com/dns/nameservers/nameserver-options/).
- Would need a registrar transfer away from Cloudflare plus Cloudflare's "multi-provider DNS" setting (same page; plan NOT STATED).
- Gain: the website and the issuer would survive Cloudflare DNS too.
- PROPOSED: not now. Bigger move, touches the website and mail.
2.5 Geo answers on two hosts
- Route 53 (design): geoproximity, lat/long, bias 0 (C/concepts/ARCHITECTURE.md:232-238).
- Google: GEO policy keyed by Google Cloud regions; the query's client subnet (ECS) or resolver IP is matched to the nearest listed region (DOCS https://docs.cloud.google.com/dns/docs/routing-policies-overview).
- Keep answers equivalent PROPOSED:
- Per landscape, list the Google region closest to it, with the same IP set.
- When a landscape has no healthy IP: Route 53 deletes its set (design); Google drops that item. Same effect: next-nearest wins.
- Never use either host's own health checks (design: none in Route 53, C/concepts/ARCHITECTURE.md:238). Google's checks fail "open" when all are unhealthy (DOCS same page), which would cancel our brakes.
- Effect of a mismatch: near a border, Route 53 may say landscape X and Google Y. Both are healthy, so the user is served either way.
- Home landscape names (
<l>.) have no geo at all, so most traffic is not affected.cluster…
2.6 Host facts (DOCS, checked 2026-11-12 by helper myra; full list in HOSTS/HOSTS.md)
| Host | Lowest TTL | All-or-nothing batch | Price/month* |
|---|---|---|---|
| Route 53 | 0 | yes | USD 4.50 (geo queries 7.50) |
| Google Cloud DNS | 1 s | yes | USD 4.20 (geo queries 7.20) |
| Cloudflare | 60 (30 Enterprise) | database yes, edge no | USD 0 (+5 for geo) |
| NS1 | not stated | no batch call | USD 108 |
| Bunny | not stated | no batch call | USD 1 |
| Gcore | 120 free / 30 Pro | no batch call | USD 0 / 2.49 |
| DigitalOcean | 30 | no batch call | USD 0 (no geo, no DNSSEC) |
*1 zone, 50 records, 10 million queries a month, no health checks. Each host's URLs: HOSTS/HOSTS.md.
Geo by host: Route 53 country/continent/lat-long/latency; NS1 country/state/lat-long; Bunny geo + latency; Gcore geo/network; Google only by Google Cloud region; Cloudflare only via its paid load balancer.
Why Google as the second host PROPOSED · levi:
- It is the only other host with a documented all-or-nothing batch call, like Route 53. Partial writes inside one host cannot happen.
- 100 % SLA (https://cloud.google.com/dns/sla). Bunny and Gcore publish none (HOSTS/HOSTS.md).
- Weak spot: geo only by Google region (2.5).
- Second choice: NS1. The big-site pattern (github, airbnb), best geo, but USD 108 a month and no batch call.
- Cheap choice: Bunny (USD 1) or Gcore. No batch call, no SLA. Fine for a lab or the spare only.
- Route 53 rule that matters: a second change to the same zone while one is still running is refused (
PriorRequestNotComplete). Limits: 10 requests/s per account (DOCS https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html). - Route 53 DNSSEC: "multi-vendor configurations are not supported" when signing (DOCS https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-configuring-dnssec.html). Google: multi-signer NOT STATED.
- So two hosts means no DNSSEC on
cluster.. atomi.cloud is not signed today anyway TESTED. The T3 draft already notes this limit (T3/:3039-3043). OPEN only if the owner wants DNSSEC.atomi. cloud - Zone transfer (one host copying from another): Route 53 none; Google none; Cloudflare Enterprise only; NS1 yes (HOSTS/HOSTS.md). So T3 writes each host by API; no copying between hosts.
- SLA: Route 53 100 % (https://aws.amazon.com/route53/sla/); Google 100 % (https://cloud.google.com/dns/sla).
What still works, per option
Pick an option, then read down: what still works when each thing fails.
How to read it: Works Until caches expire Dead Via the spare (apps)
People: Browser, first visit · Signed-in app · New sign-in · Outside caller.
Note on S and N: the tables assume today's design (no spare address for Logto). Signed-in apps then work up to 10 minutes on a dead issuer, then wait (C/concepts/ARCHITECTURE.md:423-427). With the Logto spare address (4.4), apps keep renewing and signing in via the spare.
Option 1 (as designed)
Route 53 fails
Cloudflare DNS fails
Cloudflare whole fails
AWS whole fails
.cloud TLD fails
Cloudflare account fails
our bad write fails
Primordial fails
Option 2 (spare elsewhere)
Route 53 fails
Cloudflare DNS fails
AWS whole fails
.cloud TLD fails
Cloudflare account fails
our bad write fails
Primordial fails
Option 3 (two hosts for main)
Route 53 fails
Google fails
Cloudflare DNS fails
AWS whole fails
.cloud TLD fails
Cloudflare account fails
our bad write fails
Primordial fails
Option 4 (two hosts for main and spare; RECOMMENDED)
Route 53 fails
Google fails
Cloudflare DNS fails
Cloudflare whole fails
AWS whole fails
(AWS clusters leave DNS)
.cloud TLD fails
Cloudflare account fails
Route 53 + Google fails
our bad write fails
Primordial fails
Notes:
- B under Cloudflare: the website itself runs on Cloudflare Workers; its Vercel standby needs a Cloudflare DNS edit, so it is lost DECIDED · accepted: C/concepts/ARCHITECTURE.md:328-335.
- "AWS whole" also removes CloudFront's copy of the address books; the Cloudflare R2 copy stays (C/concepts/ARCHITECTURE.md:256-266).
- "Primordial": the controller stops, DNS freezes at its last good state DECIDED · C/concepts/ARCHITECTURE.md:307-308. A cluster dying during that time stays listed; clients retry another address.
- "our bad write": brakes cap the damage at one cluster per tick and never empty a name (T3/:195-199, T3/:3061-3065).
- until caches expire length depends on the pointer TTL we set; part 5 exp. 4.
1. The chain today
1.1 Drawing
registrar: Cloudflare Registrar (TESTED RDAP)
|
root (.) --NS 172800 s--> .cloud TLD (registry Identity Digital; servers trs-dns, Tucows) (TESTED dig)
|
.cloud --NS 900 s-----> atomi.cloud on Cloudflare (tim/brianna) (TESTED dig)
| Cloudflare's own NS TTL: 86400 s
| "does not exist" cached 1800 s (SOA minimum)
| not signed (no DNSSEC) (TESTED RDAP)
|
atomi.cloud --NS (TTL ours)--> cluster.atomi.cloud on Route 53 (DECIDED design, NOT BUILT)
Route 53 NS TTL usually 172800 s
|
+-- <l>.cluster.atomi.cloud A TTL 60 (LB IPs of healthy clusters)
+-- *.<l>.cluster.atomi.cloud CNAME -> it
+-- api.lithium.aldehyde.<l>.cluster.atomi.cloud = the Logto issuer
spare (S8): <l>.cluster.<backup-base> on Route 53, same IPs (DECIDED design, NOT BUILT)
domain, TLD and registrar not chosen yet (OPEN)
Sources for the design lines:
- Route 53 primary, Cloudflare "SECONDARY/mirror": C/concepts/ARCHITECTURE.md:226-229, T3/:139-140.
- TTL 60: T3/:2859-2861 (newer, after spike D). ARCHITECTURE says "30-60s": C/concepts/ARCHITECTURE.md:230-231 (older, 2026-10-29).
- Spare base S8: C/concepts/ARCHITECTURE.md:353-362, C/goals/fleet.md:133-139.
- Issuer name: C/goals/aldehyde-logto.md:99-100.
- Today:
cluster.has no NS records yet TESTED ·atomi. cloud dig @tim.<wbr>ns.<wbr>cloudflare.<wbr>com cluster.<wbr>atomi.<wbr>cloud NS= no answer. Nothing is built. - Route 53 NS TTL: not stated in its docs page (DOCS https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/SOA-NSrecords.html). Seen live: 172800 on reddit.com and hashicorp.com, other values on zones that changed it TESTED · dig.
- Route 53 "does not exist" cache = the smaller of SOA TTL (900) and SOA minimum (86400 by default) = 900 s (DOCS same page).
1.2 Sources disagree (both shown)
- ARCHITECTURE (2026-10-29): Route 53 is primary, Cloudflare is the mirror (C/concepts/ARCHITECTURE.md:226-229).
- C/goals/fleet.md:134-136 says the spare "mirrors the CF
cluster.A-set", as if Cloudflare served it.atomi. cloud - T3 draft (newer, PR 71): the loop "diffs against cached CF state, and writes CF only on delta" (T3/:2853-2856), but its credentials still say Route 53 primary (T3/:139).
- None of them says how resolvers reach Route 53. The only way is a pointer (NS records) in the Cloudflare zone. jaeda assumed that. So did I. OPEN: confirm.
1.3 Which outage breaks what, and when
| What dies | Main name | Spare name | When |
|---|---|---|---|
| Route 53 answers | dead | dead | within 60 s (record TTL) |
| Route 53 writes only (like AWS us-east-1, 2025-10-20) | frozen, still answers | frozen | at once; dead clusters stay listed |
| Cloudflare DNS | works while caches hold the pointer | works | cold resolvers at once; warm ones after the pointer TTL |
| .cloud TLD | works while caches hold atomi.cloud | works (if not .cloud) | 900 s to 1 day |
| Cloudflare account (registrar + DNS) | lost or hijacked | works (if another registrar) | at once |
- Route 53 writes: DOCS: during a us-east-1 problem "DNS resolution and health checks ... continue", but record changes may fail. Opt-in "accelerated recovery" moves writes to another region. https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/accelerated-recovery.html
- Cloudflare DNS: the pointer TTL is ours to set. In the lab a warm resolver kept working for that TTL, then failed; a fresh one failed at once (part 5). Whether real resolvers keep Route 53's own NS list (172800 s) longer: NOT TESTED.
1.4 Why the Cloudflare "mirror" cannot answer PROPOSED · reading
- DNS rule: once a zone points a name at other servers, data under that name in the parent is not served (RFC 1034 §4.2.1, "occluded"). Cloudflare behaviour on this: NOT TESTED.
- So the mirror only works if a human deletes the pointer. Resolvers keep the old pointer for its TTL, so it takes minutes to days.
- Keeping it costs writes and risk (a stale mirror going live by mistake). PROPOSED: drop the mirror and use a real second host instead. OPEN.
4. What changes, if the owner says yes to option 4 (PROPOSED)
4.1 Order to build
- OPEN, owner: pick the spare domain name, TLD (not .cloud) and registrar (not Cloudflare).
- Create the Google Cloud DNS zones:
cluster.and the spare zone. Set SOA minimum low (60 to 300 s) on every host, so a too-early lookup is not cached as "missing" for long.atomi. cloud - Build the controller's host interface + Route 53 and Google writers. Run the lab test against them.
- Fill both hosts; check every nameserver of both hosts serves every name.
- Only then add Google's 4 servers to the Cloudflare pointer for
cluster., and set both hosts at the spare's registrar.atomi. cloud - Edge docs, allow-list, gateway names and certificates for the spare (4.3).
- Sign-in changes (4.4).
4.2 Traffic controller
- One loop, unchanged: probe once, decide once, brakes once (on the wanted state, never per host).
- Then one write per host, in parallel. Each host's write is all-or-nothing on that host (DOCS above). There is no all-or-nothing across hosts.
- Each host keeps its own "last known live" copy. The diff is per host.
- A host that fails: alert
HOST_WRITE_FAILED, retry it next tick with its own diff, keep writing the others. - Every refresh: read every host; compare each with the wanted state and with each other; alert
HOSTS_DIFFER; repair. - Route 53: one change at a time per zone; wait for INSYNC before the next.
- A host down for long: page a human. Never edit the Cloudflare pointer automatically.
- New names: create on all hosts, wait until every server of every host answers, then use (spike D rule, T3/:2989-2996, now for N hosts).
- Lease: unchanged, one leader writes all hosts (T3/:3003-3015).
- Drop the Cloudflare "mirror" writes (1.4). OPEN.
- Set the Cloudflare pointer TTL for
cluster.long PROPOSED · 86400 s, so short Cloudflare DNS outages stay invisible to warm resolvers (lab: they last exactly that TTL).atomi. cloud - Stale-host rule PROPOSED: a host that misses writes for 5 minutes pages a human; the controller keeps retrying it. Half of fresh lookups may get stale addresses meanwhile (lab exp. 5), so client retry-once stays required.
4.3 Rescue router, edge documents, gateways
- Doc C candidate list: main address first, spare after (already designed, C/concepts/ARCHITECTURE.md:266-272). Only the spare root changes.
- Baked allow-list: add the spare root (C/concepts/ARCHITECTURE.md:248-253).
- Doc A host "CloudFront via the R53 rescue domain": becomes "via the spare domain" (now on two hosts).
- Gateways: the spare wildcard certificate exists in the design (C/goals/charts/kgateway.md:149-154). Gap: routes must also accept the spare host names. Not stated in the corpus.
4.4 Sign-in (the issuer)
- The issuer
api.lives in the main zone. Option 4 keeps it alive through Route 53, Google or AWS dying.lithium. aldehyde. <l>. cluster. atomi. cloud - It still dies with Cloudflare's zone, .cloud or the Cloudflare account.
- Fix PROPOSED · TESTED on a local Logto: add a spare address
api.for the same Logto; keep the issuer string unchanged; client and service libraries fall back to it. Then S and N in the matrix become via the spare for apps. Details: LOGTO-MULTIDOMAIN.md option (iii).lithium. aldehyde. <l>. cluster. <spare>
5. Local lab (TESTED, LAB/REPORT.md)
Built by helper alan, 2026-11-12. Real servers in docker: NSD as the parent, PowerDNS as host 1, BIND as host 2, unbound 1.22 as the resolver. The spike D controller, changed to write both hosts each tick. A client sends 20 lookups a second. One run per test. USD 0. All cleaned up (LAB/CLEANUP.txt).
| Test | One host | Two hosts |
|---|---|---|
| Host stopped: lookups failed | 1798 of 3000 (60 %) | 0 of 3600 |
| First failure after stop | 45 s (cached answers ran out) | none |
| Slowest lookup | 12 s timeout | 0.7 s (p99 under 4 ms) |
| Fresh resolver (first visit) | no answer in 16 s | answer in 0.38 s |
- Parent stopped (= Cloudflare zone dead; both child hosts fine):
- Pointer TTL 300 s: warm resolver kept working 294 s, then failed.
- Pointer TTL 60 s: kept working 50 s.
- A fresh resolver failed at once in both runs.
- So the pointer TTL we set in Cloudflare = how long a Cloudflare DNS outage stays invisible. PROPOSED: set it to 86400 (one day). Cost: adding or removing a host then takes a day to reach everyone.
- NOT TESTED: whether resolvers keep the child's own NS list longer (Route 53 uses 172800 s).
- Partial write (controller could not write host 2; a cluster died):
- Host 1 updated in 14 s. Host 2 kept the dead address.
- 16 of 30 fresh resolvers got the dead address (they happened to ask host 2). 0 lookups failed: the answer was valid, just stale.
- The controller raised
HOST_WRITE_FAILEDevery tick andHOSTS_DIFFER; it fixed host 2 4.9 s after the path came back. - Meaning: a stuck host = about half the users get stale addresses until it is fixed. Client retry-once on another address covers it (spike D: 0 % failed with retry).
- Cluster death, both hosts writable: both hosts updated by 14.2 s; last stale answer 39 s after death. 0 failed lookups.
- Each host stays all-or-nothing: a bad item made PowerDNS (HTTP 422) and BIND (NOTZONE) apply nothing. There is no all-or-nothing across hosts.
- Brakes held with two hosts: last address kept, freeze on mass failure, stable re-add.
- Differs from real life: no anycast, no real geo, no real API delay, one resolver type, no DNSSEC, no lease.
6. Not tested
- Real Route 53, real Google Cloud DNS, real public resolvers with two hosts (needs owner tokens).
- Cloudflare's behaviour with records under a pointer (1.4).
- Real geo answers on two hosts.
- DNSSEC with two hosts.
Facts that shape any fix
- A name looked up before it exists is remembered as "no such name" for 1,800 s on our zone. TESTED · spike D:106
- DNS changes reach users within about TTL + 3 s (TTL 60 on Cloudflare: worst 57 s on 8.8.8.8). TESTED · spike D:93-102
- Cloudflare write → all its nameservers: p50 3.4 s, max 8.3 s. TESTED · spike D:87
- Route 53 writes, TTL 30 and geoproximity are not tested (no AWS token). NOT TESTED · how:127
- A third DNS host only helps if Route 53 and Cloudflare are both down. PROPOSED · jaeda, how.md
Other DNS hosts, and keepalived
Yes, there are other DNS hosts. What we need from one: an API, a short cache time (30 s), and routing by location.
| DNS host | By location | Note |
|---|---|---|
| Route 53 (main, decided) | yes | pay per zone and per query |
| Cloudflare (copy, decided) | paid add-on | 60 s lowest cache time below Enterprise |
| Google Cloud DNS | yes | close match to Route 53 |
| NS1 (IBM) | yes, the strongest | costly |
| Bunny DNS | yes | very cheap |
| Gcore DNS | yes | cheap |
| DigitalOcean DNS | no | free, we already have the account |
- From memory, not checked today. Exact lowest cache times and prices need a look before any swap.
- A third host only helps if Route 53 and Cloudflare are both down. The rescue router already covers that case.
keepalived: lets two machines share one address. If the first dies, the second takes the address in 1 to 3 seconds.
- It only works inside one network, like one rack or one data centre.
- Clouds block the trick it uses; they give you their own load balancer instead, which does the same job. That is our "a node dies, the load balancer moves on" step.
- It cannot move an address between regions or between clouds. So it does not help with a dead cluster.
- Useful only if we ever run our own bare machines.
5 · Client-side router
The rescue router in our apps
It sleeps. It wakes only when a name cannot connect at all, then tries spare addresses from a saved address book.
- Designed, not built and not tested. NOT TESTED · how:132
- It never changes the landscape or the sign-in address. DECIDED · bun-api-engine.md:99; ARCH:252-253
- Every address must end in one of our own domains, or the whole book is refused. DECIDED · bun-frontend-utils.md:54
Two things share the word "failover"
| Address list update | Switch to a spare name | |
|---|---|---|
| When | a cluster dies or is replaced | our DNS name itself is dead |
| Who | the traffic controller, automatic | only the rescue router in the app |
| How often | routine | rare |
| Sign-in affected | no | yes (see Sign-in) |
- Source: jaeda's wording. PROPOSED · jaeda, how:184-188
The three edge files
| File | Holds | When the app reads it |
|---|---|---|
| Doc A | the list of places that hold the books (~200 bytes) | all the time, in the background |
| Doc B | which landscapes a platform runs in (names only) | at sign-in, when the user has no home yet |
| Doc C | the full address book per service, main first, spare after | only when the rescue router wakes |
- T3 writes them from existing records; nobody edits them. DECIDED · ARCH:256-283; T3:2863
- Each file is written twice: Cloudflare R2, and AWS S3 behind CloudFront. Also reachable on
*., which needs none of our DNS. DECIDED · T3:2878; ARCH:261-264cloudfront. net - No signing; the baked allowlist replaces it. DECIDED · ARCH:282-283
- Each copy of Doc A lists the other places, so the set of places can grow without a new app release. DECIDED · ARCH, via how.md
Walk: a client when its address dies
A client when its address dies
- Client The app calls
api.. The connection fails: no answer at all. DECIDED · ARCH:240-241tin. nitroso. raichu. cluster. atomi. cloud - Client It retries exactly once, over a fresh connection. Browsers also try the name's next address by themselves. Most outages end here. DECIDED · lib-contracts:79; ARCH:292
- Client The connect fails again, so the rescue router wakes. Any HTTP reply would not wake it. DECIDED · lib-contracts:76
- Client It reads the address book (Doc C) saved on the device: main address first, spare after. It keeps the last good book and never takes an older one. DECIDED · ARCH:247-249, 271-277
- Client Every address must end in one of our own baked roots (
*.plus rescue roots), checked at use time. DECIDED · bun-frontend-utils.md:54cluster. atomi. cloud - Client It tries the next address. It changes only the address, never the landscape, and never the sign-in address. DECIDED · bun-api-engine.md:99; ARCH:252-253
- Client One address does not end in our domains, so the whole book is refused. A bad book can only point at our own hosts. DECIDED · bun-frontend-utils.md:54; ARCH:249-253
What it survives
| What breaks | What still works |
|---|---|
| Our own DNS zone | CloudFront's own name (needs none of our DNS) |
| All of Cloudflare | the AWS copy |
| All of AWS | the Cloudflare copy |
| Primordial (our control cluster) | the books freeze at the last state; apps still fail over |
| The app is offline or starts cold | the book saved on the device |
- Source: jaeda's reading of the design. PROPOSED · jaeda, how.md
What it does not survive: PROPOSED · jaeda, how.md
- Cloudflare and AWS down together.
- A dead home region: that is data to restore, not an address problem. DECIDED · ARCH:254
- An app unopened so long that every place on its old list is gone. Rule needed: keep at least one place alive forever.
- A wrong book published by us: limited to our own domains, and the sign-in address is never taken from a book.
Where it runs
- On: Flutter apps and browser code. Off: the nextjs server; its rescue is a redeploy. DECIDED · ARCH:244-246; nextjs:165-168
- Built by bun
frontend-utils(browser) anddart-api-engine(Flutter). Both have test rows. DECIDED · bun-frontend-utils.md:280; dart-api-engine.md:187 - A scheduled test exercises the sleeping path so it does not rot. DECIDED · bfu:128
- Not written: where the browser stores the book, and whether spare names have valid certificates and CORS for the web page. MISSING
Four cases where it is not instant
- The dead cluster goes silent instead of refusing: the client waits for its connect time limit first. No library sets one today. PROPOSED · jaeda, how.md MISSING · GAPS G10
- Writes: a retry is only safe when the write carries a unique key. PROPOSED · jaeda, how.md
- Only one cluster behind the name: no second address; the rescue router takes over, which is slower (seconds). PROPOSED · jaeda, how.md
- Callers we do not control: no rescue router; they wait for DNS, 30 to 60 s, unless they retry. PROPOSED · jaeda, how.md
jaeda's position on G10 client library PROPOSED · jaeda, POSITIONS.md
every client library sets a connect time limit (2 to 3 s), retries once on another address when the request is safe, and ships the rescue router. Add the missing DoD rows.
As built in the whole-system test: connect limit 3 s per address, try every address, retry once. The transport opens its own TLS socket so a timed-out connect is really closed. Before that fix a black-holed IP gave 20.6% errors for 71 s, also to the healthy cluster; after it, 0. TESTED · drill 10, HOW-IT-WORKS.md
- "Gossip"? Not strictly: apps do not talk to each other. It is "address books that list where the address books are". PROPOSED · jaeda, how.md
6 · Frontend failover
Web frontends and their standby
Web runs on Cloudflare Workers. A copy of every release waits on Vercel. If Workers break, the web name is repointed to Vercel.
- Who flips it, and how, is not written anywhere. MISSING OPEN · OD-25
- If the Cloudflare zone dies, web users are lost until it is back. This is accepted. DECIDED · ARCH:332-334
- Rollout 10 → 50 → 100 % tested on real Workers. TESTED · spike C:45-49
Release of a web frontend
Release tin's web frontend 1.4.0
- CI CI uploads a tagged Worker version (
wrangler versions upload --tag). It never deploys. DECIDED · ARCH:739-741; T3:3280 - CI CI publishes tin's primordial chart with a
CloudflareDeploynaming the tag. DECIDED · ARCH:741-742 - CI CI also builds and deploys the Vercel standby copy. DECIDED · nextjs:180-184
- Kargo Kargo sees the release and writes it into the pichu stage file (unique tag
ci-<git sha>). DECIDED · ARCH:742; T3:3340-3342 - ArgoCD ArgoCD puts the
CloudflareDeployrecord on Primordial. DECIDED · portal:114-117 - T3 T3's cf-deploy finds the version by tag, adds the secrets itself and rolls out with Cloudflare's own % split. Tested: 20 / 95 / 200 of 200 requests on the new version. DECIDED · T3:3274-3325 TESTED · spike C:45-49
- Kargo T3 reports Healthy only when the rollout is complete; Kargo moves on to pikachu. Rollback = promote an older version. DECIDED · T3:3352-3353; ARCH:742-744
- One production path: OpenNext → Cloudflare Workers. Vercel is only a standby, never a second production path. DECIDED · nextjs:39, 43-44
- First ever deploy: the chart creates the Worker once with a "not deployed yet" placeholder, because Cloudflare's first upload goes live at 100 %. DECIDED · T3:3296-3299 TESTED · hole H1, spike C:90
- The landscape is chosen at runtime by a Worker setting, never guessed by the browser. DECIDED · nextjs:328-332
- The sign-in address (issuer) is built in at build time; a different landscape issuer needs a new build. DECIDED · nextjs:266-268, 332-335
- That clashes with "one build promotes through all stages" (gap G8). OPEN · decision 27
- In-cluster frontend chart: only in the six dev/test/preview profiles, off by default. Never production. DECIDED · frontend-standalone-chart.md:57-61
The standby flip
- The Vercel standby flip was cut from the whole-system test on purpose: not tested. NOT TESTED · drill 9, poc/REPORT.md
What exists in the design: DECIDED · nextjs:180-184; ARCH:329-331
- CI deploys every release to both Cloudflare and Vercel.
- Vercel has the domain and certificates set up in advance.
- A weekly test request checks the Vercel copy works.
- Failover = repoint the CNAME (a DNS alias) to Vercel.
What is not written anywhere: MISSING
- Who or what flips the CNAME, and what triggers it.
- Who owns the weekly test: open owner decision OD-25, default a production-operations node under T3. OPEN · nextjs:413-415
- How the Vercel build gets its landscape settings and secrets.
- What replaces the Cloudflare-only parts on Vercel: R2 page cache, Durable Object queue, D1 tag cache, Cloudflare Images.
Proposed defaults: PROPOSED · this chapter, decisions 24-26
- A person flips it with a one-command runbook after the weekly test is green; no automatic flip in v1.
- Web name
web., written by the traffic controller.<service>. <platform>. <landscape>. cluster. atomi. cloud - A second "standby" build for Vercel, page cache off.
What is lost when something dies
| What dies | Web (browser) | Phone app (Flutter) | Source |
|---|---|---|---|
| One cluster | nothing; the backend name drops the dead cluster | same | DECIDED · chapter 2 |
| Cloudflare Workers only | flip the CNAME to Vercel; down until flipped | API calls fine (the API is not on Workers) | DECIDED · ARCH:329-332 |
Cloudflare zone (atomi.) | lost, accepted | the rescue router moves to the spare base name | DECIDED · ARCH:332-334, 353-360 |
| All of Cloudflare | lost; error portal lost (accepted) | rescue router via the AWS copy of the address book | DECIDED · ARCH:336-339, 350-352 |
| Vercel too | nothing named as a third host | — | MISSING |
- Phone apps in a Cloudflare zone death: API calls move to the spare name, but sign-in cannot follow (the issuer is baked). PROPOSED · reading, this chapter
A browser's first request
- A browser's first request has no rescue router: the page that holds it has not loaded yet. If DNS cannot answer, nothing we ship can help. DECIDED · ARCH:333-334
- So for browsers only DNS fixes help: the CNAME flip, or the zone coming back.
- DNS changes reach users within about TTL + 3 s (worst 57 s on 8.8.8.8 with TTL 60). Measured on A records, not a CNAME flip. TESTED · spike D:93-102
- A name looked up before it exists is cached as "does not exist" for 30 minutes, so the Vercel name must exist before any flip. TESTED · spike D:106-107
Server side, browser side, phone
- nextjs server: rescue router off; it retries once on a network error; its rescue is a redeploy. What "redeploy" means in practice is not written. DECIDED · nextjs:165-168; bfu:83-87 MISSING
- Browser: rescue router on; reads the saved Doc C; checks the allowlist; offline page from a service worker. DECIDED · nextjs:165-166; frontend-ux-doctrine.md:119-120
- Flutter: each landscape is its own store app; issuer, sign-in page and key addresses are built in; the router keeps the last good book forever. DECIDED · flutter-base.md:362-369; dart-api-engine.md:57-71
- The spare base name (S8) gives phone apps a separate built-in root if
atomi.dies. DECIDED · ARCH:326-327cloud
Error catalogs on the edge
- Problem catalog = each service's list of known errors, with a "can retry" flag. The UI uses it: retry, stay, or show a full error. DECIDED · frontend-ux-doctrine.md:112-118
- T3 publishes it as a static file next to the address books, 60 s cache; frontends never call Primordial for it. DECIDED · ARCH:284-287; portal:327-331
- Error portal (erbium) = a human-readable site of those errors, on Cloudflare only. If Cloudflare dies it is gone; accepted. DECIDED · ARCH:338-339
- Unit: per service (older) or per platform × tier (newer). OPEN · decision 28
Everything else in this chapter
What can go wrong, and what happens then
| What goes wrong | What happens | Who acts | Source |
|---|---|---|---|
| Bad Worker code | refused at upload | CI fails | TESTED · spike C:60-67 |
| Rollout error at Cloudflare | controller stops in Degraded, no loop | person | TESTED · spike C:60-67 |
| Secret change then rollback | Cloudflare refuses (10220); works with force | T3 uses version id by default | TESTED · spike G3:124 DECIDED · T3:3313 |
| CI deploys straight to production | a policy check turns red | CI | DECIDED · nextjs:99 |
| Cloudflare Workers down | web down until the CNAME is flipped to Vercel | not named (OD-25 is about the probe) | DECIDED · design, MISSING who |
| Vercel copy silently broken | weekly test should catch it; owner not chosen | OD-25 | OPEN |
| Cloudflare zone dead | web users lost; phone apps move to spare name; sign-in cannot follow | — | DECIDED · accepted |
| Error portal down | portal gone; apps keep classifying errors from the edge file | — | DECIDED |
Tested and not tested
| Item | State | Source |
|---|---|---|
| Upload tagged version, roll out 10 → 50 → 100 % | TESTED · : 20 / 95 / 200 of 200 requests on new version | spike C:45-49 |
| T3 adds Worker secrets, CI never sees them | TESTED · : secret value returned 0 times | spike G3:113-116 |
| Minimum Cloudflare token | partly: "Workers Scripts: Edit" observed; least-privilege not tested | spike C:11; MASTER:696 |
| Custom domains and routes for Workers | NOT TESTED · workers.dev only | spike C:111 |
| CNAME flip to Vercel | NOT TESTED | — |
| Dual deploy to Vercel, weekly test | NOT TESTED · dry-run row in the DoD only | nextjs:412 |
| Browser rescue router | NOT BUILT · NOT TESTED | — |
| How far back rollback can reach (version retention) | NOT TESTED | spike C:108-112 |
Open decisions in this chapter
| # | Question | Default | Whose |
|---|---|---|---|
| F-1 | Who owns the weekly Vercel test and the switch to Vercel (OD-25) | A: a production-operations node under T3 | corpus default (nextjs:414) |
| F-2 | Who flips the CNAME, and how | a person, by a one-command runbook, after the weekly test is green; no automatic flip in v1 | PROPOSED · this chapter |
| F-3 | Exact web name and zone for a frontend | web., written by the traffic controller like other names | PROPOSED · this chapter |
| F-4 | Vercel build: same build or a second build, and what replaces R2/D1/DO/Images | a second build with a "standby" setting; page cache off on Vercel | PROPOSED · this chapter |
| F-5 | One build for all stages vs one build per issuer | one build per landscape issuer (as nextjs:332-335 already says) | PROPOSED · reading |
| F-6 | Catalog file unit: per service or per platform × tier | per platform × tier (newest text) | DECIDED · by newer text; confirm |
| F-7 | Error portal template adoption (OD-15) and its hold (OD-11) | not ruled; blocks portal rows | OPEN · corpus |
| F-8 | Sign-in when the Cloudflare zone is dead | see chapter 4 | OPEN |
7 · Sign-in (Logto)
Sign-in with Logto
One Logto per landscape, at one fixed address built into every app. A cluster dying does not affect sign-in.
- One self-hosted Logto cannot be one issuer on two domains. DECIDED · Logto's source, v1.46.0
- Tested on real Logto 1.46.0: a refresh sent to a spare address works, and the new tokens keep the same issuer. TESTED · LOGTO-MULTIDOMAIN.md
- Settle first: is production one "serving" landscape, or four? OPEN · GAPS G1
How it is deployed
How a landscape's Logto starts
- T3 Bootstrap writes the operator's own machine login (M2M) into Infisical. DECIDED · alog:259-264
- Logto's chart pulls it. If it is missing, Logto refuses to start, on purpose. DECIDED · alog:330-332
- At every boot, our fork patch creates or repairs that machine app in Logto's database (Logto's API never lets us choose a secret). DECIDED · alog:303-306
- T3 logto-operator signs in with it. It is the only thing that changes objects inside Logto. DECIDED · ARCH:406
- T3 It creates every app (
LogtoApp), API (LogtoResource) and setting (LogtoInstance), and writes each app's id and secret to Infisical. Once written, never regenerated. DECIDED · ARCH:411-414
| Chart | Goes to | Holds | Source |
|---|---|---|---|
| App chart | each cluster of the landscape | Logto pods; the issuer address; database login and boot secret from Infisical | DECIDED · alog:316-332 |
| Primordial chart | Primordial, once | the landscape's database request; one boot credential per platform; the LogtoInstance record | DECIDED · alog:366-371; lop:683-687 |
- Our code: a small fork of Logto (
aldehyde.: one boot patch and a "sanitize preview copy" command) andlogto logto-operator. DECIDED · alog:20-22, 85-88 - Database: one per landscape, an ordinary T3
Database(Neon in prod by default). Moving it never changes the issuer. DECIDED · T3:675-676, 2248-2249 - Upgrades: one landscape at a time, by Kargo. Test 1.45 → 1.46 with 3 copies: tokens, refresh, machine logins, keys 0 failures; interactive sign-ins 0–2 failed per roll. DECIDED · lop:699-702 TESTED · spike E:11
- Signing keys live in the database, so every copy has the same keys. TESTED · spike E:78
- Dev and preview: each instance has its own Logto and database. Preview copies of prod data must have every machine secret replaced first; that guard is not built yet. DECIDED · alog:136-137 OPEN · env:1486-1489
- No Logto version is pinned; default proposal v1.46.0. MISSING OPEN · decision 32
The issuer address
- Issuer = the exact address that signs tokens. Every token says "issued by <this>", and every service checks it.
- Fleet:
api., made from the landscape, never typed in. DECIDED · alog:99-100; lib-contracts:150lithium. aldehyde. <landscape>. cluster. atomi. cloud - Example:
api..lithium. aldehyde. raichu. cluster. atomi. cloud
- Example:
- Permanent once live: changing it makes every token unverifiable. DECIDED · alog:102-105
- Built into apps; never from a file, setting or request. Apps carry a fixed
landscape → issuertable. DECIDED · lib-contracts:160-169 - No mew-style name for Logto. DECIDED · alog:374-377
jaeda's position on G1 landscapes PROPOSED · jaeda, POSITIONS.md
pichu, pikachu, raichu, ampharos are FOUR separate landscapes, one sign-in issuer each. "serving" is the name of the group (the class of landscapes that run apps for users), not a landscape. Why: ARCHITECTURE, fleet.md, the newer T3 goal draft and every owner walk-through (D1: a landscape has ONE region) agree; one landscape cannot have two regions. The logto-operator / helm-wrapper text that folds them into one serving landscape is the stale one. Names: <svc>..
Tokens and lifetimes
| Token | Lifetime | Source |
|---|---|---|
| Access token (the short pass) | 10 minutes | DECIDED · ARCH:423 |
| Refresh token (the long key) | 14 days, a new one at every use | DECIDED · ARCH:424-425 |
| Magic-link one-time token | 120 s | DECIDED · deferred-login.md:170 |
| Logto's own default access token | 1 hour, unless the API sets its own | DECIDED · logto@v1.46.0 init.ts:581 |
- So 10 minutes holds only if logto-operator writes it on every API. That is its job. DECIDED · lop:735-736
- Services check tokens locally and never call Logto per request; they re-fetch keys on an unknown key id and keep the last good keys while Logto is down. DECIDED · env:484-485; T3:2361
- Reuse of an old refresh token being caught is assumed, but Logto does not document it. OPEN · study I:155
- Signing-key rotation policy: not written, not tested. MISSING NOT TESTED · spike E:93
When things fail
| What fails | Signed-in users | New sign-ins | Source |
|---|---|---|---|
| A Logto pod restarts | fine | 0–1 may fail with preStop set | TESTED · spike E; T3:723 |
| One cluster dies | fine: same name, other cluster, same database | fine | PROPOSED · (jaeda), how:205-206 |
| Logto or its database down | work while their pass lasts | stop | DECIDED · env:1497-1499 |
| Our DNS name is dead | work up to 10 min, then wait | stop | PROPOSED · (jaeda), how:208-220 |
| The region dies | wait for a restore (hours) | stop | DECIDED · ARCH:291-294 |
Sign-in when our DNS name is dead
- Client Our DNS name itself is dead. The rescue router moves API calls to a spare name. The issuer (sign-in address) is baked in and does not move. DECIDED · ARCH:252; lib-contracts:160
- Client People already signed in keep calling services: tokens are checked locally with cached keys. PROPOSED · jaeda, how:208-220 DECIDED · env:484-485
- New sign-ins fail. Google sign-in fails (callback must be registered). Passkeys fail (bound to the exact host). Cookies are per exact host. DECIDED · logto@v1.46.0
- After 10 minutes the pass runs out. Today Logto has no spare address, so getting a new one fails. The lab showed a refresh over a spare address would work (option iii). DECIDED · ARCH:423 TESTED · LOGTO-MULTIDOMAIN.md
- Client The app waits; the user is not logged out, because the refresh key lasts 14 days. DECIDED · ARCH:424-425 PROPOSED · jaeda, how:208-220
- Client When the name is back, the quiet refresh works and the user carries on. PROPOSED · jaeda, how:208-220
In the whole-system test (drill 7): with the main name dead, refresh 72/72 and API 1,425/1,425 worked; new sign-ins failed while the name was dead. Logto ran per landscape on 2 providers with 4 replicas. TESTED · drill 7, poc/REPORT.md
It also found that a password rotation breaks every new Logto pod; fix: a fixed owner role owns every object (must-change 2). ampharos sign-in went from 26 s to 8 s once Logto ran next to its Oregon database. TESTED · poc/REPORT.md
Can one Logto answer on two domains?
The sign-in standard (OIDC): one issuer = one exact address string. Several network addresses can reach it, but the string never changes. DECIDED · OpenID Discovery §4.3, Core §3.1.3.7, RFC 9207
What Logto's own code says (v1.46.0)
What OIDC (the sign-in standard) allows
(Spec rules, written from memory by the reader agent – check the links before relying on wording.)
- The
issuerin the discovery file must equal the address the client expected. (OpenID Discovery 1.0 §4.3) - The token's
issmust exactly match the issuer. (OpenID Core §3.1.3.7) - RFC 9207: the sign-in reply carries
iss; the client compares it. (mix-up defence) - Passkeys work only under the host (or parent domain) they were made on. (WebAuthn rpId)
- So: one issuer = one exact address string. Several network addresses can reach it, but the string never changes.
What Logto (open source, v1.46.0) does
| Question | Answer | Source |
|---|---|---|
| What sets the issuer? | ENDPOINT + /, read once at start | logto@v1.46.0:packages/core/src/env-set/index.ts:109-113; shared/src/node/env/UrlSet.ts:21-22 |
| Does the issuer follow the request's host? | No on a normal (single-tenant) install: every request goes to the default tenant first | logto@v1.46.0:packages/core/src/utils/tenant.ts:167-172; env-set/utils.ts:22-23 |
| Custom domains? | only in multi-tenant mode (* in ENDPOINT) + Cloudflare hostname setup (Logto Cloud) | logto@v1.46.0:packages/core/src/app/init.ts:66-68; libraries/domain.ts:70-72 |
| More than one domain per tenant? | flag "only available to enterprise customers running private instances"; max 10 | logto@v1.46.0:packages/shared/src/node/env/GlobalValues.ts:262 |
| With custom domains, is it one issuer? | No: each (tenant, domain) pair gets its own issuer | logto@v1.46.0:packages/core/src/tenants/index.ts:120; Logto Cloud docs custom-domain.mdx:139 |
| Behind a proxy? | TRUST_PROXY_HEADER=1 trusts forwarded host/proto | logto@v1.46.0:packages/core/src/main.ts:25; Logto OSS docs |
| Discovery file on another host? | issuer stays fixed; endpoint links (token, keys) follow the request host | node-oidc-provider (upstream, not Logto's fork): lib/helpers/oidc_context.js:61-62 |
- Reader's inference, not run: on our install, asking the spare name for a token should return
iss = <main issuer>, with token/key links on the spare name. That is exactly what Brief 2's test checks. PROPOSED · reader agent
Options for the rare "our DNS name is dead" case
| Option | Keeps working | Needs | Cost / risk |
|---|---|---|---|
| A. Do nothing | signed-in users 10 min; then wait | nothing | nobody new signs in during the outage |
| B. Longer access tokens (e.g. 2–8 h) | most signed-in users ride it out | one operator setting | a stolen pass lives hours; revoke is slower |
| C. Refresh over the spare address, issuer unchanged | background refresh + machine tokens | client libraries learn a baked spare token/keys address; cert on spare name; a test | no browser sign-in, Google sign-in, passkeys or cookies on the spare |
| D. Issuer on its own domain at a different DNS host | everything, as long as that host lives | a new DNS host + registrar for the issuer name only; issuer change = one-time re-login for everyone | one more DNS bill; must be decided before go-live (issuer is permanent) |
| E. Logto answers as two issuers | everything on both names | a fork patch or Logto Cloud/enterprise; every service accepts two issuers | breaks the "one issuer" rule; two passkey sets; bigger attack surface |
| F. Same issuer name served by two DNS hosts | everything | the DNS fix in PROPOSAL-DNS. (two hosts for the same zone) | depends on Brief 2's DNS design |
- Options A, B, C are jaeda's 1–3 (how:221-224). D, E, F come from the reader agent's list PROPOSED. None is decided. OPEN
- Recommended default PROPOSED · this chapter: F if the DNS proposal adopts two hosts for the same zone (fixes everything, no app change); otherwise C + keep A's behaviour. Avoid E. Decide D before go-live or never, because the issuer can never change after.
TEST RESULT – to be filled from model/ (Brief 2: real Logto in docker behind two host names A and B; discovery, keys, machine token, browser sign-in, refresh on B with iss = A, userinfo, sign-out, cookies, social callback, passkey rpId). Status when this chapter was written: not yet available.
The options for a dead name
| Option | Keeps working | Needs | Cost or risk |
|---|---|---|---|
| A. Do nothing | signed-in users 10 min; then wait | nothing | nobody new signs in |
| B. Longer passes (2–8 h) | most signed-in users ride it out | one setting | a stolen pass lives hours |
| C. Refresh over the spare address, issuer unchanged | background refresh, machine tokens | libraries learn a baked spare address; a test | no browser sign-in, Google, passkeys, cookies on the spare |
| D. Issuer on its own domain at another DNS host | everything, while that host lives | a new DNS host for the issuer name only | decide before go-live: the issuer is permanent |
| E. Logto answers as two issuers | everything on both names | a fork patch or Logto Cloud | breaks "one issuer"; two passkey sets |
| F. Same issuer name served by two DNS hosts | everything | the DNS fix in the proposal | depends on the DNS proposal |
- None is decided. OPEN · decision 31
- Proposed default: F if the DNS proposal adopts two hosts for the same zone; otherwise C, keeping A's behaviour. Avoid E. PROPOSED · this chapter
- jaeda's position (G5): accept A for v1 and put the issuer name on a base the DNS proposal makes survive one host outage. Do not run two issuers. PROPOSED · jaeda, G5
Test result: Logto on two names
- Recommendation: option (iii) = a spare address for Logto under the spare base name, the issuer string unchanged, libraries fall back to it. Tokens stay at 10 minutes. This is option C above. PROPOSED · levi, LOGTO-MULTIDOMAIN.md
- Today the design has no spare name for Logto, so if the issuer's name dies: signed-in users work up to 10 minutes, then wait; nobody new signs in. TESTED · LOGTO-MULTIDOMAIN.md
Copied from model/proposal/LOGTO-MULTIDOMAIN.md as it stood at 2026-11-12 07:53 UTC; a rebuild picks up later changes.
Answer first
- Logto has one issuer name, but it answers on any address. TESTED · Logto 1.46.0
- Example: issuer A, reached via name B. Discovery on B still says issuer A; its endpoint links point at B.
- The key test passed. A was down. The app sent its refresh to B. Logto answered, and the new tokens say iss = A. A check expecting A passed, with keys fetched from B. TESTED · LAB/failover.json
- So "sign-in over the spare address, same issuer name" works. Sign-in with password on B works too, tokens say A.
- What does break on B:
- Browser sessions: cookies belong to A only, so a user must sign in again once on B TESTED · :
login_required. - Passkeys: Logto ties a passkey to the host name it was made on (SOURCE). A passkey made on A does not work on B.
- "Sign in with Google" and the like: Logto sends the provider B's callback (SOURCE). It works only if B's callback is registered with the provider beforehand.
- Browser sessions: cookies belong to A only, so a user must sign in again once on B TESTED · :
- Never change Logto's ENDPOINT to B. Then every new token says iss = B, and every app expecting A fails:
unexpected "iss" claim valueTESTED. - Today's design has no spare name for Logto (no DNS record, route or certificate). So right now, if the issuer's name dies: signed-in users work up to 10 minutes, then wait; nobody new signs in.
- Recommendation PROPOSED · levi: option (iii). Give Logto a spare address under the spare base. Keep the issuer string A. Teach the client and service libraries to fall back to B for token, sign-in, userinfo and keys. Keep tokens at 10 minutes.
- When this matters: only if the issuer's DNS chain dies (Cloudflare zone, .cloud, Cloudflare account). With option 4 of PROPOSAL-DNS.md, a dead Route 53 or Google does not touch it.
About this paper: who wrote it, tags, short names
Written 2026-11-12 by levi (helper of jaeda) for the owner. Tags: DECIDED (owner or corpus, cited) · TESTED (we ran it) · DOCS/SOURCE (Logto docs or code) · PROPOSED (levi's idea unless marked) · OPEN (owner must choose). Short names: C/ = corpus copy proposal/; LAB/ = proposal/ (real Logto; built by helper annis, last tests and clean-up by levi). "Issuer" = the sign-in server's official name. It is written into every token (iss), and every service checks it. Name A = the issuer's real name. Name B = a spare name for the same Logto.
1. What the corpus decides (DECIDED)
- One Logto issuer per landscape, shared by every platform there (C/goals/aldehyde-logto.md:92-98; C/concepts/ARCHITECTURE.md:365-376).
- Its name is fixed by convention:
api.(C/goals/aldehyde-logto.md:99-104).lithium. aldehyde. <landscape>. cluster. atomi. cloud - Example:
api..lithium. aldehyde. ampharos. cluster. atomi. cloud - "OIDC-permanent": changing it makes every token unverifiable (same lines).
- Example:
- Apps get the issuer baked in at build. It is never read from the address books (edge docs), so a bad address book cannot steal sign-ins (C/concepts/ARCHITECTURE.md:251-255).
- Services check only their own landscape's issuer: no cross-issuer checks, no second issuer (C/concepts/ARCHITECTURE.md:372-374).
- Every caller uses the public name, even inside the cluster ("privatePath: unsupported"). Reason given: "issuer, discovery, cookies, and passkeys are name-bound" (C/concepts/ARCHITECTURE.md:401-405).
- Token lifetimes: access token 10 minutes, refresh token 14 days, rotating, apps get a fresh token on open (C/concepts/ARCHITECTURE.md:423-427). The operator sets these per API resource and app (C/goals/logto-operator.md:735-739).
- Logto's own default is 1 hour TESTED · :
expires_in: 3600in LAB/flows.json. So the 10 minutes only holds once the operator applies its policy. - Honest rule already in the corpus: if every issuer host is down, sign-in and renewal stop, with no time bound (C/concepts/environments.md:1497-1499).
2. What Logto does on a second name TESTED · unless marked
Setup: Logto OSS 1.46.0 + Postgres 17, behind nginx with a local CA. ENDPOINT=https:/. Same Logto under A (auth-a.) and B (auth-b.). Full detail: LAB/REPORT.md.
| Test | On A | On B | Note |
|---|---|---|---|
Discovery issuer | A | A | endpoint links follow the host (B) |
| Signing keys (JWKS) | same | same | – |
| Machine token (M2M) | iss A | iss A | – |
| Password sign-in, browser | works | works, iss A | check "expect A" PASS |
| Refresh / userinfo | 200 | 200 | – |
| Cookies | A only | B only | A's cookies on B: login_required |
| A down, refresh at B | – | 200, iss A, PASS | new refresh token issued |
| ENDPOINT changed to B | – | new tokens iss B | apps expecting A: FAIL |
| Old A tokens after that change | still valid as A | – | same keys |
| Passkey site id | host A | host B | SOURCE web-authn-verification. |
| Social callback | A's URL | B's URL | SOURCE use-social. |
| Default token life | 1 hour | 1 hour | the operator sets 10 min (part 1) |
Logto facts behind this (SOURCE/TESTED):
ENDPOINTsets the one issuerENDPOINT/; it does not follow the request host TESTED · : B kept saying A; after restart with B, both names said B.oidc - Endpoint links in discovery follow the request host TESTED. That is why a client can talk to B and still get tokens for A.
- One Logto = one issuer. Logto Cloud has a "custom domains" feature; whether it gives several issuers: NOT CHECKED today.
3. Options for "our DNS name is dead"
When does this happen? Only when the issuer's DNS chain dies: Cloudflare's zone, the .cloud TLD, or the Cloudflare account (see PROPOSAL-DNS.md). With option 4 of that proposal, Route 53 or Google dying does not kill the issuer name.
| Option | Users in the "name dead" case | Needs | Security cost |
|---|---|---|---|
| (i) do nothing | work ≤10 min, then stuck; no new sign-ins | nothing | none |
| (ii) longer tokens (e.g. 4 h) | work ≤4 h; no new sign-ins | operator token policy | stolen token lives 4 h |
| (iii) spare address, same issuer | keep working; new password sign-ins work | see below | small |
| (iv) issuer on its own domain | depends on that domain's chain | new issuer name before go-live | none new |
| (v) two issuers | – | two Logtos or apps trusting two | breaks a corpus rule |
(i) Do nothing.
- Access tokens last 10 minutes DECIDED. Refresh tokens last 14 days, so nobody is logged out; they just wait.
- Hidden extra: services fetch Logto's keys from A too. If their key cache runs out while A is dead, every check fails, even for valid tokens. Fix in any option: cache keys for a day.
(ii) Longer access tokens.
- Example: 4 hours. Most outages pass unseen by signed-in users.
- Cost: a stolen token, or a removed user, keeps working up to 4 hours. The corpus already counts token life as the delay before a revoke bites (C/concepts/environments.md:1665) and caps it at 24 h (C/concepts/environments.md:1548).
- Needs only the operator's token policy (C/goals/logto-operator.md:735-739). Does nothing for new sign-ins.
(iii) Spare address, issuer unchanged (RECOMMENDED). TESTED in the lab.
- T3 / gateways:
- DNS record
api.written by the traffic controller (same IPs as A).lithium. aldehyde. <l>. cluster. <spare> - Gateway route + TLS certificate for that name (today missing: part 4, gap 15).
- DNS record
- Client library (bun + dart auth engines):
- Baked list of auth addresses: A first, B second. B is derived from the baked spare root, never from an address book.
- On a hard connection failure to A: send token, sign-in, userinfo and key requests to B.
- Always check
iss= A (baked). Never accept another issuer.
- Services: fetch keys from A, else B; cache them a day.
- Logto config: register B's callback at every social provider; nothing else (redirect URIs are the apps' own).
- What users see on B: native apps keep going; a fresh sign-in needs password or email code. Passkeys do not work on B. Browser sessions need one new sign-in.
- Security cost: the spare domain becomes part of sign-in. Someone who takes it over could show a fake sign-in page. Guard it like the main one: registrar lock, 2FA, CAA records.
(iv) Issuer on its own domain, at another DNS host.
- It spreads the risk; it does not remove it: the issuer then dies with that domain.
- With (iii) in place, the issuer string's own DNS matters only for the main path, so (iv) adds little.
- It is a change to a DECIDED naming rule (C/goals/aldehyde-logto.md:99-104), possible only before go-live. OPEN, not recommended.
(v) Logto answering as two issuers.
- Logto OSS has one
ENDPOINT, so one issuer TESTED. - Two issuers would need two Logtos or apps that trust both. The corpus forbids cross-issuer checks (C/concepts/ARCHITECTURE.md:372-374). Rejected.
(vi) Found along the way PROPOSED:
- Cache signing keys for a day in every service (keys did not change even when ENDPOINT changed: TESTED).
- Pre-register B's social callbacks now; it costs nothing.
- Passkeys cannot follow the host in Logto 1.46 (rpId = request host, SOURCE). Keep a second factor (password or email code) available for passkey users.
4. How Logto is deployed today (per the corpus, 15 lines)
- Our fork
AtomiCloud/= upstream Logto OSS at a pinned tag + one small boot-seed patch + a sanitizer (C/goals/aldehyde-logto.md:425-428).aldehyde. logto - Published as image
ghcr.+ an app chart and a primordial chart (C/goals/aldehyde-logto.md:4-5).io/ protodurian/ aldehyde. logto - One issuer and one user database per landscape, shared by every platform (C/goals/aldehyde-logto.md:92-98).
- Sign-in and the Management API share one host:
api., API onlithium. aldehyde. <landscape>. cluster. atomi. cloud /(C/goals/aldehyde-logto.md:99-104, 338-342).api - Database URL comes from that landscape's database binding through an ExternalSecret (C/goals/aldehyde-logto.md:320-326).
- Boot-seed: on every start, Logto upserts the operator's machine login from Infisical (
/), so a restored database heals itself (C/goals/aldehyde-logto.md:248-268).lithium - logto-operator (separate, on Primordial) is a pure API client:
LogtoAppCRs become apps and API resources; secrets go to Infisical (C/concepts/ARCHITECTURE.md:406-414). - Redirect URIs are derived from the landscape's live names, never typed by hand (C/goals/logto-operator.md:604-610).
- Exposure: the normal LB Service + gateway route; no multi-landscape name for Logto (C/goals/aldehyde-logto.md:338-342, 374-377).
- Token policy 600 s / 14 days rotating, set by the operator (C/goals/logto-operator.md:735-739).
- Garden (dev/preview): one Logto per instance, issuer
api., local database; castform uses a sanitized Neon fork (C/goals/aldehyde-logto.md:121-205).lithium. <platform>. <instance>. <landscape>. <zone> - Connectors (Google sign-in etc.): none in Garden; the live fleet list is UNSPECIFIED (C/goals/logto-operator.md:140-146; C/concepts/environments.md:1664).
Gaps I noticed:
- The Logto version to pin is not stated (C/goals/aldehyde-logto.md:425 says only "pinned to a tracked ref"). The lab used 1.46.0.
- Replica count and how many clusters run Logto per landscape: UNSPECIFIED (C/goals/aldehyde-logto.md:117-121, 357-371).
- No spare name for Logto exists in the design. The spare base (S8) gets "the SECOND base domain's wildcard" certificate (C/goals/charts/kgateway.md:149-154); its exact names are not stated. A TLS wildcard covers one label only (stated for dev hosts at C/goals/charts/kgateway.md:93-94; general TLS rule). So
*.would not cover<l>. cluster. <spare> api.. No gateway route for a spare Logto name is stated either.lithium. aldehyde. <l>. cluster. <spare>
5. Not tested
- Sign-out (end_session) on A and B.
- A real social provider round trip, and a real passkey made with a virtual authenticator (facts above are from Logto's source).
- Refresh-token rotation and reuse detection (the lab app had rotation off).
- The operator's 10-minute policy (the lab used Logto's 1-hour default).
- A real client library falling back from A to B; real DNS; the fork
aldehyde.(the lab used upstream 1.46.0).logto - Logto Cloud custom domains.
Machines (M2M)
- A machine gets its token from the target landscape's Logto and asks for the target service's audience. DECIDED · ARCH:376-377; env:1506-1507
- Example: a nightly job calling tin in raichu signs in at raichu's Logto with audience "tin in raichu".
- Only the landscape picks the issuer, never the platform. DECIDED · lop:177-178
Everything else in this chapter
What can go wrong, and what happens then
| What goes wrong | What happens | Who acts | Source |
|---|---|---|---|
| Boot secret missing | Logto refuses to start (on purpose) | person fixes Infisical | DECIDED · alog:330-332 |
| Operator regenerates a secret | forbidden: Infisical is the commit point | — | DECIDED · ARCH:411 |
| 10-min rule not written on an API | that API's tokens live 1 h (Logto default) | operator test | DECIDED · Logto code |
| Preview copy of prod data | must replace every machine secret first; guard not built | — | OPEN · R0 |
| Pod roll during sign-in | 0–1 failed sign-in; clients restart the flow | client | TESTED |
| DNS name dead | see 4.4 | — | PROPOSED |
| Signing keys rotated | immediate by default; tokens signed with the old key fail | policy not written | MISSING |
| Wrong meaning of "landscape" built | DNS names, issuers and audiences all differ | — | OPEN · GAPS #1 |
Tested and not tested
| Item | State | Source |
|---|---|---|
| Rolling upgrade 1.45 → 1.46, 3 copies | TESTED | spike E |
| Same keys on every copy | TESTED | spike E:78 |
| Role/claim changes apply at refresh (1.41) | TESTED | env:391-392 |
Preview callback missing → HTTP 400 invalid_redirect_uri | TESTED | goals/ |
| Logto's issuer behaviour on a second host name | read in code; NOT RUN | logto@v1.46.0 |
| Refresh over a spare name | NOT TESTED · Brief 2 is testing | how:225 |
| Refresh-token reuse detection | NOT TESTED | study I:155 |
| Key and cookie-key rotation | NOT TESTED | spike E:93 |
| Logto in two regions, several copies (owner D37) | NOT TESTED | OWNER-DECISIONS D37 |
Open decisions in this chapter
| # | Question | Default | Whose |
|---|---|---|---|
| L-1 | Is prod one serving landscape (one issuer) or raichu + ampharos (two issuers)? | two issuers, as ARCH §5 (matches home-sharding) | PROPOSED · this chapter |
| L-2 | Spare sign-in path (options A–F) | F if the DNS proposal allows, else C | PROPOSED · this chapter |
| L-3 | Logto version pin | v1.46.0 | reader agent |
| L-4 | Passkeys on? | off in v1 | reader agent |
| L-5 | Machine token lifetime | 10 min, cached to expiry | reader agent; study I suggests longer |
| L-6 | Signing-key rotation | staged, with a grace period ≥ access token life | PROPOSED · this chapter |
| L-7 | Social callback addresses to register | one per landscape issuer, listed in LogtoInstance | PROPOSED · this chapter |
| L-8 | ENDPOINT, ADMIN_ENDPOINT, TRUST_PROXY_HEADER values; is the admin console exposed? | ENDPOINT = issuer host; proxy header on; admin console not public | PROPOSED · this chapter |
| L-9 | Where prod Logto's database lives and whether it has a region copy | Neon Scale in the landscape's region; no copy (v1) | corpus default (ARCH:82; T3) |
8 · Global and many regions
Many regions
Each user has one home landscape. Their data and their sign-in live there. A dead region is a restore, not a failover.
- Only mercury's webhook name spans regions today. DECIDED · onboard:351
- A raichu token is refused by ampharos. DECIDED · lib-contracts:158
- No cross-region move has been run on real vendors. NOT TESTED · spike M:4
Landscapes and virtual landscapes
| Landscape (raichu) | Virtual landscape (mew) | |
|---|---|---|
| Has clusters | yes (raichu-opal, raichu-ruby) | no |
| Has its own data | yes, per platform | no |
| Has its own sign-in (Logto) | yes | no |
| Is | a place | a name over places |
- mew = the production name over raichu + ampharos. celebi = the staging name; its hosts are only inferred (
[pikachu]). DECIDED · fleet.md:231 OPEN · fleet.md:234 - The mew name answers with the addresses of every landscape row with
serve: true. DECIDED · ARCH:224-225 - Region = an airport code from an allow-list. A landscape's region never changes. DECIDED · owner D1; fleet.md:213
- The codes for raichu and ampharos are not written. Proposed: SIN and IAD. MISSING PROPOSED · decision 47
- jaeda's position (G1): pichu, pikachu, raichu, ampharos are four landscapes, one sign-in issuer each. PROPOSED · jaeda, G1
How a new user gets a home
Ana, a new user in Singapore, gets a home
- Client The app checks for a home claim on the user. Ana has none yet. DECIDED · ARCH:415
- Client It reads Doc B (which landscapes nitroso runs in; names only). A landscape appears there only once it is healthy. DECIDED · lib-contracts:187; T3:2869, 3053
- Client It pings each healthy landscape; Ana (or the app) picks raichu. DECIDED · lib-contracts:187
- Ana signs in at raichu's sign-in page. Then
GET /gives 404 (new user) and onboarding starts. DECIDED · ARCH:418-419User/ Me - Client Onboarding writes
home_landscape: raichuinto Ana's Logto record; Logto puts the claim into every new token. DECIDED · logto:407 - Client From now on the app calls only
…raichu.. Not covered: Ana on a new phone may pick ampharos and make a second account. DECIDED · ARCH:240-241 MISSINGcluster. atomi. cloud
- Phones and browsers do the picking; server libraries just honour the claim. DECIDED · lib-contracts:199
- "Home per platform" (one paper) vs one
home_landscapekey in each landscape's Logto (another). Proposed: one home per user. OPEN · decision 41 - Re-homing a user is needed before a landscape is removed, but no mechanism exists. Proposed: a manual, audited runbook in v1. MISSING PROPOSED · decision 46
Where data lives
- A landscape is its data. DECIDED · ARCH:660
- Each platform's database, key-value store and files sit in the landscape's region by default. DECIDED · T3:1195
- Each landscape has its own Logto with its own database. DECIDED · ARCH:396-400
- Caches live inside each cluster and never cross regions. DECIDED · owner D16
- No live copying between regions in v1. A dead region = restore from backup, then point the record at the new place: hours. DECIDED · T3:2649; ARCH:293
- Backups land at a different vendor (hard rule). Recovery point: minutes; recovery time: hours. DECIDED · ARCH:664-666
- A standby database at a second vendor: default no (about 2× database cost). OPEN · O3
| Thing | Per landscape | Spans landscapes | Source |
|---|---|---|---|
| Logto (sign-in) | yes, one per landscape | no | DECIDED · ARCH:367-372 |
| tin's database, KV, files | yes | no | DECIDED · T3:1195 |
| Cache | per cluster | no | DECIDED · owner D16 |
| Mercury intake name | — | yes (…mew…) | DECIDED · onboard:351 |
| Mercury hot data (Upstash) | yes | no | DECIDED · mercury:186 |
| Mercury long-lived data (one Neon) | — | yes, one database for mew | DECIDED · mercury:176 |
| Primordial (T3, ArgoCD, Kargo) | — | one for the fleet | DECIDED · ARCH:121 |
How a user reaches home
- The app holds one name, its home's, e.g.
api.. DECIDED · ARCH:240-241tin. nitroso. raichu. cluster. atomi. cloud - Sign-in for raichu is
api., from a built-in table. DECIDED · lib-contracts:150, 169lithium. aldehyde. raichu. cluster. atomi. cloud - A raichu token is refused by ampharos. DECIDED · lib-contracts:158
- Warning: One paper says the opposite: one "serving" landscape with one issuer and a raichu ↔ ampharos failover. This is gap G1. OPEN · lop:558-568
- If raichu's region dies, the rescue router does not help (it changes addresses, not landscapes). Ana waits for the restore, or is re-homed. DECIDED · ARCH:254
Machines across regions
- A machine gets its token from the target landscape's Logto. Example: a job calling tin in ampharos signs in at ampharos's Logto. DECIDED · env:1508; ARCH:376-378
- Audiences are per landscape, never reused across landscapes. DECIDED · env:1600
Moving across regions
- The owner wants cross-region moves. The
Migrationrecord is the default way. DECIDED · owner D8 OPEN · O2, default yes - Editing a "move field" (provider, account, region, major version) does nothing by itself: T3 says
MigrationRequiredand keeps serving the old one. DECIDED · T3:1231 - Steps: build beside → copy → check → short write pause → switch → keep the old one 48 h → delete. DECIDED · T3:2000-2256
| Kind | Cross-region method | The app feels | Source |
|---|---|---|---|
| Database | live copy, write pause max 5 s | writes wait once | DECIDED · T3:2030 |
| Key-value (Upstash) | write to both, then a two-step switch | none | DECIDED · T3:2162 |
| Files | copy + change journal + verify, short write hold | writes wait once | DECIDED · T3:2165 |
| Cache | not moved: a new empty cache | cold cache | DECIDED · T3:2244 |
- Measured: Postgres moves between Neon, PlanetScale and DigitalOcean, all in Singapore, with the operator in the US (217 ms away): writes waited 3.2 s, 0 failed. TESTED · spike M:4, 22
- In one region it should be 0.1–0.3 s. PROPOSED · estimate, spike M
- Measured since, in one region (Singapore): a live move works up to 100,000 writes a second, 0 failed, 0 lost, with the library. TESTED · spike L
Bar length is on a log scale (each step is 10× longer). Every rate: 0 failed, 0 lost. Reads never paused.
The numbers as a table
| Writes / s | Verdict | Failed / lost | Hold |
|---|---|---|---|
| 1,000 | PROVEN | 0 / 0 | 76 ms |
| 5,000 | PROVEN | 0 / 0 | 54 ms |
| 20,000 | PROVEN | 0 / 0 | 75 ms |
| 50,000 | PROVEN | 0 / 0 | 509 ms |
| 100,000, 2 subscriptions | PROVEN | 0 / 0 | 3.2 s |
| 100,000, 1 subscription | PROVEN | 0 / 0 | 6.1 s |
| 100,000, 8 subscriptions | PROVEN | 0 / 0 | 8.1 s and 18.1 s |
| 100,000, 4 subs, small batches | NOT REACHED | – | replication never caught up |
- The pause is set by how far the copy is behind when writes are held, not by the library. Up to about 50,000 writes a second the copy keeps up. TESTED · spike L
- What breaks first is the caller's own time limit: a 5 s limit at 100,000 writes a second → 1.54M failed. TESTED · spike L
- One Postgres took 300,000 writes a second when the library groups writes, about 20,000 one by one. TESTED · spike L
- The operator must run near the database, but Primordial's region is not written. Proposed: a small fence helper per region. DECIDED · T3:1676 MISSING PROPOSED · decision 48
{{fold:how:How a move with no errors works
Previews from a copy of prod: PlanetScale with a Neon mirror
- Is it paying double? No. A live Neon mirror adds about +50 % to the PlanetScale bill, not +100 %.
- Example: 10 GB product: PlanetScale HA ≈ USD 52/month, always-on mirror ≈ USD 24/month (+46 %).
- At 1 TB the extra is mostly Neon disk: ≈ USD 370/month on top of ≈ USD 665 (+56 %).
- Cheapest mirror = "hourly catch-up then sleep": ≈ USD 5/month at 10 GB. It works, but needs a WAL guard (below).
- Hourly full copy (pg_dump) is the bad option: ≈ USD 780/month at 10 GB (mostly PlanetScale data-out fees), and 1 TB cannot finish in an hour.
- Surprise: previews as PlanetScale branches are cheaper than a Neon mirror for one product up to ~100 GB.
- Example: 20 previews a day, 2 h each, 10 GB: PlanetScale branches ≈ USD 11/month, no mirror needed.
- Neon previews are ≈ USD 34/month plus the mirror (≥ USD 5–24/month).
- The mirror itself works well:
- Lag under load: p50 2 ms, p99 4 ms, max 15 ms. 0 rows missing.
- Neon branches off the mirror do not inherit the subscription. Neon drops it for you.
- Promote the mirror as a backup: first write 1.1 s after the cut, 0 of 56,703 acknowledged orders lost.
- Three traps you must design for:
- ID counter (sequence) values are not copied. On a fresh branch the first insert fails:
duplicate key .. A one-statement fix takes 0.4 s.. . Key (id)=(1) already exists - A sleeping mirror makes PlanetScale keep WAL with no limit (default
max_slot_wal_keep_size = -1). 1 GB per hour at our load. - A normal Neon subscription (slot
failover = false) froze a PlanetScale HA resize for 34 minutes, until I dropped the slot. Withfailover = trueit went through and the mirror survived.
- ID counter (sequence) values are not copied. On a fresh branch the first insert fails:
- Owner's resize question ("PS-5 to PS-10 without downtime??"): with 2 spares, the dedicated PgBouncer had 0 failed requests in all 3 resizes (longest stall ~5 s). Local PgBouncer (6432): 8 failed in 1 s. Direct (5432): 126–753 failed over ~2.5 min. Details in "Resize with spares".
- Spent ≈ USD 0.90 of 4. Everything deleted (33 created, 33 deleted; vendor lists show 0 left).
All verdicts of the mirror test
| # | Claim | Verdict | Key number |
|---|---|---|---|
| 1 | Continuous mirror | PROVEN | lag p99 4 ms; 900 CU-s per hour |
| 2 | Hourly catch-up, then sleep | PROVEN | 60 min of changes caught up in 43 s |
| 3 | Hourly full copy (control) | PROVEN (2 GB) | 5.3 min from far away |
| 4 | Preview branches off the mirror | PROVEN | 20 branches usable in 20 s |
| 5 | ID counters (sequences) | PROVEN | fix for 53 counters: 0.43 s |
| 6 | Schema changes | PROVEN | wrong order stops all; fix recovers in 3.3 s |
| 7 | Masked customer data | PARTLY | Neon masking branch: 294 s for 2 GB (beta) |
| 8 | Mirror as a backup | PROVEN | first write 1.1 s, 0 lost |
| 9 | Cost model | PARTLY | 2 prices not published (see 9) |
| R | Resize with spares | PROVEN | dedicated PgBouncer: 0 failed |
| How a move with no errors works}} |
Put off past v1
- Live copying between regions. DECIDED · T3:3641
- A global load balancer: ruled no. DNS movement stays; 30–60 s lag accepted. DECIDED · owner D17
- Cross-vendor backups are not put off: they are required. DECIDED · T3:3641
Everything else in this chapter
What can go wrong, and what happens then
| What goes wrong | What happens | Who acts | Source |
|---|---|---|---|
| One cluster in raichu dies | chapter 2: address removed, retry | traffic controller | DECIDED |
| raichu's database vendor dies | restore at another vendor; hours | person + T3 | DECIDED |
| raichu region dies | raichu users wait for restore; mercury moves to ampharos | person | DECIDED |
| User picks the wrong home on a new phone | second account possible | nobody | MISSING |
| raichu token sent to ampharos | refused | — | DECIDED |
| Mercury retry lands in the other region | possible double delivery | receiver must ignore repeats | DECIDED |
| Saved events stuck in a dead region's queue | delivered after recovery, within 72 h | — | MISSING rule |
| Landscape removed with users still homed | refused in design; no re-home tool | person | MISSING |
Tested and not tested
| Item | State | Source |
|---|---|---|
| Postgres moves between vendors (same region, operator far away) | TESTED · : 3.2 s write wait, 0 failed | spike M |
| Key-value and file moves between vendors | TESTED · spikes K, T; jaeda's table how:98-103 | how.md |
| Real region-to-region move | NOT TESTED | spike M:4 |
| Geoproximity on Route 53 | NOT TESTED · no AWS token | how:127 |
| Home claim flow, Doc B, ping-pick | NOT BUILT · NOT TESTED | — |
| Mercury multi-region intake, 421 protocol | NOT TESTED · design | — |
| Logto in two regions with several copies | NOT TESTED · asked for by the owner's whole-system test (owner D37) | OWNER-DECISIONS D37 |
Open decisions in this chapter
| # | Question | Default | Whose |
|---|---|---|---|
| O2 | Migration record for moves | yes | T3 default |
| O3 | Standby database at a second vendor | no | T3 default |
| G-1 | Home: one per user (per landscape Logto) or one per platform? | one per user, per landscape Logto (matches the single key) | PROPOSED · this chapter |
| G-2 | New device / returning user who picks the wrong home | after sign-in, check the other landscapes' Logto for the same email and offer to switch | PROPOSED · this chapter |
| G-3 | Which issuer for mercury's management API behind mew | caller uses its own landscape's issuer; mercury accepts each host landscape's issuer for its management audience only | PROPOSED · this chapter (security review needed) |
| G-4 | Mercury's Neon region | raichu (Singapore) | PROPOSED · this chapter |
| G-5 | Queued events in a dead region | the surviving region's mercury reads the dead region's Upstash (it is a vendor service, likely still up) | PROPOSED · this chapter |
| G-6 | Re-home tool | a manual, audited runbook in v1 | PROPOSED · this chapter |
| G-7 | Airport codes for raichu and ampharos | SIN and IAD | PROPOSED · this chapter |
| G-8 | Where T3 runs for far-away databases | a small fence helper next to each region's database | PROPOSED · this chapter |
| G-9 | celebi's hosts | [pikachu] | corpus inference (fleet.md:234) |
9 · Mercury (webhooks)
Mercury: webhook intake
One webhook intake for the whole fleet, on a name that sends each provider to the nearest healthy landscape.
- Mercury was cut from the whole-system test on purpose: not tested. NOT TESTED · poc/REPORT.md
Where it runs
- Once for the whole fleet, as an ordinary service on its own
mercuryplatform, in every serving landscape (raichu and ampharos). DECIDED · ARCH:431-437 - In-cluster, plain DNS, no Cloudflare in the path. DECIDED · ARCH:555-556
- Hot data (dedup, queue) in each landscape's Upstash; long-lived data in one Neon database for mew. DECIDED · mercury:176, 186
- Which region that Neon sits in is not named. Proposed: raichu. MISSING PROPOSED · decision 44
One webhook, step by step
One Stripe webhook for tin
- Vendor Stripe calls
hooks.withwebhook. mercury. mew. cluster. atomi. cloud/ … payment.. DECIDED · onboard:351succeeded - DNS Route 53 answers with the nearest healthy landscape, say raichu. DECIDED · ARCH:552-554 NOT TESTED · geoproximity not tested, how:127
- DNS raichu has no healthy address, so its set leaves the mew name. Route 53 answers with ampharos. DECIDED · ARCH:302-304
- Mercury checks the signature against the stored URL, never the request's host. DECIDED · ARCH:507-512
- Mercury in ampharos verifies and saves it in ampharos's own Upstash. DECIDED · ARCH:302-304
- Dedup: "seen this event id?", kept 72 h. Then it saves and queues the event, and only then answers Stripe 200. DECIDED · mercury:213, 253
- It asks every possible receiver "is this yours?". Exactly one must say 200; all others say 421 ("not mine"). DECIDED · mercury:283-287
- Done. Each delivery carries a fresh mercury signature; delivery is at least once, so receivers must cope with repeats. DECIDED · ARCH:522-525; mercury:257
- Anything else (no owner, two owners, errors) is refused loudly and retried, from seconds to hours, for 72 h; then the dead-letter queue. DECIDED · mercury:283, 337-339
- Events already saved in raichu's queue wait until raichu's mercury is back; there is no takeover rule. MISSING PROPOSED · G-5: the surviving region drains the dead region's Upstash
- 421 ("Misdirected Request") means only "not mine". DECIDED · mercury:287
- A retry may land in the other landscape and be delivered twice; receivers must ignore repeats. DECIDED · mercury:257
- About 24 h of continuous failure to one receiver opens a breaker until a probe or a person re-enables it. DECIDED · ARCH:531-533
- A receiver in the same landscape that allows private calls is called in-cluster; others by public name. DECIDED · mercury:307
Who sets it up
- Each consumer platform has one internal tenant (
WebhookEnginein its carbon chart, e.g.internal/). DECIDED · ARCH:466-484; T3:3171nitroso - Each receiver service adds a
WebhookRoutein its own chart (e.g. tin). DECIDED · ARCH:466-484 - T3's webhook controller turns those into calls to mercury's management API; only mercury writes its own Upstash config. DECIDED · T3:3171
- A tenant's home name is fixed for life; registered intake URLs are treated as permanent. DECIDED · mercury:141; T3:3201
- Previews: one fixed mercury endpoint per provider (the "callback rail"); the old per-preview tunnel is deleted. DECIDED · public-callback-tunnel.md:6, 155, 196
When things die
| What dies | What happens | Source |
|---|---|---|
| Primordial | mercury's config is frozen; intake keeps working | DECIDED · T3:3107 |
| raichu (whole landscape) | its set leaves the mew name; providers land in ampharos, which verifies and saves in its own Upstash | DECIDED · ARCH:302-304 |
| Events already saved in raichu's queue | wait until raichu's mercury is back (within 72 h); no takeover rule | MISSING |
| Both raichu and ampharos | no buffer; providers retry (often for days) | DECIDED · ARCH:342-348 |
Still open
- Which issuer for mercury's management API, which sits behind the mew name while tokens are per landscape? Proposed: the caller's own landscape issuer; needs a security review. MISSING PROPOSED · decision 43
- Queued events in a dead region: proposed, the surviving region's mercury drains the dead region's Upstash (a vendor service, likely still up). PROPOSED · decision 45
- Older text puts
WebhookEnginein mercury's own chart; newer text puts it in the consumer's carbon chart. OPEN · GAPS G9
10 · Must settle, then all open decisions
What you decide
Three gaps must be settled before building, and the test found 5 design changes. Then 48 smaller decisions, each with a default you can just accept.
- Tick "Default" (or "Agree") to accept. Untick only what you disagree with. PROPOSED · this site
- The reply line at the top builds itself; copy it and paste it to jaeda.
- Your ticks stay in this browser only.
Your reply to jaeda
Ticks and the reply line are also at the bottom of this screen.
The 3 gaps to settle first
G1. Is production one "serving" landscape or four landscapes? (contradiction, corpus main vs corpus main)
The gap
- ARCH:78-79, 367 and
goals/: pichu, pikachu, raichu, ampharos are separate landscapes; one issuer each; namesfleet. md:210 api..tin. nitroso. raichu. cluster. atomi. cloud - lop:558-568 and hw:457-462: all four are rows of one
servinglandscape, one issuer and audience; namesapi., row only as an extra slot.zinc. nitroso. serving. cluster. atomi. cloud - Why it matters: DNS names, issuers, audiences, home-sharding (ARCH:112), failover between raichu and ampharos, and whether dev and prod share a user pool all change.
- Neither is clearly newer. lop calls itself the "ruled fleet auth model" (lop:570).
jaeda's position PROPOSED · jaeda, POSITIONS.md
pichu, pikachu, raichu, ampharos are FOUR separate landscapes, one sign-in issuer each. "serving" is the name of the group (the class of landscapes that run apps for users), not a landscape. Why: ARCHITECTURE, fleet.md, the newer T3 goal draft and every owner walk-through (D1: a landscape has ONE region) agree; one landscape cannot have two regions. The logto-operator / helm-wrapper text that folds them into one serving landscape is the stale one. Names: <svc>..
What the whole-system test showed TESTED · poc/REPORT.md, Positions
Held: yes
3 issuers; killing raichu's clusters never touched pichu
G2. The DNS chain, and T3's traffic text writes the wrong host.
The gap
- The spare base (S8) sits on Route 53, the same host as the main answers (ARCH:353-358). Route 53 down = both dead. Owner asked for a fix (owner D36) → Brief 2's
PROPOSAL-DNS..md - T3:2856 says the loop "writes CF only on delta"; every write rule in T3's traffic section is Cloudflare-only. ARCH:227, 304 and T3:139 make Route 53 primary plus the S8 group. No Route 53 write rules, limits, TTL, geoproximity rule or DoD row exist in T3 (geoproximity appears once, T3:3491).
- TTL: ARCH:230 "30–60 s"; MASTER locked 30 s; T3:2860 60 s (newest). Route 53 30 s is untested.
jaeda's position PROPOSED · jaeda, POSITIONS.md
follow model/proposal/PROPOSAL-DNS.md when it lands. Until then: the traffic controller writes EVERY host in one loop (primary, mirror, spare), the spare base must not share a DNS host with the primary, TTL 60 s on Cloudflare and 30 s where the host allows.
What the whole-system test showed TESTED · poc/REPORT.md, Positions
Held: yes, with a caveat
one T3 loop wrote Route 53 + DO + Cloudflare; DO lags up to 75 s
G3. Nothing makes an app wait for its database on a first release.
The gap
- One Kargo pin moves the primordial and app deploys together (spike A:112). db-init goes red on a missing database (rules:665-670; dotnet-api:289), but no retry rule, no ArgoCD sync retry, and no ArgoCD health check for Database / KeyValue / Cache / Store / Email records exist (only CloudflareDeploy, T3:3352). The Kargo gate may pass on app health alone.
jaeda's position PROPOSED · jaeda, POSITIONS.md
(a) ArgoCD custom health for Database / KeyValue / Cache / Store / Email (Healthy only when the record is Ready and its secret is written); (b) the primordial app syncs before the app chart (sync wave or Kargo step order), and the Kargo stage gate needs BOTH healthy; (c) the app pod waits for its secret (it cannot start without it) and db-init retries with backoff. No human step.
What the whole-system test showed TESTED · poc/REPORT.md, Positions
Held: yes
pods waited in ContainerCreating until the secret came
Also proposed by jaeda
G5. Sign-in during a dead DNS name has no tested option, and the issuer is permanent.
The gap
Logto OSS cannot be one issuer on two domains (logto@v1.46.0, chapter 4). Option D (issuer on its own DNS host) must be chosen before go-live or never (alog:102-105).
jaeda's position PROPOSED · jaeda, POSITIONS.md
accept "signed-in users keep working until the pass ends; nobody new signs in" for v1, and put the issuer name on a base that the DNS proposal makes survive one host outage. Do not run Logto as two issuers.
What the whole-system test showed TESTED · poc/REPORT.md, Positions
Held: yes
refresh via spare name, iss unchanged
G10. Rescue router: not built, not tested, partly unowned.
The gap
bun-api-engine's DoD has no retry-once row and no trip-point row (bun-api-engine:192-200). No library sets a connect time limit (dart-api-engine:77; go-api-engine:63), so a silent cluster stalls clients. Browser storage for Doc C, certificates and CORS on spare names: not written. "Rescue domain on R53" (ARCH:350) and "S8 backup base" (ARCH:353): same domain or two? Not said.
jaeda's position PROPOSED · jaeda, POSITIONS.md
every client library sets a connect time limit (2 to 3 s), retries once on another address when the request is safe, and ships the rescue router. Add the missing DoD rows.
What the whole-system test showed TESTED · poc/REPORT.md, Positions
Held: yes only after a fix
the socket must really close (challenge 15)
G11.
The gap
"Dead cluster leaves DNS in ~5–10 s" (ARCH:292, 308; how:122) is impossible under 3 failed probes × 5 s (T3:2937); measured 13–19 s.
jaeda's position PROPOSED · jaeda, POSITIONS.md
say "13 to 19 s" (3 failed checks x 5 s + DNS), not "5 to 10 s".
What the whole-system test showed TESTED · poc/REPORT.md, Positions
Held: only for the fast hosts
the slowest host decides: up to 75 s + TTL 60 s
The test says the design MUST change (5)
- The whole-system test found these 5 places where the design papers are wrong or silent. Each needs a yes before building. TESTED · poc/REPORT.md
M1Name the machine credential: one GitHub App (deploy keys are off in the org; anonymous GitHub = 60 calls/h stopped discovery for 4 min).
Found by the whole-system test. Default: change the design papers as written. TESTED · poc/REPORT.md, CHALLENGES.md
M2Rotation: a fixed owner role owns every object; generation logins only become it (Logto broke otherwise).
Found by the whole-system test. Default: change the design papers as written. TESTED · poc/REPORT.md, CHALLENGES.md
M3Client spec: the connect timeout must close the socket; test with a black hole, not a stopped server.
Found by the whole-system test. Default: change the design papers as written. TESTED · poc/REPORT.md, CHALLENGES.md
M4Decommission is a lock every controller checks before creating anything; it also sweeps by label.
Found by the whole-system test. Default: change the design papers as written. TESTED · poc/REPORT.md, CHALLENGES.md
M5Database record needs a privilege field (Logto needs CREATEROLE).
Found by the whole-system test. Default: change the design papers as written. TESTED · poc/REPORT.md, CHALLENGES.md
All 48 open decisions
Deployment (chapter 1)
1Drop services. (O1)
Default: default yes, list from row names.
2Cloud account for clusters (O6)
Default: default fleet/.
3One secret store per platform per cluster (O7)
Default: default adopt.
4Which Infisical is authoritative (O8)
Default: default: leave as a setting.
5Tier on a landscape is only a label (O5)
Default: default unchanged.
6Row layout (pick 3)
Default: default by service platforms/ (test first).
7Machine files in their own fleet-state repo (pick 4)
Default: default yes (test first).
8Young/mature/scaled lives on the platform (pick 6)
Default: default yes, with a check.
9Per-landscape settings live in the service chart (pick 7)
Default: default yes.
10Who refuses a landscape with zero clusters
Default: default the compiler chart.
11Database-not-ready rule on first release
Default: default db-init retries 10 min + ArgoCD health checks for dependency records PROPOSED.
12Schema rollback
Default: default expand-then-contract, one release backward-compatible PROPOSED.
13Keep people's two fleet edits (emergency values:, break-glass)
Default: default yes PROPOSED.
14Proxy in front of every dependency vs credential swap in the app library (owner D25)
Default: jaeda: library first, proxy only for apps we cannot change.
Cluster and failover (chapter 2)
15DNS chain fix (owner D36)
Default: see PROPOSAL-DNS..
16TTL
Default: default 60 s everywhere until Route 53 30 s is tested.
17Client connect time limit
Default: default 3 s (jaeda: 2–3 s).
18Minimum clusters per prod landscape
Default: default 2.
19Who does dark-verify and what it checks
Default: default automated smoke test, then a person flips traffic.
20Where "< 0.1 % traffic for 1 h" is measured
Default: default gateway metrics in Grafana Cloud.
21Cloudflare in front for faster failover
Default: default no (owner D17); pricing open.
22Freeze rule fleet-wide or per landscape
Default: default fleet-wide, as written.
Frontend (chapter 3)
23Owner of the weekly Vercel test and the switch (OD-25)
Default: default A: a production-ops node under T3.
24Who flips the CNAME
Default: default a person, one-command runbook, no automatic flip in v1.
25Web name and zone
Default: default web., written by the traffic controller.
26Vercel build and Cloudflare-only parts
Default: default a second "standby" build, page cache off.
27One build per landscape issuer
Default: default yes.
28Problem catalog unit
Default: default per platform × tier (newest text); confirm.
29Error portal template adoption (OD-15, with OD-11)
Default: not ruled; no default given in corpus.
Logto (chapter 4)
30One "serving" issuer or one per landscape (GAPS G1)
Default: default one per landscape (ARCH §5).
31Spare sign-in path (options A–F)
Default: default F if the DNS proposal allows, else C.
32Logto version pin
Default: default v1.46.0.
33Passkeys
Default: default off in v1.
34Machine token lifetime
Default: default 10 min, cached to expiry.
35Signing-key rotation
Default: default staged, grace ≥ access token life.
36Social callback addresses
Default: default one per landscape issuer, listed in LogtoInstance.
37ENDPOINT / admin console settings
Default: default ENDPOINT = issuer host, proxy header on, admin console not public.
38Prod Logto database
Default: default Neon Scale in the landscape's region, no region copy.
Global (chapter 5)
39Migration record (O2)
Default: default yes.
40Standby database at a second vendor (O3)
Default: default no.
41Home per user or per platform
Default: default per user, in the landscape's Logto.
42New-device user picking the wrong home
Default: default check other landscapes after sign-in and offer to switch.
43Issuer for mercury's management API behind mew
Default: default caller's own landscape issuer; needs a security review.
44Mercury's Neon region
Default: default raichu.
45Queued events in a dead region
Default: default the surviving region drains the dead region's Upstash.
46Re-home tool
Default: default a manual, audited runbook in v1.
47Airport codes
Default: default raichu = SIN, ampharos = IAD.
48Where the database fence runs for far-away databases
Default: default a small fence helper per region.
Also open in the T3 goal but outside these chapters: O9–O14 (Garden rotation, Upstash alternative, Valkey operator licence, Aurora in v1, R2 as durable store, Aurora upgrades) – see T3:611-667. celebi's hosts default [pikachu] (fleet.md:234).
Your reply to jaeda
Every gap found while writing
Serious gaps (G4 to G10)
G4. Seven of spike A's 13 fixes are not in the T3 goal. Holes 2, 4, 5, 7, 9, 12, 13 live only in the unmerged fleet edit (spikes/); 2, 5, 7, 12 are unproven. jaeda's dm/ says "all in the T3 goal".
G5. Sign-in during a dead DNS name has no tested option, and the issuer is permanent. Logto OSS cannot be one issuer on two domains (logto@v1.46.0, chapter 4). Option D (issuer on its own DNS host) must be chosen before go-live or never (alog:102-105).
G6. Home claim: "per platform" vs one key. ARCH:113 says a home per platform; Logto is one per landscape for every platform with one custom_data. key (goals/). Also missing: new-device users can make a second account in the other region; no re-home mechanism although landscape removal needs one (ARCH:914); travelling users.
G7. Web failover has no owner and no mechanics. Who flips the CNAME to Vercel, by what signal, which token: not written. Web hostname shape and zone: not written. Vercel has no plan for Cloudflare-only parts (R2, D1, Durable Objects, Images; nextjs:296-298). OD-25 (probe owner) open.
G8. One frontend build vs one issuer per landscape. nextjs:332-335 says one Worker build promotes through "issuer-compatible stages" and a different issuer needs a new build; with one issuer per landscape every stage is a different issuer, so "promote one Freight" does not hold for frontends.
G9. Mercury contradictions.
WebhookEnginehome: mercury.md:150-152 (mercury's chart) vs ARCH:477-478 and T3:3094-3096 (consumer's carbon chart; newer).- Mercury's mew-wide Neon: rendered by mercury's service chart (mercury.md:150-151, onboard:159-160) vs T3:3692-3695, which refuses that and needs carbon (newer).
- Its management API sits behind mew, but tokens are per landscape: which issuer? Not answered.
- Events already queued in a dead region's Upstash: no takeover rule. Neon region not named.
G10. Rescue router: not built, not tested, partly unowned. bun-api-engine's DoD has no retry-once row and no trip-point row (bun-api-engine:192-200). No library sets a connect time limit (dart-api-engine:77; go-api-engine:63), so a silent cluster stalls clients. Browser storage for Doc C, certificates and CORS on spare names: not written. "Rescue domain on R53" (ARCH:350) and "S8 backup base" (ARCH:353): same domain or two? Not said.
Medium gaps (G11 to G20)
G11. "Dead cluster leaves DNS in ~5–10 s" (ARCH:292, 308; how:122) is impossible under 3 failed probes × 5 s (T3:2937); measured 13–19 s. G12. Decommission gate: text = traffic < 0.1 %/1 h AND no empty v-landscape name (T3:824); DoD tests "traffic:true or probes pass" and skips the empty-name check (T3:916). Who measures 0.1 % and where: not written. G13. Geoproximity: ARCH:302 deletes a landscape's whole set when it has no healthy IP; T3:2919 never removes a name's last record. Coordinates have no field, no writer, no values. G14. Primordial's region is not written, but the database fence needs the operator in each database's region (T3:1676). G15. Logto: no version pinned; 10-minute access only if the operator writes it on every API (Logto default 1 h); refresh reuse detection not documented upstream (study I:155); signing-key rotation policy not written; social callback addresses not listed; production preview sanitizer unbuilt (risk R0, env:1486). G16. Ordinary-service rollback is not written (only Workers, ARCH:744); schema rollback after a forward migration is not written. G17. Service Decommission removes rows, but the materializer "never deletes rows" (T3:1039 vs 1052): which controller does it? Landscape removal: "refused while referenced", but Landscape has no reconciler (T3:3475). G18. Owner D3 says people touch fleet only for landscapes/clusters; fleet.md:112 still lets people edit row values: and make break-glass commits. G19. Region codes: regions. not defined (MASTER:538); raichu/ampharos codes and the US city never written. Spike A used regions. as a landscape list (spike A:35), T3 as a code table. G20. The rule "a ~1 min stale window is fine" assumes outside callers retry (ARCH:44); nothing tells partners to.
Small: wording and stale text
- ID clash: corpus D7, D11, D21 (mercury) ≠ owner D7, D11, D21. Use the
DM-prefix (T3:431) or "owner D…". machinery-stablerollback: ARCH:715 "move the tag back" vs fleet.md:376 "never moves backward" (detailed rule).- fleet-root and compiler chart under
registry/(fleet.md:149) vsmachinery/(owner D2; spike A). - OrphanedSource flips rows (ARCH:725; fleet.md:1162) vs rows not touched (T3:1036, newer).
- Kargo objects made by the platform controller (secrets-chain:22; T3:3520) vs by the compiler chart (T3:976, newer).
- Cluster account field
providerAccountRef(fleet.md:212) vsspec.(T3:602). CloudflareDeploy account: three names (T3:3288; spike C sketch; lens-t3-crs:176).account - Cluster phases: 7-phase list (lens-t3-crs:41, with Crossplane) vs provisioning → ready → decommissioned (T3:820).
- Ops wildcard written by "copper" (lens-adversarial-review:45) vs only the traffic controller (T3:2859).
- Old ArgoCD "RollingSync" promotion (seed-cluster:103) vs Kargo.
- Upstash "availability-only" (ARCH:662) vs owner D12 "durable key-value store".
PlatformDependency(secrets-chain:319; ARCH §7) vs five split kinds (owner D4, T3:3468).- Custom Infisical roles "licence-gated" (secrets-chain:53) vs disproven (T3:695-700).
- Login via mew (ARCH:95; env:782; lens-adversarial-review:67) vs v-landscape issuer superseded (ARCH:371).
- Problem catalog per service (ARCH:284) vs per platform × tier (portal:327; T3, newer).
- ARCH §14 says every verification spike is green; its own note says no evidence backs it (ARCH:957).
- Weekly Vercel probe in nextjs deliverables (nextjs:184) but struck from its DoD (nextjs:412).
- celebi's hosts are an inference (
[pikachu], fleet.md:234).
Words
Words used on this site
Every tool and every word we made up, in one line each.
- Tap any dotted word on any screen to see its line here.
- Source names (ARCH, T3, spike A …) are explained at the bottom.
- Owner rulings are written "owner D…"; they override older design text. DECIDED · OWNER-DECISIONS
Tools we use
- ArgoCD
- makes clusters match what git says.
- ApplicationSet (AppSet)
- an ArgoCD rule that stamps out one deploy per file, repo or cluster it finds.
- Kargo
- moves a version from stage to stage by editing one file per stage.
- Helm chart
- a package of Kubernetes files with settings.
- Infisical
- our secret store, on Primordial.
- ESO (External Secrets)
- copies secrets from Infisical into each cluster.
- cert-manager
- gets TLS certificates from Let's Encrypt.
- Route 53
- AWS's DNS host; holds our main cluster answers.
- Cloudflare
- holds the
atomi.zone, a copy of our DNS, Workers and R2.cloud - Cloudflare Workers
- Cloudflare runs small programs at its edge; our web frontends run there.
- OpenNext
- turns a nextjs app into a Worker.
- Vercel
- a nextjs host; our standby for web.
- R2 / S3 / CloudFront
- file hosting at Cloudflare / AWS / AWS's CDN.
- Logto
- open-source sign-in server (OIDC).
- Neon, PlanetScale, DigitalOcean Postgres, Aurora
- hosted Postgres vendors.
- Upstash
- hosted key-value store (Redis protocol), durable.
- Dragonfly / Valkey
- in-cluster caches.
- Tigris
- hosted file store.
- Grafana Cloud
- where metrics and alerts go.
- k3s / k3d
- small Kubernetes / k3s in docker (used in tests).
- Gitea
- a self-hosted git server (stood in for GitHub in tests).
Words we invented
- Landscape
- one environment with its own data: pichu, pikachu, raichu, ampharos.
- Virtual landscape (v-landscape)
- a DNS name over several landscapes (mew, celebi); owns nothing else.
- Platform
- a product (nitroso); one
<p>.repo.carbon - Service
- one part of a platform (tin); one
<p>.repo.<svc> - Module
- one named thing inside a service (api, web, maindb).
- LPSM
- Landscape / Platform / Service / Module: every thing's four-part address.
- Primordial
- our control cluster (ArgoCD, Kargo, Infisical, T3); safe to be down for hours.
- T3 / fleet-operator
- our operator: clusters, platforms, dependencies, DNS, webhooks, Workers.
- Materializer
- the part of T3 that writes empty row files when a service repo appears.
- Traffic controller
- the part of T3 that runs the 5 s health check and writes DNS.
- cf-deploy controller
- the part of T3 that rolls out Worker versions.
- Carbon
- a platform's home repo (
nitroso.):carbon platform.and shared charts.yaml - fleet
- the one public deploy repo:
registry/(people),machinery/(static),platforms/(machines). - Row file
- one file per service per landscape saying which version runs there.
- App chart / primordial chart
- the two charts every service ships: pods (to clusters) / outside needs (to Primordial).
- Compiler chart (
diene-platform) - the chart in
fleet/that turnsmachinery platform.into Kargo and AppSets.yaml machinery-stable- the git tag real platforms use for the compiler chart.
- Mark / gem
- a cluster's second name (opal, ruby) / sibling clusters of one landscape.
traffic: true/false - a cluster's switch: in or out of DNS.
- Dark-verify
- test a new cluster on its own name before users arrive.
- Decommission
- the record that really deletes something (name typed twice).
- OrphanedSource
- the alarm when a service repo is deleted; nothing is removed.
- Gray / orange
- plain DNS / behind Cloudflare's proxy (orange is unused).
- S8 / backup base
- a second base domain on Route 53 with the same addresses.
- Rescue domain
- a domain on Route 53 that reaches the AWS copy of the address books.
- Rescue router
- app code that, only when a name cannot connect, tries spare addresses from Doc C.
- Doc A / B / C
- edge files: where the books are / which landscapes exist / full address book.
- Allowlist
- the built-in list of our own domain endings; any other address is refused.
- Issuer
- the exact sign-in address in every token.
- Lithium / aldehyde
- our Logto element / the platform that owns it.
- logto-operator
- our controller that manages apps and settings inside Logto.
- LogtoApp / LogtoResource / LogtoInstance
- records for an app / an API audience / one landscape's Logto settings.
- Home landscape / home claim
- the user's region / the field in Logto that says it.
- Doc B ping-pick
- at first sign-in, ping each landscape and pick the home.
- Mercury
- our webhook intake platform.
- WebhookEngine / WebhookRoute
- a platform's webhook tenant / one receiver's route.
- 421 rule
- mercury asks every receiver; exactly one says 200 ("mine"), all others 421 ("not mine").
- 72 h obligation
- mercury keeps retrying a webhook for 72 h, then parks it in a dead-letter queue.
- Callback rail
- the fixed mercury endpoints that receive vendor callbacks for previews.
- Migration record
- the record that moves a database, store or key-value store to another vendor, account or region.
- Ledger
- T3's memory of everything it created, in an R2 bucket outside Primordial.
- Garden
- dev/test/preview environments (lapras, ditto, rotom, absol, eevee, castform), each with its own Logto.
- ENTEI
- the infrastructure cluster that hosts Garden previews; never serves users.
- Problem catalog / erbium
- each service's known-error list / the human error portal on Cloudflare.
- boron
- private admin access through Cloudflare tunnels.
- SoS
- the Infisical area for operator-internal secrets.
Source names
ARCH= corpusconcepts/.ARCHITECTURE. md fleet.= corpusmd goals/.fleet. md carbon.= corpusmd goals/.charts/ carbon. md T3= T3 goal draft, PR 71,goals/(branchdependency-operator. md goal/). Newer than corpus main.t3-lock-operations spike A=spikes/(real ArgoCD v3.6.1 + Kargo v1.12.5 on local k3d, T3 played by scripts, 11 Nov).a-deploy/ REPORT. md spike B=spikes/(secrets chain).b-secrets/ REPORT. md picks= jaeda'sdm/.a/ picks. md rules= corpusrules/.SAMPLE-CREATING-RULES. md - Owner rulings D1–D36 =
spikes/(T3 calls them DM-D1…).lock/ OWNER-DECISIONS. md ARCH= corpusconcepts/(main branch).ARCHITECTURE. md T3= the T3 goal draft, PR 71,goals/on branchdependency-operator. md goal/. Newer than ARCH.t3-lock-operations spike D=spikes2/(real Cloudflare DNS, 200 pretend users, 6 pretend clusters, 11–12 Nov).d-cfdns/ REPORT. md spike G=spikes/(local PowerDNS stand-in, 11 Nov).g-control/ REPORT. md how= jaeda's plain notesagenda-deps/.v2/ how. md lib-contracts= corpusconcepts/.lib-contracts. md ARCH= corpusconcepts/.ARCHITECTURE. md T3= T3 goal draft, PR 71 (newer than ARCH).nextjs= corpusgoals/.nextjs-frontend. md portal=goals/.error-portal. md bfu=goals/.lib/ bun-frontend-utils. md spike C=spikes/(real Cloudflare Workers API, 11 Nov).c-cfdeploy/ REPORT. md spike G3=spikes/part G3.g-control/ REPORT. md spike D=spikes2/.d-cfdns/ REPORT. md ARCH= corpusconcepts/.ARCHITECTURE. md T3= T3 goal draft, PR 71 (newer).alog= corpusgoals/(our Logto fork and charts).aldehyde-logto. md lop=goals/.logto-operator. md lib-contracts=concepts/.lib-contracts. md env=concepts/.environments. md hw=goals/.helm-wrapper. md spike E=spikes/(Logto rolling upgrade, local).e-localops/ REPORT. md study I=spikes/(docs-only).i-signin/ REPORT. md logto@v1.= Logto's open-source code on GitHub, tag v1.46.0 (latest 1.x, 5 Nov 2026), read with46. 0:<file>:<line> gh.how= jaeda's notesagenda-deps/.v2/ how. md ARCH= corpusconcepts/.ARCHITECTURE. md T3= T3 goal draft, PR 71 (newer).fleet.= corpusmd goals/.fleet. md mercury= corpusgoals/.mercury. md onboard=goals/.mercury-onboarding. md lib-contracts=concepts/.lib-contracts. md env=concepts/.environments. md logto= corpusgoals/.aldehyde-logto. md lop=goals/.logto-operator. md spike M=spikes2/.m-pgmoves/ REPORT. md - ID warning: the corpus has its own "D11", "D21", "D7" (e.g. mercury's callback rule). They are not the owner's D11/D21/D7. Owner rulings here are written "owner D…". see the gaps
GAPS=model/.content/ GAPS. md POSITIONS= jaeda's proposed answers,model/.POSITIONS. md how/how.= jaeda's plain-words answers,md agenda-deps/.v2/ how. md