Distributed Wasm Apps with wasmCloud, A Production Tutorial
TL;DR — wasmCloud 1.4 is a distributed actor platform built on Wasm components and NATS. Write a component, declare its capabilities via WIT, link to providers (HTTP, KV, SQL) at deploy time. Same code runs on a laptop or a fleet.
If Spin is Rails for Wasm microservices, wasmCloud is Erlang/OTP. The mental model is actors plus messaging plus pluggable capabilities, where the “actors” are WASI 0.2 components and the messaging is NATS. It’s a real distributed platform with a control plane, not a single-process runtime.
I’ll be honest about who this is for. wasmCloud has more concepts to learn than Spin does, and the payoff shows up at scale: multi-region, edge plus core, polyglot components, or “we don’t want to be tied to Kubernetes.” If you’re building a single HTTP service for a startup, use Spin. If you’re building a platform, keep reading.
This tutorial walks the full loop. Local dev. A real component. A capability provider. A link declaration. Deployment to a multi-host lattice. We’re not building hello-world.
Mental Model
The unit of work is a component. Components do not embed I/O. They declare imports (capabilities they need) via WIT. At runtime, the lattice links the component to providers that implement those capabilities. Providers are conventional Linux processes or containers that bridge Wasm’s capability surface to the outside world.
NATS Lattice (cluster messaging)
+------+ +-------------------------------------+
| wash | CLI | |
+--+---+--------+ |
| | +------------+ +-----------+ |
| | | component | | provider | |
| | | links.wasm|<->| http | |
| | +------------+ +-----------+ |
| | | |
| | v |
| | +-----------+ +-----------+ |
| | | provider | | provider | |
| | | kv-redis | | sql-pg | |
| | +-----------+ +-----------+ |
+--v-----+ +-------------------------------------+
| wadm | declarative state (k8s for wasmCloud)
+--------+
wash is the CLI. wadm is the controller that reconciles desired state against the lattice. NATS is the message bus. Components publish/subscribe via WIT calls that get serialized over NATS to the provider that implements the interface.
The decoupling between “what the component needs” (WIT imports) and “how the lattice provides it” (links to a provider at runtime) is the headline feature. You write the component once; you swap the KV implementation from in-memory to Redis to Postgres without touching the component.
Setup
Step 1, Install Tooling
# wash, the CLI
curl -s https://packagecloud.io/install/repositories/wasmcloud/core/script.deb.sh | sudo bash
sudo apt-get install wash
# verify
wash --version
# wash 0.36.0 (wasmCloud 1.4 compatible)
On macOS, brew install wasmcloud/wasmcloud/wash works.
Step 2, Start a Local Lattice
wash up
# Starting NATS server...
# Starting wasmCloud host...
# Host id: NBXXX...
# Lattice id: default
That’s a single-host lattice on your laptop. Multi-host is the same command on multiple machines, with a shared NATS cluster.
Step 3, Verify
wash get inventory
# Host: NBXXX...
# components: 0
# providers: 0
Build a Component
We’ll build a tiny KV-backed counter service that exposes HTTP and uses a KV capability. The component imports wasi:keyvalue and wasi:http. At deploy time we link them to actual providers.
Step 1, Scaffold
wash new component counter --template-name hello-world-rust
cd counter
wasmcloud.toml is the project manifest. The default targets WASI 0.2.
Step 2, Write the Component
src/lib.rs:
use wasmcloud_component::http;
use wasmcloud_component::wasi::keyvalue::{atomics, store};
struct Counter;
http::export!(Counter);
impl http::Server for Counter {
fn handle(
request: http::IncomingRequest,
) -> http::Result<http::Response<impl http::OutgoingBody>> {
let path = request.uri().path().trim_start_matches('/').to_string();
let key = if path.is_empty() {
"default".to_string()
} else {
path
};
let bucket = store::open("default")
.map_err(|e| anyhow::anyhow!("kv open: {e:?}"))?;
let count = atomics::increment(&bucket, &key, 1)
.map_err(|e| anyhow::anyhow!("kv inc: {e:?}"))?;
Ok(http::Response::builder()
.status(200)
.header("content-type", "text/plain")
.body(format!("{key} -> {count}\n"))?)
}
}
A few things. The component imports wasi:http/server and wasi:keyvalue. It does not name Redis, NATS, Postgres, or any provider. It just says “I want a key-value store.” The lattice will hand it one.
Step 3, Build
wash build
# Compiling counter...
# Wrote ./build/counter_s.wasm
That’s a WASI 0.2 component. You can verify with wasm-tools component wit build/counter_s.wasm.
Link to Providers
Now we declare the wiring. wasmCloud uses a declarative manifest called a wadm application.
wadm.yaml:
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: counter
annotations:
version: v0.1.0
description: "kv-backed counter"
spec:
components:
- name: counter
type: component
properties:
image: file:///absolute/path/to/build/counter_s.wasm
traits:
- type: spreadscaler
properties:
instances: 3
- type: link
properties:
target: kvredis
namespace: wasi
package: keyvalue
interfaces: [atomics, store]
target_config:
- name: redis-url
properties:
url: redis://127.0.0.1:6379
- name: httpserver
type: capability
properties:
image: ghcr.io/wasmcloud/http-server:0.23.0
traits:
- type: link
properties:
target: counter
namespace: wasi
package: http
interfaces: [incoming-handler]
source_config:
- name: http-listen
properties:
address: 0.0.0.0:8080
- name: kvredis
type: capability
properties:
image: ghcr.io/wasmcloud/keyvalue-redis:0.28.0
This is the wadm OAM-style spec. Three pieces. The component (3 instances). The HTTP server provider, linked to the component (incoming requests). The KV Redis provider, linked from the component (outbound KV calls).
Step 4, Deploy
# you need redis running locally for kvredis to talk to:
docker run -d --name redis -p 6379:6379 redis:7
wash app deploy ./wadm.yaml
# Application counter deployed
wash app list
# NAME VERSION STATUS
# counter v0.1.0 Deployed
wadm will scale the component to 3 instances across the lattice, start the providers, and establish the links.
Step 5, Test
curl http://127.0.0.1:8080/hello
# hello -> 1
curl http://127.0.0.1:8080/hello
# hello -> 2
curl http://127.0.0.1:8080/world
# world -> 1
The component never knew anything about Redis or HTTP server framing. The lattice composed it at runtime.
Multi-Host Production Lattice
A single-host lattice is just the dev story. For production, you run NATS as a cluster and start hosts on each machine pointing at it.
# On each host (assuming NATS at nats.cluster:4222)
wash up \
--nats-host nats.cluster \
--nats-port 4222 \
--lattice production \
--label region=us-east-1 \
--label tier=edge
The --label flags let wadm target placement.
traits:
- type: spreadscaler
properties:
instances: 50
spread:
- name: edge-spread
requirements:
region: us-east-1
tier: edge
weight: 100
That places 50 instances of the component on hosts labeled region=us-east-1, tier=edge. The lattice handles failure detection and reschedules if a host drops out.
Operational Surface
wash get inventory --lattice productionto see hosts and what’s running.wash logs <host-id>to stream host logs.wash app listfor declared apps.wash devfor local hot reload during dev.- NATS observability tools (nats-top, surveyor) for lattice-level metrics.
OTel integration is built in. Components and providers emit traces correlated by NATS subject. Point your collector at localhost:4318 for OTLP/HTTP.
Edge to Core Topology
The wasmCloud topology I see most often in production is edge plus core. Edge hosts in many regions run a thin set of components (auth, rate limiting, caching, simple transformations). Core hosts in a few primary regions run the heavier components (business logic, data access). Both populations are part of one lattice with a single shared NATS cluster, typically arranged with NATS leaf nodes at the edge so cross-region message latency is contained.
The wadm manifest uses spread requirements to control which components land at the edge and which at the core. A typical pattern:
- type: spreadscaler
properties:
instances: 200
spread:
- name: edge-tier
requirements:
tier: edge
weight: 100
- type: spreadscaler
properties:
instances: 30
spread:
- name: core-tier
requirements:
tier: core
region: us-east-1
weight: 100
When a request lands at an edge host, the auth component handles it locally. If it needs to call the business-logic component, that call goes over NATS to a core host. NATS handles the routing transparently. The component code doesn’t know whether the target is local or remote.
The downside of this topology is latency on inter-component calls. NATS message round trips are typically sub-millisecond intra-region but can be tens of milliseconds across regions. Design components so that common operations stay within a region; only fall back to cross-region for cold data or coordination operations.
Capability Providers
The provider model is what gives wasmCloud its swap-anything property. Capability providers are external processes (typically containers or static binaries) that implement a WIT interface and bridge it to a real system.
Built-in providers include:
http-server,http-clientkeyvalue-redis,keyvalue-nats,keyvalue-vaultmessaging-nats,messaging-kafkasqldb-postgresblobstore-fs,blobstore-s3
You can also write your own. The provider SDK in Rust gives you wasmcloud-provider-sdk and a project template. A custom provider is the right call when you’re integrating a service the standard set doesn’t cover (your own SaaS, a queue tech, a vector store).
The component remains portable. Same counter component would work over keyvalue-vault if you swapped the link. The component code stays.
Common Pitfalls
Forgetting to start providers before the component. wadm handles ordering, but if you bypass wadm and start things by hand, the component will run and immediately error on KV calls because the link doesn’t resolve. Always deploy via wadm in production.
Mismatched WIT versions between component and provider. If your component is built against wasi:keyvalue@0.2.0-draft and the provider implements 0.2.0-final, the link establishes but calls error with version mismatch. Pin both sides explicitly.
Confusing source links and target links. target is where the call goes. An HTTP provider linked “to” a component means HTTP requests flow into the component. A KV provider linked “from” a component (via target: kvredis on the component’s trait) means the component calls KV. Read the OAM spec carefully.
Running without persistence. wadm state lives in NATS. If your NATS cluster doesn’t have JetStream persistence configured, restarting NATS loses your deployed apps. Configure JetStream with disk storage from day one.
Troubleshooting
Component instantiation failed: missing import. The component imports a WIT interface that no provider implements in the lattice. Run wash get inventory and confirm the relevant provider is healthy. Then check the link manifest matches namespace, package, and interface exactly.
Provider crashes immediately on start. Check provider logs with wash logs <host> filtered by provider ID. Most common cause is config (Redis URL wrong, Postgres credentials wrong). Provider config comes from target_config / source_config in the wadm manifest.
Slow RPC between component and provider. wasmCloud RPC goes over NATS. If NATS is on a different host with high latency, every WIT call pays that cost. Co-locate NATS with hosts when possible, or use NATS leaf nodes for edge topologies. The wasmCloud docs cover topology in detail.
Security Model
wasmCloud’s security story has two layers. At the runtime layer, each component is a wasmtime sandbox: no host syscall access, capabilities granted by the host. At the lattice layer, NATS authentication and authorization control which hosts can join the lattice, which components can talk to which providers, and which actors can publish on which subjects.
The lattice security is what’s distinctive. NATS supports JWT-based authentication with fine-grained subject permissions. wasmCloud uses this to enforce that a component can only call providers it’s been linked to. Even if a malicious component tries to publish on the wrong NATS subject directly, the lattice rejects it. This is much stronger than a typical microservice mesh where authorization is per-service, not per-call.
For production, run NATS with mutual TLS, JWTs signed by a private CA, and explicit subject permissions per component class. The wasmCloud docs cover the policy configuration. Don’t skip this for “we’ll add it later.” The lattice security model is a feature you bake in at the foundation, not a layer you bolt on.
For secret management, providers can be configured to fetch secrets from Vault, AWS Secrets Manager, or any external source. The component never sees the raw secret; it asks the provider for “the database connection” and the provider injects the credential. Same swap-anything property as everything else.
Wrapping Up
wasmCloud 1.4 is the production answer when you want a distributed Wasm platform without bolting onto Kubernetes. The actor + capability model is genuinely different from a typical microservice framework, and once you internalize it, the swap-anything property is liberating. The trade is a steeper learning curve than Spin. If your shop has more than one team and you’re heading toward multi-region or edge, the investment pays off. The next article steps back to the component model itself and how to use it in production whether or not you’re on wasmCloud.