Updated

Releases and updating

How the desktop app learns there is a newer version of itself, how it resolves an installable build for the machine it is on, and how CI publishes a release.

Four endpoints, and they answer four different questions that are easy to confuse:

Question
/version Is there a newer app? — for a banner.
/update/… Give me the newer app. — for the updater.
/apps/:id/build Where is this game’s build, for this machine?
/releases Here is a new app release. — for CI, not for clients.

Is there a newer app?

GET/api/app/v1/versionToken

The newest desktop app there is.

A version string and a download page. No artifact URL, no signature and no checksum — this is the endpoint behind a banner, and the app compares its own version against latest to decide whether to draw one.

Give me the newer app

GET/api/app/v1/update/{target}/{arch}/{current}

The updater manifest. No authentication.

The app updates itself, and this is the route that lets it. The path segments are the placeholders tauri-plugin-updater substitutes — {{target}}, {{arch}} and {{current_version}} — and the body is the manifest that plugin parses.

Current builds ask the downloads bucket first

Release builds of the app are compiled with an updater endpoint pointing at a signed latest.json (or, on the beta channel, latest-beta.json) in the downloads bucket, which CI writes last, after every artifact it names. This route is the fallback for a build with no endpoint compiled in (a build whose bucket cannot be reached falls back to /version for its banner, not to this route), and it answers the same Tauri shape, so either source installs through the same signature check. What a reader sees of this is described in Updates.

The shape is Tauri's, not ours

Inventing a shape here would mean writing the client half by hand, and the client half is the one whose bugs are silent: an updater that quietly stops checking looks exactly like an updater with nothing to report.

204 means you are current

Not a 200 carrying the version you already have. The plugin treats anything that is not a 204 as a manifest it should act on, so answering an up-to-date client with a manifest naming an older build is how you downgrade somebody who is already ahead.

It is unauthenticated, on purpose

The answer is the same for everybody and is public the moment a release is published — and the normal state for an app that has just been installed is signed out. What bounds the route is not a token: nothing it names is installable without a signature, and the key that makes one is not in this database.

There is no rate limit, also on purpose

Every other route in this family has one. The plugin checks once per launch, and a ceiling reached by an office behind a single address would answer a real client with a 429 that it surfaces as “update check failed” — a failure whose remedy is invisible to the person it happens to. The route reads one indexed row and one cached setting.

Never cached

Cache-Control: no-store. The window between publishing a release and it being offered should be zero, and an edge holding a stale manifest is a release nobody receives with nothing to look at.

Where is this game’s build?

GET/api/app/v1/apps/{id}/build?platform=LINUX_X64

Resolve one installable build, for the machine asking. Authentication is optional.

The install half of /play/launch, and built the same way for the same reasons.

platformenumrequired

The machine asking. There is deliberately no “all of them” — the whole reason this is a second request is that a device receives one download address, for itself.

`null` is the whole refusal vocabulary

The app is hidden, the build is disabled, nothing is published for that platform, or the row is malformed. None of them changes what the device does next, and naming which one would tell a caller which column to probe.

Every decision is made server-side

The app sends an id and its own platform. It never sends a URL and never chooses a file.

The catalogue at /apps says a build exists; this says where it is. A device browsing the app list must not come away holding download addresses for every game on every machine it is not.

`installable` narrows the catalogue

/apps?installable=true asks for only the games this app could actually put on disk, so a launcher does not draw an install button it will then have to retract.

Publishing a release

POST/api/app/v1/releasesShared secret

Publish one desktop release — every platform at once. Called by CI, not by clients.

The step that used to be a person running a script per platform and then remembering to promote. The release workflow calls this once, after it has uploaded the artifacts to the downloads bucket and GitHub, with the bucket’s URLs, and the site’s own update route can offer the release from the moment it returns. The step is optional — a release the site was never told about still reaches every app that reads the bucket’s manifest.

versionstringrequired

Dotted numeric, optionally with a -pre or +build suffix. The app compares this, and one it cannot order is one it never offers.

notesstring

Shown in the app before it installs. Plain text.

artifactsarrayrequired

One to sixteen. Each names a target, an architecture, a URL and the signature the updater verifies.

promotebooleandefault false

Also move the app.version.latest setting, which is what actually offers the release to anybody. false publishes the artifacts and leaves them unreachable — the right shape for a pre-release somebody wants staged.

Why a shared secret, and why it 404s

The caller is a CI job on the internet. It cannot be an /api/internal/ route — nginx refuses those on both public server blocks, which is right for a container-to-container hook and fatal for one called from GitHub — and it cannot be a member session, because there is no member.

The comparison against the secret is constant-time. === on a secret leaks its length and its matching prefix through timing.

Every artifact moves at once

Every artifact and the app.version.latest setting are written in one transaction. Publishing platform by platform and promoting afterwards has a window in it where the setting names a version some machines have no artifact for, and what those users get is being told there is an update and then handed a 404.