Device updates
The update-check contract: one GET request, staged rollouts, update policies.
The contract
GET /u/<appRef>/<channel>/<platform>/<arch>/<currentVersion>| Header | Purpose |
|---|---|
X-Relayer-Device | Anonymous UUID, generated and persisted by your app. Enables sticky rollout bucketing and the fleet dashboard. Optional but strongly recommended. |
Optional query parameters: l.<key>=<value> attaches targeting labels to
the check (see Label targeting below), e.g.
?l.region=eu&l.tier=canary.
Responses:
200- update available; JSON manifest below.204- up to date, no matching artifact, or this device is not in the rollout cohort yet. Either way: do nothing, check again later.
{
"version": "1.5.0",
"notes": "markdown release notes",
"pub_date": "2026-07-12T10:00:00Z",
"url": "https://your-storage/…/app-darwin-arm64.dmg",
"sha512": "…",
"signature": "…",
"updateMode": "recommended",
"minimumSupportedVersion": "1.2.0",
"allowDowngrade": true
}Version comparison is semver-aware (prereleases handled). Artifact matching:
exact platform+arch first, then platform+universal, then platform-only.
allowDowngrade appears (as true) only on rollback fallbacks - the
device's current version was pulled and this manifest points below it.
Try it
# one device, currently on 1.0.0
curl -H "X-Relayer-Device: $(uuidgen)" \
https://www.relayercli.com/u/YOUR_APP_REF/stable/darwin/aarch64/1.0.0
# simulate a small fleet (each device = a distinct UUID)
for i in $(seq 1 10); do
curl -s -o /dev/null -H "X-Relayer-Device: $(uuidgen)" \
https://www.relayercli.com/u/YOUR_APP_REF/stable/darwin/aarch64/1.0.0
doneStaged rollouts
Every release has a rollout percentage, controlled from the Releases tab
(1 → 10 → 25 → 50 → 100 quick steps, or any value). Devices are bucketed
deterministically - sha256(deviceId:releaseId) % 100 - so cohorts are
sticky: a device offered 2.1.0 keeps being offered 2.1.0; a device outside
the cohort keeps getting the previous release until you ramp.
The percentage is monotonic once the release has been offered: from
the first real offer it can only increase. Buckets are sticky, so lowering
the number would never un-serve anyone - it would only make the dashboard
lie about the field. To stop offering, pause; to pull the release,
roll back. The API answers attempts to decrease with 422. Before the
first offer, the percentage is just a plan and moves freely in both
directions.
Devices that don't send X-Relayer-Device can't be bucketed and sit out
partial rollouts - they only receive fully-rolled-out (100%) releases.
Label targeting
A release can carry targeting rules: label keys with allowed values, AND
across keys, any-of within a key (region=eu|uk AND tier=canary). Set
them per release with Target on the Releases tab; the editor suggests
keys and values already observed in your fleet. The rollout percentage then
applies within the targeted cohort.
Labels reach Relayer three ways, merged key-by-key with the check's own
labels winning: l.<key>=<value> query params on /u/ checks, inline
labels on push-mode decisions calls, and labels stored earlier by
push-mode report. Devices missing a targeted key fail closed on that
release and fall through to the newest untargeted release below it -
targeting a canary build never breaks the rest of the fleet.
Like the percentage, targeting is monotonic once a release has been offered: it can only broaden (add values, drop keys), and broadening is itself permanent - you cannot narrow back afterwards. The editor warns before you cross that line. Narrowing needs a rollback and a new release; before the first offer, targeting is freely editable.
Pause and rollback
Pause stops serving a release temporarily; devices already on it stay, nobody new is offered it, and Resume picks the rollout up where it stopped.
Roll back withdraws a release permanently. Devices already ON the
rolled-back version are then served the newest still-published release
below it as a downgrade: the manifest carries "allowDowngrade": true
and its update mode is floored at recommended. The Releases tab shows
how many devices are still stranded on a rolled-back version and exactly
what they fall back to - the rollback confirm dialog shows the same before
you commit. Clients must accept down-version manifests for this to work:
the Relayer CLI does natively, electron-updater needs its allowDowngrade
flag, and Tauri's updater refuses downgrades (publish a higher-numbered
re-release instead, e.g. 2.1.1 with 2.0.0 contents).
If no published release exists below the rolled-back one, those devices hold until you publish something. Rollbacks cannot be un-done by design.
Update policies
Per channel, set a minimum supported version. Devices reporting a
version below it receive the update with "updateMode": "required" and the
minimumSupportedVersion field - your app decides how to enforce it (block
UI, force install, nag). Everything else gets your channel's default mode
(optional or recommended).
Client loop (any language)
on schedule (e.g. every 6h + on launch):
res = GET /u/APP/stable/{platform}/{arch}/{installed_version}
with X-Relayer-Device: {stored_uuid}
if res is 204: done
if res is 200:
verify sha512/signature after download
if updateMode == "required": install now
else: prompt or install per your UXTen lines in any runtime. If you ship Tauri or Electron, you don't even need that - see Adapters.
Why this update?
The Releases tab includes a decision preview that runs the exact serving walk for a real device id (using its stored labels and rollout bucket) or a hypothetical device you describe. It shows every release considered and why each was skipped - outside the rollout bucket, targeting mismatch, no artifact for the platform - plus the manifest the device would receive. The preview and the serving endpoints share one code path, so what it says is by construction what devices get. Each device's detail sheet also shows a "Next check" line answering the same question per device.