HighX
Reference

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 configuration

With 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_TOKEN

Cancellation, 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, placement

Cloudflare 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
CheckFails whenWarns when
config:project / config:adapterNo config or no adapter
auth:<platform>The platform refused the credentialsIt could not be reached
binding:<name>A required binding is not declaredOne is declared but nothing requires it
binding:<name>:resourceThe binding points at a placeholder ID
binding:<name>:plaintextA credential-looking name sits in vars
bindings:conflicting_binding_kindOne binding is used two incompatible ways
migrations:*Versions malformed, a skipped version, or driftMigrations written but not applied
retention:policyA retention period is missing or not positive
retention:archiveExpired 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 CI
Plan (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 it

Only 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 CI

Migrations are versioned SQL files in a migrations/ directory next to the project:

migrations/
  0001_create_invites.sql
  0002_index_invites.sql

Each 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_invites

What 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 it

A 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_ID and CLOUDFLARE_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
SignalExampleConfidence
Passed to an adapter constructornew D1GlobalStore(env.GLOBAL_DB)exact — it accepts only that kind
A method only one kind hasenv.ACCOUNT_DO.getByName(…)exact
fetch() is called on itenv.ASSETS.fetch(request)a Fetcher; the adapter says which kind
Read as a plain valueenv.SESSION_SECRETa 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>
.env

Terminal 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.env
Files 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.local

shadows 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

FlagApplies to
--config <path>allProject config path
--wrangler <path>allPlatform config path
--cwd <path>allDirectory to resolve paths against
--stage <name>allWhich stage to act on
--env KEY=valueallSet a variable for this run. Repeatable
--env-file <path>allLoad a .env file. Repeatable
--from-env [VAR]secret putTake the value from an environment variable
--jsondoctorPrint the report as JSON
--dry-runprovision, secret load, migrateShow the plan without writing
--yesprovision, secret load, migrateDo not prompt
--statusmigrateShow applied and pending migrations
--binding <name>migrateAct on one D1 binding when several exist
-h, --helpallUsage

Exit codes

Code
0Success. For doctor, deployable — possibly with warnings
1A failing check, a cancelled plan, missing credentials, unreadable configuration, or an ID that could not be written

Both are safe to gate CI on.

On this page