Versions and upgrades

Three different versions matter, and they answer three different questions. Keeping them apart is most of understanding what an upgrade does.

VersionAnswers
The capability versionWhich features this Worker has
The Cloudflare build idWhich build is answering right now
The compatibility dateWhich runtime behaviors this Worker opted into

Every capability reports its own version

Upgrading the kit is the walkthrough, pithy upgrade is the command, and compatibility is what a version promises about the runtime under it.

Each one stamps its package version onto the capability object, and the control-plane manifest reports it per composed capability.

That is what answers the questions a build id cannot: should this customer upgrade, which customers are exposed to something just fixed, does this project predate the capability a pane needs.

The build id says which build. This says which features.

The build id says which build is answering

Injected by Cloudflare and read from a binding the scaffold declares. It reaches five places: every log record, the control-plane manifest, a header on every control-plane response, every audit event, and the check pithy deploy runs to prove the Worker it just shipped is the one answering at your domain.

That last one is why it exists at all. A liveness probe proves a Worker is there; it does not prove it is the one you just deployed — and the old version answering happily is exactly the failure worth catching.

A Worker that does not declare the binding still works. The field is simply absent, which reads as cannot say rather than as a build to trust, and deploy reports its check as inconclusive rather than failing it.

The compatibility date is a behavior contract, not a version number

It is the date the runtime pretends it is. Every fix and every semantic change whose default-on date is later than yours is withheld from your Worker, on purpose, so nothing changes under a deploy nobody made.

Moving it is therefore not an upgrade. It is adopting a set of behavior changes, all at once — and the whole cost of a compatibility date is that the set is invisible until something in it matters.

The kit learned that the expensive way. Nine deployed Workers sat fifteen months behind because each new capability’s config was written by copying a sibling’s, so the first Worker’s unconsidered default became the ninth’s — and a day went into a phantom bug that only a test harness pinned older than the Worker could still produce.

A harness pinned older than the Worker it is evidence about is not evidence.

Two rules came out of that, and they are worth copying:

One date, stated once, imported everywhere it can be and stated with a comment pointing at the source where it cannot — a JSONC file has no imports.

A copy is a second place for a number to be right, so it is gated rather than trusted. A test reads every wrangler config in the tree and fails on any date older than the floor. A tenth Worker copied from a ninth cannot start behind again without the build going red in front of whoever copied it.

Moving it means running every Workers suite and naming any difference in the changeset, rather than leaving it to be discovered.

pithy init scaffolds your Worker at the same date the kit’s own Workers run, so a defect you report reproduces where it is fixed.

What pithy upgrade actually does

It is not npm update. It reconciles your wiring against the manifests you already have installed.

A capability’s manifest grows: a new release declares a binding it did not need before, or a config option it now takes. Upgrade closes that gap.

Five things per Worker: missing bindings per environment, missing config keys, ejected capabilities named and untouched, missing Durable Object exports, and a count of pending migrations.

It writes config, one line of source, and — only with --migrate — the database. The one source file it touches is your Worker’s entry, for a Durable Object export, and both halves of that write are named.

It touches no Cloudflare account.

What a minor release may change

It may add a required binding, a config option with a default, a route, a table, an error code, and English.

It may not remove a route you call, rename an error code, or change what an existing config key means. Those are the things a project depends on without declaring that it does.

A field added to an admin response is optional rather than merely nullable, and that rule exists because those schemas are the one thing in the kit read across a version boundary: a management client validates a response using the capability’s exported schema, against a customer’s Worker running whatever version it happens to be on.

For a project talking to its own Worker this never comes up — both halves ship together. For a dashboard it is a hard break: an additive required key fails validation on every Worker below that release, and the whole pane refuses, on the day the dashboard deploys rather than on any day the customer acted. It happened twice, and the second went unnoticed because both sides were upgraded in one step by one person on one afternoon.

Optional-and-nullable leaves a reader the three states it genuinely has: a value, null for asked and never chose, and absent for this Worker cannot say.

Migrations are append-only once something has shipped

While nothing is published, a capability’s schema is one migration per database, and the initial one is the schema — because no database holds a row a second migration would have to carry across.

The day a version is cut, that inverts. A migration that has run somewhere real is history and is never edited.

A migrationOrder is stable forever for the same reason: renumbering renames the composed keys, and the migrator then reads applied migrations as unapplied and runs them again.

An ejected capability is never reconciled

--eject copies the source into your repo and repoints the wiring at your copy. Upgrade reports it as skipped, forever after.

That is the trade, and it is a one-way door. What you gain is the ability to change it; what you lose is every fix that arrives afterwards.

Checking where you stand

pithy doctor

Reports the installed version of each composed capability against the latest known, and the CLI’s own. pithy upgrade --dry-run says what reconciling would change, and writes nothing.

ESC