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
On 2026-09-07 every public read under stado://releases/ answered HTTP 503.
The durable origin, https://stado.wisent.com/api/release/object, forwarded to
https://charless-mac-mini.tail6443b3.ts.net — a value held only in the
deployment's STADO_RELEASE_ORIGIN environment variable. Nothing in the
product declared that value, validated it, converged it or reported it, so the
only way to read which origin the deployment had selected was to provoke a
failure and read the origin out of the 503 body.
The node was serving. Tailscale Funnel was enabled on it and already published
/api/release/object to http://127.0.0.1:8765/api/release/object. The name
was not resolvable: ts.net's own authoritative nameserver,
ns1.dnsimple.com, answered NXDOMAIN for it, so nothing outside that tailnet
could reach the origin, whatever it was serving. Every public read carried
originDiagnosis with state dns_unresolved (channels), and
the version-check gate refused 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.
The declaration
"public_origins": [
{
"name": "release-object",
"hostname": "charless-mac-mini.tail6443b3.ts.net",
"target": "charless-mac-mini",
"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 that target publishes it. tailscale-funnel is the implemented publication. |
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 charless-mac-mini.tail6443b3.ts.net \
--target charless-mac-mini \
--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
That order is the incident's own lesson. 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 emits an array of
stado.public-origin-report.v1 rows — one per declaration, or one for the name
given — and exits non-zero when any row's verdict is not serving.
[
{
"schema": "stado.public-origin-report.v1",
"name": "release-object",
"hostname": "charless-mac-mini.tail6443b3.ts.net",
"origin": "https://charless-mac-mini.tail6443b3.ts.net",
"target": "charless-mac-mini",
"publication": "tailscale-funnel",
"upstream": "http://127.0.0.1:8765",
"paths": ["/api/release/object"],
"verdict": "origin-not-public",
"origin_error": "charless-mac-mini.tail6443b3.ts.net has no public A or AAAA record, so nothing outside this deployment's own network can reach that origin, whatever it is serving",
"resolution": { "state": "dns_unresolved", "resolver": "…", "answers": [], "detail": "…" },
"publication_state": {
"state": "published",
"published_paths": ["/api/release/object"],
"missing_paths": [],
"funnel_enabled": true,
"detail": "…"
},
"edge_selection": {
"state": "agrees",
"origin": "https://charless-mac-mini.tail6443b3.ts.net",
"endpoint": "https://stado.wisent.com/api/release/origin",
"detail": "…"
}
}
]
That row is the incident, reported rather than guessed: the node publishes the
path, and the name is still not public. origin_error is the one sentence for
the verdict, or null when there is nothing wrong to say.
Each registry, public-edge, actual release-proxy, public-DNS and host-publication
read has a ten-second budget. Independent reads run together; a failed source
does not discard completed readings. registry_observation, observations,
edge_selection.observation and edge_selection.readback_observation retain the
operation, source, start and finish times, elapsed milliseconds, budget, state
and actual error. A report that cannot complete uses complete: false and
diagnostic-incomplete, not a conclusion that the origin is down. If the registry
cannot be read, the row still carries the public edge and release-proxy results,
but its declaration fields are absent.
Verdicts
verdict |
What it means |
|---|---|
serving |
Public resolution and publication agree with the selected origin, and a request through the actual public release proxy received an authoritative object response. This is channel evidence, not proof that a particular artifact exists. |
origin-undeclared |
The registry carries no public_origins entry of that name. Nothing was measured, because nothing was declared. |
origin-not-public |
The name has no public A or AAAA record. Nothing outside this deployment's network can reach it, whatever the node is serving. |
resolver-unavailable |
The public resolver could not be asked. That is not evidence the name is missing, and it is never reported as one. |
origin-unpublished |
The name resolves, but the target's publication does not carry every declared path. missing_paths names which. |
origin-unreachable |
The target, edge or origin read-back could not be obtained, failed, or supplied no usable read-back evidence. |
origin-mismatch |
The deployment's own selected origin is not the declared one, so the public edge is fetching something this document does not describe. |
diagnostic-incomplete |
At least one diagnostic source failed or exceeded its read budget. Completed measurements remain available; missing data is not proof that the origin or host is down. |
resolution.state
The registry-origin DNS reader reports the three states defined by
channels. The deployment's originDiagnosis additionally
uses dns_not_required for a literal IP address, with no invented DNS answer.
resolution.state |
What it means |
|---|---|
dns_unresolved |
The resolver answered, and the name has no public A or AAAA record. |
dns_resolved |
The resolver answered and the name has public records; answers carries them, and the detail names the resolver that said so. It states nothing about whether a client can read the origin: that is what a read reports, and the public edge's own diagnosis carries a second read with the status or the error it got. |
dns_unavailable |
The resolver itself could not be asked. A resolver that did not answer says nothing about the name. |
publication_state.state
published when the target's Funnel configuration carries every declared path,
unpublished when it carries some or none — missing_paths names them — and
unknown when the target could not be asked. funnel_enabled is that node's
own answer about Funnel, never an inference from the DNS result.
edge_selection.state
Read from /api/release/origin on the public deployment: agrees when the
origin it selected is the declared one, differs when it selected another,
undeclared when it has no origin configured at all, and unreadable when
that route could not be read.
edge_selection.readback records a request through the public /api/release/object route itself. Stado uses a fresh, valid release coordinate and accepts only that route's authoritative HTTP 404 document naming the same absent URI. The result retains the requested URI, HTTP status, response and actual failure cause. A missing or failed read cannot become serving merely because configuration matches.
edge_selection.diagnosis retains the deployment's complete DNS queries and socket-level origin probe. That probe may use a different HTTP client and must not replace the original proxy request's result. Its success is not sufficient for serving. Stado Desktop shows the actual public release request, every read's source and error, and the full DNS, TCP, TLS and HTTP evidence in Web → Public origins. To prove availability of specific software bytes, run stado storage get URI DESTINATION for that artifact.
Stado 0.18.0 replaces edge_selection.diagnostic_probe with the complete edge_selection.diagnosis object described above; clients that need the same probe read edge_selection.diagnosis.probe. This is a preview JSON contract change, not a registry or persisted-state migration. Rolling back to 0.17.2 restores the old field. The deployment's GET /api/release/origin returns the complete originDiagnosis independently of the installed CLI version.
Release downloads and authenticated object reads, writes and chunk composition resolve the selected origin through the same public DNS reader used by diagnostics. A and AAAA queries run together; an available address family can serve the request even if the other query fails. The original URL, HTTP Host, TLS server name and certificate checks are unchanged, and literal IP origins do not require DNS. The independent socket probe still uses the deployment's system resolver: its ENOTFOUND can therefore coexist with a successful public release request, and neither observation replaces the other.
If no usable address is returned, the actual request records ORIGIN_DNS_UNRESOLVED when DNS answered without addresses, or ORIGIN_DNS_UNAVAILABLE when a resolver request failed. Its cause includes the resolver and hostname. The gateway returns HTTP 503; Stado preserves unavailable, exits 69 and does not save that error as a downloaded artifact.
Converging one
stado web origin converge NAME [--apply] --json makes the target's own
publication match the declaration. Without --apply it is a plan and its
status is unchanged or refused; nothing is written.
{
"schema": "stado.public-origin-converge-receipt.v1",
"name": "release-object",
"target": "charless-mac-mini",
"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 charless-mac-mini.tail6443b3.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