CLI Walkthrough
This walkthrough takes a brand-new project from edgezero new myapp through every CLI command you'll use day-to-day: auth, provision, config validate, config push, build, deploy. It's a companion to the CLI reference, which documents each command exhaustively — this page tells the story of how they fit together.
The full command surface in your generated myapp-cli:
myapp-cli build # cargo build for a target adapter
myapp-cli deploy # push to production (per-adapter)
myapp-cli serve # local dev server (per-adapter)
myapp-cli new # scaffold another project
myapp-cli auth # sign in / out / status against the platform CLI
myapp-cli provision # create the platform resources backing your stores
myapp-cli config validate # typed validate of edgezero.toml + myapp.toml
myapp-cli config push # typed push of myapp.toml to the platform config storeThe default edgezero binary exposes the same commands but runs the raw validate / push paths because it has no typed app-config struct in scope. Downstream CLIs upgrade to the typed paths so validator rules, #[secret] / #[secret(store_ref)] checks, and Spin's flat-namespace collision check all run.
1. Scaffold
edgezero new myapp
cd myappYou get a Cargo workspace with one core crate, one CLI crate, and one adapter crate per target (axum, cloudflare, fastly, spin). The CLI crate (crates/myapp-cli) wires myapp_core::config::MyappConfig into the typed config validate / config push paths — that's the whole reason a downstream CLI exists.
Adapter discovery is link-time. The scaffolder includes every adapter that's compiled into the edgezero-cli binary you ran new from.
2. Sign in
myapp-cli auth login --adapter cloudflare # → wrangler login
myapp-cli auth login --adapter fastly # → fastly profile create
myapp-cli auth login --adapter spin # → spin cloud login
myapp-cli auth login --adapter axum # → no-op (no remote auth)EdgeZero stores no credentials of its own. auth delegates to whatever the adapter declares — typically a shell-out to the platform's native CLI. Per-project overrides live in edgezero.toml:
[adapters.cloudflare.commands]
auth-login = "./scripts/cf-login.sh"
auth-status = "wrangler whoami --json"3. Provision platform resources
Once you've declared store ids in edgezero.toml:
[stores.kv]
ids = ["sessions", "cache"]
default = "sessions"
[stores.config]
ids = ["app_config"]
[stores.secrets]
ids = ["default"]…provision creates the backing resources on whichever adapter you target:
myapp-cli provision --adapter cloudflare --dry-run
myapp-cli provision --adapter cloudflarePer-adapter behaviour:
axum — local-only. Prints one note per declared store id (KV is in-memory; config reads
.edgezero/local-config-<id>.json; secrets read env vars).cloudflare — for each KV / config id, shells out to:
bashwrangler kv namespace create <platform-name>where
<platform-name>resolves fromEDGEZERO__STORES__<KIND>__<ID>__NAMEand falls back to the logical<id>. Parses the namespace id from stdout and appends[[kv_namespaces]] binding = "<platform-name>", id = "<extracted>"towrangler.toml. Idempotent on the binding name. Secrets are runtime-managed viawrangler secret put— no-op here.fastly — for each id, shells out to:
bashfastly <kind>-store create --name=<platform-name>using the same
<platform-name>resolution, then appends[setup.<kind>_stores.<platform-name>]+[local_server.<kind>_stores.<platform-name>]tables tofastly.toml. Idempotent on the[setup.*]block presence.spin — pure
spin.tomlediting (no shell-out — Spin KV stores are runtime-resolved by the Fermyon stack). For each KV id AND each[stores.config]id (both KV-backed at runtime since the KV-config migration), appends the platform-resolved label to the resolved[component.<component>].key_value_stores = [...]array. Secrets stay manual — see §5 Spin manual secret declarations.
If your spin.toml declares more than one [component.*], set [adapters.spin.adapter].component = "<id>" in edgezero.toml so provision knows which component receives the labels.
4. Validate
Before pushing config, validate the manifest + typed app-config against each adapter's contract:
myapp-cli config validate --strictThis runs:
- TOML / schema checks on
edgezero.tomlandmyapp.toml. - Typed deserialise into
MyappConfig+validator::Validate::validate(). #[secret]field presence + non-empty +[stores.secrets]declared.#[secret(store_ref)]value is one of[stores.secrets].ids.- Spin
[component.*]discovery + within-#[secret]flat-namespace collision check — ifspinis in your declared adapter set. (Spin config keys live in KV and accept arbitrary UTF-8; only secret values still share the variable namespace.) --strictadds capability-aware completeness (rejects e.g. multi-id[stores.secrets]when Spin is targeted, since Spin is Single-capable for secrets).
The default edgezero binary runs the same checks except the typed ones (it has no MyappConfig to deserialise into). Use the typed flow for the strongest signal.
5. Push config
myapp-cli config push --adapter axum --dry-run
myapp-cli config push --adapter axumTyped push runs the strict pre-flight validation, serialises MyappConfig via serde_json, strips every #[secret] and #[secret(store_ref)] top-level field (runtime store ids and secret values both belong out of the config-store payload), flattens nested structs into dotted keys (service.timeout_ms), JSON-encodes arrays as single string values, and pushes per-adapter:
axum — writes the flat
string -> stringJSON object to.edgezero/local-config-<id>.json(the same fileAxumConfigStorereads back at runtime).cloudflare — reads the namespace id from
wrangler.toml(matched by binding =<platform-name>, resolved fromEDGEZERO__STORES__CONFIG__<ID>__NAMEor the logical<id>; errors with "did you runprovision?" if absent), writes the entries to a temp file in wrangler's bulk format, then runs:bashwrangler kv bulk put <tempfile> --namespace-id=<id>fastly — resolves the platform config-store id on demand via
fastly config-store list --json(matched byname = <platform-name>, resolved the same way), then per entry:bashfastly config-store-entry create --store-id=<id> --key=<k> --value=<v>spin — reads
runtime-config.toml(next tospin.tomlby default; override with--runtime-config <path>) to dispatch per-backend. Decision order:--localforces SQLite-direct against<spin.toml dir>/.spin/sqlite_key_value.db. Non-defaultlabels still require a[key_value_store.<label>]stanza in runtime-config.toml — without it, the dispatcher refuses the push and tells you the exact stanza to add, since the file you'd write would be unreadable from a runningspin up.- If the manifest's
[adapters.spin.commands].deployshells tospin deploy/spin cloud deploy, push batches entries intospin cloud key-value set --app <APP> --label <LABEL> KEY=VALUE [KEY=VALUE …]invocations (one shellout per ≤96 KiB argv chunk, ≥1000 entries per invocation).<APP>comes from[application].namein spin.toml;<LABEL>is the env-resolved platform label per Fermyon's app-scoped label model. Pre-link the label to a cloud KV store withspin cloud link key-value(or the dashboard) before the first push; authenticate first viaspin cloud login. - Otherwise dispatch on
runtime-config.toml's[key_value_store.<label>].type:type = "spin"→ SQLite-direct write (stanza required for non-defaultlabels);type = "redis"/azure_cosmos/ unknown → clear error pointing at the backend's native CLI (e.g.redis-cli -u <url> SET <key> <value>). - Default: SQLite-direct at Spin's
.spin/sqlite_key_value.db, but ONLY for thedefaultlabel (Spin auto-provides). Other labels require a stanza per point 1.
No internet-facing endpoint is involved on the EdgeZero side: the SQLite writer opens the file directly via
rusqlite(using Spin's exactspin_key_valueschema, vendored from upstream + drift-tested at build time), and the cloud writer shells out to the official Fermyon plugin.
Spin manual secret declarations
config push never writes secret variables — #[secret] fields are stripped before push, and a #[secret(store_ref)] field's runtime key is code-local (e.g. ctx.secret_store(&cfg.vault)?.require_str("active")), so the CLI cannot infer it. Declare them manually in spin.toml:
[variables]
api_token = { required = true, secret = true } # the #[secret] field
[component.myapp.variables]
api_token = "{{ api_token }}"Then set the value at run time via SPIN_VARIABLE_API_TOKEN=<value> or spin up --env API_TOKEN=<value>.
6. Env-var overlay
Every key in myapp.toml can be overridden at load time by an <APP_NAME>__…__<KEY> environment variable, where <APP_NAME> is the manifest's [app].name uppercased with - → _. For an app named myapp the prefix is MYAPP__; for my-app it would be MY_APP__. Dotted config keys are joined with __. The overlay applies to both config validate and config push so the values you see match the runtime:
# myapp.toml: service.timeout_ms = 1500
MYAPP__SERVICE__TIMEOUT_MS=5000 myapp-cli config push --adapter axum
# .edgezero/local-config-app_config.json now has "service.timeout_ms": "5000"Pass --no-env to skip the overlay (useful when CI builds want the on-disk values verbatim). Setting the lowercase / source-form spelling (myapp__...) is silently ignored at runtime — the prefix must be the normalised form.
7. Build + deploy
myapp-cli build --adapter cloudflare
myapp-cli deploy --adapter cloudflarebuild runs the compiled [adapters.<name>.commands].build (or falls back to the adapter's built-in builder). deploy does the same for the deploy command. Native axum has no remote deploy — use standard container/binary deployment instead.
8. The full loop in one go
For a Cloudflare-targeted project:
edgezero new myapp && cd myapp
myapp-cli auth login --adapter cloudflare
myapp-cli provision --adapter cloudflare
myapp-cli config validate --strict
myapp-cli config push --adapter cloudflare
myapp-cli build --adapter cloudflare
myapp-cli deploy --adapter cloudflareFor Spin (which has the most manual setup because of secret variables):
edgezero new myapp && cd myapp
# Add manual secret declarations to crates/myapp-adapter-spin/spin.toml first
# (see "Spin manual secret declarations" above)
myapp-cli auth login --adapter spin
myapp-cli provision --adapter spin
myapp-cli config validate --strict
myapp-cli config push --adapter spin
myapp-cli build --adapter spin
SPIN_VARIABLE_API_TOKEN=<your_token> myapp-cli deploy --adapter spinFor local dev (axum), the flow is simpler — no auth, no provision, just push + serve:
myapp-cli config push --adapter axum
myapp-cli serve --adapter axumMigrating from the pre-rewrite manifest
If you're upgrading a project from the pre-Stage-2 manifest schema ([stores.kv] name = "...", [stores.config.defaults], [adapters.<name>.stores.*]), see the migration guide. The pre-rewrite fields are now a hard load error — every project must migrate.
Next Steps
- CLI reference — every flag, exit code, and per-adapter behaviour.
- Configuration —
edgezero.tomlschema in detail. - Manifest store migration — pre- → post-rewrite mapping.