Skip to content

CLI Reference

The edgezero CLI provides commands for scaffolding, development, building, and deployment.

Installation

Follow the Getting Started guide to install the CLI.

Commands

edgezero new

Scaffold a new EdgeZero project:

bash
edgezero new <name> [options]

Arguments:

  • <name> - Project name (used for directory and crate names)

Options:

  • --dir <path> - Directory to create the project in (default: current directory)

Examples:

bash
# Create project with all registered adapters
edgezero new my-app

# Create in a specific directory
edgezero new my-app --dir /path/to/projects

Generated structure:

my-app/
├── Cargo.toml
├── edgezero.toml
├── crates/
│   ├── my-app-core/
│   ├── my-app-cli/
│   ├── my-app-adapter-fastly/
│   ├── my-app-adapter-cloudflare/
│   ├── my-app-adapter-axum/
│   └── my-app-adapter-spin/

The scaffolder includes all adapters registered at CLI build time, plus a my-app-cli crate — your project's own CLI binary built on the edgezero-cli library.

edgezero demo

Run the bundled app-demo example locally on the axum dev server. This is a contributor-only command — it depends on the in-repo examples/app-demo crate and is compiled only under the demo-example feature, so it is not part of an installed edgezero binary:

bash
cargo run -p edgezero-cli --features demo-example -- demo
# Server starts at http://127.0.0.1:8787

edgezero demo always runs the built-in example — it does not read your project's edgezero.toml or delegate to its adapters. To run your project's axum adapter, use edgezero serve --adapter axum (which runs [adapters.axum.commands].serve from edgezero.toml).

The subcommand is named demo — the name dev is reserved for a future dev-workflow command.

edgezero build

Build for a specific adapter:

bash
edgezero build --adapter <name>

Arguments:

  • --adapter <name> - Target adapter (fastly, cloudflare, spin, axum)

Examples:

bash
# Build for Fastly
edgezero build --adapter fastly

# Build for Cloudflare
edgezero build --adapter cloudflare

# Build for Spin
edgezero build --adapter spin

# Build native binary
edgezero build --adapter axum

The command executes the build command from [adapters.<name>.commands] in edgezero.toml, or falls back to the built-in adapter helper.

Any arguments after -- are forwarded to the adapter command:

bash
edgezero build --adapter fastly -- --flag value

edgezero serve

Run the provider-specific local server:

bash
edgezero serve --adapter <name>

Arguments:

  • --adapter <name> - Target adapter (fastly, cloudflare, spin, axum)

Examples:

bash
# Run Fastly's Viceroy
edgezero serve --adapter fastly

# Run Wrangler dev server
edgezero serve --adapter cloudflare

# Run Spin dev server
edgezero serve --adapter spin

# Run native Axum server
edgezero serve --adapter axum

Provider behavior:

  • Fastly: Runs fastly compute serve
  • Cloudflare: Runs wrangler dev
  • Spin: Runs spin up
  • Axum: Runs cargo run -p <adapter-crate>

edgezero deploy

Deploy to production:

bash
edgezero deploy --adapter <name>

Arguments:

  • --adapter <name> - Target adapter (fastly, cloudflare, spin)
  • --service-id <id> - Platform service id the deploy targets (Fastly). Passed through to the provider; adapters that don't need one ignore it.
  • --staging - Deploy to a staged draft version instead of activating production (Fastly staging lifecycle). Non-Fastly adapters reject it. This is the same --staging verb healthcheck/rollback/config push use.
  • -- <passthrough...> - Args after -- are forwarded verbatim to the adapter deploy command (e.g. -- --comment "ci build"). A hyphenated token before -- is rejected, so a mistyped flag can never silently route a staging deploy to production.

Examples:

bash
# Deploy to Fastly
edgezero deploy --adapter fastly

# Stage a Fastly draft version (no activation)
edgezero deploy --adapter fastly --service-id "$FASTLY_SERVICE_ID" --staging

# Deploy to Cloudflare
edgezero deploy --adapter cloudflare

# Deploy to a Spin runtime
edgezero deploy --adapter spin

Provider behavior:

  • Fastly: Runs fastly compute deploy
  • Cloudflare: Runs wrangler deploy
  • Spin: Runs spin deploy

WARNING

The axum adapter doesn't support deploy - use standard container/binary deployment instead.

edgezero active-version

Resolve and print the currently-active service version as version=<N> (Fastly staging lifecycle). Used to capture a production rollback target before a deploy supersedes it, since Fastly exposes no metadata to infer it afterward. The deploy-fastly recovery snippets in the GitHub Actions guide invoke it directly.

bash
edgezero active-version --adapter <name> --service-id <id>

Arguments:

  • --adapter <name> — target adapter (required). Fastly implements it; other adapters have no active-version concept.
  • --service-id <id> — platform service id whose active version to resolve (required).

Reads the Fastly API token from FASTLY_API_TOKEN in the environment. Emits version=<N> on stdout, or an empty version= when the service has no active version yet (a first-ever deploy). Exits non-zero with a one-line diagnostic on error.

edgezero healthcheck

Probe a deployed (or staged) version's health, retrying until it responds or the attempts are exhausted (Fastly staging lifecycle). Exits non-zero when the deployment is not provably healthy — that non-zero exit is what gates a rollback.

bash
edgezero healthcheck --adapter <name> --service-id <id> --version <n> --domain <domain> [--path </>] [--staging] [--retry <n>] [--retry-delay <secs>] [--timeout <secs>]

Arguments:

  • --adapter <name> — target adapter (required).
  • --service-id <id> — platform service id to probe (required).
  • --version <n> — service version to probe (required; thread it from a prior deploy/stage).
  • --domain <domain> — public domain to probe, e.g. www.example.com (required).
  • --path <path> — URL path to probe; must begin with /. Applies to production and staging alike (staging reroutes the same URL to the resolved staging IP). Default: /.
  • --staging — probe the staged version via its resolved staging IP rather than the live production endpoint. The same --staging verb deploy/rollback/config push use.
  • --retry <n> — total number of attempts before declaring the probe unhealthy. Default: 3.
  • --retry-delay <secs> — seconds to wait between attempts. Default: 5.
  • --timeout <secs> — per-attempt connect/read timeout in seconds. Default: 10.

Only a staging probe needs FASTLY_API_TOKEN (to resolve the staging IP); a production probe just curls the domain and needs no token. Emits healthy=<bool> and status-code=<code>. Exits 0 only when the probe succeeds.

edgezero rollback

Roll a service back to a previous version, or deactivate a staged version (Fastly staging lifecycle).

bash
edgezero rollback --adapter <name> --service-id <id> --version <n> [--rollback-to <n>] [--staging]

Arguments:

  • --adapter <name> — target adapter (required).
  • --service-id <id> — platform service id to roll back (required).
  • --version <n> — the current (bad) version to roll back from (required; staging deactivates it).
  • --rollback-to <n>production only: the version to re-activate. Fastly cannot tell a previously-live version from a staged draft, so the target cannot be inferred — capture it before the superseding deploy (run active-version, or use deploy-fastly's previous-version output) and pass it here. Required for a production rollback; ignored for staging.
  • --staging — deactivate the staged version instead of activating --rollback-to.

Reads the Fastly API token from FASTLY_API_TOKEN in the environment. A production rollback emits rolled-back-to=<N> (the version it activated). Exits non-zero with a one-line diagnostic on error.

edgezero config validate

Validate edgezero.toml together with the typed <name>.toml app config (see Application config).

bash
edgezero config validate [--manifest <path>] [--app-config <path>] [--strict] [--no-env]

Arguments:

  • --manifest <path> — manifest path (default: edgezero.toml).
  • --app-config <path> — typed app-config path (default: <app_name>.toml next to the manifest).
  • --strict — additionally check capability-aware completeness for the declared adapter set (spec §6.6) and well-formed Rust handler paths.
  • --no-env — skip the <APP_NAME>__…__<KEY> env-var overlay when loading the app config. By default the validator reads the overlay so it sees the same values the runtime would.

Two flavours:

  • The default edgezero binary runs the raw validator — manifest + app-config TOML/schema + the two Spin checks that don't need the typed struct (key syntax, component discovery).
  • A downstream CLI built on edgezero-cli that owns its app-config struct (e.g. app-demo-cli) runs the typed validator: everything the raw flow does, plus the typed deserialise, validator rules, the #[secret] / #[secret(store_ref)] checks, and the Spin config / secret namespace collision check.

Examples:

bash
# Raw flow on the default binary — manifest + Spin key syntax.
edgezero config validate

# Strict mode on a downstream CLI — typed deserialise + secrets +
# capability completeness for the declared adapter set.
app-demo-cli config validate --strict

Exit codes: 0 on success, non-zero with a one-line diagnostic on the first failure (the loader / validator returns early at the first mismatch).

edgezero config push

Push the resolved <name>.toml app-config into the target adapter's config store (spec §13). Same dispatch shape as the other commands: each adapter crate owns its own implementation, the CLI is a thin delegate.

The flags below belong to your app's typed CLI (<your-app>-cli). On the bundled edgezero binary config push is a hidden stub that absorbs any flags and exits 2 with a pointer to the typed CLI — it cannot push (see Two flavours below).

bash
<your-app>-cli config push --adapter <name> [--manifest <path>] [--app-config <path>] [--store <id>] [--key <key>] [--staging] [--no-env] [--local] [--runtime-config <path>] [--no-diff] [--yes] [--dry-run]

Arguments:

  • --adapter <name> — target adapter (axum, cloudflare, fastly, spin).
  • --manifest <path> — manifest path (default: edgezero.toml).
  • --app-config <path> — typed app-config path (default: <app_name>.toml next to the manifest).
  • --store <id> — logical config-store id to push to. Defaults to [stores.config].default (or the only declared id when [stores.config].ids has length 1).
  • --key <key> — override the config-store key the blob is written under (spec §5.4).
  • --staging — write the <logical-store-id>_staging variant in the SAME store, so a staged push never overwrites the key the live service reads. The staging key is derived from the store's logical id and is mutually exclusive with --key (an explicit staging key would be written where no staged version reads, so the combination is refused). A staged deploy points the staged version's edgezero_runtime_env link at this key via the service-scoped EDGEZERO__SERVICES__<SERVICE_ID>__STORES__CONFIG__<ID>__KEY entry in its staging selector store (see the blob migration guide).
  • --no-env — skip the <APP_NAME>__…__<KEY> env-var overlay when loading the app config. By default the loader reads the overlay so the push sends the same values the runtime would.
  • --local — push into the adapter's local-emulator state instead of the live platform. Fastly edits [local_server.config_stores] in fastly.toml (Viceroy reads it on startup); Cloudflare runs wrangler kv bulk put --local so writes land in .wrangler/state; Spin forces SQLite-direct against <spin.toml dir>/.spin/sqlite_key_value.db even when the manifest's deploy command targets Fermyon Cloud (the runtime-config [key_value_store.<label>].type is also ignored for SQLite path resolution); Axum is local-only already so it's a no-op there.
  • --runtime-config <path> — adapter runtime configuration file. Currently only consumed by Spin, which reads [key_value_store.<label>] stanzas to dispatch per-backend (type = "spin" → SQLite-direct, redis / azure_cosmos / other → error pointing at the native backend CLI). Default: runtime-config.toml next to the adapter manifest. Ignored by the Fermyon Cloud branch — cloud pushes consult only spin.toml's [application].name.
  • --no-diff — skip the inline diff render of local-vs-remote before writing. By default the push reads back the current remote blob and shows what would change.
  • --yes / -y — skip the confirmation prompt and write unconditionally (for non-interactive/CI use). Without it, an interactive push prompts before overwriting a differing remote blob — but with no TTY (a CI runner, a piped shell) there is no prompt to answer, so a push without --yes fails closed with non-interactive run requires --yes. Always pass --yes when invoking config push yourself in CI. The config-push-fastly action appends --yes --no-diff for you, so a push run through the action needs neither flag.
  • --dry-run — print the would-be operations without performing them. It makes no WRITE and no delete — but it is NOT fully offline: because dry-run's contract is to show the diff, it does a read-only remote read-back (a shell-out on Fastly/Cloudflare). That read is one logical read but may be several provider calls for a chunked Fastly value (describe the root pointer, then describe each referenced chunk to reassemble it). It errors against Spin Cloud (whose read-back is unsupported); use --local for the on-disk SQLite write, or drop --dry-run and write unconditionally with --yes.

Two flavours (same split as config validate):

  • The default edgezero binary does not pushconfig push on the bundled binary is a hidden stub that absorbs whatever flags you pass and exits 2 with a pointer to the typed downstream CLI. The blob app-config model needs the app's typed AppConfig<C> (for validation and canonical serialisation), which the bundled binary doesn't embed.
  • A downstream CLI built on edgezero-cli that owns its app-config struct (e.g. app-demo-cli) runs the typed push: strict pre-flight validation (validator::Validate, secret presence, store-ref membership, adapter checks), then serialises the struct into a single BlobEnvelope — one JSON { version, generated_at, sha256, data } value written under one config-store key. data carries every field VERBATIM, including #[secret] / #[secret(store_ref)] fields: their value at rest is the operator-supplied key NAME (e.g. "demo_api_token"), which the runtime AppConfig<C> extractor swaps for the resolved secret at request time. Nothing is flattened or stripped, and the blob never contains resolved secret bytes.

Per-adapter behaviour: every adapter writes the single blob envelope as one (key, envelope_json) config-store entry — with one exception: on Fastly, an oversized envelope is split into multiple content-addressed chunk entries plus a root pointer, all under the one logical key. Fastly's documented per-entry cap is 8,000 characters; EdgeZero splits at 8,000 bytes, a deliberately conservative v1 threshold (bytes are always ≥ characters, so it never over-stores) that is fixed so previously stored values stay readable.

Reclaiming the superseded chunks differs by target: a --local re-push prunes the prior generation as part of the same file rewrite, whereas a cloud push only ever writes — it never deletes. Cloud orphans are reclaimed by running config gc explicitly.

The store-resolution and shell mechanics below are unchanged; see the blob migration guide for the authoritative per-adapter blob details, including Fastly's oversized-envelope chunking.

--adapterBehaviour
axumWrites the envelope JSON to .edgezero/local-config-<id>.json (the file AxumConfigStore reads back). Creates .edgezero/ on first use. No shell-out.
cloudflareReads the namespace id from wrangler.toml (matched by binding = <platform-name>, where <platform-name> resolves from EDGEZERO__STORES__CONFIG__<ID>__NAME or falls back to the logical <id>), writes the single-entry bulk file ([{"key": "<key>", "value": "<envelope_json>"}]), and runs wrangler kv bulk put <tempfile> --namespace-id=<id> (--remote live, --local against .wrangler/state). Errors with "did you run provision?" if the binding is absent.
fastlyResolves the platform config-store id on demand via fastly config-store list --json (matched by name = <platform-name>, where <platform-name> resolves from EDGEZERO__STORES__CONFIG__<ID>__NAME or falls back to the logical <id>), then upserts the envelope with fastly config-store-entry update --store-id=<id> --key=<key> --upsert --stdin. --upsert makes re-runs idempotent. Errors with "did you run provision?" if the store name isn't found. Oversized envelopes are auto-chunked (see the blob migration guide).
spinReads runtime-config.toml (default: next to spin.toml, override with --runtime-config <path>) to dispatch per-backend. --local forces SQLite-direct writes into <spin.toml dir>/.spin/sqlite_key_value.db (Spin's local KV file) regardless of manifest deploy config; non-default labels still require a [key_value_store.<label>] stanza or the dispatcher refuses to write a file Spin can't read. Otherwise, if [adapters.spin.commands].deploy shells to spin deploy / spin cloud deploy, push writes the single envelope entry via spin cloud key-value set --app <APP> --label <LABEL> <KEY>=<envelope_json>. <APP> from [application].name in spin.toml; <LABEL> is the env-resolved platform label that must be pre-linked to a cloud KV store (spin cloud link key-value); auth via spin cloud login. Otherwise dispatches on runtime-config.toml's [key_value_store.<label>].type: type = "spin" → SQLite-direct (still requires the stanza for non-default labels); type = "redis" / azure_cosmos / unknown → error pointing at the backend's native CLI. SQLite writer uses Spin's vendored spin_key_value(store, key, value) schema (drift-tested at build time).

Examples:

bash
# The bundled binary cannot push — this errors with a pointer to the
# downstream CLI (the blob model needs the typed AppConfig<C>).
edgezero config push --adapter axum

# Typed push from a downstream CLI — runs strict validation, then writes
# one blob envelope (every field verbatim, incl. #[secret] key names).
app-demo-cli config push --adapter axum --dry-run

Exit codes: 0 on success, non-zero with a one-line diagnostic on the first failure.

edgezero config diff

Show what config push would change: builds the local blob envelope from your typed <name>.toml and compares it against the remote (or local-emulator) config-store entry, without writing anything. Like config push, this is typed-only — the bundled edgezero binary errors with a pointer to the downstream CLI, since the diff needs the app's typed AppConfig<C>.

Like config push, the flags below belong to your app's typed CLI (<your-app>-cli); on the bundled edgezero binary config diff is a hidden stub that absorbs its flags and exits 2 with a pointer to the typed CLI.

bash
<your-app>-cli config diff --adapter <name> [--manifest <path>] [--app-config <path>] [--store <id>] [--key <key>] [--staging] [--no-env] [--local] [--runtime-config <path>] [--format <fmt>] [--exit-code]

Arguments:

  • --adapter <name> — target adapter (axum, cloudflare, fastly, spin).
  • --manifest <path> — manifest path (default: edgezero.toml).
  • --app-config <path> — typed app-config path (default: <app_name>.toml next to the manifest).
  • --store <id> — logical config-store id to diff against. Defaults to [stores.config].default (or the only declared id when [stores.config].ids has length 1).
  • --key <key> — override the config-store key to diff against (spec §5.4). Defaults to the logical store id; matches config push --key.
  • --staging — diff against the derived <logical-store-id>_staging variant, matching what config push --staging would write. Mutually exclusive with --key, like config push.
  • --no-env — skip the <APP_NAME>__…__<KEY> env-var overlay when loading the app config.
  • --local — diff against the adapter's local-emulator state instead of the live platform (same resolution as config push --local).
  • --runtime-config <path> — adapter runtime configuration file (Spin only; same semantics as config push).
  • --format <fmt> — output format: unified (default, POSIX unified-diff text), json ({ local_sha256, remote_sha256, added, removed, changed }), or structured (key/old/new triples).
  • --exit-code — exit 1 when the local and remote blobs differ, for CI gating (like git diff --exit-code). Without it, a completed diff exits 0 whether or not there are changes. Either way, if the adapter cannot read back the remote (an Unsupported outcome — e.g. Spin Cloud), the diff is structurally impossible and exits 2 regardless of --exit-code.

Examples:

bash
# Human-readable diff of local app-config vs the live remote blob.
app-demo-cli config diff --adapter fastly

# CI gate: fail the job if the deployed config is stale.
app-demo-cli config diff --adapter fastly --exit-code --format json

Exit codes: 0 on success (or when there are no changes); with --exit-code, 0 when local and remote match and 1 when they differ. An Unsupported outcome (the adapter cannot read back the remote, e.g. Spin Cloud) exits 2 regardless of --exit-code. Errors return non-zero (≥ 2) with a one-line diagnostic.

edgezero config gc

Reclaim orphaned chunk entries from a config store. Only Fastly needs this: its Config Store caps a value at 8 000 characters, so an oversized app-config envelope is split into content-addressed chunks plus a root pointer. Because the chunk keys are content-addressed, changing the config produces an entirely new set — and a cloud config push deletes nothing, so the previous generation is left orphaned (inert, but it accumulates). config gc is how you reclaim it.

Unlike push/validate/diff, config gc is untyped: it inspects the store's physical entries, not your AppConfig<C>. The bundled edgezero binary can run it.

Local (fastly.toml) pushes prune their own prior chunks eagerly and never need gc.

bash
edgezero config gc --adapter fastly [--manifest <path>] [--store <id>] [--older-than <dur>] [--no-env] [--dry-run] [--yes]

Arguments:

  • --adapter <name> — target adapter. Only fastly implements reclamation; others error.
  • --manifest <path> — manifest path (default: edgezero.toml).
  • --store <id> — logical config-store id to reclaim. Defaults to [stores.config].default (or the only declared id when [stores.config].ids has length 1).
  • --older-than <dur>your safety assertion (see below). Accepts 7d, 24h, 90m, 30s, or a bare number of seconds.
  • --no-env — ignore EDGEZERO__STORES__CONFIG__<ID>__NAME, so the logical store id <ID> is used as the physical store name. This is not the app-config overlay that validate/push/diff mean by --no-envgc never loads your typed app config. Because that variable is normally what maps a logical id onto the real store, --no-env changes which store is swept, and this command deletes. Check the store id gc reports before passing --yes.
  • --dry-run — preview only: name every key and age it would delete, and delete nothing. This is already the default (a run without --yes never deletes); the flag just states that intent explicitly to double-check a sweep. It conflicts with --yes — a single run cannot both preview and delete.
  • --yes — actually delete. Without it, config gc is a dry run that names every key and age it would delete and deletes nothing.

--older-than is an assertion only you can make, and it covers the whole store. Fastly's config store is eventually consistent and offers no compare-and-swap, so nothing in the API records when a pointer stopped being served by every POP — which is the one fact needed to delete a chunk safely. You know it.

config gc sweeps every root in the selected physical store, so --older-than <dur> asserts: "no root in this store changed within this window, and no writer is targeting it." Not merely the one config you have in mind — a sibling root you re-pushed minutes ago is enough to make a wide window unsafe. So:

  • it is required for --yes, and --older-than 0 is rejected there — a zero window asserts nothing;
  • pick a window that is at least Fastly's propagation time (so POPs have stopped serving the superseded pointer) and no longer than the time since any root in this store last changed (so it's a window you actually observed);
  • do not run config gc alongside a config push to the same store;
  • dry-run first on a store with many roots — it lists every orphan with its age.

config gc fails closed: an unreadable, paginated, or duplicate-keyed listing; a root it cannot classify; a pointer whose chunk list is internally inconsistent or that does not reconstruct the envelope it claims; an unreadable timestamp; or a live pointer referencing a key absent from the listing — all abort with nothing deleted. It never deletes a chunk a live pointer references, however old, and never deletes a root.

It also only deletes entries that are byte-identical to what config push itself would have written for the bytes they contain — same split boundaries, same content-addressed keys, same count. Anything else in the chunk namespace (plain text, another tool's data, a half-written generation) is left untouched and reported, not deleted and not fatal — one foreign entry does not block reclaiming the rest of the store. Note this is a format check, not a proof of authorship: another writer that reproduced config push's exact output under the .__edgezero_chunks. namespace would be indistinguishable, and would be reclaimed. Do not store unrelated data under that reserved namespace.

bash
# Dry run FIRST — previews every orphan and its age, deletes nothing.
edgezero config gc --adapter fastly

# Reclaim generations superseded more than 7 days ago.
edgezero config gc --adapter fastly --older-than 7d --yes

Exit codes: 0 on success. Non-zero on any fail-closed refusal (nothing deleted) and non-zero if any delete failed, so automation detects a partial pass.

Deletion works one generation at a time and stops a generation at its first failure. A failed remote delete has an unknown outcome — Fastly may commit it before returning an error — so gc does not promise a clean retry. A failure with no confirmed prior sibling delete leaves the generation uncertain: a re-run may reclaim it if it is still whole, or report it as an unprovable fragment if the delete did commit. A failure part-way through a generation (a sibling already confirmed deleted) strands the survivors: they are an incomplete generation gc can no longer verify, so re-running will not reclaim them. In both cases the command names the affected keys and prints the fastly config-store-entry delete commands (shell-escaped) to remove them by hand. They are inert in the meantime: no pointer references them.

edgezero provision

Create the platform resources backing the [stores.<kind>].ids the manifest declares — KV namespaces, config stores, secret stores (spec §12). Same dispatch shape as the other commands: each adapter crate owns its own implementation, the CLI is a thin delegate.

bash
edgezero provision --adapter <name> [--manifest <path>] [--dry-run]

Per-adapter behaviour:

--adapterBehaviour
axumLocal-only — prints one note per declared store id and exits 0 (KV in-memory; config in .edgezero/local-config-<id>.json).
cloudflareFor each KV id + config id: shells out to wrangler kv namespace create <platform-name> (where <platform-name> resolves from EDGEZERO__STORES__<KIND>__<ID>__NAME or falls back to the logical <id>), parses the namespace id from stdout, appends [[kv_namespaces]] binding = "<platform-name>", id = "<extracted>" to wrangler.toml (idempotent on the binding name; preserves existing entries and comments). Secrets are runtime-managed via wrangler secret put — no-op.
fastlyFor each KV / config / secret id: shells out to fastly <kind>-store create --name=<platform-name> (using the same <platform-name> resolution), then appends the [setup.<kind>_stores.<platform-name>] table to fastly.toml. Provision writes ONLY [setup.*] (the remote/deploy half); the [local_server.*] seeding is written by config push --local (config stores only). If the setup table is already present, resource creation and manifest editing are skipped. A live run still reconciles declared logical-to-physical name mappings in edgezero_runtime_env, storing them under EDGEZERO__SERVICES__<SERVICE_ID>__* and removing stale mappings only within that service namespace when an override returns to its logical default. Non-default mappings require top-level service_id in fastly.toml or FASTLY_SERVICE_ID. Store IDs are not persisted — config push resolves them on demand.
spinPure spin.toml editing — no shell-out (Spin KV stores are runtime-resolved). For each declared KV id AND each declared [stores.config] id (both KV-backed at runtime), appends the platform-resolved label to the resolved [component.<component>].key_value_stores = [...] array (idempotent on the label). Secret variables are still manual: [stores.secrets] ids get a nothing to do here status line and the operator declares [variables].<name> = { secret = true } + the per-component binding by hand.

--dry-run prints what each adapter would do without performing it. For axum the output is identical to a real run (there's nothing to actually perform). For cloudflare, fastly, and spin, dry-run does not invoke any native CLI and does not edit the adapter manifest.

The cloudflare flow requires wrangler on PATH and [adapters.cloudflare.adapter].manifest pointing at the project's wrangler.toml. Re-running after a successful provision is safe: existing bindings are detected and skipped.

The fastly flow requires fastly on PATH and [adapters.fastly.adapter].manifest pointing at the project's fastly.toml. Re-running is safe: provision skips resource creation for any id whose [setup.<kind>_stores.<id>] block already exists, then reads edgezero_runtime_env and reconciles service-scoped mappings for the app's declared logical ids. It does not delete legacy unscoped keys, another service's namespace, undeclared ids, or unrelated runtime entries; the runtime ignores unscoped keys, which must be migrated manually. If non-default mappings are requested, top-level service_id in fastly.toml or FASTLY_SERVICE_ID must identify the owning service before provision performs any remote mutation.

The spin flow needs no native CLI but does require [adapters.spin.adapter].manifest pointing at the project's spin.toml. If spin.toml declares more than one [component.*], [adapters.spin.adapter].component = "<id>" selects which one receives the KV labels (single-component manifests resolve implicitly).

edgezero auth

Sign in, sign out, or check session against the adapter's native auth surface. EdgeZero stores no credentials of its own — auth delegates to the adapter, which decides whether to shell out to the platform CLI, hit an HTTP API, or no-op (spec §11).

bash
edgezero auth login  --adapter <name>
edgezero auth logout --adapter <name>
edgezero auth status --adapter <name>

Dispatch follows the same path as build / deploy / serve: the CLI looks up [adapters.<name>.commands].auth-login (or auth-logout / auth-status) in edgezero.toml first; if absent, it delegates to the adapter crate's built-in implementation.

Adapter built-ins:

--adapterloginlogoutstatus
axumno-op (no remote auth)no-opno-op
cloudflarewrangler loginwrangler logoutwrangler whoami
fastlyfastly profile createfastly profile deletefastly profile list
spinspin cloud loginspin cloud logoutspin cloud info

Per-project override — pin to a script or a different binary in edgezero.toml (same precedence as build / deploy / serve overrides):

toml
[adapters.cloudflare.commands]
auth-login  = "./scripts/cf-login.sh"
auth-status = "wrangler whoami --json"

The native CLI must be on PATH; a missing binary surfaces with an install hint. A non-zero exit propagates with its stderr verbatim.

Axum is local-only

auth --adapter axum is intentionally a no-op — the native dev server reads secrets from process env vars (EDGEZERO__STORES__SECRETS__<ID>__…), not from a remote auth provider.

Environment Variables

The CLI respects these environment variables:

VariableDescription
EDGEZERO_MANIFESTPath to manifest (default: edgezero.toml)
FASTLY_API_TOKENFastly API token. Required by the Fastly lifecycle commands (deploy, active-version, rollback, and a staging healthcheck); they fail closed without it.
FASTLY_SERVICE_IDDefault Fastly service id, used when --service-id is not passed. The lifecycle commands need a service id from one source or the other.

Working Directory

All commands expect to run from the project root where edgezero.toml is located. If the file is missing, the CLI falls back to built-in adapters (when compiled in) instead of manifest-driven commands.

Adapter Discovery

Adapters register themselves via the edgezero-adapter registry at build time. There is currently no edgezero --list-adapters command; the scaffolder includes all adapters that were compiled in.

Built-in adapters (default CLI build):

  • fastly - Fastly Compute@Edge
  • cloudflare - Cloudflare Workers
  • spin - Fermyon Spin
  • axum - Native Axum/Tokio

Troubleshooting

Missing Wasm Target

error: target may not be installed

Install the required target:

bash
rustup target add wasm32-wasip1            # For Fastly
rustup target add wasm32-wasip2            # For Spin
rustup target add wasm32-unknown-unknown   # For Cloudflare

Manifest Not Found

If you rely on manifest-driven commands, ensure edgezero.toml exists or set EDGEZERO_MANIFEST. When no manifest is present, the CLI falls back to built-in adapter implementations (if compiled in) instead of using manifest commands.

Provider CLI Not Found

error: fastly: command not found

Install the provider CLI:

Building Your Own CLI

edgezero-cli ships a library as well as a binary. Every downstream command is exposed as a (*Args, run_*) pair — BuildArgs / run_build, DeployArgs / run_deploy (with --staging and --service-id), the Fastly staging-lifecycle trio HealthcheckArgs / run_healthcheck, RollbackArgs / run_rollback, and ActiveVersionArgs / run_active_version, plus NewArgs / run_new and ServeArgs / run_serve — so a downstream project can build its own CLI binary that reuses any subset of the built-ins and adds its own subcommands.

The crate is not on crates.io — EdgeZero crates are publish = false until the first registry release — so depend on it by Git (or by path, in a local checkout):

toml
[dependencies]
# The `args` types and `run_*` handlers live behind the `cli` feature, and you
# need at least one adapter to deploy — so enable them explicitly. (Omitting
# `default-features = false` also works: the defaults are `cli` plus all four
# adapters.)
edgezero-cli = { git = "https://github.com/stackpop/edgezero.git", default-features = false, features = [
  "cli",
  "edgezero-adapter-fastly",
] }
rust
use clap::{Parser, Subcommand};
use edgezero_cli::args::{BuildArgs, DeployArgs};

#[derive(Parser)]
struct Args {
    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    Build(BuildArgs),       // reuse the built-in
    Deploy(DeployArgs),     // reuse the built-in
    Migrate,                // your own subcommand
}

fn main() {
    edgezero_cli::init_cli_logger();
    let result = match Args::parse().cmd {
        Cmd::Build(args) => edgezero_cli::run_build(&args),
        Cmd::Deploy(args) => edgezero_cli::run_deploy(&args),
        Cmd::Migrate => run_migrate(),
    };
    // ...
}

edgezero new <name> scaffolds exactly this pattern into a crates/<name>-cli crate, and examples/app-demo/crates/app-demo-cli is the in-tree reference.

Next Steps

Released under the Apache License 2.0.