Deploying a Heximon app
heximon deploy builds your app and ships the artifact to whatever platform your heximon.config.ts
declares — the same vp build you'd run by hand, then a handoff to that platform's own CLI. It never
reimplements the vendor's upload logic; it resolves credentials, runs the build, and shells wrangler deploy, vercel deploy --prebuilt, netlify deploy, or deployctl deploy for you.
pnpx heximon deploy
What it ships to
The platform comes from your config's platform value — the same instance that shapes the build already
carries its own ship logic:
| Platform | Ships with | Credentials |
|---|---|---|
Node (default, no platform declared) | Reports the node dist/server.js run path — no vendor upload | none |
| Cloudflare Workers | wrangler deploy --config dist/wrangler.json | none required locally (wrangler login); CLOUDFLARE_API_TOKEN for CI |
| Vercel | vercel deploy --prebuilt (--prod for the production environment) | none required locally (vercel login); VERCEL_TOKEN + VERCEL_ORG_ID + VERCEL_PROJECT_ID for CI |
| Netlify | netlify deploy (--prod for the production environment) | none required locally (netlify login); NETLIFY_AUTH_TOKEN + NETLIFY_SITE_ID for CI |
| Deno Deploy | deployctl deploy <entrypoint> (--prod for the production environment) | none required locally (deployctl login); DENO_DEPLOY_TOKEN for CI |
A production environment ships with the vendor's --prod flag; anything else (staging,
preview:<id>) ships a preview. Override the resolved platform with --platform cloudflare when your
config doesn't declare one — useful for a project that sets platform in its Vite config instead of the
heximon.config.ts config file this command reads.
pnpx heximon deploy --env staging
pnpx heximon deploy --platform cloudflare
aws lambda update-function-code
against an already-existing function. heximon deploy fails loud on a Lambda-configured project rather
than shipping an unverified command.Before anything ships: preflight
On a real run (not --dry-run), the command first checks that the platform's own CLI is installed and
authenticated — wrangler whoami / vercel whoami / netlify api getCurrentUser — before touching the
build or any side-effecting step. A missing binary halts with install guidance; a logged-out CLI halts with
the login command. Nothing runs half-finished because the vendor CLI turned out not to be logged in.
Bootstrap a fresh project — --create
On a brand-new Vercel or Netlify account, the project or site doesn't exist yet — deploy --prebuilt and
netlify deploy both need something to link to. --create <name> creates it and links your directory to
it, before the build:
pnpx heximon deploy --create my-app-production --team my-team
It's idempotent — an existing target with that exact name is linked, never re-created — and it classifies
by exact name match rather than trusting the vendor CLI's own exit code (Vercel's link --project
auto-creates a missing project and exits 0; Netlify's sites:create --name auto-renames on a collision
and also exits 0).
--team scopes the bootstrap to a multi-team account and only makes sense together with --create.
Cloudflare needs no bootstrap — wrangler deploy creates the Worker by name on first ship — so
--create there is a reported no-op.
Apply pending migrations with --migrate
A schema change on a filesystem-less edge target can't migrate itself at boot, so --migrate applies every
auto-detected database's pending migrations through its local drizzle-kit, sequenced around the ship:
pnpx heximon deploy --migrate --env-file .env.production
Detection reuses the same DatabaseConfigDetector as heximon migrations — every
config it classifies as plain drizzle-kit-applicable becomes a migration target; a Cloudflare D1 config
(wrangler-d1) or a Durable Object SQLite config (skip) becomes an informational note instead, since
neither applies through drizzle-kit.
--env-file supplies the connection env the spawned drizzle-kit child needs — the deploy never
loads your dev .env cascade for this (a stray dev DATABASE_URL would silently migrate the wrong
database during a production deploy), so the child inherits only this process's env overlaid by
--env-file.
A pending batch that looks destructive (a DROP, a narrowing, a new NOT NULL) needs an explicit
--allow-destructive acknowledgment — without it the deploy halts rather than apply a drop alongside code
that stops using the column, which would break a safe rollback:
pnpx heximon deploy --migrate --allow-destructive --env-file .env.production
A bare --migrations <dir> (no --migrate) is the read-only sibling: it gates the deploy on pending
migrations existing in that directory without applying anything.
Create the resources the vendor CLI won't — --provision
Some infrastructure the vendor's deploy command doesn't auto-create — Cloudflare Queues, for one.
--provision creates it after the build (so a strategy can read the emitted deploy config) and before the
ship (so the code that binds it finds it already there):
pnpx heximon deploy --provision --env-file .env.production
--env-file here pushes your app's runtime secrets — DATABASE_URL, API keys, CRON_SECRET — into
the target platform's native secret store (wrangler secret put, vercel env add, netlify env:import),
never as a command-line argument. --env-file needs --provision or --migrate to consume it; passing it
alone is rejected, since otherwise the values would silently go nowhere.
Deploy with a named wrangler profile — --profile
Cloudflare-only: ships (and every provisioning/secret call in the same run) as a named wrangler auth
profile rather than the ambient login.
pnpx heximon deploy --profile my-client-account
Any other resolved platform rejects --profile up front. A CLOUDFLARE_API_TOKEN in the environment still
takes precedence over the profile, matching wrangler's own precedence.
Confirm the app actually came up — --verify
A vendor CLI exiting 0 means the artifact uploaded, not that the app booted. --verify GETs the
deployed URL after a real ship and asserts a 2xx, retrying briefly to absorb cold-start latency:
pnpx heximon deploy --verify
There's no assumed /health route — the framework mounts none by default — so this checks root-URL
liveness only. A boot-crashed deploy (5xx, or nothing answering) becomes a loud, non-zero exit instead of a
silent success.
Preview without shipping — --dry-run
Runs the build and every resolution step, and reports exactly what would ship — no vendor upload, no secret push, no resource creation:
pnpx heximon deploy --dry-run --provision --migrate
Combine it with --provision / --migrate / --create to preview the whole plan for a change you haven't
run yet.
Tear it down — --destroy
The inverse of a ship — currently wired only for Cloudflare (wrangler delete, reading the committed
wrangler.{jsonc,json,toml} for the Worker's name; no build needed). A platform with nothing to remove
(Node) reports unsupported.
pnpx heximon deploy --destroy
Interactively this asks for confirmation first; in a non-interactive shell it refuses to run without
--yes, since a destructive teardown should never fire from an unattended script by accident.
What it detects along the way
Before shipping, the command scans your app's imports for classes that declare themselves
DeployableService — an Upstash Redis client, a Turso/libsql connection, a Resend transport, and so on —
and reports what each needs:
Detected 2 external service(s) from your imports:
· upstash (redis-kv) — needs UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN (not set here)
· resend (email) — needs RESEND_API_KEY
Ensure each is configured in your platform's secret store before deploying.
This is purely informational — no central registry to maintain, and no annotation for you to write. A vendor class declares its own requirements once; the CLI extends automatically as your imports change.
The foundational-id verb — heximon provision
Some resources return an id the compiler needs at build time — a Cloudflare Hyperdrive binding with an
empty id fails the build outright, but provisioning is what creates that id. That chicken-and-egg can't
ride the post-build --provision flag above, so it has its own pre-build command:
pnpx heximon provision --save-ids
It reads the resources your config declares (not the built artifact — there's nothing built yet), creates
the missing ones via the vendor's REST API, and reports each binding → id. --save-ids records them in a
per-environment ledger (.heximon/deploy-env.<env>.json, non-secret ids only) so heximon deploy injects
them into the next build automatically — the one-time flow is heximon provision once, then heximon deploy from then on.
--profile isn't accepted here: provisioning talks to the Cloudflare REST API with CLOUDFLARE_API_TOKEN,
which a wrangler auth profile doesn't carry.
From zero to deployed on Cloudflare
Putting the pieces above together, end to end, for a fresh Cloudflare project with a Hyperdrive
binding in its config (an id --provision alone can't create — the build needs it up front):
pnpx heximon provision --save-ids
This reads heximon.config.ts (no build yet — there's nothing built), creates the missing
Hyperdrive binding via the Cloudflare REST API, and writes its id to
.heximon/deploy-env.production.json — a per-environment, non-secret ledger. Expect output shaped
like:
┌ heximon provision · production
│
◇ Platform: cloudflare · target class: edge
│
◇ Provisioning foundational resources…
│
◇ Provisioned hyperdrive-binding → id 3f1a9c2e-....
│
◇ Recorded HEXIMON_HYPERDRIVE_ID_HYPERDRIVE_BINDING in .heximon/deploy-env.production.json — `heximon deploy` injects it into the build.
│
└ Provisioning complete — ids recorded in the deploy ledger; deploy when ready.
Run it once per environment. From here on, heximon deploy reads the ledger automatically — the id
never needs a manual paste into wrangler.json or an env var.
pnpx heximon deploy --env-file .env.production --migrate --verify
--env-file pushes your app's runtime secrets to wrangler secret put; --migrate applies pending
Drizzle migrations against every auto-detected database before the ship; --verify confirms the
worker actually answers after wrangler deploy exits. A representative run:
┌ heximon deploy · production
│
◇ Platform: cloudflare · target class: edge
│
◇ Resolved 1 credential(s).
│
◇ Loaded 4 secret(s) from .env.production.
│
◇ Injecting 1 deploy-ledger value(s) into the build: HEXIMON_HYPERDRIVE_ID_HYPERDRIVE_BINDING.
│
◇ Applying migrations for 1 database(s): app.
│
◇ Deploy steps complete.
│
◇ Database migrations applied [pre-ship] — app: 0001_init, 0002_add_orders.
│
◇ Uploaded Worker "my-app" (current_version_id: 3f1a9c2e-...).
│
◇ Liveness OK — https://my-app.workers.dev returned 2xx.
│
└ Deployed to cloudflare.
The two commands are the whole lifecycle for a new Cloudflare project: provision runs once to mint
the ids the build depends on, and deploy runs every time after — build, secrets, migrations, ship,
and a liveness check, in one command.
See also
- The heximon CLI — the full command surface and the shared flags every command honors.
- Migrations from the CLI — the
generate/migrate/status/pushbridge--migratereuses. - Deploy targets — the
PlatformHostStrategyeach platform package supplies, which this command's target resolution reads.
Migrations from the CLI
heximon migrations generate/migrate/status/push — DatabaseConfigDetector auto-detects your DrizzleXConfig files, DotenvCascade loads .env, and DrizzleKitRunner drives .bin-resolved drizzle-kit with no hand-written drizzle.config.ts.
How the Compiler Works
Why Heximon compiles a static DI graph ahead of time instead of resolving it at boot, what the compiler reads from your source, what it emits into .heximon/, and the verdicts.gen.d.ts feedback loop that surfaces DI mistakes in your editor.