CLI and codegen
Everything user-facing ships in one binary. The CLI in
crates/nimbus-bin is not a thin wrapper around a separate server — it
is the server, plus the developer loop, plus the code generator, plus
the host-management surface. This page explains how those pieces
compose. The full flag-by-flag reference lives in
CLI commands.
One binary, one command tree
Section titled “One binary, one command tree”crates/nimbus-bin/src/main.rs declares a single clap command tree:
start, dev, deploy, codegen, init, token, auth, ui,
machine, node, compose, policy, encryption, and packages,
plus one hidden internal subcommand used for sandbox supervision. Each
subcommand owns a module under crates/nimbus-bin/src/; main.rs stays
a thin dispatcher. The practical consequence: a production node, a
laptop dev loop, and a CI codegen step all run the same binary with the
same embedded assets, so there is no version skew between “the CLI” and
“the server”.
How start composes the server
Section titled “How start composes the server”nimbus start is the composition root. The boot sequence in
crates/nimbus-bin/src/start/boot.rs resolves configuration, loads the
license, wires function registries, mints serve options, and only then
binds the listener.
Configuration resolution (crates/nimbus-bin/src/start/config.rs)
follows a strict precedence: flag, then environment, then file. The
implementation makes the order structural — inputs from the command line
are built first and each lower layer is applied only as a fallback for
fields still unset. The file layer is a JSON document pointed to by
NIMBUS_CONFIG, contains only the persistence section, and rejects
unknown fields outright rather than ignoring them. Defaults are minimal:
the data directory is ./data, the control-plane directory defaults to
the data directory, and the tenant persistence provider is chosen from
sqlite, libsql-replica, redb, postgres, or mysql — with conflicting
cross-provider overrides rejected at resolve time. The full matrix is in
Configuration.
Two safety gates shape binding. First, a non-loopback bind requires an explicit host opt-in, checked before any local state is initialized. Second, after the admin token is loaded, a freshness check refuses to serve on a non-loopback bind with a stale token. Under systemd socket activation, boot verifies the activation contract — exactly one passed file descriptor, addressed to this process — and adopts the inherited listener instead of binding.
License resolution checks the flag, then the environment, then a
license.json in the global config directory. Function registries are
wired only when generated artifacts actually exist on disk
(.nimbus/convex/functions.json, .nimbus/firebase/artifact.json);
nimbus start without an app directory boots at generation zero and
waits for deploys.
How dev differs
Section titled “How dev differs”nimbus dev (crates/nimbus-bin/src/dev.rs) is start plus a watch
loop, racing in the same process: the server runs in-process on port
3210 while a poll-based watcher drives regeneration, and whichever
finishes first ends the session.
The dev plan (crates/nimbus-bin/src/dev/plan.rs) pins
development-friendly choices: state lives under <app>/.nimbus/dev, the
provider is sqlite, a demo tenant is auto-created, tenant isolation
runs in a local-development mode, and a fresh random deploy token is
generated per run.
The watch loop (crates/nimbus-bin/src/dev/watch.rs) polls every 500ms
with a 300ms debounce, fingerprinting files by modification time and
length and skipping generated and vendored directories (_generated,
node_modules, .git, .nimbus, and build outputs). On change it runs
codegen, then deploys to itself: an HTTP deploy request posted to
its own local URL, authenticated with that per-run token. Dev does not
have a privileged side door into the engine — a dev reload exercises the
same deploy path a remote client would.
App-directory detection walks up from the working directory looking for
a nimbus/ or convex/ directory, generated Convex functions, or a
firebase.json. The walk is bounded at the repository boundary:
crates/nimbus-bin/src/path_boundary.rs stops at the first ancestor
containing .git — checked by existence, so worktree and submodule
.git files count — ensuring a lookalike directory outside the repo
can never be adopted as the app.
Codegen without a toolchain
Section titled “Codegen without a toolchain”Code generation (schema parsing, function manifest emission, generated
TypeScript) is implemented in JavaScript in packages/codegen, but
users do not install it. The package is bundled at build time and
embedded in the binary, and crates/nimbus-bin/src/codegen.rs chooses
between two runners:
- Embedded runner (default). The binary materializes its embedded
tooling closure into
<app>/.nimbus/tmp— inside the app directory, because the runtime’s filesystem capability boundary denies temp paths outside it — writes a small bootstrap module, and executes the bundle on the in-binary V8 runtime with Node-22-shaped tooling limits, a run-to-completion policy, a startup snapshot cache, and a host bridge that rejects database and host calls. Codegen is pure input-to-output; it cannot touch the engine. - External Node runner. Cloud Functions layouts route to a system
Node automatically (their build contract needs a real Node), and
NIMBUS_CODEGEN_RUNNER=external-nodeexists as a diagnostic opt-out elsewhere. Even this path runs the embedded tooling closure — the app never needs a@nimbus/codegendependency.
When external Node is required, crates/nimbus-bin/src/node.rs enforces
a version floor: Node majors below 22 are rejected, newer majors are
allowed with a warning.
Embedded assets and package provisioning
Section titled “Embedded assets and package provisioning”crates/nimbus-assets is the catalog of everything the binary carries:
built JS packages, project templates, and the embedded UI. Packages are
embedded with a manifest recording a SHA-256 digest per file, and
digests are re-verified when files are materialized to disk — a
corrupted or tampered payload fails closed instead of being written.
Project templates (schema, example functions, tsconfig, scaffold
package.json with project-name substitution) are compiled in as
strings.
SDK packages reach an app through provisioning
(crates/nimbus-bin/src/provision.rs): the binary writes its embedded
packages into <app>/.nimbus/packages/<name>/ and scaffolded
package.json files reference them with file:./.nimbus/packages/...
specifiers — so npm install links the SDK that shipped inside this
binary, with no registry fetch and no version skew. Provisioning is
idempotent and stamp-guarded: a .version file holding the manifest
digest is written last, a matching stamp short-circuits the work, and a
binary upgrade (different digest) re-provisions automatically.
Provisioning runs implicitly on the init, dev, codegen, and
deploy paths, and explicitly via nimbus packages.
Related pages
Section titled “Related pages”- CLI commands — the flag-level reference.
- Configuration — the resolved settings matrix.
- Node lifecycle — how the binary becomes a supervised service.
- SDKs and packages — what the provisioned packages contain.