helipod
Core Concepts

CLI

Every helipod command and its flags.

Every helipod project runs on one binary: helipod (package @helipod/cli). This page covers every command it ships, exactly as implemented in packages/cli/src/cli.ts and its command modules: every flag, every environment-variable equivalent, and every fail-fast check each command performs.

helipod <command> [options]

Commands at a glance

CommandPurpose
devRun the engine locally with hot reload, codegen, and the dashboard
serveRun the production server, single-node, fleet (Tier 2), or object-storage (Tier 3)
deployDeploy the app to a target: serve (live hot-swap), cloudflare, docker, railway, fly, or aws
buildCompile the app to a self-contained executable
migrateMigrate a Convex project into helipod (imports + divergence report)
migrate exportDump a deployment's data to a file
migrate importLoad a data dump into a (fresh) deployment
codegenRegenerate helipod/_generated/ types only, no server
fleet reshardChange a stopped Postgres fleet's shard count
objectstore reshardChange a stopped object-storage deployment's shard count
help / --help / -hPrint usage

How flags get parsed

Every command's flags are parsed by hand, with no argument-parsing library. An unrecognized flag is silently ignored rather than rejected, and a value flag with no following argument is treated as absent, falling back to its default, unless a command says otherwise.

helipod dev

Loads helipod/, generates _generated/, boots the embedded engine over local SQLite (or Postgres, if configured), serves HTTP + WebSocket + the dashboard, and watches the directory for changes to hot-reload functions and schema. This is the command you run while building an app.

helipod dev
FlagDefaultMeaning
--dir <path>helipodThe app directory to load
--port <n>3000HTTP/WebSocket port
--ip <addr>127.0.0.1Bind address
--data <path>.helipod/data.dbSQLite file path (ignored if --database-url is set)
--database-url <url>HELIPOD_DATABASE_URL env, else unset (SQLite)Postgres connection string, see Postgres
--storage-bucket <name>HELIPOD_STORAGE_BUCKET env, else unset (filesystem)Select the S3-compatible file-storage backend, see File storage
--storage-endpoint <url>HELIPOD_STORAGE_ENDPOINT env, else unsetS3-compatible endpoint (MinIO, R2, …)
--web <dir>unsetServe a static web UI directory at the site root, alongside the API

The database and storage settings fall back to the same environment variables under dev as under serve: HELIPOD_DATABASE_URL, HELIPOD_STORAGE_BUCKET, and HELIPOD_STORAGE_ENDPOINT (plus the rest of the HELIPOD_STORAGE_* family) all work, with the flag winning when both are set. Both commands resolve storage through the same boot path.

Admin key handling

dev reads HELIPOD_ADMIN_KEY if set (blank/whitespace-only counts as unset, with a warning). If it's unset, dev generates an ephemeral key for this run only. That generated key gets embedded directly into the (otherwise unauthenticated) dashboard HTML, but only when the key is ephemeral and the bind address is loopback (127.0.0.1, ::1, or localhost). A persistent operator-supplied key, or any non-loopback bind, never gets embedded: the dashboard SPA prompts for it instead.

Startup output

helipod dev → http://127.0.0.1:3000  (dashboard: http://127.0.0.1:3000/_dashboard)
admin key → u3K9xQ1vZ8pL5mW2rT7cY4bN6dF0hJsG
web UI → http://127.0.0.1:3000/        (only if --web was passed)

If the dashboard package isn't built, dev still starts and prints a note to run bun run --filter @helipod/dashboard build.

Hot reload

dev watches --dir recursively (fs.watch, ignoring changes under _generated) and on any change: reloads helipod/, re-runs codegen, replaces the runtime's function modules (runtime.setModules, which also re-applies the always-on _storage:* built-ins), and updates httpAction routes. All of this happens without restarting the process or dropping WebSocket connections. It prints ↻ pushed (N functions) on success, or ✗ reload failed: <message> on a load/type error (the previous, still-working version keeps serving in that case).

The function loader is top-level only: it does not recurse into subdirectories of helipod/. Keep query/mutation/action modules directly under helipod/*.ts (e.g. helipod/items.ts), not nested under a subfolder (e.g. helipod/lib/items.ts); nested modules are silently not loaded. This applies to dev, serve, and deploy alike.

dev runs until killed (Ctrl-C); there's no --once/exit mode.

helipod serve

The production entrypoint. Unlike dev: requires a persistent admin key, binds 0.0.0.0 by default, never generates codegen (an app's _generated/ must already exist and be committed before you serve it), and shuts down gracefully on SIGTERM/SIGINT. See Self-hosting for the Docker walkthrough and Postgres for the Postgres backend.

HELIPOD_ADMIN_KEY=<secret> helipod serve

Fail-fast checks (before anything boots)

ConditionResult
HELIPOD_ADMIN_KEY unset or blank✗ HELIPOD_ADMIN_KEY is required for `serve`: set it to a strong secret. (exit 1)
<dir>/_generated/server.ts missing✗ <dir>/_generated not found: run `helipod codegen --dir <dir>` and commit _generated/ before deploying. (exit 1)
--fleet and --object-store both set✗ --object-store cannot be combined with --fleet (Tier 2): pick one write-scaling story. (exit 1)
--replica without --object-store✗ --replica requires --object-store: … (exit 1)
--shards > 1 without --object-store✗ --shards N (N>1) requires --object-store: … (exit 1)
--shards > 1 with --replica✗ --shards cannot be combined with --replica: a replica is single-shard … (exit 1)
--shards not a positive integer✗ --shards must be a positive integer, got "<value>". (exit 1)
--writer-url without --replica✗ --writer-url only applies to --replica: … (exit 1)
--fleet set but @helipod/fleet not installed✗ fleet mode requires @helipod/fleet: install it (bun add @helipod/fleet). (exit 1)
--fleet without --database-url/HELIPOD_DATABASE_URL (Postgres)✗ fleet mode requires --database-url (Postgres): … (exit 1)
--fleet without --advertise-url/HELIPOD_ADVERTISE_URL✗ fleet mode requires --advertise-url: the URL other fleet nodes reach this node at, e.g. --advertise-url http://10.0.0.2:3000 (exit 1)

Core flags

FlagEnv equivalentDefaultMeaning
--dir <path>nonehelipodThe app directory (must already contain _generated/)
--data <path>HELIPOD_DATA_DIR./data/db.sqliteSQLite file path (HELIPOD_DATA_DIR sets the containing directory; the filename db.sqlite is fixed)
--ip <addr>none0.0.0.0Bind address
--port <n>PORT3000HTTP/WebSocket port
--no-dashboardHELIPOD_DASHBOARD=offdashboard onDisable the dashboard entirely
--allow-deployHELIPOD_ALLOW_DEPLOY=1offEnable POST /_admin/deploy for helipod deploy
--database-url <url>HELIPOD_DATABASE_URLunset (SQLite)Postgres connection string (flag wins if both set)
--storage-bucket <name>HELIPOD_STORAGE_BUCKETunset (filesystem)Selects the S3-compatible file-storage backend
--storage-endpoint <url>HELIPOD_STORAGE_ENDPOINTunsetS3-compatible endpoint (MinIO, R2, …)
--web <dir>HELIPOD_WEB_DIRunsetServe a static web UI at the site root, on the same origin as the sync WebSocket

Why there's no --admin-key flag

HELIPOD_ADMIN_KEY (required, no default, no flag) is read directly from the environment. There is deliberately no --admin-key flag, so the secret never shows up in ps/shell history. S3 storage settings present (--storage-endpoint/HELIPOD_STORAGE_ENDPOINT/_REGION/_PUBLIC_URL) without a bucket fail boot fast: this is treated as a durability misconfiguration (uploads would otherwise silently land on ephemeral local disk), not a fallback.

The flags above cover a single node. If you're running more than one, expand whichever setup below applies to you.

Startup and shutdown

On success, serve prints one JSON line to stdout (fields beyond level/msg/url appear only in the relevant mode):

{"level":"info","msg":"helipod serve","url":"http://0.0.0.0:3000","dir":"helipod","data":"./data/db.sqlite","dashboard":true,"allowDeploy":false}

A fleet node adds "fleet":true,"role":"writer"|"sync"; an object-storage node adds "objectStore":true; a replica adds "replica":true (and "writerUrl":"…" if forwarding is configured). serve never embeds an admin key in the dashboard HTML (it's served key-less; the SPA prompts for it) since it binds 0.0.0.0 by default.

SIGTERM/SIGINT trigger graceful shutdown: stop any fleet node, close the server (which stops every registered driver, including any object-store lease-heartbeat), release the object-store lease if held (best-effort, bounded to 2s so an unreachable bucket can't hang shutdown past a container's grace period), close the store, then process.exit(0), printing {"level":"info","msg":"shutting down"} first.

helipod deploy

Deploys the app through a pluggable target adapter. The default target, serve, pushes the local helipod/ tree to an already-running serve --allow-deploy and hot-swaps it live, no restart. The other targets (cloudflare, docker, railway, fly, aws) provision or shell out to their platform's own CLI. See Deploy & build for the full mechanism (what applyDeploy validates, exactly which schema changes are additive, the atomic-swap order) and Cloudflare for that target's setup; this is the flag/behavior reference.

HELIPOD_ADMIN_KEY=<secret> helipod deploy --url https://my-deployment.example.com
FlagEnv equivalentDefaultMeaning
--target <name>nonedeploy.defaultTarget in helipod.config.ts, else serveOne of serve, cloudflare, docker, railway, fly, aws
--env <name>noneproductionSelects a per-environment settings block from deploy.targets.<name>.environments
--url <url>HELIPOD_DEPLOY_URLnone (required for serve unless set in config)Target serve deployment's base URL
--dir <path>nonehelipodThe app directory to push
--dry-runnoneoffRun preflight and package, skip the actual push
--checknoneoffFail (exit 1) if committed helipod/_generated/ has drifted from a fresh codegen run. Never pushes; combine with --dry-run to also run preflight and packaging

An unknown --target value fails with no deploy adapter for provider "<name>" (v1 supports: serve, cloudflare, docker, railway, fly, aws). Target credentials and settings can live in the deploy block of helipod.config.ts (see Configuration) instead of flags.

The serve target

HELIPOD_ADMIN_KEY is required (bearer-authenticates the request; the config's deploy.targets.serve.adminKey setting can supply it instead). Unlike migrate export/migrate import, deploy has no --admin-key flag. The target must have been started with --allow-deploy (or HELIPOD_ALLOW_DEPLOY=1).

What it does, in order: (1) transpiles every .ts file under --dir with esbuild's transform (types stripped, import specifiers untouched: bare @helipod/* imports resolve against the target's node_modules, relative imports resolve within the pushed tree); (2) probes GET /_admin/deploy/modules for the server's current per-module hashes; (3) if the probe answers, POSTs a delta payload ({changed, unchanged}) to /_admin/deploy with Authorization: Bearer <adminKey>, falling back to a full {files} push when the probe fails or the server reports a stale base.

The serve target does not run codegen

Unlike every other target, which regenerates _generated/ as part of the deploy, the serve target pushes your tree as-is. Run helipod codegen yourself (and commit the result) before deploying if your schema or functions changed. --check verifies exactly this.

Schema changes must be additive only: new tables, new optional fields. A destructive change (dropped/renamed table, changed table number, removed field, changed field type, a field turned required, a new required field) is rejected and the running deployment stays completely untouched; nothing is swapped until both load and schema-diff succeed.

On success (exit 0), verbatim:

✓ deployed via serve (production) — rev 4f2a91c8 (12 functions, 3 changed)
  https://my-deployment.example.com

The parenthetical is (<n> functions) on a full push, (<n> functions, <m> changed) on a delta push, and (<n> functions, full retry) when a delta was retried as a full push. Failures (exit 1), verbatim:

✗ deploy failed: deploy not enabled on target (start serve with --allow-deploy)
✗ serve target needs a url — pass --url or set deploy.targets.serve settings / HELIPOD_DEPLOY_URL
✗ HELIPOD_ADMIN_KEY is required to deploy to a serve target
✗ deploy failed: could not reach <url>: <error>
✗ deploy failed: <server-reported reason, e.g. a rejected schema change>

The component set (e.g. @helipod/scheduler, @helipod/workflow) is fixed at serve boot. deploy can push new functions and additive schema against that fixed set, but adding/removing a component from helipod.config.ts requires a restart.

The other targets

cloudflare reconciles wrangler.jsonc bindings and shells out to wrangler deploy (Cloudflare); docker, railway, fly, and aws likewise shell out to their platform CLIs, which must be installed. Every non-serve target runs codegen (ctx.codegen()) before packaging, so a fresh _generated/ is part of what ships. Provider CLIs are never bundled; a missing one fails preflight with a clear message.

helipod build

Compiles the app (engine, composed components, schema, functions, and by default the dashboard) into one self-contained executable via bun build --compile. See Deploy & build for the full entrypoint-codegen mechanism.

helipod build --target linux-x64
FlagDefaultMeaning
--dir <path>helipodThe app directory to compile
--outfile <path>./helipod-serverOutput executable path (.exe is appended automatically when --target windows-x64 is used and the given path doesn't already end in .exe)
--target <name>host platformCross-compile target, see the table below
--no-dashboarddashboard includedExclude the dashboard from the binary
--verboseoffStream the underlying bun build process's own output
--target valueBun target
linux-x64bun-linux-x64
linux-arm64bun-linux-arm64
darwin-x64bun-darwin-x64
darwin-arm64bun-darwin-arm64
windows-x64bun-windows-x64

Any other value is rejected with unknown target "<name>" (expected one of: linux-x64, linux-arm64, darwin-x64, darwin-arm64, windows-x64).

build shells out to bun build --compile --minify (requires bun on PATH even if the CLI itself is invoked under Node), without --bytecode, because the generated entrypoint's top-level await is rejected by --bytecode. This trades a slower cold start for correctness, which is fine for a long-running server. On success it prints ✓ built <outfile> (<n>MB).

The database file is never embedded: only the binary itself. At runtime the compiled binary reads its own, separate flag set (not serve's flag names):

Runtime flagDefaultMeaning
--port <n>PORT env, else 3000HTTP/WebSocket port
--hostname <addr>0.0.0.0Bind address
--data-dir <dir>./dataDirectory for db.sqlite
--database-url <url>HELIPOD_DATABASE_URL envPostgres connection string; unset → SQLite

HELIPOD_ADMIN_KEY is still required (fails fast if unset, same message as serve). On success the binary prints one machine-readable line and then runs until SIGTERM/SIGINT:

{"ready":true,"port":3000,"url":"http://0.0.0.0:3000"}

That line is meant for a parent process (Electron/Tauri sidecars, an orchestrator) to detect readiness. There is no live hot-swap path for a compiled binary: functions, schema, and the component set are fixed at compile time. Use helipod deploy against a serve process instead if you need that.

helipod migrate

Turns an existing Convex project into a helipod project: rewrites Convex import specifiers to their @helipod/* equivalents, scans for API divergences it can't auto-fix, writes MIGRATION-REPORT.md at the project root, and regenerates _generated/. See Migrate from Convex for the complete behavior (every rewritten import, every detected pattern, the report format); this is the flag reference.

helipod migrate --from convex
FlagDefaultMeaning
--from <source>convexMigration source, only convex ships today; an unknown source errors immediately
--dir <path>convexThe app directory to migrate
--dry-runoffReport what would change (writes MIGRATION-REPORT.md) without touching any other file
--forceoffProceed even if the project has uncommitted git changes

Refuses to run against a dirty git working tree (git status --porcelain non-empty) unless --force is passed, with refusing to migrate: <dir> has uncommitted changes (commit/stash first, or pass --force). If the directory isn't a git repo at all, it warns and proceeds regardless (there is no revert safety net in that case). If no Convex project is detected at the target directory (no convex/schema.ts, no Convex dependency), it errors with no <source> project detected at <dir>.

On success: migrated <n> files. <m> item(s) need manual attention: see MIGRATION-REPORT.md (or, on --dry-run, [dry-run] <n> files would change, <m> scaffolded. See MIGRATION-REPORT.md). If the import rewrite succeeds but the subsequent codegen fails, exit code 1 with imports migrated, but codegen failed: <error>. The report is still written, since it's written before codegen runs.

helipod migrate export / helipod migrate import

Move an app's data (not code) between two running deployments (e.g. a portable SQLite/Postgres deployment and a Cloudflare DO-native one). Both are plain HTTP clients against a running deployment's bearer-gated admin endpoints (GET /_admin/export, POST /_admin/import); they don't touch helipod/ at all.

helipod migrate export --url <source-url> --out dump.json
helipod migrate import --url <target-url> --in dump.json
FlagApplies toDefaultMeaning
--url <url>bothnone (required)The deployment to export from / import into
--out <file>exportnone (required)Where to write the dump
--in <file>importnone (required)The dump file to read
--admin-key <key>bothHELIPOD_ADMIN_KEYOverrides the environment variable, unlike deploy, these two subcommands do have a flag for the admin key

On success, export prints ✓ exported <n> documents, <m> index rows → <file>; import prints ✓ imported <n> documents, <m> index rows. A 401 from the target prints ✗ unauthorized: check HELIPOD_ADMIN_KEY / --admin-key; a table-number collision or malformed dump on import surfaces as ✗ import failed: <reason>. Missing --url/file flag/admin key each produce their own ✗ missing … message and exit 1 before any network call is made.

helipod codegen

Regenerates helipod/_generated/ (typed api, Doc, Id, server helpers) without starting the engine or serving anything. Useful in CI to verify generated types are committed and current, or before helipod serve (which never generates codegen itself).

helipod codegen --dir helipod
FlagDefaultMeaning
--dir <path>helipodThe app directory to load and generate types for

Prints generated <dir>/_generated on success.

helipod fleet reshard

Changes a stopped Postgres fleet's shard count. This is an offline maintenance tool, not a live-resharding feature. Every fleet node must be stopped first (an in-flight fleet leaves the lease/shard bookkeeping inconsistent with a reshard in progress). Requires the @helipod/fleet package (dynamic-imported; core packages/cli has zero static dependency on it, mirroring serve --fleet's own gate).

helipod fleet reshard --shards 4 --database-url postgres://...
FlagEnv equivalentMeaning
--shards <n>noneTarget shard count. Required; must be an integer ≥ 1
--database-url <url>HELIPOD_DATABASE_URLRequired; must be a Postgres URL, this command is Postgres-only

On success:

✓ resharded 8 → 4 shards (created: none, deleted: s4, s5, s6, s7; frontier floor: 000000000012ab34); update HELIPOD_FLEET_SHARDS to 4 (or unset) before restarting the fleet

Failure modes surface as ✗ <message>, exit 1: a missing/non-integer --shards, a missing/ non-Postgres --database-url, the @helipod/fleet package not installed, or an error thrown by the reshard itself (e.g. the fleet is still live, or a post-reshard verification pass fails).

helipod objectstore reshard

Changes a stopped object-storage deployment's shard count N→M: physically re-partitions every document's current state to its new shard lane based on the table's .shardKey(). Also an offline, non-atomic tool: back up the bucket first, and make sure no writer/replica is running against it.

helipod objectstore reshard --object-store <url> --dir helipod --shards 4
FlagEnv equivalentMeaning
--object-store <url>HELIPOD_OBJECT_STORERequired, the bucket URL (same grammar as serve --object-store, above)
--dir <path>noneDefault helipod. Loaded only to read each table's .shardKey() off the schema, this is the reshard's only schema dependency
--shards <n>noneRequired, target shard count, a positive integer

On success:

✓ resharded 1 → 4 shard(s) (moved 128 doc(s); per-lane: 0=0, s1=40, s2=44, s3=44). A node booting this bucket now uses 4 shard(s): set --shards 4 (or HELIPOD_FLEET_SHARDS), or drop it (the bucket's persisted count is authoritative).

If the bucket is already at the requested count: ✓ bucket is already at <n> shard(s): nothing to do. Any other failure (unsupported --object-store scheme, missing flags, a CAS-unsupported backend) surfaces as ✗ <message>, exit 1. A missing --shards/--object-store value where a flag was given (e.g. a trailing --dir with nothing after it) is rejected explicitly (✗ --dir requires a value.) rather than silently falling back to a default.

helipod help / --help / -h

Prints usage and exits 0. Also the fallback for no command at all, and (with exit 1 instead) for an unrecognized command:

helipod - the reactive backend you self-host

Usage: helipod <command> [options]

Commands:
  dev        Run the engine with hot reload + dashboard
  serve      Run the production server (requires HELIPOD_ADMIN_KEY)
  deploy     Deploy the app: --target <serve|cloudflare|docker|railway|fly|aws> --env <name> [--dry-run] [--check]
  build      Compile the app to a self-contained executable (bun build --compile)
  migrate    Migrate a Convex project into Helipod (imports + report)
  migrate export --url <src> --out dump.json   Export app data to a portable dump
  migrate import --url <dst> --in  dump.json   Import a dump into a deployment
  codegen    Regenerate <functionsDir>/_generated types
  fleet reshard --shards M --database-url <url>   Change a STOPPED fleet's shard count
  objectstore reshard --shards M --object-store <url> --dir <functionsDir>   Change a STOPPED object-storage deployment's shard count
  help       Show this help

Options: --port <n>  --ip <addr>  --dir <functionsDir>  --data <dbPath>  --database-url <url>
Deploy:  --target <name>  --env <name>  --dry-run  --check   (default target: serve; default env: production)

Environment variables (full reference)

Flags always win over their environment-variable equivalent when a command accepts both.

Core

VariableRead byMeaning
HELIPOD_ADMIN_KEYserve, dev, deploy, migrate export/import, compiled build binariesBearer token for the dashboard and admin/deploy/export/import endpoints. Required for serve/build binaries (fails fast if unset/blank); dev generates an ephemeral one if unset
PORTserve, compiled build binariesHTTP/WebSocket port (default 3000). dev uses --port only, no env fallback
HELIPOD_DATA_DIRserveDirectory for the SQLite data file (<dir>/db.sqlite)
HELIPOD_DATABASE_URLdev, serve, compiled build binaries, fleet reshardPostgres connection string. Unset selects SQLite. Not read by deploy (a deploy pushes code, it never touches the database). See Postgres
HELIPOD_PG_STREAMany process on the Postgres backend0 or false disables the Postgres adapter's streaming index scans (server-side cursors for paginated/limited reads), falling back to buffered reads. Default: streaming on. See Postgres
HELIPOD_DASHBOARDserveSet to off to disable the dashboard (equivalent to --no-dashboard)
HELIPOD_ALLOW_DEPLOYserveSet to 1 to enable POST /_admin/deploy (equivalent to --allow-deploy)
HELIPOD_WEB_DIRserveStatic web UI directory to serve at the site root
HELIPOD_DEPLOY_URLdeployDefault target URL (overridden by --url)

File storage

VariableMeaning
HELIPOD_STORAGE_BUCKETSelects the S3-compatible backend (any bucket name set → S3 instead of the filesystem default)
HELIPOD_STORAGE_ENDPOINTS3-compatible endpoint URL (MinIO, R2, …)
HELIPOD_STORAGE_REGIONS3 region
HELIPOD_STORAGE_PUBLIC_URLPublic base URL for "public"-visibility files
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEYStandard AWS-style credentials for the S3 backend

Any of the last three set without HELIPOD_STORAGE_BUCKET fails serve boot fast (S3 settings imply S3 intent; without a bucket that's an unambiguous misconfiguration, not a case for silently falling back to local disk). Unset bucket → files live under <data dir>/storage. See File storage.

The two tables above cover the common case. The rest apply only to multi-node and specialized hosts, expand whichever applies to you.

See also

On this page