HighX
Platform

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 it

provision reads the bindings out of your generated configuration, lists what the account already has, and reconciles the two:

OutcomeMeaningCreates anything?
readyThe resource exists and the config points at itno
adoptThe resource exists; only the recorded ID needs updatingno
createNothing in the account matchesyes

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 doctor

It 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 deploy

This 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 deploy

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

  1. Sign in and open the app in two browser profiles. A change in one should reach the other. That single exercise covers a Secure, HttpOnly session cookie, a mutation committing through the tenant object, a WebSocket upgrading through the edge, and a patch arriving at a subscriber.
  2. 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

On this page