Release Catalog Contract
What a Release Catalog holds, how core writes it, and how the client routes serve it.
Responsibilities
Bundle immutable artifact identity and storage metadata
Release rollout, compatibility, Channel and message policy for one Bundle
ReleaseCatalog the compiled desired state of one scope
Core compiles a scope's catalog with every Release change
Database stores rows through a database adapter
Device accepts a catalog and selects its update locallyA database stores rows and never decides which Release an installation runs. Core writes every catalog the same way on every database, so a new backend needs only a database adapter. For migration from an older version, see Move from v0 to v1.
Scopes and catalog rows
A Release Catalog covers one scope: a Channel, a platform, and an update
strategy, APP_VERSION or FINGERPRINT with a fingerprint hash. Every Release
belongs to one scope, named by its scope_key.
A catalog row holds:
| Field | Meaning |
|---|---|
scope_key | The scope |
catalog_id | An opaque identity, allocated on the scope's first write and kept after it |
strategy, channel_id, channel_key, platform, fingerprint_hash | The scope's parts |
generation | Increases by one with every write and never resets |
payload | The canonical catalog JSON, which only the compiler interprets |
catalog_hash, byte_size | The payload's SHA-256 hash and size |
is_tombstone | Whether the scope has no enabled Release |
updated_at_ms | When the row was written |
Release changes
Every Release change (a deploy, a policy update, a promotion or a deletion) runs in one transaction with its scope's catalog:
- Read the catalog row, the scope's enabled Releases, and, for a new Release, the scope's newest Release ID.
- Apply the change. A new Release ID sorts after the scope's newest.
- Compile the enabled Releases and write the catalog's next generation.
The Release reads are guarded through the catalog row, so a concurrent change to the same scope makes the transaction run again instead of publishing a stale catalog. A scope left without enabled Releases keeps its row as a tombstone with a higher generation.
The compiler keeps a frontier of 11 artifacts for update and crash selection, plus every compatible enabled Release, so a rollback can find the predecessor of any active Release. Rollback candidates ignore rollout and target cohorts. The catalog is never truncated: a change whose compiled catalog exceeds 256 KiB fails before anything is written, and the previous Releases and catalog stay.
Rebuilding a catalog recompiles it from the scope's enabled Releases and writes it only when the result changed. Back up catalog rows together with Releases: a scope that has Releases but no catalog row refuses deploys and rebuilds until the row is restored, because its identity and generation history cannot be recovered from the Releases.
Client routes
GET /release-catalogs/app-version/:platform/:channelKey/:appVersion
GET /release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash
GET /artifacts/v1/:targetBundleId/from/:currentBundleIdThe update check is one point read of the scope's catalog row; it scans no Releases or Bundles. Artifact resolution reads both Bundles in one batch and their patch with one unique read. Catalog URLs carry no installation state: the device selects its update from the catalog.
| Answer | Status and headers |
|---|---|
| The scope's catalog | 200 with cache-control: public, max-age=0, s-maxage=5, cdn-cache-control: public, max-age=5, stale-while-revalidate=5, stale-if-error=0, an etag, and vary: Accept-Encoding plus the header of the plugin that provides client auth, such as x-api-key for apiKeys(). A matching If-None-Match answers 304. |
| No catalog for the scope | 404 with x-hot-updater-catalog: none and the catalog's cache-control and cdn-cache-control, so a shared cache also answers a scope with no catalog yet. Clients read it as no update. |
| An app version in a form other than its canonical one | 400 |
| A channel key that is not the base64url of a channel name, or a malformed fingerprint hash | 400 |
| A missing or invalid client key | 401 |
| Client auth that cannot read its keys, or a database the schema fence refuses | 503 |
| An artifact | 200 |
Every answer other than a catalog, its 304, and a scope's missing catalog is
private, no-store. A shared cache in front of the client routes keys a
catalog by host, path, Accept-Encoding and the client-auth header, never by
installation state. A database can purge those copies: core calls its
onCachedRoutesChange, which createEngineDatabase
takes, after
each committed write that changes a catalog.
A GraphQL, RPC or other transport needs an HTTP adapter or proxy that serves these routes with the same answers.
How long a replaced catalog stays visible
A shared cache keeps a catalog fresh for five seconds. s-maxage in
Cache-Control forbids serving it stale, so a cache that reads only
Cache-Control revalidates an expired catalog while devices wait. A CDN that reads CDN-Cache-Control (RFC 9213), such as
Cloudflare's Workers Cache, uses it instead: for five more seconds it answers
with the expired catalog while it revalidates in the background, and
stale-if-error=0 still passes origin errors through.
Each server also reuses a catalog it read for five seconds. Without a purge,
a device can therefore read a catalog for up to 10 seconds after a deploy or a
Roll back replaced it behind a cache that reads Cache-Control, and up to 15
seconds behind one that reads CDN-Cache-Control. The stale window adds its
five seconds only when a scope's checks are a few seconds apart; a busy scope
refreshes in the background as soon as the catalog expires.
Artifact responses
An artifact response for protocol 1 has the target manifest's URL and hash, an optional archive URL, and the original file for every asset of the target manifest. The Hermes bundle may also carry a patch from the current Bundle. When both download sizes are known, the server includes the patch only if it is smaller than the original. The original stays in the response, so a device can recover from a missing or corrupt base file or a failed patch. See bundle diffing for patches and the manifest's download fields.
Self-hosted servers
standaloneRepository gives the CLI and console a self-hosted server's
database through its admin API. The server runs core, its plugins and every
transaction on its own database. See the
Standalone repository reference.