Skip to content

Fermyon Spin

Run EdgeZero applications on Fermyon Spin, a WebAssembly-first application platform with a wasm32-wasip2 target and component-scoped KV / variable stores.

Prerequisites

  • Rust toolchain with wasm32-wasip2 target (rustup target add wasm32-wasip2)
  • Spin CLI (install)

Project Setup

When scaffolding with edgezero new my-app, the Spin adapter includes:

crates/my-app-adapter-spin/
├── Cargo.toml
├── spin.toml
└── src/
    └── lib.rs

Entrypoint

The Spin entrypoint wires the adapter via #[http_service]:

rust
use spin_sdk::{http::IntoResponse, http::Request, http_service};
use my_app_core::App;

#[http_service]
async fn handle(req: Request) -> anyhow::Result<impl IntoResponse> {
    edgezero_adapter_spin::run_app::<App>(req).await
}

run_app reads the portable store metadata baked into App by the app! macro plus EDGEZERO__* environment variables; it does not require an edgezero.toml to be present at runtime.

Building

Build the Spin component:

bash
# Using the CLI
edgezero build --adapter spin

# Or directly
cargo build --target wasm32-wasip2 --release -p my-app-adapter-spin

Local Development

bash
# Using the CLI
edgezero serve --adapter spin

# Or directly
spin up --from crates/my-app-adapter-spin

Deployment

bash
# Using the CLI
edgezero deploy --adapter spin

# Or directly
spin deploy --from crates/my-app-adapter-spin

KV Storage

Spin KV is label-backed and multi-store — each logical id in [stores.kv].ids maps to a Spin store label declared in spin.toml. Override the label per id with EDGEZERO__STORES__KV__<ID>__NAME; with the variable unset the label defaults to the logical id.

toml
# edgezero.toml
[stores.kv]
ids     = ["sessions", "cache"]
default = "sessions"
toml
# spin.toml
[component.my-app]
key_value_stores = ["sessions", "cache"]

Two Spin-specific KV constraints (see §6.7 of the design spec for the full rationale):

  • TTL is unsupported. Spin's key_value::Store::set accepts no expiry. put_bytes_with_ttl returns KvError::Unsupported { operation: "put_bytes_with_ttl" } (mapped to HTTP 501); never silently strips the TTL.
  • Listing is capped. Store::get_keys() is unbounded, so the adapter materialises the key list, filters by prefix, sorts, and pages client-side. A max_list_keys cap (default 1000, override via EDGEZERO__STORES__KV__<ID>__MAX_LIST_KEYS) guards against runaway lists and yields KvError::LimitExceeded (HTTP 503) when exceeded.

Config Store

Spin config is KV-backed and multi-store — each logical id in [stores.config].ids opens a separate spin_sdk::key_value::Store at runtime. The store accepts arbitrary UTF-8 keys, so the canonical dotted key (service.timeout_ms) is read back verbatim — no key translation. Override the label per id with EDGEZERO__STORES__CONFIG__<ID>__NAME; with the variable unset the label defaults to the logical id.

toml
# edgezero.toml
[stores.config]
ids     = ["app_config", "feature_flags"]
default = "app_config"
toml
# spin.toml — declare every label in the component's `key_value_stores`
[component.my-app]
key_value_stores = ["app_config", "feature_flags"]
toml
# runtime-config.toml — register each custom label with a backend
# (the default `default` label is auto-provided by Spin; everything
# else needs an entry here, or `spin up` errors with
# "unknown key_value_stores label <name>").
[key_value_store.app_config]
type = "spin"

[key_value_store.feature_flags]
type = "spin"

edgezero new --adapter spin scaffolds both files; edgezero serve --adapter spin runs spin up --runtime-config-file runtime-config.toml so locally-declared labels resolve to the SQLite-backed Spin KV implementation. For production, swap type = "spin" for a managed backend (type = "azure_cosmos", type = "redis", …) per the Spin runtime-config docs.

provision writes the [component.<id>].key_value_stores array for you (it does NOT touch runtime-config.toml — keep that one hand-edited).

Seeding the store

edgezero config push --adapter spin reads runtime-config.toml and dispatches to the right per-backend writer — no embedded HTTP endpoint, no token to manage. Resolution order:

  1. --local set: forces SQLite-direct against <spin.toml dir>/.spin/sqlite_key_value.db (Spin's local KV file). Useful for poking values in your local dev loop without authenticating against Fermyon Cloud. Even under --local, every non-default label MUST be declared in runtime-config.toml (see point 4 below) — without the stanza, spin up errors with unknown key_value_stores label <name> and the file you just wrote is unreadable from the running app, so the dispatcher refuses the push and tells you exactly which stanza to add.
  2. Manifest's deploy command targets Fermyon Cloud (auto-detected from [adapters.spin.commands].deploy containing spin deploy or spin cloud deploy): one batched shellout per ≤96 KiB chunk of spin cloud key-value set --app <APP> --label <LABEL> KEY=VALUE [KEY=VALUE …]. <APP> comes from [application].name in spin.toml; <LABEL> is the env-resolved platform label (your [stores.config].id after the EDGEZERO__STORES__CONFIG__<ID>__NAME overlay). EdgeZero uses Fermyon's app-scoped label model, so the operator pre-links the label to a cloud KV store via spin cloud link key-value (or the Fermyon dashboard) before the first push; an unlinked label produces an actionable error suggesting the link command. Authentication comes from spin cloud login — EdgeZero does not store cloud credentials.
  3. runtime-config.toml declares this label's backend:
    • type = "spin" → SQLite-direct (honours an explicit path field if set; otherwise the default .spin/sqlite_key_value.db).
    • type = "redis" / azure_cosmos / unknown → clear error pointing at the backend's native CLI (e.g. redis-cli -u <url> SET <key> <value>). Native-CLI dispatch for redis / azure is planned for a follow-up release.
  4. Default: only the default label falls through to SQLite without a runtime-config stanza (Spin auto-provides default). Any other label MUST have a [key_value_store.<label>] entry — absent it, the dispatcher errors before any write.

Schema-coupling note: the SQLite writer uses the exact spin_key_value schema and INSERT … ON CONFLICT DO UPDATE statement vendored from spinframework/spin's crates/key-value-spin/src/store.rs. A contract test in edgezero-adapter-spin/src/cli/push_sqlite.rs asserts byte-equality against the upstream string, and the workspace's spin-sdk = "~6.0" pin blocks any Spin minor bump that would change the schema until the operator opts in.

bash
# Local dev: writes through to .spin/sqlite_key_value.db.
edgezero config push --adapter spin --local

# Production (Fermyon Cloud): shells `spin cloud key-value set`.
spin cloud login    # one-time
edgezero config push --adapter spin

Secret Store

Spin secrets use spin_sdk::variables, which exposes a single flat variable namespace per component (no notion of multiple named secret stores). [stores.secrets].ids.len() > 1 while targeting Spin is caught by config validate --strict. Secret variables are declared manually in spin.toml with secret = true:

toml
# spin.toml
[variables]
api_token = { required = true, secret = true }

[component.my-app.variables]
api_token = "{{ api_token }}"

config validate runs a within-secrets canonicalisation check: each #[secret] field value is lowercased to mirror the runtime SpinSecretStore::get_bytes lookup, must be a valid Spin variable name (^[a-z][a-z0-9_]*$), and must not collide with another #[secret] value that lowercases to the same form.

Spin component discovery

provision and config push (Stages 6 and 7) write [component.<id>.*] blocks to spin.toml, which requires knowing the component id. Resolution:

  • The CLI parses spin.toml and enumerates [component.*] ids.
  • If exactly one component exists, it is used.
  • If more than one exists, [adapters.spin.adapter] must carry an explicit component = "<id>" field; otherwise the command errors.

config validate --strict performs this resolution as part of its adapter-set checks when spin is in the target list, so the failure surfaces before provision / config push run.

Manifest Configuration

Configure the Spin adapter in edgezero.toml. See Configuration for the full manifest reference.

Next Steps

Released under the Apache License 2.0.