CLI
Every command, what it checks, and the flags.
The highx package ships one binary. Every command reads your project rather
than a running server, so they all work before a first deploy — which is exactly
when configuration mistakes are cheapest to fix.
highx init <name> # scaffold a new project
highx generate # write the typed API, the Env type, and wrangler.jsonc
highx doctor # is this project configured correctly?
highx provision # create the cloud resources it binds to
highx secret # manage the deployment's secrets
highx env # what HighX resolved, and from where
highx deploy # deploy, using the generated configurationWith HighX as a dev dependency, run them as bunx highx … or add them to your
package.json scripts.
Where files are found and written
highx.config.ts is looked for in the working directory and then in highx/.
Everything generated is written beside the project's package.json, found by
walking up from the config. highx.lock.json stays beside the config — it is
HighX's own bookkeeping.
highx init
Scaffolds the files a project owns — config, auth, functions, retention, a worker entry — and nothing else. It never overwrites an existing file, and inside an existing project the dependencies are merged into your manifest: nothing renamed, no pinned version changed.
highx create <name> is the primary spelling. highx init <name> remains an
alias for existing scripts.
Operational Commands
Operational commands target the generated _highx routes and do not load a
local project configuration. Set the bearer token in an environment variable:
export HIGHX_TOKEN='...'
export HIGHX_URL='https://app.example.com'highx workflow inspect run_123 --url "$HIGHX_URL" --token-env HIGHX_TOKEN
highx workflow cancel run_123 --url "$HIGHX_URL" --token-env HIGHX_TOKEN --yes
highx dead-letter list webhooks --url "$HIGHX_URL" --token-env HIGHX_TOKEN
highx dead-letter replay webhooks letter_123 --url "$HIGHX_URL" --token-env HIGHX_TOKEN --yes
highx tenant export --url "$HIGHX_URL" --token-env HIGHX_TOKEN > tenant-backup.json
highx tenant restore --url "$HIGHX_URL" --token-env HIGHX_TOKEN --file tenant-backup.json --yes
highx tenant migration-status --url "$HIGHX_URL" --token-env HIGHX_TOKENCancellation, replay, discard, and restore require --yes. Tokens are read from
the environment rather than command-line arguments. Tenant restore refuses a
different tenant, schema-version drift, or a non-empty target.
highx generate
highx generate✓ .highx/api.ts, .highx/server-api.ts, .highx/auth.ts (2 functions)
query notes.list can("notes:read")
mutation notes.save can("notes:write")
✓ wrangler.jsonc
✓ highx/highx-env.d.ts (2 bindings)The Env type
highx-env.d.ts declares Env globally from the inferred bindings, so
env.SESSION_SECRET is checked against what your code actually uses rather than
a type you maintain by hand. It sits beside highx.config.ts because Env is
ambient and a conventional include skips dot-prefixed directories.
interface Env {
SESSION_SECRET: string;
TENANT_OBJECT: DurableObjectNamespace<import("./index.js").TenantObject>;
}A project that also runs wrangler types gets a second global Env, and the
two merge. Identical members merge cleanly; where they disagree, TypeScript
reports the conflict — exclude one of them.
What it owns in wrangler.jsonc
Generation rewrites the keys it can derive — name, main,
compatibility_date, compatibility_flags, d1_databases, kv_namespaces,
r2_buckets, durable_objects, exports, assets, vars, observability —
and preserves every other key untouched, naming them:
✓ Wrote wrangler.jsonc
Preserved untouched: routes, placementCloudflare has features HighX does not model, and being unable to use them would be worse than writing them by hand.
Comments are not preserved. Put anything you want to keep in highx.config.ts.
Durable Objects are found, not declared
An exported class extending DurableObject is discovered in your code. With
more than one it refuses to guess — the wrong pairing points a binding at the
wrong storage — and asks for statefulClasses in your config.
highx doctor
Compares what your application requires against what the platform configuration provides, in both directions. Failures block a deployment; warnings do not.
highx doctor
highx doctor --json # machine-readable, for CI
highx doctor --cwd apps/api[ok ] binding:ACCOUNT_DO: durable_object binding ACCOUNT_DO is present
[FAIL] binding:GLOBAL_DB: d1 binding GLOBAL_DB is missing
Add a d1 binding named GLOBAL_DB to wrangler.jsonc and redeploy.
[warn] migrations:pending: 1 migration(s) pending: storage is at 1, build defines 2| Check | Fails when | Warns when |
|---|---|---|
config:project / config:adapter | No config or no adapter | |
auth:<platform> | The platform refused the credentials | It could not be reached |
binding:<name> | A required binding is not declared | One is declared but nothing requires it |
binding:<name>:resource | The binding points at a placeholder ID | |
binding:<name>:plaintext | A credential-looking name sits in vars | |
bindings:conflicting_binding_kind | One binding is used two incompatible ways | |
migrations:* | Versions malformed, a skipped version, or drift | Migrations written but not applied |
retention:policy | A retention period is missing or not positive | |
retention:archive | Expired records are deleted rather than archived |
A binding that exists but nobody declared is reported because that is usually a rename that left the old one behind. A build older than the deployed schema is a failure, not a warning: the fix is to roll the code forward, never the data backward.
The placeholder check is the one worth knowing about — an ID like
00000000-0000-0000-0000-000000000000 passes every check that only looks for
presence, deploys perfectly, and then fails on first use.
Without credentials the auth check is skipped entirely, so a purely local run stays local.
highx provision
Reconciles your account against the account-level resources your code binds to, then writes the resolved IDs back.
Which resources exist and how one is created belongs to the platform adapter, so this command reads the same whichever platform you deploy to. On Cloudflare it covers D1, KV, and R2.
highx provision --dry-run # show the plan, change nothing
highx provision # plan, confirm, create, write IDs back
highx provision --yes # skip the prompt, for CIPlan (1 to create, 1 to adopt, 1 already correct):
create d1 GLOBAL_DB highx-global
no d1 named highx-global exists in this account
adopt kv SESSION_KV highx-sessions
highx-sessions already exists; the configuration only needs its ID
ready r2 ARCHIVE_BUCKET highx-archive
highx-archive exists and the configuration already points at itOnly create costs money, and nothing is created until the plan is confirmed.
Without a TTY and without --yes, the command refuses rather than creating
resources unattended.
IDs are written back textually, replacing only the quoted value of one field, so comments and formatting survive. A binding whose ID cannot be located is a failure rather than a skip — reporting success while leaving a placeholder behind is the exact bug this command prevents.
What a platform creates at deploy time rather than here is named by the adapter
and printed when there is nothing to plan. On Cloudflare that is Durable
Objects, which the deploy creates from the migration in wrangler.jsonc;
secrets are set with highx secret put on every platform.
An adapter that provisions nothing — a self-hosted Node or Bun deployment — says so and exits successfully rather than planning against an account it has not got.
highx migrate
Applies pending migrations, or reports them with --status. Which databases
exist, what SQL dialect they speak, and where "applied" is recorded all belong
to the platform adapter; the migrations/ directory and its forward-only,
immutable, contiguous rules are HighX's and are the same everywhere.
highx migrate --status # what is applied and what is pending
highx migrate # plan, confirm, apply
highx migrate --yes # skip the prompt, for CIMigrations are versioned SQL files in a migrations/ directory next to the
project:
migrations/
0001_create_invites.sql
0002_index_invites.sqlEach filename is NNNN_description.sql, and versions must be contiguous from 1.
A .sql file that does not fit that shape is refused rather than skipped — a
migration that silently never runs is a missing table in production.
highx-global (GLOBAL_DB)
applied 0001_create_invites
pending 0002_index_invitesWhat has run is recorded in a _highx_migrations table inside the D1 database
itself, so the history lives with the data it describes. Applied migrations are
immutable: rename a released migration and the next run refuses rather than
guessing which name is right.
highx migrate is a thin wrapper over the platform adapter's migration
manager. Discovering and ordering the .sql files is HighX convention; how
the SQL reaches a database and where "applied" is recorded is the adapter's. The
Cloudflare adapter applies migrations to D1 through the REST API — no wrangler d1 migrations apply — and records them in a _highx_migrations table inside
the database. A future adapter supplies its own manager, and the command above
does not change.
The Cloudflare manager needs the same credentials as highx provision. A D1
migration is not a transaction: its SQL and the row recording it are separate
statements, so write migrations that tolerate a partial previous attempt
(idempotent CREATE TABLE IF NOT EXISTS, and so on). This is the tenant
migration model (highx tenant migration-status) applied to D1, but the two are
independent: tenant migrations run inside each Durable Object on wake, D1
migrations run through this command.
highx secret
Manages the secrets your deployment holds.
highx secret list # what your code needs vs what is set
highx secret put NAME # prompt, hidden
echo "$VALUE" | highx secret put NAME # from stdin, for scripts
highx secret put NAME --from-env # from the environment, for CI
highx secret load .env.prod # set every variable in a file
highx secret delete NAME[ok ] SESSION_SECRET required by your code, and set
[FAIL] STRIPE_KEY required by your code, and not set
[warn] OLD_TOKEN set, but nothing in your code reads itA secret nothing reads is a warning, not an error — it may be a leftover, or read through indirection the compiler cannot follow.
Three refusals worth knowing:
- A value as a command-line argument is refused. It is already in your shell history and visible in the process list.
- An empty value is refused. An unresolved
${{ secrets.X }}expands to an empty string, and writing it replaces a working secret with one that fails only when your code finally reads it. - Platform credentials are withheld by
load. The adapter names its own — on Cloudflare,CLOUDFLARE_ACCOUNT_IDandCLOUDFLARE_API_TOKEN— and they authenticate the CLI rather than the application. A deployment that held them could read them.
Secrets attach to a deployed Worker, so every one of these fails until you have deployed once. The order is deploy → set secrets → redeploy.
Binding inference
doctor works out which bindings your application needs by reading the code. A
hand-written list can only agree with the code or be wrong, and it is wrong
precisely when someone adds a binding and forgets.
Bindings required by this project (inferred from your code):
durable_object ACCOUNT_DO inferred: ACCOUNT_DO.getByName() is called
r2 ARCHIVE_BUCKET inferred: passed to new R2ArchiveSink()
assets ASSETS inferred: ASSETS.fetch() is called
d1 GLOBAL_DB inferred: passed to new D1GlobalStore()
kv SESSION_KV inferred: passed to new KvSessionStore()
secret SESSION_SECRET inferred: read as a plain value| Signal | Example | Confidence |
|---|---|---|
| Passed to an adapter constructor | new D1GlobalStore(env.GLOBAL_DB) | exact — it accepts only that kind |
| A method only one kind has | env.ACCOUNT_DO.getByName(…) | exact |
fetch() is called on it | env.ASSETS.fetch(request) | a Fetcher; the adapter says which kind |
| Read as a plain value | env.SESSION_SECRET | a var or secret |
Inference is deliberately conservative. A binding used two incompatible ways is
reported as conflicting_binding_kind rather than resolved by guessing. A
confident wrong answer is worse than a question — correct it under
bindings.
Credentials for a command
Supply them however suits: exported, injected by a CI runner, or read from a
file with --env-file. HighX resolves them itself, in one fixed order — highest
first:
the ambient environment (highest)
--env KEY=value
--env-file <path> in the order given, first wins
.env.<stage>.local
.env.local
.env.<stage>
.envTerminal exports outrank inline --env, and inline values outrank dotenv files.
Some runtimes load .env files into the process before your command starts — Bun
does, Node does not — so those preloaded values behave like terminal exports. Use
--env when an inline override must win over a value Bun already loaded.
This is safe because HighX gitignores every .env* file, so a CI image carries
none of them and the values it exports are the only source there is.
Commands report which source supplied each variable, and never print a value:
Environment for this command only, not deployed:
CLOUDFLARE_API_TOKEN .env.local (also in environment)
OPENROUTER_API_KEY .env.local (also in environment)highx env
Answers "why is this variable that value" without disclosing the value.
highx env
highx env --json
highx env --env-file ci.envFiles consulted, in lookup order:
.env.local found
.env absent
Resolved by HighX:
CLOUDFLARE_API_TOKEN set, 53 characters environment shadows .env.local
OPENROUTER_API_KEY set, 73 characters --env shadows .env.localshadows names the sources that also carried the variable and lost. Values are
masked to their length, which distinguishes an empty value from a truncated
paste — the thing people are usually checking — without putting a token on
somebody's screen.
It needs no configuration, so it still answers in a project whose
highx.config.ts does not yet load.
These variables authenticate the CLI and go nowhere else. --env-file is
not how you set a Worker secret. Keep .env.local out of version control — it
holds a token that can create and delete resources in your account.
The file format is the usual one: KEY=value, export prefixes, # comments,
quotes with escapes inside double quotes. A # inside a value is data. A line
that cannot be read is an error rather than a skipped variable. Multi-line
values are not supported.
Flags
| Flag | Applies to | |
|---|---|---|
--config <path> | all | Project config path |
--wrangler <path> | all | Platform config path |
--cwd <path> | all | Directory to resolve paths against |
--stage <name> | all | Which stage to act on |
--env KEY=value | all | Set a variable for this run. Repeatable |
--env-file <path> | all | Load a .env file. Repeatable |
--from-env [VAR] | secret put | Take the value from an environment variable |
--json | doctor | Print the report as JSON |
--dry-run | provision, secret load, migrate | Show the plan without writing |
--yes | provision, secret load, migrate | Do not prompt |
--status | migrate | Show applied and pending migrations |
--binding <name> | migrate | Act on one D1 binding when several exist |
-h, --help | all | Usage |
Exit codes
| Code | |
|---|---|
0 | Success. For doctor, deployable — possibly with warnings |
1 | A failing check, a cancelled plan, missing credentials, unreadable configuration, or an ID that could not be written |
Both are safe to gate CI on.