Deployment
Taking a project to a real Cloudflare account, and proving it works.
Four commands, in this order. The order matters once — secrets attach to a Worker that exists.
Provision the resources
bunx highx provision --dry-run # see the plan
bunx highx provision # create itprovision reads the bindings out of your generated configuration, lists what
the account already has, and reconciles the two:
| Outcome | Meaning | Creates anything? |
|---|---|---|
ready | The resource exists and the config points at it | no |
adopt | The resource exists; only the recorded ID needs updating | no |
create | Nothing in the account matches | yes |
Nothing is created until the plan has been shown and confirmed. Adoption is what makes it safe to re-run: a second run against a half-provisioned account fills in what is missing and creates no duplicates.
It needs CLOUDFLARE_ACCOUNT_ID and a CLOUDFLARE_API_TOKEN with D1:Edit,
Workers KV Storage:Edit, and Workers R2 Storage:Edit. A .env.local is
picked up automatically.
A project that binds only a Durable Object has nothing to provision — Cloudflare creates those during the deploy. That is the expected result for the default scaffold.
Check
bunx highx doctorIt compares what your code needs against what your configuration provides, in both directions, confirms your credentials, and fails on a binding still pointing at a placeholder ID. That last check exists because a placeholder deploys perfectly and then fails on first use.
Deploy
bunx highx deployThis runs the project's own Wrangler against the generated configuration. The
version being the project's own matters: the exports block declaring a
SQLite-backed Durable Object needs Wrangler 4.118 or newer, and an older one
fails the deploy demanding a migrations list instead.
Set secrets, then deploy again
bunx highx secret put SESSION_SECRET
bunx highx deployNever move a secret into vars to avoid this step. A vars entry is committed
to source control, visible in the dashboard, and shared by every environment.
In CI, pass the value through the environment rather than a shell:
- run: bunx highx secret put SESSION_SECRET --from-env
env:
SESSION_SECRET: ${{ secrets.SESSION_SECRET }}Reading a secret at runtime
highx secret put writes; a SecretSource reads. Application code asks the
source rather than reaching for env, so where a value lives is a deployment
decision:
const secrets = cachedSecrets(chainSecrets(secretsStore(env), envSecrets(env)));
const models = createModelRegistry(catalog, secrets);envSecrets(env) | Plain secret bindings — a string already on env |
secretsStore(env) | Cloudflare Secrets Store, held by the account and rotatable without a redeploy |
chainSecrets(...) | First source holding it wins: the managed store in production, the plain binding locally |
cachedSecrets(source) | A TTL over a source that must be asked. The window is the ceiling on how long a rotated secret keeps failing |
The contract is asynchronous, because a managed store is a call. Sources that
genuinely hold their values also answer peek, and that is what lets
createModelRegistry fail at startup naming a missing binding rather than
several turns into an agent run. A chain containing a store cannot peek, so it
resolves on first use instead — the failure then names the binding and both
places it looked.
Adding a provider is one object with a get. Nothing above it changes.
Prove it
A deployment is not finished until two browsers have proved it. Local runs cannot: what only a deployment shows is that the bindings are real, the routes resolve, and a WebSocket survives the edge.
- Sign in and open the app in two browser profiles. A change in one should
reach the other. That single exercise covers a
Secure,HttpOnlysession cookie, a mutation committing through the tenant object, a WebSocket upgrading through the edge, and a patch arriving at a subscriber. - Pull the network on one of them. The connection indicator should change, edits made meanwhile should queue, and restoring the network should send them — or raise a conflict if that record moved on while you were away.
What a deployment adds that local tests cannot
Local runs use one machine and one isolate, which is why the engine figures are algorithmic. Hot-tenant behaviour under real concurrency, cross-colo cache staleness, and real reconnect storms need a deployed environment — and should be measured there before any tenant limit is published as a number. → Load testing