Appflare

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:

  1. Checks out the upstream repository at the pinned commit.
  2. Installs its dependencies with install scripts disabled (--ignore-scripts), and builds it with wrangler 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.
  3. 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's appflare.jsonc.
  4. Signs manifest.json with the catalog's Ed25519 key, in a separate job that never runs app code.
  5. Publishes a GitHub Release tagged <slug>@<version> with three files: <slug>-<version>.zip, manifest.json, and manifest.sig.
  6. Regenerates index.json, the list of apps with their latest version, artifact URLs, and manifest digest, and publishes it to https://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, features and alternativeTo, which index.json reads 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 (its access block, and adding "access" to requires), made together with a higher revision. CI builds nothing: the release stays as it is. The signing job signs the revised appflare.jsonc with the catalog's release key (the same key and key id as the release's manifest.sig), and the catalog publishes it next to index.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.

On this page