How the catalog works
Pinned commits, signed artifacts, versioned releases, and the nightly install check.
The catalog is a public GitHub repository,
appflare/catalog. Each app is one folder,
apps/<slug>/, holding a hand-written manifest, appflare.jsonc, and an optional
README.md. Everything else is produced by the catalog's CI.
Appflare does not use an app's Deploy to Cloudflare button: it installs a signed build of the pinned commit and keeps it updated.
An entry pins one commit
The manifest names the upstream repository and one exact commit:
"source": { "ref": "v3.3.1", "sha": "<40-character commit SHA>" }Builds always use sha. ref says which tag or branch the commit belongs to, and a
semver tag becomes the app's version (v3.3.1 gives 3.3.1). An untagged commit
gets a version from its date and SHA, such as 0.0.0-20251222.61a83bd.
Moving an app to a newer upstream version means changing the pin in a pull request. The bump bot opens those pull requests.
CI packs and signs each version
When a change to an app reaches the main branch, CI:
- Checks out the upstream repository at the pinned commit.
- Installs its dependencies with install scripts disabled (
--ignore-scripts), and builds it withwrangler deploy --dry-run, which bundles the Worker without deploying it. The build runs with no Cloudflare credentials, no GitHub token, and no signing key in its environment. - Packs the result into an artifact: an uncompressed zip of the Worker modules,
static assets, and D1 migrations, plus
manifest.json, which lists every file with its size, offset, and sha256, and embeds the app'sappflare.jsonc. - Signs
manifest.jsonwith the catalog's Ed25519 key, in a separate job that never runs app code. - Publishes a GitHub Release tagged
<slug>@<version>with three files:<slug>-<version>.zip,manifest.json, andmanifest.sig. - Regenerates
index.json, the list of apps with their latest version, artifact URLs, and manifest digest, and publishes it tohttps://appflare.github.io/catalog/index.json.
An app of several Workers is built and
bundled one Worker at a time, in the order its entry lists them, and packed into one
artifact. In its manifest.json the primary Worker is worker, exactly as the
only Worker of a one-Worker app is, and every other Worker is listed in workers
with its own modules and static assets. Where a wrangler config names another Worker
of the entry, the artifact records that Worker's name in the entry instead, and the
manager fills in the name it installed that Worker under.
The artifact records each D1 binding's SQL once, under d1, keyed by the binding,
since Workers that share a binding share the database: its migrations, the
schema files and post-deploy migrations (postDeploy) that resources.d1
declares, and its baseline, when it has one.
manifest.json says which format it is written in, and there is one format, 1. A
manager that meets a later format refuses the artifact and asks the admin to update
Appflare, rather than install the app without something it cannot read. The packer
and the catalog checks refuse a manifest field the schema does not know, so a
misspelled field fails the build instead of being left out.
Releases are immutable. A change to appflare.jsonc alone, without a new pin, cannot
be published as a new release; re-pin source to ship it. Two exceptions need no
release:
authors,tagline,licenseNote,featuresandalternativeTo, whichindex.jsonreads from the current manifest.- A change to the app's form or copy (its
secrets,vars,postInstall, name, summary, and similar), or to how it goes with Cloudflare Access (itsaccessblock, and adding"access"torequires), made together with a higherrevision. CI builds nothing: the release stays as it is. The signing job signs the revisedappflare.jsoncwith the catalog's release key (the same key and key id as the release'smanifest.sig), and the catalog publishes it next toindex.json, which lists its sha256 and signature. The version stays the same, so managers offer no update and start no job; they show the new form on the install page and on the Settings tab of apps already installed from that release.
A revision may change only form fields, copy and Cloudflare Access protection, but
those still reach the app: var defaults become the Worker's vars, generated secrets
its secrets, and access decides who reaches it. That is why the revised file is
signed. A manager uses it only when its sha256 matches the index, its signature
verifies with the keys built into the manager under the release's key id, and,
compared with the signed manifest.json, it is for the same app and changes only
those fields (requires may only gain "access", or "secret-keys" with keys for
secrets the revision adds). The Worker, the resources an install
creates, and the permissions it asks for always come from the signed
manifest.json. While a release lists a revision, installs and updates to it need
that file: if it cannot be downloaded or does not verify, they fail instead of
falling back to the older form.
The manager installs from the artifact
The manager reads index.json, downloads manifest.json and manifest.sig, and
checks the signature against the public keys built into it. It then reads each file
straight out of the release zip with HTTP range requests and checks its sha256
before uploading it to your account. Nothing from the app's repository runs anywhere
except in the catalog's build. See Security model.
The nightly install check
Every night, CI takes the current release of every app, installs it into a dedicated
Cloudflare test account, requests its health path, and deletes it again. Each app
that passes gets a new check date in index.json. The manager shows it as Install
checked on the catalog.
An app that fails stays in the catalog and keeps its previous date, so a date that stops moving means recent checks failed.
The install check uses random secrets and default settings. It proves the Worker deploys and starts, not that the app is fully configured.