From 15ff2d909682bf90b4cbe22023f1925e58a2aa78 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Thu, 23 Apr 2026 12:59:25 -0600 Subject: [PATCH 1/8] Announcing Spin v4.0 Signed-off-by: Brian Hardock --- content/blog/announcing-spin-4-0.md | 458 ++++++++++++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 content/blog/announcing-spin-4-0.md diff --git a/content/blog/announcing-spin-4-0.md b/content/blog/announcing-spin-4-0.md new file mode 100644 index 00000000..0f9d1b9f --- /dev/null +++ b/content/blog/announcing-spin-4-0.md @@ -0,0 +1,458 @@ +title = "Announcing Spin v4.0" +date = "2026-04-23T12:00:00Z" +template = "blog_post" +description = "Announcing the release of Spin v4.0: WASIp3 stabilization, async interfaces across the board, build profiles, and fine-grained capability inheritance for dependencies." +tags = [] + +[extra] +type = "post" +author = "The Spin Project" + +--- + +The CNCF Spin project is excited to announce [Spin v4.0](https://github.com/spinframework/spin/releases/tag/v4.0.0). This release marks the stabilization of WebAssembly System Interface Preview 3 (WASIp3) support in Spin, shipping the March RC (RC 2026-03-15) as a first-class, long-term supported target. Alongside this milestone, v4 delivers async interfaces throughout the entire API surface, build profiles for managing multiple build configurations, and fine-grained capability inheritance for component dependencies. + +For the full list of changes, see the [release notes](https://github.com/spinframework/spin/releases/tag/v4.0.0). + +## WASIp3: Now Stable in Spin + +[WASIp3](https://wasi.dev) is the next generation of the WebAssembly System Interface. At its core, it brings **first-class concurrency and asynchronous I/O** to WebAssembly components — meaning your component can handle multiple requests concurrently within a single instance, stream data to clients as it is produced rather than buffering everything upfront, and coordinate asynchronous tasks that outlive the top-level entry point. + +Previous Spin releases shipped WASIp3 under an experimental, opt-in executor: `executor = { type = "wasip3-unstable" }`. **In Spin v4, that gate is gone.** The March RC of WASIp3 (RC 2026-03-15) is the default execution model. All HTTP and Redis trigger components now run on WASIp3, and all Spin SDK interfaces — key-value, SQLite, PostgreSQL, outbound HTTP, variables, and more — are now `async`-native. + +### What Changes for Your Components? + +If you were using WASIp2 in Spin v3, here is what has changed: + +- Your handler functions must now be `async` (in all SDKs). +- All Spin SDK API calls now return futures/coroutines; use your language's native `await` syntax. +- You no longer declare `executor = { type = "wasip3-unstable" }` in your trigger configuration. +- Component instance reuse is the default: a single component instance can serve multiple concurrent requests. Avoid storing per-request state in instance-level globals without synchronisation. + +### Rust: `spin-sdk` v6 + +The Spin Rust SDK v6 makes WASIp3 the default, stable experience. The `#[http_service]` macro replaces `#[http_component]`, and handler functions are `async`: + +```rust +use spin_sdk::http::{IntoResponse, Request, Response}; +use spin_sdk::http_service; + +#[http_service] +async fn handle(req: Request) -> anyhow::Result { + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body("Hello from Spin v4!")?) +} +``` + +```toml +[package] +name = "hello-spin" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +spin-sdk = "6.0" +``` + +Build target is still `wasm32-wasip2` — WASIp3 extends the binary format rather than replacing it: + +```bash +$ cargo build --target wasm32-wasip2 --release +``` + +Or use `spin build` with the Rust template. + +#### Streaming Responses with WASIp3 + +One of the most powerful WASIp3 capabilities is streaming. You can spawn a background task that writes response body data asynchronously, allowing your handler to send the HTTP response _before_ all of the data is ready: + +```rust +use bytes::Bytes; +use futures::{SinkExt, StreamExt}; +use http_body_util::StreamBody; +use spin_sdk::http::{IntoResponse, Request, Response}; +use spin_sdk::http_service; + +#[http_service] +async fn handle(_req: Request) -> anyhow::Result { + let store = spin_sdk::key_value::Store::open_default().await?; + + // Create a channel that will carry the response body chunks. + let (mut tx, rx) = futures::channel::mpsc::channel::(1024); + let rx = rx.map(|chunk| anyhow::Ok(http_body::Frame::data(chunk))); + let response = Response::new(StreamBody::new(rx)); + + // Spawn a background task. It runs even after `handle` returns. + spin_sdk::wasip3::spawn(async move { + if let Ok(Some(greeting)) = store.get("greeting").await { + tx.send(greeting.into()).await.unwrap(); + } + if let Ok(Some(name)) = store.get("name").await { + tx.send(" ".into()).await.unwrap(); + tx.send(name.into()).await.unwrap(); + } + // `tx` dropped here → body stream ends → Spin closes the response. + }); + + // Return immediately. Spin starts streaming the response to the client + // as chunks arrive from the background task. + Ok(response) +} +``` + +#### Concurrent Outbound Requests + +WASIp3 makes it trivial to fire multiple outbound HTTP requests concurrently — no threads, no blocking: + +```rust +use spin_sdk::http::{EmptyBody, IntoResponse, Request, Response}; +use spin_sdk::http_service; + +#[http_service] +async fn handle(_req: Request) -> anyhow::Result { + // Kick off both requests at the same time. + let (docs_res, spec_res): (Response, Response) = futures::try_join!( + spin_sdk::http::send(Request::get("https://spinframework.dev").body(EmptyBody::new())?), + spin_sdk::http::send(Request::get("https://wasi.dev").body(EmptyBody::new())?), + )?; + + let body = format!( + "Spin docs: {}, WASI spec: {}", + docs_res.status(), + spec_res.status() + ); + Ok(Response::new(body)) +} +``` + +### Python: `spin-python-sdk` v4 + +The Python SDK v4 updates `handle_request` to be an `async` method. Existing synchronous patterns still work — simply add `async`/`await`: + +```python +from spin_sdk.http import Handler, Request, Response, send + +class HttpHandler(Handler): + async def handle_request(self, request: Request) -> Response: + animal_resp = await send( + Request("GET", "https://random-data-api.fermyon.app/animals/json", {}, None) + ) + return Response( + 200, + {"content-type": "text/plain"}, + bytes(f"Animal fact: {str(animal_resp.body, 'utf-8')}", "utf-8"), + ) +``` + +The `spin.toml` build command now targets the v4 HTTP trigger WIT: + +```toml +[component.hello-world] +source = "app.wasm" +allowed_outbound_hosts = ["https://random-data-api.fermyon.app"] + +[component.hello-world.build] +command = "componentize-py -w spin:up/http-trigger@4.0.0 componentize app -o app.wasm" +``` + +Install the updated Python SDK and templates: + +```bash +$ spin templates install --git https://github.com/spinframework/spin-python-sdk --update +$ spin new -t http-py my-app --accept-defaults +``` + +### Go: `spin-go-sdk` v3 + +The Go SDK v3 drops TinyGo + WASIp1 in favour of standard Go tooling with `componentize-go`. HTTP handlers use the raw WASIp3 bindings exposed by the SDK. The idiomatic way to return a streaming body is with a goroutine — the same `go` keyword you already know: + +```go +package main + +import ( + "net/http" + + handler "github.com/spinframework/spin-go-sdk/v3/exports/wasi_http_service_0_3_0_rc_2026_03_15/export_wasi_http_0_3_0_rc_2026_03_15_handler" + _ "github.com/spinframework/spin-go-sdk/v3/exports/wasi_http_service_0_3_0_rc_2026_03_15/wit_exports" + . "github.com/spinframework/spin-go-sdk/v3/imports/wasi_http_0_3_0_rc_2026_03_15_types" + . "go.bytecodealliance.org/pkg/wit/types" +) + +func Handle(request *Request) Result[*Response, ErrorCode] { + // Create a stream for the body. + tx, rx := MakeStreamU8() + + // Goroutine writes the body asynchronously. + go func() { + defer tx.Drop() + tx.WriteAll([]uint8("Hello from Spin v4!")) + }() + + response, send := ResponseNew( + FieldsFromList([]Tuple2[string, []uint8]{ + {"content-type", []uint8("text/plain")}, + }).Ok(), + Some(rx), + trailersFuture(), + ) + send.Drop() + return Ok[*Response, ErrorCode](response) +} + +func trailersFuture() *FutureReader[Result[Option[*Fields], ErrorCode]] { + tx, rx := MakeFutureResultOptionFieldsErrorCode() + go tx.Write(Ok[Option[*Fields], ErrorCode](None[*Fields]())) + return rx +} + +func init() { handler.Exports.Handle = Handle } +func main() {} +``` + +Build using `componentize-go`: + +```bash +$ componentize-go -w wasi:http/service@0.3.0-rc-2026-03-15 -w platform build +``` + +### Async Spin Interfaces Across the Board + +In Spin v4 every Spin host interface is async. Where you previously called synchronous SDK functions, you now `await` them. Here is a side-by-side comparison for the key-value store across all three languages: + +**Rust** + +```rust +// Open the default store +let store = spin_sdk::key_value::Store::open_default().await?; + +// Read a value +let value: Option> = store.get("my-key").await?; + +// Write a value +store.set("my-key", b"my-value").await?; + +// Iterate all keys (streamed) +let (keys, result) = store.get_keys().await; +while let Some(key) = keys.next().await { + println!("{key}"); +} +result.await?; +``` + +**Python** + +```python +from spin_sdk import key_value + +async def handle_request(self, request): + async with await key_value.open_default() as store: + await store.set("visits", b"1") + value = await store.get("visits") + print(value) +``` + +**Go** (using the SDK's async key-value bindings) + +```go +// Key-value usage via the spin-go-sdk v3 bindings follows the same +// goroutine-and-stream pattern as the HTTP example above. +// See https://github.com/spinframework/spin-go-sdk for full examples. +``` + +The same async pattern applies to SQLite, PostgreSQL, MySQL, Redis, MQTT, and all other Spin APIs — no more blocking the event loop. + +## Build Profiles + +Managing different build configurations — release vs. debug, with or without profiling instrumentation — previously meant hand-editing `spin.toml` before every build. Spin v4 introduces **build profiles**: named, per-component configurations that you define once and activate with a flag. + +### Defining Profiles + +Add a `[component..profile.]` table (and optionally a `[component..profile..build]` table) to your `spin.toml`: + +```toml +spin_manifest_version = 2 + +[application] +name = "my-app" + +[component.api] +source = "target/wasm32-wasip2/release/api.wasm" + +[component.api.build] +command = "cargo build --release --target wasm32-wasip2" +watch = ["src/**/*", "Cargo.toml"] + +# A debug build profile for the api component. +[component.api.profile.debug] +source = "target/wasm32-wasip2/debug/api.wasm" + +[component.api.profile.debug.build] +command = "cargo build --target wasm32-wasip2" + +# The static-assets component has no debug variant; it always uses the default. +[component.static-assets] +source = { url = "https://example.com/spin_static_fs.wasm", digest = "sha256:abc123" } +``` + +### Activating a Profile + +Pass `--profile ` to `spin build`, `spin up`, `spin watch`, `spin deploy`, or `spin registry push`: + +```bash +# Default (release) build +$ spin build + +# Debug build — uses the debug profile where defined, default elsewhere +$ spin build --profile debug +$ spin up --profile debug + +# Mix: watch in debug mode, live-reloading on source changes +$ spin watch --profile debug +``` + +Components that do not define the requested profile automatically fall back to their default configuration. This means you can add a profile to one component at a time without affecting the rest. + +### What Can a Profile Override? + +A profile can override the following fields: + +| Field | Description | +|---|---| +| `source` | The Wasm binary to use | +| `build.command` | The command to compile the component | +| `environment` | Environment variables passed to the component | +| `dependencies` | Component dependencies to use in this profile | + +## Fine-Grained Capability Inheritance for Dependencies + +Spin has supported component dependencies since [SIP 020](https://github.com/spinframework/spin/blob/main/docs/content/sips/020-component-dependencies.md). Previously, the `dependencies_inherit_configuration` boolean on a component was all-or-nothing: either every dependency inherited every capability (outbound hosts, key-value stores, databases, AI models, …), or none of them did. + +Spin v4 ships [SIP 023](https://github.com/spinframework/spin/blob/main/docs/content/sips/023-fine-grained-capability-inheritance.md), which replaces this coarse toggle with a **per-dependency `inherit_configuration` field** that lets you specify exactly which capabilities each dependency may access. + +### Three Inheritance Modes + +#### Inherit all capabilities + +```toml +[component.dashboard.dependencies] +"aws:client" = { version = "1.0.0", inherit_configuration = true } +``` + +Equivalent to the old `dependencies_inherit_configuration = true`, but scoped to a single dependency. + +#### Inherit no capabilities (the default) + +```toml +[component.dashboard.dependencies] +"vendor:analytics" = { version = "2.0.0", inherit_configuration = false } +``` + +The dependency is fully isolated — all capability imports are satisfied by deny adapters. This is also the behaviour when `inherit_configuration` is omitted entirely. + +#### Inherit a specific subset + +```toml +[component.dashboard] +allowed_outbound_hosts = ["https://s3.us-west-2.amazonaws.com"] +key_value_stores = ["cache"] +ai_models = ["llama2-chat"] + +[component.dashboard.dependencies] +# Only allow s3-client to make outbound HTTP requests. +# It cannot touch the key-value store or AI models. +"aws:s3-client" = { version = "1.0.0", inherit_configuration = ["allowed_outbound_hosts"] } + +# Allow the templating library to read variables (e.g. secrets) from the parent. +"acme:templates" = { version = "0.5.0", inherit_configuration = ["variables"] } + +# Internal analytics component can access the cache. +"acme:analytics" = { component = "analytics", inherit_configuration = ["key_value_stores"] } +``` + +The supported configuration keys are: + +| Key | What it grants | +|---|---| +| `allowed_outbound_hosts` | Outbound HTTP, PostgreSQL, MySQL, Redis, MQTT, and socket access | +| `key_value_stores` | Key-value store access | +| `sqlite_databases` | SQLite database access | +| `ai_models` | LLM inferencing | +| `variables` | Dynamic variables / secrets | +| `environment` | Environment variables | +| `files` | Mounted files | + +### Backward Compatibility + +The component-level `dependencies_inherit_configuration = true` still works. It is equivalent to setting `inherit_configuration = true` on every dependency, and is expanded internally during manifest normalisation. However, **mixing** the component-level boolean with per-dependency `inherit_configuration` entries is not allowed — Spin will report an error directing you to pick one form: + +``` +Component `dashboard` specifies both `dependencies_inherit_configuration` and per-dependency +`inherit_configuration`. These are mutually exclusive; use one or the other. +``` + +The per-dependency form also applies uniformly to all dependency source types — registry packages, local paths, HTTP URLs, and app component references: + +```toml +[component.my-app.dependencies] +# Registry dependency +"aws:s3-client" = { version = "1.0.0", inherit_configuration = ["allowed_outbound_hosts"] } +# Local Wasm file +"my:lib/utils" = { path = "lib/utils.wasm", inherit_configuration = true } +# HTTP URL dependency +"vendor:dep/api" = { url = "https://example.com/dep.wasm", digest = "sha256:abc123", inherit_configuration = ["variables"] } +# Component within the same Spin app +"infra:dep/svc" = { component = "svc-component", inherit_configuration = ["key_value_stores", "variables"] } +``` + +## Getting Started with Spin v4 + +Install or upgrade Spin: + +```bash +$ curl -fsSL https://spinframework.dev/downloads/install.sh | bash +``` + +Or use a package manager — see the [Spin install docs](https://spinframework.dev/v4/install) for all options. + +Install the latest templates (including the WASIp3-based HTTP templates for Rust, Python, and Go): + +```bash +$ spin templates install --upgrade --git https://github.com/spinframework/spin +``` + +Create a new HTTP application: + +```bash +# Rust +$ spin new -t http-rust my-app-rs --accept-defaults + +# Python +$ spin new -t http-py my-app-py --accept-defaults + +# Go (requires componentize-go) +$ spin new -t http-go my-app-go --accept-defaults +``` + +Then build and run: + +```bash +$ cd my-app-rs +$ spin build --up +``` + +Visit the [Spin v4 quickstart](https://spinframework.dev/v4/quickstart) for a full walkthrough. + +## Thank You + +A huge thank you to every contributor who helped bring Spin v4 together as well as the broader Bytecode Alliance and WASI community for their tireless work on the WASIp3 standard. + +Thank you to the CNCF for their continued support of the Spin project. + +## Stay In Touch + +Please join us for weekly [project meetings](https://github.com/spinframework/spin#getting-involved-and-contributing), chat in the [Spin CNCF Slack channel](https://cloud-native.slack.com/archives/C089NJ9G1V0), and follow [@spinframework](https://twitter.com/spinframework) on X (formerly Twitter)! \ No newline at end of file From d4c4fae4d49e714461df26693f9b8c8737c7f8b8 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Thu, 23 Apr 2026 13:20:53 -0600 Subject: [PATCH 2/8] Forgot to commit the latest changes Signed-off-by: Brian Hardock --- content/blog/announcing-spin-4-0.md | 511 +++++++++++----------------- 1 file changed, 195 insertions(+), 316 deletions(-) diff --git a/content/blog/announcing-spin-4-0.md b/content/blog/announcing-spin-4-0.md index 0f9d1b9f..2414eaaf 100644 --- a/content/blog/announcing-spin-4-0.md +++ b/content/blog/announcing-spin-4-0.md @@ -1,7 +1,7 @@ title = "Announcing Spin v4.0" -date = "2026-04-23T12:00:00Z" +date = "2026-04-23T17:00:00Z" template = "blog_post" -description = "Announcing the release of Spin v4.0: WASIp3 stabilization, async interfaces across the board, build profiles, and fine-grained capability inheritance for dependencies." +description = "Announcing Spin v4.0: stabilized WASIp3 support, async host APIs across the board, build profiles, and fine-grained capability inheritance for dependencies." tags = [] [extra] @@ -10,449 +10,328 @@ author = "The Spin Project" --- -The CNCF Spin project is excited to announce [Spin v4.0](https://github.com/spinframework/spin/releases/tag/v4.0.0). This release marks the stabilization of WebAssembly System Interface Preview 3 (WASIp3) support in Spin, shipping the March RC (RC 2026-03-15) as a first-class, long-term supported target. Alongside this milestone, v4 delivers async interfaces throughout the entire API surface, build profiles for managing multiple build configurations, and fine-grained capability inheritance for component dependencies. +The CNCF Spin project just released [Spin v4.0.0](https://github.com/spinframework/spin/releases/tag/v4.0.0), the first major release of Spin in over a year, and the release we've been building toward all through the 3.x series. This release turns WASIp3 from an experimental, opt-in toggle into a first-class, long-term-supported feature; rewrites Spin's host APIs around `async`; adds build profiles; and introduces fine-grained capability inheritance for component dependencies. -For the full list of changes, see the [release notes](https://github.com/spinframework/spin/releases/tag/v4.0.0). +There's a lot in this release, so this post is part release-notes and part tutorial. We'll walk through each headline feature with a working example. -## WASIp3: Now Stable in Spin +- [WASIp3: stabilized and supported long-term](#wasip3-stabilized-and-supported-long-term) +- [Async everywhere: Spin's host interfaces are now async](#async-everywhere-spins-host-interfaces-are-now-async) +- [Build profiles](#build-profiles) +- [Fine-grained capability inheritance for dependencies](#fine-grained-capability-inheritance-for-dependencies) +- [Upgrading to Spin 4.0](#upgrading-to-spin-40) -[WASIp3](https://wasi.dev) is the next generation of the WebAssembly System Interface. At its core, it brings **first-class concurrency and asynchronous I/O** to WebAssembly components — meaning your component can handle multiple requests concurrently within a single instance, stream data to clients as it is produced rather than buffering everything upfront, and coordinate asynchronous tasks that outlive the top-level entry point. +## WASIp3: stabilized and supported long-term -Previous Spin releases shipped WASIp3 under an experimental, opt-in executor: `executor = { type = "wasip3-unstable" }`. **In Spin v4, that gate is gone.** The March RC of WASIp3 (RC 2026-03-15) is the default execution model. All HTTP and Redis trigger components now run on WASIp3, and all Spin SDK interfaces — key-value, SQLite, PostgreSQL, outbound HTTP, variables, and more — are now `async`-native. +WASIp3 is the next major revision of the WebAssembly System Interface. It brings first-class async, concurrent component exports, and significantly simpler WIT definitions to the component model. If you followed along in [Spin 3.5](https://spinframework.dev/v3/blog/announcing-spin-3-5) and [Spin 3.6](https://spinframework.dev/v3/blog/announcing-spin-3-6), you've seen WASIp3 progress from "experimental, opt-in, might break between releases" to something much closer to production. -### What Changes for Your Components? +**Spin 4.0 ships with the March 2026 release candidate of WASIp3, and we are committing to supporting it long-term.** You no longer have to flip an experimental executor switch, WASIp3 is now the default path for new applications, and `spin-sdk`, the Spin Python SDK, and the Spin Go SDK have all been updated to use it. -If you were using WASIp2 in Spin v3, here is what has changed: +What this means in practice: -- Your handler functions must now be `async` (in all SDKs). -- All Spin SDK API calls now return futures/coroutines; use your language's native `await` syntax. -- You no longer declare `executor = { type = "wasip3-unstable" }` in your trigger configuration. -- Component instance reuse is the default: a single component instance can serve multiple concurrent requests. Avoid storing per-request state in instance-level globals without synchronisation. +- **No more `executor = { type = "wasip3-unstable" }`.** The HTTP trigger speaks WASIp3 natively. WASIp2 components continue to run unchanged. +- **Concurrent, async component exports.** A single component instance can service multiple in-flight requests concurrently, instead of one instance per request. +- **One idiomatic story per language.** Rust handlers are `async fn` using `http` / `hyper` types. Python handlers are `async def`. Go handlers use standard `net/http`. -### Rust: `spin-sdk` v6 - -The Spin Rust SDK v6 makes WASIp3 the default, stable experience. The `#[http_service]` macro replaces `#[http_component]`, and handler functions are `async`: +Here's the new minimum-viable Rust HTTP component in 4.0: ```rust use spin_sdk::http::{IntoResponse, Request, Response}; use spin_sdk::http_service; #[http_service] -async fn handle(req: Request) -> anyhow::Result { +async fn handle(_req: Request) -> anyhow::Result { Ok(Response::builder() .status(200) .header("content-type", "text/plain") - .body("Hello from Spin v4!")?) -} -``` - -```toml -[package] -name = "hello-spin" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -anyhow = "1" -spin-sdk = "6.0" -``` - -Build target is still `wasm32-wasip2` — WASIp3 extends the binary format rather than replacing it: - -```bash -$ cargo build --target wasm32-wasip2 --release -``` - -Or use `spin build` with the Rust template. - -#### Streaming Responses with WASIp3 - -One of the most powerful WASIp3 capabilities is streaming. You can spawn a background task that writes response body data asynchronously, allowing your handler to send the HTTP response _before_ all of the data is ready: - -```rust -use bytes::Bytes; -use futures::{SinkExt, StreamExt}; -use http_body_util::StreamBody; -use spin_sdk::http::{IntoResponse, Request, Response}; -use spin_sdk::http_service; - -#[http_service] -async fn handle(_req: Request) -> anyhow::Result { - let store = spin_sdk::key_value::Store::open_default().await?; - - // Create a channel that will carry the response body chunks. - let (mut tx, rx) = futures::channel::mpsc::channel::(1024); - let rx = rx.map(|chunk| anyhow::Ok(http_body::Frame::data(chunk))); - let response = Response::new(StreamBody::new(rx)); - - // Spawn a background task. It runs even after `handle` returns. - spin_sdk::wasip3::spawn(async move { - if let Ok(Some(greeting)) = store.get("greeting").await { - tx.send(greeting.into()).await.unwrap(); - } - if let Ok(Some(name)) = store.get("name").await { - tx.send(" ".into()).await.unwrap(); - tx.send(name.into()).await.unwrap(); - } - // `tx` dropped here → body stream ends → Spin closes the response. - }); - - // Return immediately. Spin starts streaming the response to the client - // as chunks arrive from the background task. - Ok(response) + .body("Hello, Spin 4!".to_string())?) } ``` -#### Concurrent Outbound Requests +A few things to notice: -WASIp3 makes it trivial to fire multiple outbound HTTP requests concurrently — no threads, no blocking: - -```rust -use spin_sdk::http::{EmptyBody, IntoResponse, Request, Response}; -use spin_sdk::http_service; - -#[http_service] -async fn handle(_req: Request) -> anyhow::Result { - // Kick off both requests at the same time. - let (docs_res, spec_res): (Response, Response) = futures::try_join!( - spin_sdk::http::send(Request::get("https://spinframework.dev").body(EmptyBody::new())?), - spin_sdk::http::send(Request::get("https://wasi.dev").body(EmptyBody::new())?), - )?; - - let body = format!( - "Spin docs: {}, WASI spec: {}", - docs_res.status(), - spec_res.status() - ); - Ok(Response::new(body)) -} -``` +- The handler is `async fn`. That's not cosmetic, it's a real WASIp3 async export. While this handler is awaiting I/O, Spin can dispatch another request into the same component instance. +- `Request` and `Response` are re-exports from the ecosystem `http` crate, so this code composes with Axum, Tower, `http-body-util`, and friends with no glue types. +- There's no `wasip3-unstable` feature flag in `Cargo.toml` anymore. -### Python: `spin-python-sdk` v4 +### Python and Go -The Python SDK v4 updates `handle_request` to be an `async` method. Existing synchronous patterns still work — simply add `async`/`await`: +The same story holds in Python and Go. The Python SDK exposes a single `handle_request` coroutine: ```python -from spin_sdk.http import Handler, Request, Response, send +from spin_sdk.http import Handler, Request, Response class HttpHandler(Handler): async def handle_request(self, request: Request) -> Response: - animal_resp = await send( - Request("GET", "https://random-data-api.fermyon.app/animals/json", {}, None) - ) return Response( 200, {"content-type": "text/plain"}, - bytes(f"Animal fact: {str(animal_resp.body, 'utf-8')}", "utf-8"), + bytes("Hello from Python on WASIp3!", "utf-8"), ) ``` -The `spin.toml` build command now targets the v4 HTTP trigger WIT: - -```toml -[component.hello-world] -source = "app.wasm" -allowed_outbound_hosts = ["https://random-data-api.fermyon.app"] - -[component.hello-world.build] -command = "componentize-py -w spin:up/http-trigger@4.0.0 componentize app -o app.wasm" -``` - -Install the updated Python SDK and templates: +Build it with: ```bash -$ spin templates install --git https://github.com/spinframework/spin-python-sdk --update -$ spin new -t http-py my-app --accept-defaults +componentize-py -w spin:up/http-trigger@4.0.0 componentize app -o app.wasm ``` -### Go: `spin-go-sdk` v3 - -The Go SDK v3 drops TinyGo + WASIp1 in favour of standard Go tooling with `componentize-go`. HTTP handlers use the raw WASIp3 bindings exposed by the SDK. The idiomatic way to return a streaming body is with a goroutine — the same `go` keyword you already know: +On the Go side, the 4.0 release lines up with Go SDK `v3`, which drops the TinyGo requirement. Spin Go components now build with the standard Go toolchain (Go 1.25.5+) via `componentize-go`: ```go package main import ( + "fmt" "net/http" - handler "github.com/spinframework/spin-go-sdk/v3/exports/wasi_http_service_0_3_0_rc_2026_03_15/export_wasi_http_0_3_0_rc_2026_03_15_handler" - _ "github.com/spinframework/spin-go-sdk/v3/exports/wasi_http_service_0_3_0_rc_2026_03_15/wit_exports" - . "github.com/spinframework/spin-go-sdk/v3/imports/wasi_http_0_3_0_rc_2026_03_15_types" - . "go.bytecodealliance.org/pkg/wit/types" + spinhttp "github.com/spinframework/spin-go-sdk/v3/http" ) -func Handle(request *Request) Result[*Response, ErrorCode] { - // Create a stream for the body. - tx, rx := MakeStreamU8() - - // Goroutine writes the body asynchronously. - go func() { - defer tx.Drop() - tx.WriteAll([]uint8("Hello from Spin v4!")) - }() - - response, send := ResponseNew( - FieldsFromList([]Tuple2[string, []uint8]{ - {"content-type", []uint8("text/plain")}, - }).Ok(), - Some(rx), - trailersFuture(), - ) - send.Drop() - return Ok[*Response, ErrorCode](response) +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "text/plain") + fmt.Fprintln(w, "Hello from Go on WASIp3!") + }) } -func trailersFuture() *FutureReader[Result[Option[*Fields], ErrorCode]] { - tx, rx := MakeFutureResultOptionFieldsErrorCode() - go tx.Write(Ok[Option[*Fields], ErrorCode](None[*Fields]())) - return rx -} - -func init() { handler.Exports.Handle = Handle } func main() {} ``` -Build using `componentize-go`: - -```bash -$ componentize-go -w wasi:http/service@0.3.0-rc-2026-03-15 -w platform build +```toml +[component.hello-go.build] +command = "go tool componentize-go build" ``` -### Async Spin Interfaces Across the Board +If you've been writing Spin Go components against `spin-go-sdk/v2` with TinyGo, this is the big one, standard Go, standard `net/http`, and no `tinygo build -target=wasip1 -gc=leaking ...` incantation. -In Spin v4 every Spin host interface is async. Where you previously called synchronous SDK functions, you now `await` them. Here is a side-by-side comparison for the key-value store across all three languages: +### Concurrent outbound HTTP: the canonical demo -**Rust** +Because component exports are async, you can now fan out concurrent I/O from inside a component. The old ceremony of spinning up an async runtime by hand is gone, just `await` futures: ```rust -// Open the default store -let store = spin_sdk::key_value::Store::open_default().await?; +use futures::future::{select, Either}; +use spin_sdk::http::{EmptyBody, IntoResponse, Request, send}; +use spin_sdk::http_service; +use std::pin::pin; -// Read a value -let value: Option> = store.get("my-key").await?; +#[http_service] +async fn handle(_req: Request) -> anyhow::Result { + let spin = pin!(content_length("https://spinframework.dev")); + let book = pin!(content_length("https://component-model.bytecodealliance.org/")); + + let (winner, len) = match select(spin, book).await { + Either::Left((len, _)) => ("Spin docs", len?), + Either::Right((len, _)) => ("Component model book", len?), + }; -// Write a value -store.set("my-key", b"my-value").await?; + Ok(format!("{winner} responded first, content-length = {len:?}\n")) +} -// Iterate all keys (streamed) -let (keys, result) = store.get_keys().await; -while let Some(key) = keys.next().await { - println!("{key}"); +async fn content_length(url: &str) -> anyhow::Result> { + let req = Request::get(url).body(EmptyBody::new())?; + let res = send(req).await?; + Ok(res + .headers() + .get("content-length") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.parse().ok())) } -result.await?; ``` -**Python** +Both requests are in flight at once, inside a single Wasm instance, scheduled by Spin. That same instance may also be serving other requests concurrently. -```python -from spin_sdk import key_value +## Async everywhere: Spin's host interfaces are now async -async def handle_request(self, request): - async with await key_value.open_default() as store: - await store.set("visits", b"1") - value = await store.get("visits") - print(value) -``` +WASIp3 unlocks async, but the benefit only lands if the *host APIs* your component calls are also async. A lot of Spin 4.0's work happened here: we've asyncified Spin's host interfaces so I/O-heavy handlers actually get concurrency instead of blocking the instance. -**Go** (using the SDK's async key-value bindings) +In 4.0, the following interfaces are async from the guest's perspective: -```go -// Key-value usage via the spin-go-sdk v3 bindings follows the same -// goroutine-and-stream pattern as the HTTP example above. -// See https://github.com/spinframework/spin-go-sdk for full examples. +- **Key-value**: `Store::open_default().await`, `store.get(key).await`, `store.set(key, value).await` +- **SQLite**: `Connection::open_default().await`, `connection.execute(...).await` +- **PostgreSQL**: new async client API +- **Redis (outbound and trigger)**: `Connection::open(...).await`, plus async Redis subscriber handlers +- **Outbound HTTP**: `spin_sdk::http::send(req).await` + +Here's a key-value handler in Rust that both *uses* async host APIs and *produces* a streaming HTTP response via `spin_sdk::wasip3::spawn`: + +```rust +use bytes::Bytes; +use futures::{SinkExt, StreamExt}; +use http_body_util::StreamBody; +use spin_sdk::http::{IntoResponse, Request, Response}; +use spin_sdk::http_service; + +#[http_service] +async fn handle(_req: Request) -> anyhow::Result { + let store = spin_sdk::key_value::Store::open_default().await?; + + // A channel the spawned task will push body chunks into. + let (mut tx, rx) = futures::channel::mpsc::channel::(1024); + let rx = rx.map(|value| anyhow::Ok(http_body::Frame::data(value))); + let response = Response::new(StreamBody::new(rx)); + + // Spawn a background Wasm task. It outlives `handle` returning. + spin_sdk::wasip3::spawn(async move { + if let Ok(Some(greeting)) = store.get("greeting").await { + let _ = tx.send(greeting.into()).await; + } + if let Ok(Some(who)) = store.get("greetee").await { + let _ = tx.send(" ".into()).await; + let _ = tx.send(who.into()).await; + } + // Dropping `tx` closes the body stream. + }); + + Ok(response) +} ``` -The same async pattern applies to SQLite, PostgreSQL, MySQL, Redis, MQTT, and all other Spin APIs — no more blocking the event loop. +Spin starts streaming the response as soon as `handle` returns, while the spawned task keeps reading from the key-value store in the background. In the WASIp2 world, each of those KV reads would have blocked the entire component instance. In 4.0, they don't. -## Build Profiles +The same pattern is available in Python (via `componentize_py_async_support.spawn`) and Go (via plain `go func() { ... }()` goroutines). -Managing different build configurations — release vs. debug, with or without profiling instrumentation — previously meant hand-editing `spin.toml` before every build. Spin v4 introduces **build profiles**: named, per-component configurations that you define once and activate with a flag. +> **Heads up on global state.** Because a single instance can now serve multiple concurrent requests, instance-scoped "global" state is shared across in-flight requests. This is the same rule you already follow in Axum, Express, or Flask, but it's a genuine shift from "one instance per request" if you've been writing Spin components against WASIp2 semantics. Audit any `static`, module-level, or `OnceCell` state and reach for per-request state or explicit synchronization where needed. -### Defining Profiles +## Build profiles -Add a `[component..profile.]` table (and optionally a `[component..profile..build]` table) to your `spin.toml`: +Until now, building a Spin component in debug vs. release mode, or profiling builds, or "use a pre-built version in CI", meant hand-editing `[component.X.build]` tables, usually on a branch you hoped nobody merged. [SIP-022](https://github.com/spinframework/spin/blob/main/docs/content/sips/022-build-profiles.md) fixes this. Spin 4.0 introduces named **build profiles**, inspired by Cargo profiles. + +You declare alternate profiles inline under each component, and select one at the command line: ```toml spin_manifest_version = 2 [application] -name = "my-app" +name = "sentiment-analysis" -[component.api] -source = "target/wasm32-wasip2/release/api.wasm" +[component.sentiment-analysis] +source = "target/spin-http-js.wasm" -[component.api.build] -command = "cargo build --release --target wasm32-wasip2" -watch = ["src/**/*", "Cargo.toml"] +[component.sentiment-analysis.build] +command = "npm run build" +watch = ["src/**/*", "package.json", "package-lock.json"] -# A debug build profile for the api component. -[component.api.profile.debug] -source = "target/wasm32-wasip2/debug/api.wasm" +# A `debug` profile for the sentiment-analysis component. +[component.sentiment-analysis.profile.debug] +source = "target/spin-http-js.debug.wasm" -[component.api.profile.debug.build] -command = "cargo build --target wasm32-wasip2" +[component.sentiment-analysis.profile.debug.build] +command = "npm run build:debug" -# The static-assets component has no debug variant; it always uses the default. -[component.static-assets] -source = { url = "https://example.com/spin_static_fs.wasm", digest = "sha256:abc123" } -``` +# The `ui` component has no debug profile, it'll fall back to the default. +[component.ui] +source = { url = "https://.../spin_static_fs.wasm", digest = "sha256:..." } -### Activating a Profile +# The `kv-explorer` component pulls a pre-built debug build from a registry. +[component.kv-explorer] +source = { url = "https://.../spin-kv-explorer.wasm", digest = "sha256:..." } + +[component.kv-explorer.profile.debug] +source = { url = "https://.../spin-kv-explorer.debug.wasm", digest = "sha256:..." } +``` -Pass `--profile ` to `spin build`, `spin up`, `spin watch`, `spin deploy`, or `spin registry push`: +Now you can run the same manifest in two modes: ```bash -# Default (release) build -$ spin build +# Production: every component uses its default build. +$ spin up -# Debug build — uses the debug profile where defined, default elsewhere -$ spin build --profile debug +# Debug: every component that defines `debug` uses it; others fall back. $ spin up --profile debug - -# Mix: watch in debug mode, live-reloading on source changes -$ spin watch --profile debug ``` -Components that do not define the requested profile automatically fall back to their default configuration. This means you can add a profile to one component at a time without affecting the rest. - -### What Can a Profile Override? +The `--profile` flag is recognized by `spin build`, `spin watch`, `spin up`, `spin deploy`, and `spin registry push`, so profiles carry all the way from local dev through deployment. -A profile can override the following fields: +The fields a profile can override are intentionally scoped to build-time concerns: -| Field | Description | -|---|---| -| `source` | The Wasm binary to use | -| `build.command` | The command to compile the component | -| `environment` | Environment variables passed to the component | -| `dependencies` | Component dependencies to use in this profile | +- `source` +- `build.command` +- `environment` +- `dependencies` -## Fine-Grained Capability Inheritance for Dependencies +Profiles are *not* atomic, fields in a profile are individual overrides. If a component doesn't define a requested profile, it falls back to its default configuration rather than erroring. This is exactly what you want for mixed-language apps, where only some components have meaningful debug builds. -Spin has supported component dependencies since [SIP 020](https://github.com/spinframework/spin/blob/main/docs/content/sips/020-component-dependencies.md). Previously, the `dependencies_inherit_configuration` boolean on a component was all-or-nothing: either every dependency inherited every capability (outbound hosts, key-value stores, databases, AI models, …), or none of them did. +## Fine-grained capability inheritance for dependencies -Spin v4 ships [SIP 023](https://github.com/spinframework/spin/blob/main/docs/content/sips/023-fine-grained-capability-inheritance.md), which replaces this coarse toggle with a **per-dependency `inherit_configuration` field** that lets you specify exactly which capabilities each dependency may access. +[SIP-020](https://github.com/spinframework/spin/blob/main/docs/content/sips/020-component-dependencies.md) introduced component dependencies, along with a single component-level boolean: `dependencies_inherit_configuration`. It was all-or-nothing, either every dependency inherited every capability of the parent component, or none did. -### Three Inheritance Modes +That's a problem in the real world. You might be happy letting an `aws:client/s3` dependency make outbound HTTPS calls to S3, but not letting it read your key-value store or invoke an LLM. [SIP-023](https://github.com/spinframework/spin/blob/main/docs/content/sips/023-fine-grained-capability-inheritance.md) replaces that coarse toggle with a per-dependency `inherit_configuration` field that accepts three forms. -#### Inherit all capabilities +**1. Inherit everything** (equivalent to the old global flag, but scoped to one dep): ```toml -[component.dashboard.dependencies] +[component."infra-dashboard".dependencies] "aws:client" = { version = "1.0.0", inherit_configuration = true } ``` -Equivalent to the old `dependencies_inherit_configuration = true`, but scoped to a single dependency. - -#### Inherit no capabilities (the default) +**2. Inherit nothing** (the default): ```toml -[component.dashboard.dependencies] -"vendor:analytics" = { version = "2.0.0", inherit_configuration = false } +[component."infra-dashboard".dependencies] +"aws:client" = { version = "1.0.0", inherit_configuration = false } ``` -The dependency is fully isolated — all capability imports are satisfied by deny adapters. This is also the behaviour when `inherit_configuration` is omitted entirely. +The dependency is fully isolated from the parent's configuration. All capability imports are satisfied by deny adapters. -#### Inherit a specific subset +**3. Inherit a specific subset.** This is the new power: ```toml -[component.dashboard] +[component."infra-dashboard"] allowed_outbound_hosts = ["https://s3.us-west-2.amazonaws.com"] -key_value_stores = ["cache"] -ai_models = ["llama2-chat"] - -[component.dashboard.dependencies] -# Only allow s3-client to make outbound HTTP requests. -# It cannot touch the key-value store or AI models. -"aws:s3-client" = { version = "1.0.0", inherit_configuration = ["allowed_outbound_hosts"] } +key_value_stores = ["my-key-value-cache"] +ai_models = ["llama2-chat"] -# Allow the templating library to read variables (e.g. secrets) from the parent. -"acme:templates" = { version = "0.5.0", inherit_configuration = ["variables"] } - -# Internal analytics component can access the cache. -"acme:analytics" = { component = "analytics", inherit_configuration = ["key_value_stores"] } +[component."infra-dashboard".dependencies] +"aws:client" = { version = "1.0.0", inherit_configuration = ["allowed_outbound_hosts"] } ``` -The supported configuration keys are: - -| Key | What it grants | -|---|---| -| `allowed_outbound_hosts` | Outbound HTTP, PostgreSQL, MySQL, Redis, MQTT, and socket access | -| `key_value_stores` | Key-value store access | -| `sqlite_databases` | SQLite database access | -| `ai_models` | LLM inferencing | -| `variables` | Dynamic variables / secrets | -| `environment` | Environment variables | -| `files` | Mounted files | +Here `aws:client` can make outbound HTTPS calls to the parent's allowed hosts, enough to reach S3, but it *cannot* see `my-key-value-cache` and *cannot* invoke `llama2-chat`. Every other capability is denied. -### Backward Compatibility +The keys you can list are the configuration families Spin already knows about: `ai_models`, `allowed_outbound_hosts`, `environment`, `files`, `key_value_stores`, `sqlite_databases`, and `variables`. Each key maps to a set of WIT interfaces, for example, listing `allowed_outbound_hosts` covers `wasi:http`, `fermyon:spin/mysql`, `fermyon:spin/postgres`, `fermyon:spin/redis`, `wasi:sockets`, and so on. See the [SIP-023 table](https://github.com/spinframework/spin/blob/main/docs/content/sips/023-fine-grained-capability-inheritance.md) for the full mapping. -The component-level `dependencies_inherit_configuration = true` still works. It is equivalent to setting `inherit_configuration = true` on every dependency, and is expanded internally during manifest normalisation. However, **mixing** the component-level boolean with per-dependency `inherit_configuration` entries is not allowed — Spin will report an error directing you to pick one form: - -``` -Component `dashboard` specifies both `dependencies_inherit_configuration` and per-dependency -`inherit_configuration`. These are mutually exclusive; use one or the other. -``` - -The per-dependency form also applies uniformly to all dependency source types — registry packages, local paths, HTTP URLs, and app component references: +`inherit_configuration` works uniformly across every dependency source type: ```toml -[component.my-app.dependencies] +[component."my-app".dependencies] # Registry dependency -"aws:s3-client" = { version = "1.0.0", inherit_configuration = ["allowed_outbound_hosts"] } -# Local Wasm file -"my:lib/utils" = { path = "lib/utils.wasm", inherit_configuration = true } -# HTTP URL dependency -"vendor:dep/api" = { url = "https://example.com/dep.wasm", digest = "sha256:abc123", inherit_configuration = ["variables"] } -# Component within the same Spin app -"infra:dep/svc" = { component = "svc-component", inherit_configuration = ["key_value_stores", "variables"] } +"aws:client" = { version = "1.0.0", inherit_configuration = ["allowed_outbound_hosts"] } +# Local path dependency +"my:lib/utils" = { path = "lib/utils.wasm", inherit_configuration = true } +# HTTP dependency +"vendor:dep/api" = { url = "https://example.com/dep.wasm", digest = "sha256:abc123", inherit_configuration = ["variables"] } +# Reference to another component in this same app +"infra:dep/svc" = { component = "svc-component", inherit_configuration = ["key_value_stores", "variables"] } ``` -## Getting Started with Spin v4 +A couple of rules to be aware of: -Install or upgrade Spin: +- The shorthand form (`"fizz:buzz" = ">=0.1.0"`) doesn't support `inherit_configuration`, use the full table form. +- `dependencies_inherit_configuration = true` still works as a convenience for "turn it on for every dependency," but **mixing** it with per-dependency `inherit_configuration` is a hard error. Pick one. -```bash -$ curl -fsSL https://spinframework.dev/downloads/install.sh | bash -``` - -Or use a package manager — see the [Spin install docs](https://spinframework.dev/v4/install) for all options. - -Install the latest templates (including the WASIp3-based HTTP templates for Rust, Python, and Go): - -```bash -$ spin templates install --upgrade --git https://github.com/spinframework/spin -``` - -Create a new HTTP application: +Practically, this means you can hand out dependencies to components from across your organization, or from the registry, and grant them exactly the capabilities they need, and nothing more. It's the principle of least privilege, expressed in `spin.toml`. -```bash -# Rust -$ spin new -t http-rust my-app-rs --accept-defaults - -# Python -$ spin new -t http-py my-app-py --accept-defaults - -# Go (requires componentize-go) -$ spin new -t http-go my-app-go --accept-defaults -``` +## Upgrading to Spin 4.0 -Then build and run: - -```bash -$ cd my-app-rs -$ spin build --up -``` +1. **Install Spin 4.0** from [spinframework.dev/install](https://spinframework.dev/install) or grab a binary from the [release page](https://github.com/spinframework/spin/releases/tag/v4.0.0). +2. **Update your templates:** + ```bash + spin templates install --git https://github.com/spinframework/spin --update + ``` +3. **Update the SDK** your component uses: + - Rust: `spin-sdk = "6.0"` and target `wasm32-wasip2` (WASIp2 and WASIp3 share the same binary target). + - Python: pull the latest `spin-sdk` and `componentize-py` into your virtualenv. + - Go: switch to `github.com/spinframework/spin-go-sdk/v3` and use Go 1.25.5+ with `go tool componentize-go build`. +4. **Remove** any `executor = { type = "wasip3-unstable" }` lines from your manifest, they're no longer needed. +5. **Audit instance-scoped state.** Concurrent in-flight requests on a single instance is now the default. -Visit the [Spin v4 quickstart](https://spinframework.dev/v4/quickstart) for a full walkthrough. +For a full walkthrough, see the [v4 quickstart](https://spinframework.dev/v4/quickstart) and the updated [language guides](https://spinframework.dev/v4/language-support-overview). -## Thank You +## Thank you -A huge thank you to every contributor who helped bring Spin v4 together as well as the broader Bytecode Alliance and WASI community for their tireless work on the WASIp3 standard. +Spin 4.0 is the work of a lot of people across a lot of organizations, contributors to Spin itself, to `wasmtime`, to `wit-bindgen`, to `componentize-py`, to the Spin Go SDK, and to the WASIp3 standardization effort in the Bytecode Alliance. Thank you, and thank you to the CNCF for continuing to support the project. -Thank you to the CNCF for their continued support of the Spin project. +## Stay in touch -## Stay In Touch +Join us at weekly [project meetings](https://github.com/spinframework/spin#getting-involved-and-contributing), say hi on the [Spin CNCF Slack channel](https://cloud-native.slack.com/archives/C089NJ9G1V0), and follow [@spinframework](https://twitter.com/spinframework) on X. -Please join us for weekly [project meetings](https://github.com/spinframework/spin#getting-involved-and-contributing), chat in the [Spin CNCF Slack channel](https://cloud-native.slack.com/archives/C089NJ9G1V0), and follow [@spinframework](https://twitter.com/spinframework) on X (formerly Twitter)! \ No newline at end of file +Ready to build? Head to the [Spin quickstart](https://spinframework.dev/v4/quickstart), or browse the [Spin Hub](https://spinframework.dev/hub) for inspiration. From 432fa7f2cdd020b100c5c96c36db4540aba9fec8 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Thu, 23 Apr 2026 20:29:15 -0600 Subject: [PATCH 3/8] Address wasip3 and SDK feedback Signed-off-by: Brian Hardock --- content/blog/announcing-spin-4-0.md | 59 ++++++++++++++++++----------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/content/blog/announcing-spin-4-0.md b/content/blog/announcing-spin-4-0.md index 2414eaaf..5ad89a97 100644 --- a/content/blog/announcing-spin-4-0.md +++ b/content/blog/announcing-spin-4-0.md @@ -10,27 +10,29 @@ author = "The Spin Project" --- -The CNCF Spin project just released [Spin v4.0.0](https://github.com/spinframework/spin/releases/tag/v4.0.0), the first major release of Spin in over a year, and the release we've been building toward all through the 3.x series. This release turns WASIp3 from an experimental, opt-in toggle into a first-class, long-term-supported feature; rewrites Spin's host APIs around `async`; adds build profiles; and introduces fine-grained capability inheritance for component dependencies. +The CNCF Spin project just released [Spin v4.0.0](https://github.com/spinframework/spin/releases/tag/v4.0.0), the first major release of Spin in over a year. This release delivers a production-grade, stable implementation of WASI Preview 3; rewrites Spin's host APIs around `async`; adds build profiles; and introduces fine-grained capability inheritance for component dependencies. There's a lot in this release, so this post is part release-notes and part tutorial. We'll walk through each headline feature with a working example. -- [WASIp3: stabilized and supported long-term](#wasip3-stabilized-and-supported-long-term) +- [WASI Preview 3: stabilized and supported long-term](#wasip3-stabilized-and-supported-long-term) - [Async everywhere: Spin's host interfaces are now async](#async-everywhere-spins-host-interfaces-are-now-async) - [Build profiles](#build-profiles) - [Fine-grained capability inheritance for dependencies](#fine-grained-capability-inheritance-for-dependencies) - [Upgrading to Spin 4.0](#upgrading-to-spin-40) -## WASIp3: stabilized and supported long-term +## WASI Preview 3: stabilized and supported long-term -WASIp3 is the next major revision of the WebAssembly System Interface. It brings first-class async, concurrent component exports, and significantly simpler WIT definitions to the component model. If you followed along in [Spin 3.5](https://spinframework.dev/v3/blog/announcing-spin-3-5) and [Spin 3.6](https://spinframework.dev/v3/blog/announcing-spin-3-6), you've seen WASIp3 progress from "experimental, opt-in, might break between releases" to something much closer to production. +WASI Preview 3 (WASIp3) is the next major revision of the WebAssembly System Interface. It brings first-class async, concurrent component exports, and significantly simpler WIT definitions to the component model. In practice, that means your components can handle multiple in-flight requests on a single instance, fan out concurrent I/O with plain `await`, and talk to host interfaces using idiomatic async code in each language. -**Spin 4.0 ships with the March 2026 release candidate of WASIp3, and we are committing to supporting it long-term.** You no longer have to flip an experimental executor switch, WASIp3 is now the default path for new applications, and `spin-sdk`, the Spin Python SDK, and the Spin Go SDK have all been updated to use it. +**Spin 4.0 ships with the March 2026 release candidate of WASIp3, and we are committing to supporting it long-term.** WASIp3 is now the default platform for new applications, and the Spin Rust, Python, and Go SDKs have all been updated to use it. + +If you followed along in [Spin 3.5](https://spinframework.dev/v3/blog/announcing-spin-3-5) and [Spin 3.6](https://spinframework.dev/v3/blog/announcing-spin-3-6), you've seen WASIp3 progress from "experimental, opt-in, might break between releases" to something ready for production use. What this means in practice: -- **No more `executor = { type = "wasip3-unstable" }`.** The HTTP trigger speaks WASIp3 natively. WASIp2 components continue to run unchanged. - **Concurrent, async component exports.** A single component instance can service multiple in-flight requests concurrently, instead of one instance per request. -- **One idiomatic story per language.** Rust handlers are `async fn` using `http` / `hyper` types. Python handlers are `async def`. Go handlers use standard `net/http`. +- **One idiomatic story per language.** Rust handlers are `async fn` using `http` / `hyper` types, and Python handlers are `async def`. Go handlers remain standard `net/http`, but now build with the standard Go toolchain (see below). +- **WASIp2 components continue to run unchanged.** The HTTP trigger speaks WASIp3 natively, and existing WASIp2 components keep working without changes. Here's the new minimum-viable Rust HTTP component in 4.0: @@ -51,7 +53,8 @@ A few things to notice: - The handler is `async fn`. That's not cosmetic, it's a real WASIp3 async export. While this handler is awaiting I/O, Spin can dispatch another request into the same component instance. - `Request` and `Response` are re-exports from the ecosystem `http` crate, so this code composes with Axum, Tower, `http-body-util`, and friends with no glue types. -- There's no `wasip3-unstable` feature flag in `Cargo.toml` anymore. + +For a richer illustration of concurrent async exports, see the [gRPC sample](https://github.com/spinframework/spin-rust-sdk/tree/main/examples/grpc) in the Spin repo. ### Python and Go @@ -75,7 +78,9 @@ Build it with: componentize-py -w spin:up/http-trigger@4.0.0 componentize app -o app.wasm ``` -On the Go side, the 4.0 release lines up with Go SDK `v3`, which drops the TinyGo requirement. Spin Go components now build with the standard Go toolchain (Go 1.25.5+) via `componentize-go`: +### No more TinyGo + +On the Go side, the 4.0 release lines up with Go SDK `v3`, which **drops the TinyGo requirement**. Spin Go components now build with the standard Go toolchain (Go 1.25.5+) via `componentize-go`: ```go package main @@ -102,7 +107,7 @@ func main() {} command = "go tool componentize-go build" ``` -If you've been writing Spin Go components against `spin-go-sdk/v2` with TinyGo, this is the big one, standard Go, standard `net/http`, and no `tinygo build -target=wasip1 -gc=leaking ...` incantation. +If you've been writing Spin Go components against `spin-go-sdk/v2` with TinyGo, this is the big one: standard Go, and no `tinygo build -target=wasip1 -gc=leaking ...` incantation. ### Concurrent outbound HTTP: the canonical demo @@ -138,7 +143,7 @@ async fn content_length(url: &str) -> anyhow::Result> { } ``` -Both requests are in flight at once, inside a single Wasm instance, scheduled by Spin. That same instance may also be serving other requests concurrently. +Both requests are in flight at once, inside a single Wasm instance, scheduled by Spin. That same instance may also be serving other requests concurrently. And because WASIp3 supports streaming response bodies, a handler can start writing its response before it's finished computing the rest, something we'll use in the next section. ## Async everywhere: Spin's host interfaces are now async @@ -148,11 +153,11 @@ In 4.0, the following interfaces are async from the guest's perspective: - **Key-value**: `Store::open_default().await`, `store.get(key).await`, `store.set(key, value).await` - **SQLite**: `Connection::open_default().await`, `connection.execute(...).await` -- **PostgreSQL**: new async client API +- **PostgreSQL**: `Connection::open(...).await`, `connection.query(...).await` - **Redis (outbound and trigger)**: `Connection::open(...).await`, plus async Redis subscriber handlers - **Outbound HTTP**: `spin_sdk::http::send(req).await` -Here's a key-value handler in Rust that both *uses* async host APIs and *produces* a streaming HTTP response via `spin_sdk::wasip3::spawn`: +Here's a Rust handler that runs a SQLite query and streams each row to the client as it arrives, using `spin_sdk::wasip3::spawn`: ```rust use bytes::Bytes; @@ -160,33 +165,43 @@ use futures::{SinkExt, StreamExt}; use http_body_util::StreamBody; use spin_sdk::http::{IntoResponse, Request, Response}; use spin_sdk::http_service; +use spin_sdk::sqlite::Connection; #[http_service] async fn handle(_req: Request) -> anyhow::Result { - let store = spin_sdk::key_value::Store::open_default().await?; + let db = Connection::open_default().await?; // A channel the spawned task will push body chunks into. let (mut tx, rx) = futures::channel::mpsc::channel::(1024); let rx = rx.map(|value| anyhow::Ok(http_body::Frame::data(value))); - let response = Response::new(StreamBody::new(rx)); + let response = Response::builder() + .header("content-type", "application/x-ndjson") + .body(StreamBody::new(rx))?; // Spawn a background Wasm task. It outlives `handle` returning. spin_sdk::wasip3::spawn(async move { - if let Ok(Some(greeting)) = store.get("greeting").await { - let _ = tx.send(greeting.into()).await; - } - if let Ok(Some(who)) = store.get("greetee").await { - let _ = tx.send(" ".into()).await; - let _ = tx.send(who.into()).await; + let mut query_result = db + .execute("SELECT id, name FROM users ORDER BY id", []) + .await?; + let id_idx = query_result.columns().iter().position(|c| c == "id").unwrap(); + let name_idx = query_result.columns().iter().position(|c| c == "name").unwrap(); + + while let Some(row) = query_result.next().await { + let id: i64 = row.get(id_idx).unwrap(); + let name: &str = row.get(name_idx).unwrap(); + let line = format!("{{\"id\":{id},\"name\":\"{name}\"}}\n"); + let _ = tx.send(line.into()).await; } + query_result.result().await?; // Dropping `tx` closes the body stream. + anyhow::Ok(()) }); Ok(response) } ``` -Spin starts streaming the response as soon as `handle` returns, while the spawned task keeps reading from the key-value store in the background. In the WASIp2 world, each of those KV reads would have blocked the entire component instance. In 4.0, they don't. +Spin starts streaming the response as soon as `handle` returns, and each row hits the client as soon as SQLite yields it, without buffering the full result set in memory. That's a real win for time-to-first-byte and for large queries: you don't have to wait for every row before the client starts receiving bytes. The same pattern is available in Python (via `componentize_py_async_support.spawn`) and Go (via plain `go func() { ... }()` goroutines). From 597515d7bc31dcb83300578cf15b78347210e969 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Thu, 23 Apr 2026 20:30:09 -0600 Subject: [PATCH 4/8] Address build profile feedback Signed-off-by: Brian Hardock --- content/blog/announcing-spin-4-0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/blog/announcing-spin-4-0.md b/content/blog/announcing-spin-4-0.md index 5ad89a97..fa00e831 100644 --- a/content/blog/announcing-spin-4-0.md +++ b/content/blog/announcing-spin-4-0.md @@ -209,7 +209,7 @@ The same pattern is available in Python (via `componentize_py_async_support.spaw ## Build profiles -Until now, building a Spin component in debug vs. release mode, or profiling builds, or "use a pre-built version in CI", meant hand-editing `[component.X.build]` tables, usually on a branch you hoped nobody merged. [SIP-022](https://github.com/spinframework/spin/blob/main/docs/content/sips/022-build-profiles.md) fixes this. Spin 4.0 introduces named **build profiles**, inspired by Cargo profiles. +Development tools often offer ways to build code in different ways for different purposes: for example, debug builds that favor diagnostic value over speed and size, or profiling builds that inject performance instrumentation. Spin 4.0 introduces named **build profiles**, so you can leverage those options in Wasm applications, for example, to define a debug or profile build of your application. You declare alternate profiles inline under each component, and select one at the command line: From fd3ccdbc1163476654d390000d1d0db67ea4c158 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Thu, 23 Apr 2026 20:30:32 -0600 Subject: [PATCH 5/8] Address dependencies feedback Signed-off-by: Brian Hardock --- content/blog/announcing-spin-4-0.md | 55 ++++++++--------------------- 1 file changed, 15 insertions(+), 40 deletions(-) diff --git a/content/blog/announcing-spin-4-0.md b/content/blog/announcing-spin-4-0.md index fa00e831..70967e5d 100644 --- a/content/blog/announcing-spin-4-0.md +++ b/content/blog/announcing-spin-4-0.md @@ -255,38 +255,35 @@ $ spin up $ spin up --profile debug ``` -The `--profile` flag is recognized by `spin build`, `spin watch`, `spin up`, `spin deploy`, and `spin registry push`, so profiles carry all the way from local dev through deployment. +The `--profile` flag is recognized by `spin build`, `spin watch`, `spin up`, and `spin registry push`, so profiles carry from local development into your published artifacts. -The fields a profile can override are intentionally scoped to build-time concerns: - -- `source` -- `build.command` -- `environment` -- `dependencies` - -Profiles are *not* atomic, fields in a profile are individual overrides. If a component doesn't define a requested profile, it falls back to its default configuration rather than erroring. This is exactly what you want for mixed-language apps, where only some components have meaningful debug builds. +Learn more in the [Spin docs on build profiles](https://spinframework.dev/v4/build-profiles). ## Fine-grained capability inheritance for dependencies -[SIP-020](https://github.com/spinframework/spin/blob/main/docs/content/sips/020-component-dependencies.md) introduced component dependencies, along with a single component-level boolean: `dependencies_inherit_configuration`. It was all-or-nothing, either every dependency inherited every capability of the parent component, or none did. +Spin 3 introduced component dependencies, which let you use off-the-shelf Wasm components as libraries without having to write composition scripts or commands. Once you start using component dependencies like that, a natural concern is what capabilities those dependencies have, your application needs to connect to your database, but your regex matcher sure doesn't. + +Spin 3 offered a partial solution where dependencies could be fully isolated, and Spin 4 improves on this by letting you manage dependency capabilities more selectively, on a dependency-by-dependency, capability-by-capability basis. This is the principle of least privilege, expressed in `spin.toml`, and it means you can hand out dependencies from across your organization, or from the registry, and grant them exactly the capabilities they need, and nothing more. -That's a problem in the real world. You might be happy letting an `aws:client/s3` dependency make outbound HTTPS calls to S3, but not letting it read your key-value store or invoke an LLM. [SIP-023](https://github.com/spinframework/spin/blob/main/docs/content/sips/023-fine-grained-capability-inheritance.md) replaces that coarse toggle with a per-dependency `inherit_configuration` field that accepts three forms. +Concretely, Spin 4 replaces the coarse `dependencies_inherit_configuration` toggle with a per-dependency `inherit_configuration` field that accepts three forms. -**1. Inherit everything** (equivalent to the old global flag, but scoped to one dep): +**1. Inherit everything** — the dependency has access to all the capabilities of the main component: ```toml [component."infra-dashboard".dependencies] -"aws:client" = { version = "1.0.0", inherit_configuration = true } +"acme:s3-client" = { version = "1.0.0", inherit_configuration = true } ``` +This is similar to `dependencies_inherit_configuration = true` in Spin 3, but scoped to just this dependency. + **2. Inherit nothing** (the default): ```toml [component."infra-dashboard".dependencies] -"aws:client" = { version = "1.0.0", inherit_configuration = false } +"acme:s3-client" = { version = "1.0.0", inherit_configuration = false } ``` -The dependency is fully isolated from the parent's configuration. All capability imports are satisfied by deny adapters. +The dependency is fully isolated from the parent's configuration, any capability import the component calls will return an error. Per-dependency isolation like this is itself new in Spin 4. **3. Inherit a specific subset.** This is the new power: @@ -294,36 +291,14 @@ The dependency is fully isolated from the parent's configuration. All capability [component."infra-dashboard"] allowed_outbound_hosts = ["https://s3.us-west-2.amazonaws.com"] key_value_stores = ["my-key-value-cache"] -ai_models = ["llama2-chat"] [component."infra-dashboard".dependencies] -"aws:client" = { version = "1.0.0", inherit_configuration = ["allowed_outbound_hosts"] } +"acme:s3-client" = { version = "1.0.0", inherit_configuration = ["allowed_outbound_hosts"] } ``` -Here `aws:client` can make outbound HTTPS calls to the parent's allowed hosts, enough to reach S3, but it *cannot* see `my-key-value-cache` and *cannot* invoke `llama2-chat`. Every other capability is denied. - -The keys you can list are the configuration families Spin already knows about: `ai_models`, `allowed_outbound_hosts`, `environment`, `files`, `key_value_stores`, `sqlite_databases`, and `variables`. Each key maps to a set of WIT interfaces, for example, listing `allowed_outbound_hosts` covers `wasi:http`, `fermyon:spin/mysql`, `fermyon:spin/postgres`, `fermyon:spin/redis`, `wasi:sockets`, and so on. See the [SIP-023 table](https://github.com/spinframework/spin/blob/main/docs/content/sips/023-fine-grained-capability-inheritance.md) for the full mapping. - -`inherit_configuration` works uniformly across every dependency source type: - -```toml -[component."my-app".dependencies] -# Registry dependency -"aws:client" = { version = "1.0.0", inherit_configuration = ["allowed_outbound_hosts"] } -# Local path dependency -"my:lib/utils" = { path = "lib/utils.wasm", inherit_configuration = true } -# HTTP dependency -"vendor:dep/api" = { url = "https://example.com/dep.wasm", digest = "sha256:abc123", inherit_configuration = ["variables"] } -# Reference to another component in this same app -"infra:dep/svc" = { component = "svc-component", inherit_configuration = ["key_value_stores", "variables"] } -``` - -A couple of rules to be aware of: - -- The shorthand form (`"fizz:buzz" = ">=0.1.0"`) doesn't support `inherit_configuration`, use the full table form. -- `dependencies_inherit_configuration = true` still works as a convenience for "turn it on for every dependency," but **mixing** it with per-dependency `inherit_configuration` is a hard error. Pick one. +Here `acme:s3-client` can make outbound HTTPS calls to the parent's allowed hosts, enough to reach S3. But every other capability is denied. Specifically, the S3 client *cannot* see `my-key-value-cache`. -Practically, this means you can hand out dependencies to components from across your organization, or from the registry, and grant them exactly the capabilities they need, and nothing more. It's the principle of least privilege, expressed in `spin.toml`. +For the full list of configuration keys you can inherit and how they map to WIT interfaces, see the [Spin docs on component dependencies](TODO). ## Upgrading to Spin 4.0 From fc52bd8935f1eff80ea95b68e91c1f2e46d03696 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Fri, 24 Apr 2026 10:44:18 -0600 Subject: [PATCH 6/8] Shuffling bits and bobs. Removing TODO. Signed-off-by: Brian Hardock --- content/blog/announcing-spin-4-0.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/content/blog/announcing-spin-4-0.md b/content/blog/announcing-spin-4-0.md index 70967e5d..e67a6a01 100644 --- a/content/blog/announcing-spin-4-0.md +++ b/content/blog/announcing-spin-4-0.md @@ -145,6 +145,10 @@ async fn content_length(url: &str) -> anyhow::Result> { Both requests are in flight at once, inside a single Wasm instance, scheduled by Spin. That same instance may also be serving other requests concurrently. And because WASIp3 supports streaming response bodies, a handler can start writing its response before it's finished computing the rest, something we'll use in the next section. +### Heads up on global state + +> Because a single instance can now serve multiple concurrent requests, instance-scoped "global" state is shared across in-flight requests. This is the same rule you already follow in Axum, Express, or Flask, but it's a genuine shift from "one instance per request" if you've been writing Spin components against WASIp2 semantics. Audit any `static`, module-level, or `OnceCell` state and reach for per-request state or explicit synchronization where needed. + ## Async everywhere: Spin's host interfaces are now async WASIp3 unlocks async, but the benefit only lands if the *host APIs* your component calls are also async. A lot of Spin 4.0's work happened here: we've asyncified Spin's host interfaces so I/O-heavy handlers actually get concurrency instead of blocking the instance. @@ -205,8 +209,6 @@ Spin starts streaming the response as soon as `handle` returns, and each row hit The same pattern is available in Python (via `componentize_py_async_support.spawn`) and Go (via plain `go func() { ... }()` goroutines). -> **Heads up on global state.** Because a single instance can now serve multiple concurrent requests, instance-scoped "global" state is shared across in-flight requests. This is the same rule you already follow in Axum, Express, or Flask, but it's a genuine shift from "one instance per request" if you've been writing Spin components against WASIp2 semantics. Audit any `static`, module-level, or `OnceCell` state and reach for per-request state or explicit synchronization where needed. - ## Build profiles Development tools often offer ways to build code in different ways for different purposes: for example, debug builds that favor diagnostic value over speed and size, or profiling builds that inject performance instrumentation. Spin 4.0 introduces named **build profiles**, so you can leverage those options in Wasm applications, for example, to define a debug or profile build of your application. @@ -255,8 +257,6 @@ $ spin up $ spin up --profile debug ``` -The `--profile` flag is recognized by `spin build`, `spin watch`, `spin up`, and `spin registry push`, so profiles carry from local development into your published artifacts. - Learn more in the [Spin docs on build profiles](https://spinframework.dev/v4/build-profiles). ## Fine-grained capability inheritance for dependencies @@ -298,7 +298,7 @@ key_value_stores = ["my-key-value-cache"] Here `acme:s3-client` can make outbound HTTPS calls to the parent's allowed hosts, enough to reach S3. But every other capability is denied. Specifically, the S3 client *cannot* see `my-key-value-cache`. -For the full list of configuration keys you can inherit and how they map to WIT interfaces, see the [Spin docs on component dependencies](TODO). +For the full list of configuration keys you can inherit and how they map to WIT interfaces, see the [Spin docs on component dependencies](https://spinframework.dev/v4/writing-apps#using-component-dependencies). ## Upgrading to Spin 4.0 From 0d0b0d378a76c0972b652989cc6f6d5de7d04956 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Fri, 24 Apr 2026 11:05:19 -0600 Subject: [PATCH 7/8] Fix incorrect links Signed-off-by: Brian Hardock --- content/blog/announcing-spin-4-0.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/blog/announcing-spin-4-0.md b/content/blog/announcing-spin-4-0.md index e67a6a01..1aad9996 100644 --- a/content/blog/announcing-spin-4-0.md +++ b/content/blog/announcing-spin-4-0.md @@ -26,7 +26,7 @@ WASI Preview 3 (WASIp3) is the next major revision of the WebAssembly System Int **Spin 4.0 ships with the March 2026 release candidate of WASIp3, and we are committing to supporting it long-term.** WASIp3 is now the default platform for new applications, and the Spin Rust, Python, and Go SDKs have all been updated to use it. -If you followed along in [Spin 3.5](https://spinframework.dev/v3/blog/announcing-spin-3-5) and [Spin 3.6](https://spinframework.dev/v3/blog/announcing-spin-3-6), you've seen WASIp3 progress from "experimental, opt-in, might break between releases" to something ready for production use. +If you followed along in [Spin 3.5](https://spinframework.dev/blog/announcing-spin-3-5) and [Spin 3.6](https://spinframework.dev/blog/announcing-spin-3-6), you've seen WASIp3 progress from "experimental, opt-in, might break between releases" to something ready for production use. What this means in practice: @@ -257,7 +257,7 @@ $ spin up $ spin up --profile debug ``` -Learn more in the [Spin docs on build profiles](https://spinframework.dev/v4/build-profiles). +Learn more in the [Spin docs on build profiles](https://spinframework.dev/v4/build#building-with-profiles). ## Fine-grained capability inheritance for dependencies From ef70b5f2cb5e6035bb5c4b1350975dc07cc15d7a Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Mon, 15 Jun 2026 12:05:02 -0600 Subject: [PATCH 8/8] Update announcement with a few more features Signed-off-by: Brian Hardock --- content/blog/announcing-spin-4-0.md | 41 ++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/content/blog/announcing-spin-4-0.md b/content/blog/announcing-spin-4-0.md index 1aad9996..9bd38adf 100644 --- a/content/blog/announcing-spin-4-0.md +++ b/content/blog/announcing-spin-4-0.md @@ -1,5 +1,5 @@ title = "Announcing Spin v4.0" -date = "2026-04-23T17:00:00Z" +date = "2026-06-15T17:00:00Z" template = "blog_post" description = "Announcing Spin v4.0: stabilized WASIp3 support, async host APIs across the board, build profiles, and fine-grained capability inheritance for dependencies." tags = [] @@ -18,6 +18,8 @@ There's a lot in this release, so this post is part release-notes and part tutor - [Async everywhere: Spin's host interfaces are now async](#async-everywhere-spins-host-interfaces-are-now-async) - [Build profiles](#build-profiles) - [Fine-grained capability inheritance for dependencies](#fine-grained-capability-inheritance-for-dependencies) +- [Targeting a deployment environment](#targeting-a-deployment-environment) +- [Shell completions](#shell-completions) - [Upgrading to Spin 4.0](#upgrading-to-spin-40) ## WASI Preview 3: stabilized and supported long-term @@ -300,6 +302,43 @@ Here `acme:s3-client` can make outbound HTTPS calls to the parent's allowed host For the full list of configuration keys you can inherit and how they map to WIT interfaces, see the [Spin docs on component dependencies](https://spinframework.dev/v4/writing-apps#using-component-dependencies). +## Targeting a deployment environment + +Some Spin platforms ship custom templates and plugins tailored to that platform, for example, templates that use only the APIs available in the platform, plus the deployment plugin you need to ship to it. Spin 4.0 makes targeting one of these environments a single step with the new `-E` flag: + +```bash +$ spin new -E +``` + +You don't need to pre-install anything: Spin fetches the environment's templates on demand, and installs any plugins the environment requires (typically a deployment plugin) at the same time. Your platform's documentation will tell you the `environment` name to use. + +The same `-E` flag works when managing plugins directly, so you can list or install just the plugins associated with an environment: + +```bash +$ spin plugins list -E +$ spin plugins install -E +``` + +Learn more in the docs on [creating an application for a specific deployment environment](https://spinframework.dev/v4/writing-apps#creating-an-application-for-a-specific-deployment-environment) and [installing plugins for a specific deployment environment](https://spinframework.dev/v4/managing-plugins#installing-plugins-for-a-specific-deployment-environment). + +## Shell completions + +Spin 4.0 can now generate shell command completions for bash and zsh. To enable them, run the following during shell startup (for example, in your `.bashrc`): + +```bash +source <(COMPLETE=bash spin maintenance generate-completions) +``` + +For zsh, change the `COMPLETE` variable to `zsh`. + +Shell completions are a work in progress, and have a few known limitations: + +- You do not get trigger option completions in `spin up` (which, unfortunately, includes a lot of options). +- You do not get `spin up` option completions on `spin build --up` or `spin watch`. +- You do not get completions for plugin commands. + +See the [installation docs](https://spinframework.dev/v4/install#shell-completions) for more detail. + ## Upgrading to Spin 4.0 1. **Install Spin 4.0** from [spinframework.dev/install](https://spinframework.dev/install) or grab a binary from the [release page](https://github.com/spinframework/spin/releases/tag/v4.0.0).