carlos / platform Public

Instance catch-alls: serve each instance of an app at <label>.<parent> Feature Request closed

On branch instance-catchall

Proposal: add a third kind of catch-all, built on branch instance-catchall, on behalf of James. A claim on *.<parent> makes each of an app's instances also answer at <label>.<parent>, where <label> is the first label of the instance's own host:

carlos domains attach --app teams example.com --route www.bab.oncarlos.com   # the parent, held by any app in the account
carlos domains attach --app teams '*.example.com'                            # the instance catch-all

The problem

Managed Jelly runs one CARLOS instance per team (<team>.bad.carlos.exciting.io), and each team should also answer at <team>.letsjelly.dev. Today that takes one exact claim per team, made by the Jelly manager at signup, plus a DNS wait. The existing catch-all (--wildcard --catchall) cannot do it, for two reasons:

  • it sends every subdomain to ONE route;
  • it belongs to whoever holds the apex claim. On letsjelly.dev that is a static site.

How it works

The claim is stored at domains/*.<parent>.json. The star makes it unique per parent through the existing bucket-key gate, and no attach door can produce a *.

Each box's domains.ReconcileAliases derives one ordinary route_aliases row per local instance of the app. Those are the same rows an exact alias claim produces today. So the edge, the edges file, hibernation and wake, the readings sweep and the certificate issuer needed no change. A deleted or moved instance takes its name with it on the next pass.

The rules that keep it safe

  1. Who may attach one. Only a person (never an app token) in the account that already holds <parent>'s own claim may attach a catch-all. The parent claim can belong to any app in that account, so a static site at the apex and teams underneath both work. Boxes re-check the parent on every pass. A catch-all whose parent left the account is inert, and the parent's new holder can replace it.
  2. Refused parents. These are refused at the parent, under it or above it: deployment zones and deny lists, console, tunnel and steering names, the apps domain (whatever CONSOLE_RESERVE_PLATFORM_DOMAINS says), public suffixes, and the sidecar suffix.
  3. Exact claims win. An exact claim on a derived name wins, whoever holds it. That is decided from the bucket listing, so an unreadable claim body still wins. Real routes win too.
  4. Other accounts can't take a live instance's name. While a catch-all is live, the attach doors refuse other accounts the names of the app's existing instances, with the neutral 409. Without that, any account could take a live team's address, because attach needs no DNS proof. Labels with no instance stay open, so an unproven apex claim cannot block a whole domain. A caller's own older claim is never refused.
  5. What derives. Only provisioned, non-sidecar, non-replica routes of the claim's own account and app. At most 1000 per box: status snapshots over 1 MiB are skipped, and per-alias DNS stamping is serial.

Old binaries decode the claim as one with no route, wildcard or paths, and skip it. Every wire change is additive.

What it touches beyond internal/domains

  • Console: the attach, detach, list and per-host doors and the Settings page show and edit the catch-all and the names it derives. The derived names are reported only when they are provably the app's own.
  • CLI: carlos domains list marks the catch-all per-instance / instances, and each derived name via *.<parent>.
  • Docs: docs/site/cli.md has a new subsection, and AGENTS.md records the four rules.

Evidence

  • Design and research: docs/superpowers/specs/2026-10-05-instance-catchall-design.md covers every deployment type I could find: - the flagship, Tito/titogo, the Pages hub and woodstar.app; - self-hosted boxes, fleets, multi-region edges and replicas; - moves, hibernation, isolated exec and path maps; - the platform wildcard, the deny tiers and Let's Encrypt limits; - the app and instance lifecycle.

It also covers the security cases, and records an Opus review whose three medium findings are fixed with tests.

  • CI: green on df6029c8.
  • Live since 2026-10-05 on carlos.exciting.io, where the letsjelly.dev testbed was rebuilt on it. A throwaway instance got catchtest.letsjelly.dev within seconds, with a Let's Encrypt certificate and no claim, and lost it when the instance was deleted. A real signup through the manager then made team alpha-bravo-123.

Known limits

  • One certificate per instance, unless the parent's claim also has --wildcard. Let's Encrypt allows about 50 new certificates per registered domain per week. That is the same as per-team claims today.
  • First come, first served before the instance exists. A name claimed by another account before its instance exists still wins. That is the first-come rule every name has.
  • Combining with a route catch-all. Adding one on a parent that already has a route catch-all (woodstar's shape) takes instance labels away from that catch-all, label by label. This only happens within the same account.

Asks

  • Review of the branch, especially rules 1 and 4 and the parent refusals.
  • Whether --platform should allow an operator to put a catch-all on a deployment zone. It is refused today.
  • Agreement on the CLI spelling: the star in the host is the mode, so --catchall stays a plain on/off flag.

Related, but not part of this branch. While tearing the testbed down I found that "Delete now" leaves two things behind:

  • the app's domain claims, which can no longer be detached once the app is in the trash;
  • its instances' parked data: replicas in the data bucket and /data files on the box. A re-created instance at the same host restores that data and skips the seed.

That extends #219. I'll open it separately if wanted.

Update (2026-10-05): carlos.exciting.io now runs this branch together with two small fixes that came up while giving the letsjelly.dev testbed a wildcard certificate:

  • #237 (wildcard-zone-check): check the DNS-01 zone before placing an order;
  • #238 (first-route-wildcard): --wildcard keeps a first-route claim first-route.

The combined build is 2cf481bc. With *.letsjelly.dev issued through a Route53 delegation zone (delegated mode), a new instance answered at <label>.letsjelly.dev 8s after instances create, with no per-host certificate order. That bears out the design note's point that a wildcard certificate layers on top of an instance catch-all without either knowing about the other.

Review of df6029c8 (instance-catchall). Two readers: Astra (GPT-6, via codex, high effort) independently from a brief, and Claude Opus reading the core code. Every code citation below was checked against the source. Nothing was run: the tests could not start in Astra's read-only sandbox.

The design is sound. Deriving ordinary alias rows means the edge, certificate and hibernation code needed no change, and old binaries skip the claim safely. Not mergeable yet: two P1s and three P2s.

P1: a team's name can be taken before the team exists (wildcard DNS)

The catch-all's guidance points *.<parent> at the edge, and attach needs no DNS proof. So any account can attach acme.letsjelly.dev before Acme signs up. Its exact claim beats the derived name for good. With DNS already pointing at the edge, it gets a real certificate and serves its own content on Jelly's domain. A deleted team's label opens up to the same thing.

The spec calls this first-come (design §6 (c)). But the feature makes this setup the recommended one, and Jelly no longer claims per team, so it never sees the 409 the spec tells it to watch for.

Suggested fix: once the parent holder has proven DNS control, refuse other accounts every one-label name under a live catch-all, not just live instance names. An unproven parent stays open, which keeps your review finding 3.

P1: the guard checks only the current instance domain (Astra)

catchallInstanceName (internal/console/domaincatchall.go:175) checks <label>.<current manifest domain>. After an operator changes the manifest domain, or removes the manifest, instances under the old domain still derive names. Those names lose rule 4's protection, so another account can attach them. Fix: check existing instance records using the same eligibility rules as derivation.

P2: a duplicate label routes differently per box (Astra)

deriveInstanceAliases (internal/domains/aliases.go:140) picks a winner from local routes only. Put acme.old.example on box A and acme.new.example on box B: each box maps acme.<parent> to its own instance. Which database a request reaches then depends on the box it lands on. The comment's "the same on every box" does not hold across boxes. Fix: pick one winner from the deployment-wide instance set, or refuse duplicate labels.

P2: an old owner's detach can delete the new owner's catch-all (Astra)

DetachClaim reads the claim, checks the owner, then deletes without a condition (internal/domains/claims.go:327). Sequence: the parent moves from A to B. A's detach reads A's catch-all. B replaces it through AttachInstanceCatchall. A's delete then removes B's catch-all, and all of B's derived names go with it. Fix: keep the ETag from the read and use release.DeleteIfMatch.

P2: a route catch-all on one box beats an instance name on another (Astra)

lookupUpstream's wildcard fallback runs in the local lookup at internal/edge/edge.go:1397, before the backhaul to a sibling box. If box A serves the parent's route catch-all (woodstar's shape) and box B serves an instance's derived name, requests landing on A go to the parent app, even after fleet publication converges. Fix: prefer a known remote exact host over a local wildcard, with a two-box routing test.

The asks

  • Rules 1 and 4 and the parent refusals: the refusals are thorough. They cover parents above, at and under every reserved name, public suffixes and the sidecar suffix. The weak points are the two P1s above.
  • --platform catch-all on a deployment zone: keep refusing it until there is a real need.
  • CLI spelling: agreed. The star in the host is the mode, and --catchall stays a plain on/off flag.
  • "Delete now" leftovers: please open it. A re-created instance at the same host restoring the old instance's parked data crosses the boundary between the old team and the new one. That makes it more than clutter, and worth more than a footnote to #219.

Thanks, all five are real and all five are fixed in 0be3398e on instance-catchall. Each fix has a test that was watched to fail without it. go vet and go test ./... are clean. (0b063dba only untracks a stray file.)

  1. P1, names taken before the team exists. Done the way you suggested, with a DNS proof. The account publishes TXT _carlos-catchall.<parent> = carlos-catchall-account=<sqid>, the same shape as the #216 mail proof. While that record is present, the catch-all reserves every one-label name under the parent. An unproved catch-all still reserves only its instances' names, which keeps finding 3 of the first review. - The guard checks the record live, so it follows the domain's DNS rather than a stamp taken once. A lookup failure counts as "not proved", never as a refusal. - domains list reports reserves: all or reserves: instances and names the record when it is missing.
  2. P1, only the current manifest domain was checked. The guard now counts the app's instance record keys by first label, across every domain, with one listing and no bodies (domains.InstanceLabelCounts).
  3. P2, duplicate labels. A label shared by two records now derives nothing, on every box alike. It is decided from that same listing, asked only when a box has an instance to name, and the refusal is logged. If the listing fails, the catch-all's existing rows are kept for that pass, never dropped. The "first in host order" wording is withdrawn.
  4. P2, the detach race. DetachClaim uses release.DeleteIfMatch for instance catch-alls, the one claim that can change hands in place. Ordinary claims are unchanged. S3 and DirStore both support the conditional delete; the console's test double needed a passthrough.
  5. P2, a local star key beating a remote exact host. In ServeHTTP, a local match made only through *.<parent> now yields to another box's exact host (homeMap.ExactHome, then backhaulTo). That is the single-box precedence, exact then star, across the deployment. Arrived hops and door calls keep the local answer. Covered by a two-box test (TestLocalWildcardYieldsToARemoteExactHost).

The design note has a new §8 recording these.

On the asks:

  • --platform stays refused.
  • The CLI spelling is agreed.
  • The purge leftovers are filed separately as the next discussion.

carlos.exciting.io still runs the earlier combined build, so these fixes aren't live there yet.

Correction: the purge-leftovers thread is #240, not #239.

Landed on main as 1d1a6f1: one squashed commit, f53000ea, carrying the review fixes, with CI green on the tip and on main. GitHub's main is fast-forwarded. The purge leftovers continue in #240.