On this page

Public origins

A public origin is a declaration. public_origins in the canonical registry names each hostname this fleet publishes to the open internet: which target publishes it, by which publication, to which local upstream, and which paths that publication carries. stado web origin declares, lists, reports and converges them, and refuses to declare a hostname the public DNS root does not carry.

This is the third of the three choices channels keeps apart — a host-control route, a service endpoint and a public download origin are not one decision. The edge that serves a web product is web hosting's web_api.edge; the one durable origin release clients read is the promise in release.

Why this is declared

An origin held only in a deployment's environment variable is not declared, validated, converged or reported, so the only way to learn which origin the deployment selected is to provoke a failure and read it out of the 503 body. And a node can be serving while its name does not resolve: when the public nameserver for the node's *.ts.net name answers NXDOMAIN, nothing outside the tailnet reaches the origin, every public read carries originDiagnosis with state dns_unresolved (channels), and release gates refuse with error_code=infra_down.

A hostname published by Funnel becomes public only once Tailscale's control plane creates the public record for the tailnet. The product does not own that record and cannot converge it. So this capability converges what the product does own — the node's own publication of declared paths to a loopback upstream — and reports the rest by name, with the public resolver's own answer beside it.

Publications

publication names how the hostname reaches the internet, and stado web origin declare defaults to tailscale-funnel.

publication What publishes the name What it needs
tailscale-funnel The target node's own Funnel handler table. The node's own *.ts.net name and a tailnet policy that grants that node Funnel; the public record is created by Tailscale's control plane while Funnel is on, never by this fleet.
web-edge The stado web declaration that owns the same hostname, published in front of the serving service with --upstream-service, on the edge that declaration names (web hosting). converge runs stado web route for it. A hostname in a zone the operator holds, and a declared edge: stado web edge declare or stado web edge provision for the stado edge, or a Cloudflare-served zone and API credential for the cloudflare edge.

A web-edge origin is itself the address release clients read, so api.url (STADO_API_URL) points at https://<hostname>. Its edge_selection.state is agrees when it does and differs when it does not, and the release read-back through that URL is the proof it serves. declare refuses a web-edge origin no web declaration owns, and the registry refuses a web-edge origin whose hostname is a tailnet name.

The declaration

"public_origins": [
  {
    "name": "release-object",
    "hostname": "example-host.example-tailnet.ts.net",
    "target": "example-host",
    "publication": "tailscale-funnel",
    "upstream": "http://127.0.0.1:8765",
    "paths": ["/api/release/object"]
  }
]
Field What it declares
name A lowercase identifier, unique in the document. It is the argument every stado web origin subcommand takes.
hostname A bare public DNS name — no scheme, no port, no path. What a client outside this deployment resolves.
target The declared registry target that publishes the name. A target the document does not carry is refused.
publication How the name is published: tailscale-funnel (the default) or web-edge, as publications describes.
upstream The loopback address the publication forwards to, http://127.0.0.1:<port>. A public origin forwards to a local service, never to another public name.
paths The absolute paths that publication carries, at least one, none with a trailing slash. A path outside the list is not published by this declaration.

The document is pushed the way every registry change is pushed: validated as a whole, compare-and-swap against the generation counter (registry). It is readable at /api/registry.json.

Declaring one

stado web origin declare release-object \
  --hostname example-host.example-tailnet.ts.net \
  --target example-host \
  --publication tailscale-funnel \
  --upstream http://127.0.0.1:8765 \
  --path /api/release/object

--path repeats. --json prints the receipt. The write goes through the registry's own validated compare-and-swap, and a hostname with no public A or AAAA record is refused before any write:

refusing to declare public origin "<name>": <hostname> has no public A or AAAA record, so no public edge could fetch it; publish the name first, then declare it

One name is exempt: a tailscale-funnel declaration of the target's own tailnet name. Tailscale publishes that record only while the node's Funnel is on, and converge is what turns Funnel on for a declared origin, so demanding the record first made such an origin impossible to declare. It is accepted with "publicness": "pending-publication" in the receipt, and converge and status judge the public name afterwards. A resolved name is declared with "publicness": "public".

A web-edge origin whose hostname no web declaration owns is refused first:

refusing to declare public origin "<name>": no `stado web` declaration owns <hostname>, so no edge terminates it; declare one in front of the service that serves the paths with `stado web declare <name> --hostname <hostname> --upstream-service <service>` and publish it with `stado web route <name>`

That order is deliberate. A declaration whose name the DNS root does not carry describes an origin no public edge can fetch, and writing it would publish a promise the fleet cannot keep.

stado web origin list [--json] prints the declarations this registry carries. stado web origin remove NAME [--json] withdraws one. Removing a declaration withdraws the product's claim about the name; it does not unpublish the node.

Reading the report

stado web origin status [NAME] --json reports each declared public origin with its verdict, DNS resolution, publication and edge selection, and exits non-zero when any verdict is not serving: Reading a public origin report explains every field and state.

Converging one

stado web origin converge NAME [--apply] --json makes the declared publication match the declaration. Without --apply it is a plan and its status is unchanged, planned or refused; nothing is written. A web-edge origin converges through stado web route for the web declaration that owns its hostname — that command prints its own receipt first — and the origin receipt carries web_declaration with the product, edge and upstream service instead of handlers and funnel.

{
  "schema": "stado.public-origin-converge-receipt.v1",
  "name": "release-object",
  "target": "example-host",
  "publication": "tailscale-funnel",
  "status": "converged",
  "handlers": [
    { "path": "/api/release/object", "upstream": "http://127.0.0.1:8765/api/release/object", "change": "present" }
  ],
  "funnel": { "enabled": true, "ports": [443] },
  "resolution": { "state": "dns_unresolved", "resolver": "…", "answers": [], "detail": "…" },
  "readback": { "state": "unreachable", "status": null, "detail": "…" },
  "refusal": "the declared origin example-host.example-tailnet.ts.net is published by this node's Tailscale Funnel but has no public A or AAAA record, so no public edge can fetch it"
}

status is converged when the node's publication changed, unchanged when it already matched, and refused when convergence cannot produce a public origin — with refusal saying why in one sentence. handlers names each declared path and whether it was already present or was added; readback is the result of reading the origin afterwards, which is unreachable for a name the DNS root does not carry. Convergence does not create that record: the receipt reports the state and refuses, and the tailnet name is published through Tailscale's own control plane.

GET /api/release/origin

A public, bearer-free route on the deployment that answers which origin it selected and attempts a read of that origin's object route without a URI. It serves no object payload. HTTP 200 means the selection and diagnostic document were returned, not that the origin is serving: inspect originDiagnosis.probe.status and .error. The separately identified node:http or node:https probe reports its DNS, TCP, TLS and HTTP phases, actual peer address, timings and structured errors. It does not replace the object proxy's original fetch result.

The probe records phase (configuration, dns, tcp, tls, http or complete), elapsed ms, timings_ms, resolved and connected addresses, connected port, TLS protocol and certificate authorization, and the received HTTP status. A response-body failure retains any status already received. failure and the original request's cause_chain preserve runtime error codes, system call, address, port, nested causes and separate connection attempts. DNS answers alone never assign the failure to TCP or TLS.

The Node requests use a three-second socket inactivity budget, explicitly reported as timeout_kind: socket_inactivity. This is not a total-operation deadline or a DNS lookup deadline. An HTTP error status is a received response; outcome: error or timed_out means the read failed. The Stado CLI applies its own ten-second bound when reading this diagnostic endpoint. Every answer carries cache-control: no-store.

Answer Body
HTTP 200 {"schema":"stado.release-origin-selection.v1","origin":"<selected origin>","originDiagnosis":{…}}. The diagnosis retains each DNS query and its outcome, plus the separate origin probe. States are dns_resolved, dns_unresolved, dns_unavailable, or dns_not_required for a literal IP address.
HTTP 503 {"error":"release origin is not configured"} — the deployment selected no origin at all. The sibling object route answers that same sentence for that same condition.
HTTP 400 {"error":"exactly zero query fields are accepted"} — the route takes no input, so any query field is a caller reading another route's contract.

stado web origin status reads this route and records what it said in edge_selection, which is how a declaration and a deployment that disagree become one reported row instead of two separate investigations.

What this does not do

This capability does not choose a network provider or derive an origin from a host-control route. A release client still reads one durable origin, and the publication provider owns its public DNS records. DNS resolution, a configured Funnel and an equal selected-origin string do not prove that object bytes can be read. Read an exact release coordinate with stado storage get URI DESTINATION or inspect stado storage stat URI --json: an unavailable channel remains unavailable, never an absent release.

Stado Desktop never shells out for any of this. It runs web origin status as a read-only command and web origin converge as a mutation through the authenticated loopback operator route, which carries an argument vector rather than a shell string, and shows the receipt it got back.

Source: this website