background-shape
Distributed Wasm Apps with wasmCloud, A Production Tutorial
February 19, 2025 · 10 min read · by Muhammad Amal programming

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.

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 production to see hosts and what’s running.
  • wash logs <host-id> to stream host logs.
  • wash app list for declared apps.
  • wash dev for 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-client
  • keyvalue-redis, keyvalue-nats, keyvalue-vault
  • messaging-nats, messaging-kafka
  • sqldb-postgres
  • blobstore-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.