HotupdaterHot Updater
Concepts

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 locally

A 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:

FieldMeaning
scope_keyThe scope
catalog_idAn opaque identity, allocated on the scope's first write and kept after it
strategy, channel_id, channel_key, platform, fingerprint_hashThe scope's parts
generationIncreases by one with every write and never resets
payloadThe canonical catalog JSON, which only the compiler interprets
catalog_hash, byte_sizeThe payload's SHA-256 hash and size
is_tombstoneWhether the scope has no enabled Release
updated_at_msWhen 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:

  1. Read the catalog row, the scope's enabled Releases, and, for a new Release, the scope's newest Release ID.
  2. Apply the change. A new Release ID sorts after the scope's newest.
  3. 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/:currentBundleId

The 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.

AnswerStatus and headers
The scope's catalog200 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 scope404 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 one400
A channel key that is not the base64url of a channel name, or a malformed fingerprint hash400
A missing or invalid client key401
Client auth that cannot read its keys, or a database the schema fence refuses503
An artifact200

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.

On this page