# wasmCloud > Build applications in any language. Deploy them anywhere. This file contains all documentation content in a single document following the llmstxt.org standard. ## Contributing Guide wasmCloud projects accept contributions via GitHub pull requests. This document outlines the process to help get your contribution accepted. ## Table of Contents - [Table of Contents](#table-of-contents) - [How to Contribute Code](#how-to-contribute-code) - [Pull Requests](#pull-requests) - [PR Lifecycle](#pr-lifecycle) - [Documentation PRs](#documentation-prs) - [Conventional Commits](#conventional-commits) - [Reporting a Security Issue](#reporting-a-security-issue) - [Developer Certificate of Origin](#developer-certificate-of-origin) - [Support Channels](#support-channels) - [Semantic Versioning](#semantic-versioning) - [Issues](#issues) - [Issue Types](#issue-types) - [Issue Lifecycle](#issue-lifecycle) - [Proposing an Idea](#proposing-an-idea) - [Labels](#labels) - [Common](#common) - [Issue Specific](#issue-specific) - [PR Specific](#pr-specific) ## How to Contribute Code 1. Identify or create the related issue. If you're proposing a larger change to wasmCloud, see [Proposing an Idea](#proposing-an-idea). 2. Fork the desired repo; develop and test your code changes. 3. Submit a pull request, making sure to sign your work and link the related issue. In general, most repos in the wasmCloud project have linters and other coding standards to follow. Those standards should be followed when you contribute your code. ## Pull Requests Like any good open source project, we use Pull Requests (PRs) to track code changes. ### PR Lifecycle 1. PR creation - We more than welcome PRs that are currently in progress. They are a great way to keep track of important work that is in-flight, but useful for others to see. If a PR is a work in progress, it **must** be prefaced with "WIP: [title]". Once the PR is ready for review, remove "WIP" from the title. - It is preferred, but not required, to have a PR tied to a specific issue. There can be circumstances where if it is a quick fix then an issue might be overkill. The details provided in the PR description would suffice in this case. - It is preferred, but not required, to use [Conventional Commits][conventional-commits]. In the case that your commits do not match the conventional commit standards, maintainers will help where possible. 2. Triage - The maintainer in charge of triaging will apply the proper labels for the issue. This should include at least a `bug` or `feature` label once all labels are applied. See the [Labels section](#labels) for full details on the definitions of labels. 3. Assigning reviews - Reviewers will either be autoassigned using a CODEOWNERS file or by maintainers of the repo when they triage PRs, maintainers will review them as schedule permits. The maintainer who takes the issue should self-request a review. - PRs from a community member with that are any larger than 10-ish lines requires 2 review approvals from maintainers before it can be merged. For contributions from contributors and maintainers, 2 reviews are only required if the PR is large, or if the first maintainer requests a second review. These size and review requirements are implemented per the judgement of the maintainers. In the future, we may adopt a more standardized approach 4. Reviewing/Discussion - All reviews will be completed using GitHub review tool. - A "Comment" review should be used when there are questions about the code that should be answered, but that don't involve code changes. This type of review does not count as approval. - A "Changes Requested" review indicates that changes to the code need to be made before they will be merged. - Reviewers should update labels as needed (such as `breaking`, if the PR contains a breaking change) - If a comment is a nit, it should be prefaced with the text `Nit:` to indicate to the submitter that addressing this comment is optional 5. Address comments by answering questions or changing code 6. LGTM (Looks good to me) - Once a Reviewer has completed a review and the code looks ready to merge, an "Approve" review is used to signal to the contributor and to other maintainers that you have reviewed the code and feel that it is ready to be merged. 7. Merge or close - PRs should stay open until merged or if they have not been active for more than 30 days. This will help keep the PR queue to a manageable size and reduce noise. Should the PR need to stay open (like in the case of a WIP), the `keep open` label can be added. - If the owner of the PR is a maintainer, that user **must** merge their own PRs or explicitly request another maintainer do that for them. - If the owner of a PR is _not_ a maintainer, any maintainer may merge the PR. As a rule of thumb, we usually recommend one of the reviewers be the one to merge the PR, but this is not required #### Documentation PRs Documentation PRs will follow the same lifecycle as other PRs. They will also be labeled with the `documentation` label. For documentation, special attention will be paid to spelling, grammar, and clarity (whereas those things don't matter _as_ much for comments in code). #### Conventional Commits [Conventional Commits][conventional-commits] is a standard for creating consistently human and machine readable commit messages (in particular commit titles). To enable better automation and more consistency across codebases, the wasmCloud project encourages the use of Conventional Commits, also introducing checks (ex. via CI) in some projects to enable automated checking of the standard. We allow multiple conventional commits in a single PR. Maintainers may choose to squash merge PRs to combine them into a single conventional commit. [conventional-commits]: https://www.conventionalcommits.org/en/v1.0.0 ## Reporting a Security Issue Most of the time, when you find a bug in wasmCloud, it should be reported using GitHub issues. However, if you are reporting a _security vulnerability_, please follow the guidelines outlined in our [security process](https://github.com/wasmCloud/wasmCloud/blob/main/SECURITY.md) ## Developer Certificate of Origin As with other CNCF projects, wasmCloud has adopted a [Developers Certificate of Origin (DCO)](https://developercertificate.org/). A DCO is a lightweight way for a developer to certify that they wrote or otherwise have the right to submit code or documentation to a project. The sign-off is a simple line at the end of the explanation for a commit. All commits need to be signed. Your signature certifies that you wrote the patch or otherwise have the right to contribute the material. The rules are pretty simple, if you can certify the below (from [developercertificate.org](https://developercertificate.org/)): ``` Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 1 Letterman Drive Suite D4700 San Francisco, CA, 94129 Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` Then you just add a line to every git commit message: ```text Signed-off-by: Joe Smith ``` Use your real name (sorry, no pseudonyms or anonymous contributions.) If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`. Note: If your git config information is set properly then viewing the `git log` information for your commit will look something like this: ``` Author: Joe Smith Date: Thu Feb 2 11:41:15 2018 -0800 Update README Signed-off-by: Joe Smith ``` Notice the `Author` and `Signed-off-by` lines match. If they don't your PR will be rejected by the automated DCO check. - In case you forgot to add it to the most recent commit, use `git commit --amend --signoff` - In case you forgot to add it to the last N commits in your branch, use `git rebase --signoff HEAD~N` and replace N with the number of new commits you created in your branch. - If you have already pushed your branch to a remote, will need to push your changes to overwrite the branch: `git push --force-with-lease origin my-branch` ## Support Channels Whether you are a user or contributor, official support channels include: - The GitHub Issues in each subproject repository - [Slack](https://slack.wasmcloud.com/) - Please note that Slack is meant for help in discussing specific problems or asynchronous debugging/help. If you are reporting a specific bug, please do so it in GitHub Issues Before opening a new issue or submitting a new pull request, it's helpful to search the project - it's likely that another user has already reported the issue you're facing, or it's a known issue that we're already aware of. It is also worth asking on the Slack channels. ## Semantic Versioning If you are not familiar with SemVer (the standard), [give it a quick read](https://semver.org/). We follow SemVer with a high degree of rigor. As a post-1.0 project, wasmCloud maintains a strong commitment to backward compatibility. All of our changes to protocols and formats will be backward compatible from one minor release to the next. No features, flags, commands, or APIs will be removed or substantially modified without a major version release(unless absolutely needed to fix a security issue). This often means that we have to tell people "no" or "wait" in order to preserve backward compatibility. ## Issues Issues are used as the primary method for tracking anything to do with a wasmCloud project. ### Issue Types There are 5 types of issues (each with their own corresponding [label](#labels)) in any wasmCloud project: - `question`: These are support or functionality inquiries that we want to have a record of for future reference. Generally these are questions that are too complex or large to store in the Slack channel or have particular interest to the community as a whole. Depending on the discussion, these can turn into `feature` or `bug` issues. - `enhancement`: These track specific feature requests and ideas until they are complete. They can evolve from an [ADR](#proposing-an-idea) or can be submitted individually depending on the size. - `bug`: These track bugs with the code - `documentation`: These track problems with the documentation (i.e. missing or incomplete) ### Issue Lifecycle The issue lifecycle is mainly driven by the project and org maintainers, but is good information for those contributing to wasmCloud projects. All issue types follow the same general lifecycle. Differences are noted below. 1. Issue creation 2. Triage - A maintainer or contributor will apply the proper labels for the issue. This includes labels for priority, type, and metadata (such as `good first issue`). The only issue priority we will be tracking is whether or not the issue is "critical." If additional levels are needed in the future, we will add them. - (If needed) Clean up the title to succinctly and clearly state the issue. 3. Discussion - Issues that are labeled `enhancement` must write an Architectural Decision Record (ADR) (see [Proposing an Idea](#proposing-an-idea)). Smaller quality-of-life enhancements are exempt. The decision about which enhancements are exempt are left to the decision of project maintainers - Issues that are labeled as `enhancement` or `bug` should be connected to the PR that resolves it once it is opened (see [How to Contribute Code](#how-to-contribute-code)). - Whoever is working on an `enhancement` or `bug` issue (whether a maintainer or someone from the community), should either assign the issue to themself or make a comment in the issue saying that they are taking it. - `question` issues should stay open until resolved or if they have not been active for more than 30 days. This will help keep the issue queue to a manageable size and reduce noise. Should the issue need to stay open, the `keep open` label can be added. 4. Issue closure/resolution ## Proposing an Idea Before proposing a new idea to a wasmCloud project, please make sure to write up an [Architectural Decision Record](https://wasmcloud.github.io/adr/). An Architectural Decision Record is a design document that describes a new feature for a wasmCloud project. The proposal should provide a concise technical specification and rationale for the feature. It is also worth considering vetting your idea with the community via Slack. Vetting an idea publicly before going as far as writing a proposal is meant to save the potential author time. ADRs are submitted to the [wasmcloud/adr repository](https://github.com/wasmCloud/adr/tree/gh-pages) (submitted against the `gh-pages` branch). See [ADR0000](https://wasmcloud.github.io/adr/0000-use-markdown-architectural-decision-records.html) for a the specific structure chosen and the [provided template](https://wasmcloud.github.io/adr/template.html) to write your own After your proposal has been approved, you can go ahead and get started implementing it! ## Labels The following tables define all label types used for wasmCloud projects. This does not preclude individual projects from having additional labels, but all wasmCloud projects will have the same base labels. The labels below are split up by category ### Common | Label | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `bug` | Marks an issue as a bug or a PR as a bugfix | | `critical` | Marks an issue or PR as critical. This means that addressing the PR or issue is top priority and must be addressed as soon as possible | | `documentation` | Indicates the issue or PR is a documentation change | | `enhancement` | Marks the issue as a feature request or a PR as a feature implementation | | `keep open` | Denotes that the issue or PR should be kept open past 30 days of inactivity | | `refactor` | Indicates that the issue is a code refactor and is not fixing a bug or adding additional functionality | ### Issue Specific | Label | Description | | ------------------ | ----------------------------------------------------------------------------------------------- | | `help wanted` | Marks an issue needs help from the community to solve | | `proposal` | Marks an issue as a proposal | | `question` | Marks an issue as a support request or question | | `good first issue` | Marks an issue as a good starter issue for someone new to the project | | `wontfix` | Marks an issue as discussed and will not be implemented (or accepted in the case of a proposal) | ### PR Specific | Label | Description | | ---------- | --------------------------------------------------------- | | `breaking` | Indicates a PR has breaking changes (such as API changes) | --- ## Documentation The wasmCloud documentation site is [hosted on GitHub](https://github.com/wasmCloud/wasmcloud.com) and built with [Docusarus](https://docusaurus.io/). Choosing a [`good first issue`](https://github.com/wasmCloud/wasmcloud.com/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) is a great way to get started. ## Running the site locally ```bash npm run start ``` This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. ## Build ```bash npm run build ``` This command generates static content into the `build` directory and can be served using any static contents hosting service. It's a good idea build in order to check for errors like broken links that may prevent a successful deployment. ## Serving static content To serve the generated static content: ```bash npm run serve ``` ## Diagram assets The official wasmCloud & Wasm Excalidraw library contains reusable components including wasmCloud cluster diagrams and architecture diagram elements. **Library URL:** `https://raw.githubusercontent.com/excalidraw/excalidraw-libraries/ricochet-wasmcloud-and-wasm-1770917744834/libraries/ricochet/wasmcloud-and-wasm.excalidrawlib?raw=true` **Add to Excalidraw:** [Install Library](https://excalidraw.com/?addLibrary=https%3A%2F%2Fraw.githubusercontent.com%2Fexcalidraw%2Fexcalidraw-libraries%2Fricochet-wasmcloud-and-wasm-1770917744834%2Flibraries%2Fricochet%2Fwasmcloud-and-wasm.excalidrawlib%3Fraw%3Dtrue) Additional image assets for diagrams are available in the [community wasmCloud room on Excalidraw](https://excalidraw.com/#json=iHIzqpeDMnzn3Wq78Qwv8,8cEWBZDy2DIlUGGzf2rWHg). Copy assets to your own Excalidraw session and export as PNG with a transparent background for compatibility with the site's dark and light modes. We recommend copying assets that you would like to use to your own Excalidraw session and exporting your image as a PNG file with a transparent background (and careful use of black and white) for maximum compatibility with the documentation site's dark and light modes. wasmCloud's brand colors are: - Green Aqua - #00C389 - Light Gray - #768692 - Yellow - #FFB600 - Space Blue - #002E5D - Gunmetal - #253746 - Gainsboro - #D9E1E2 ## Making a pull request See the [Pull Request](/docs/contributing/contributing-guide#pull-requests) section of the Contributing Guide. Note that a `--signoff` argument is required with your commits. --- ## Contributing ### wasmCloud is a community-driven project and welcomes contributions of all kinds. This page explains how to get started as a contributor. For information on project roles, community meetings, and more, see [Governance](../governance.mdx). There are several ways to get involved with the project: ## Development Contribute to the wasmCloud codebase, which can be found in the [`wasmCloud/wasmCloud`](https://github.com/wasmCloud/wasmcloud) [monorepo](https://en.wikipedia.org/wiki/Monorepo). Choosing a [`good first issue`](https://github.com/wasmCloud/wasmCloud/labels/good%20first%20issue) is a great way to get started. Read the Contributing Guide ## Documentation Contribute to this documentation site. The site is built with [Docusaurus](https://docusaurus.io/) and you can find the source code in the [`wasmCloud/wasmcloud.com`](https://github.com/wasmCloud/wasmcloud.com) repository. Contribute Documentation --- ## Templates & Examples Get started building components for wasmCloud. * **Templates** are scaffolds for new applications. Use [`wash new`](./wash/commands.mdx#wash-new) to create a project from a template, then customize it for your use case. * **Examples** are complete applications demonstrating specific wasmCloud capabilities. :::info[Compatibility] All templates and examples on this page target wasmCloud v2 and require `wash` 2.0.0+; a small number of entries require a newer `wash` version and call this out inline (for example, [WebGPU TensorFlow](#webgpu-tensorflow) requires 2.1.0+ because WebGPU in services landed in that release). Components target the [WASI 0.2 component model](./overview/interfaces.mdx). Interface versions shown reflect what is pinned in each template or example at time of writing; `wash new` always pulls current source from the repository. ::: ## Templates | Template | Language | Description | |---|---|---| | [HTTP Hello World](#http-hello-world) | Rust | HTTP server using `wstd` | | [TCP Service](#tcp-service) | Rust | HTTP API + TCP service workspace | | [HTTP Client (Rust)](#http-client-rust) | Rust | Outgoing HTTP proxy with `wstd::http::Client` | | [HTTP Handler (axum)](#http-handler-axum) | Rust | Full REST API handler using `axum` via `wstd-axum` | | [HTTP Handler with Key-Value Storage (Rust)](#http-handler-with-key-value-storage-rust) | Rust | REST API + `wasi:keyvalue` | | [HTTP API with Distributed Workloads (Rust)](#http-api-with-distributed-workloads-rust) | Rust | Two components coordinating via `wasmcloud:messaging` | | [HTTP Hello World (Hono)](#http-hello-world-hono) | TypeScript | HTTP server using Hono | | [HTTP Hello World (Fetch API)](#http-hello-world-fetch-api) | TypeScript | HTTP server using the Fetch API | | [HTTP Client](#http-client) | TypeScript | Outgoing HTTP proxy | | [HTTP Handler (Hono)](#http-handler-hono) | TypeScript | Full REST API handler | | [HTTP Handler with Blob Storage (Hono)](#http-handler-with-blob-storage-hono) | TypeScript | REST API + `wasi:blobstore` | | [HTTP Handler with Filesystem (Hono)](#http-handler-with-filesystem-hono) | TypeScript | REST API + `wasi:filesystem` | | [HTTP Handler with Key-Value Storage (Hono)](#http-handler-with-key-value-storage-hono) | TypeScript | REST API + `wasi:keyvalue` | | [HTTP Handler with Logging (Hono)](#http-handler-with-logging-hono) | TypeScript | REST API + structured logging via `wasi:logging` | | [HTTP API with Distributed Workloads](#http-api-with-distributed-workloads) | TypeScript | Two components coordinating via `wasmcloud:messaging` | | [TCP Echo Service](#tcp-echo-service) | TypeScript | HTTP component + TCP service workload | ### Rust Requires the [Rust toolchain](https://www.rust-lang.org/tools/install) with the `wasm32-wasip2` target. After scaffolding with `wash new`, run `wash dev` from the project directory to start the development loop. See [Quickstart prerequisites](./quickstart/index.mdx#install-wash) for setup instructions. #### HTTP Hello World A minimal HTTP server component built with the [`wstd`](https://docs.rs/wstd) async runtime for Wasm components. - **Source**: [`wasmCloud/wasmCloud — templates/http-hello-world`](https://github.com/wasmCloud/wasmCloud/tree/main/templates/http-hello-world) - **Interfaces**: Exports `wasi:http/incoming-handler` Create from template: ```shell wash new https://github.com/wasmCloud/wasmCloud.git --name my-project --subfolder templates/http-hello-world ``` #### TCP Service A two-component Rust workspace. `http-api` is an HTTP server that accepts text input via a web UI and forwards it to `service-leet`, a TCP server that transforms text to "leet speak" (e.g., `hello world` → `h3110 w0r1d`). Together they demonstrate a multi-component service pattern with TCP transport. - **Source**: [`wasmCloud/wasmCloud — templates/service-tcp`](https://github.com/wasmCloud/wasmCloud/tree/main/templates/service-tcp) - **`http-api` interfaces**: Exports `wasi:http/incoming-handler`; makes outgoing TCP connections - **`service-leet` interfaces**: TCP server (listens on port 7777) Create from template: ```shell wash new https://github.com/wasmCloud/wasmCloud.git --name my-service --subfolder templates/service-tcp ``` #### HTTP Client (Rust) An HTTP proxy component that makes outgoing HTTP requests using [`wstd::http::Client`](https://docs.rs/wstd/latest/wstd/http/struct.Client.html). Uses the `wstd` `#[http_server]` proc macro to handle the incoming request and proxies the call upstream. - **Source**: [`wasmCloud/wasmCloud — templates/http-client`](https://github.com/wasmCloud/wasmCloud/tree/main/templates/http-client) - **Interfaces**: Imports `wasi:http/outgoing-handler@0.2.2`; exports `wasi:http/incoming-handler@0.2.2` Create from template: ```shell wash new https://github.com/wasmCloud/wasmCloud.git --name my-project --subfolder templates/http-client ``` #### HTTP Handler (axum) A full-featured HTTP handler component using [`axum`](https://docs.rs/axum) for routing, wired to `wasi:http/incoming-handler` via [`wstd-axum`](https://docs.rs/wstd-axum). The template demonstrates routing, query parameters, and JSON request and response bodies, with `axum`'s default features (which depend on `tokio` and `hyper`) disabled in favor of the Wasm-compatible feature set. - **Source**: [`wasmCloud/wasmCloud — templates/http-handler`](https://github.com/wasmCloud/wasmCloud/tree/main/templates/http-handler) - **Interfaces**: Exports `wasi:http/incoming-handler@0.2.2` Create from template: ```shell wash new https://github.com/wasmCloud/wasmCloud.git --name my-project --subfolder templates/http-handler ``` #### HTTP Handler with Key-Value Storage (Rust) An HTTP handler component that stores and retrieves key-value pairs over HTTP, backed by `wasi:keyvalue/store`. The component speaks only the WASI interface; the host runtime selects the underlying storage backend (in-memory, filesystem, NATS, Redis, and others) based on `.wash/config.yaml`, so the same component code runs against any supported backend without modification. - **Source**: [`wasmCloud/wasmCloud — templates/http-kv-handler`](https://github.com/wasmCloud/wasmCloud/tree/main/templates/http-kv-handler) - **Interfaces**: Imports `wasi:keyvalue/store@0.2.0-draft`; exports `wasi:http/incoming-handler@0.2.2` Create from template: ```shell wash new https://github.com/wasmCloud/wasmCloud.git --name my-project --subfolder templates/http-kv-handler ``` #### HTTP API with Distributed Workloads (Rust) A two-component Rust workspace demonstrating distributed workloads over messaging. An `http-api` component receives `POST /task` requests and dispatches work via `wasmcloud:messaging/consumer`; a `task-leet` component exports `wasmcloud:messaging/handler` and transforms the payload (e.g., text → "leet speak"). During `wash dev`, the runtime routes calls between components in-process — no NATS server required. - **Source**: [`wasmCloud/wasmCloud — templates/http-api-with-distributed-workloads`](https://github.com/wasmCloud/wasmCloud/tree/main/templates/http-api-with-distributed-workloads) - **`http-api` interfaces**: Imports `wasmcloud:messaging/consumer@0.2.0`; exports `wasi:http/incoming-handler` (via the `wstd` `#[http_server]` proc macro) - **`task-leet` interfaces**: Imports `wasmcloud:messaging/consumer@0.2.0`; exports `wasmcloud:messaging/handler@0.2.0` Create from template: ```shell wash new https://github.com/wasmCloud/wasmCloud.git --name my-project --subfolder templates/http-api-with-distributed-workloads ``` ### TypeScript Requires [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) v14.17+ and [TypeScript](https://www.typescriptlang.org/) v5.6+. After scaffolding with `wash new`, run `npm run dev` from the project directory to start the development loop. See [Quickstart prerequisites](./quickstart/index.mdx#install-wash) for setup instructions. TypeScript templates use the [`@bytecodealliance/jco-std`](https://github.com/bytecodealliance/jco) adapter for WASI compatibility and are available in the [`templates/` directory of `wasmCloud/typescript`](https://github.com/wasmCloud/typescript/tree/main/templates). #### HTTP Hello World (Hono) A minimal HTTP server component using the [Hono](https://hono.dev/) web framework. - **Source**: [`wasmCloud/typescript — templates/http-hello-world-hono`](https://github.com/wasmCloud/typescript/tree/main/templates/http-hello-world-hono) - **Interfaces**: Exports `wasi:http/incoming-handler@0.2.6` Create from template: ```shell wash new https://github.com/wasmCloud/typescript.git --name my-project --subfolder templates/http-hello-world-hono ``` #### HTTP Hello World (Fetch API) A minimal HTTP server component using the Service Worker [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) pattern. - **Source**: [`wasmCloud/typescript — templates/http-hello-world-fetch`](https://github.com/wasmCloud/typescript/tree/main/templates/http-hello-world-fetch) - **Interfaces**: Exports `wasi:http/incoming-handler@0.2.3` Create from template: ```shell wash new https://github.com/wasmCloud/typescript.git --name my-project --subfolder templates/http-hello-world-fetch ``` #### HTTP Client An HTTP proxy component that makes outgoing HTTP requests. Supports GET, POST, PUT, DELETE, and PATCH with custom header forwarding, request body handling, JSON parsing, and response header extraction. - **Source**: [`wasmCloud/typescript — templates/http-client`](https://github.com/wasmCloud/typescript/tree/main/templates/http-client) - **Interfaces**: Imports `wasi:http/outgoing-handler@0.2.3`; exports `wasi:http/incoming-handler@0.2.3` Create from template: ```shell wash new https://github.com/wasmCloud/typescript.git --name my-project --subfolder templates/http-client ``` #### HTTP Handler (Hono) A full-featured HTTP handler component using Hono, demonstrating middleware patterns including CORS, request logging, response timing, and request ID tracking, with RESTful CRUD endpoints and error handling. - **Source**: [`wasmCloud/typescript — templates/http-handler-hono`](https://github.com/wasmCloud/typescript/tree/main/templates/http-handler-hono) - **Interfaces**: Exports `wasi:http/incoming-handler@0.2.6` Create from template: ```shell wash new https://github.com/wasmCloud/typescript.git --name my-project --subfolder templates/http-handler-hono ``` #### HTTP Handler with Blob Storage (Hono) An HTTP handler component using Hono, backed by `wasi:blobstore` for persistent file storage. Provides a complete REST API for blob CRUD operations. - **Source**: [`wasmCloud/typescript — templates/http-blobstore-handler-hono`](https://github.com/wasmCloud/typescript/tree/main/templates/http-blobstore-handler-hono) - **Interfaces**: Imports `wasi:blobstore/blobstore@0.2.0-draft`; exports `wasi:http/incoming-handler@0.2.6` Create from template: ```shell wash new https://github.com/wasmCloud/typescript.git --name my-project --subfolder templates/http-blobstore-handler-hono ``` #### HTTP Handler with Filesystem (Hono) An HTTP handler component using Hono that serves files from a mounted directory via `wasi:filesystem`. Demonstrates reading from a preopened host directory inside a Wasm component. - **Source**: [`wasmCloud/typescript — templates/http-filesystem-handler-hono`](https://github.com/wasmCloud/typescript/tree/main/templates/http-filesystem-handler-hono) - **Interfaces**: Imports `wasi:filesystem/types@0.2.2`, `wasi:filesystem/preopens@0.2.2`; exports `wasi:http/incoming-handler@0.2.6` Create from template: ```shell wash new https://github.com/wasmCloud/typescript.git --name my-project --subfolder templates/http-filesystem-handler-hono ``` #### HTTP Handler with Key-Value Storage (Hono) An HTTP handler component using Hono, backed by `wasi:keyvalue` for persistent key-value storage. Demonstrates CRUD operations, atomic incrementation, and Hono middleware patterns. When run with `npm run dev`, a NATS-KV key-value provider is automatically configured. - **Source**: [`wasmCloud/typescript — templates/http-kv-handler-hono`](https://github.com/wasmCloud/typescript/tree/main/templates/http-kv-handler-hono) - **Interfaces**: Imports `wasi:keyvalue/store@0.2.0-draft`, `wasi:keyvalue/atomics@0.2.0-draft`; exports `wasi:http/incoming-handler@0.2.6` Create from template: ```shell wash new https://github.com/wasmCloud/typescript.git --name my-project --subfolder templates/http-kv-handler-hono ``` #### HTTP Handler with Logging (Hono) An HTTP handler component using Hono that emits structured logs via `wasi:logging`. Every request and response is logged at the appropriate level (`trace`, `debug`, `info`, `warn`, `error`, `critical`) so you can see how WASI logging integrates with the wasmCloud host's structured log output. - **Source**: [`wasmCloud/typescript — templates/http-logging-handler-hono`](https://github.com/wasmCloud/typescript/tree/main/templates/http-logging-handler-hono) - **Interfaces**: Imports `wasi:logging/logging@0.1.0-draft`; exports `wasi:http/incoming-handler@0.2.6` Create from template: ```shell wash new https://github.com/wasmCloud/typescript.git --name my-project --subfolder templates/http-logging-handler-hono ``` #### HTTP API with Distributed Workloads A two-component TypeScript project demonstrating component-to-component coordination over `wasmcloud:messaging`. An `http-api` component accepts `POST /task` requests and dispatches work via `wasmcloud:messaging/consumer`; a `task-worker` component exports `wasmcloud:messaging/handler`, processes the payload, and returns a reply. During `wash dev`, the runtime routes calls between components in-process — no NATS server required. - **Source**: [`wasmCloud/typescript — templates/http-api-with-distributed-workloads`](https://github.com/wasmCloud/typescript/tree/main/templates/http-api-with-distributed-workloads) - **`http-api` interfaces**: Imports `wasmcloud:messaging/consumer@0.2.0`; exports `wasi:http/incoming-handler@0.2.6` - **`task-worker` interfaces**: Imports `wasmcloud:messaging/consumer@0.2.0`; exports `wasmcloud:messaging/handler@0.2.0` Create from template: ```shell wash new https://github.com/wasmCloud/typescript.git --name my-project --subfolder templates/http-api-with-distributed-workloads ``` #### TCP Echo Service A two-workload TypeScript project demonstrating the wasmCloud service model. A TCP `service` workload accepts connections on `127.0.0.1:7878`, reads lines, and replies with the word count. A `component` workload exposes `POST /count`, forwards text to the TCP service over the in-process loopback network, and returns a JSON response. - **Source**: [`wasmCloud/typescript — templates/service-tcp-echo`](https://github.com/wasmCloud/typescript/tree/main/templates/service-tcp-echo) - **`service` interfaces**: Imports `wasi:sockets/tcp@0.2.3` (and related socket interfaces); exports `wasi:cli/run@0.2.0` - **`component` interfaces**: Imports `wasi:sockets/tcp@0.2.3` (and related socket interfaces); exports `wasi:http/incoming-handler@0.2.6` Create from template: ```shell wash new https://github.com/wasmCloud/typescript.git --name my-project --subfolder templates/service-tcp-echo ``` ## Examples Complete example applications are available in the [`examples/` directory of `wasmCloud/wasmCloud`](https://github.com/wasmCloud/wasmCloud/tree/main/examples) (Rust) and the [`examples/components/` directory of `wasmCloud/typescript`](https://github.com/wasmCloud/typescript/tree/main/examples/components) (TypeScript). Run `wash dev` from any example directory to build and start a local development environment. | Example | Language | OCI Artifact | |---|---|---| | [Blobby](#blobby) | Rust | [`ghcr.io/wasmcloud/components/blobby`](https://github.com/orgs/wasmCloud/packages/container/package/components%2Fblobby) | | [gRPC Hello World](#grpc-hello-world) | Rust | [`components/grpc-hello-world-client`](https://github.com/orgs/wasmCloud/packages/container/package/components%2Fgrpc-hello-world-client), [`components/grpc-hello-world-server`](https://github.com/orgs/wasmCloud/packages/container/package/components%2Fgrpc-hello-world-server) | | [OpenTelemetry HTTP Counter](#opentelemetry-http-counter) | Rust | [`ghcr.io/wasmcloud/components/otel-http`](https://github.com/orgs/wasmCloud/packages/container/package/components%2Fotel-http) | | [QR Code Generator](#qr-code-generator) | Rust | [`ghcr.io/wasmcloud/components/qrcode`](https://github.com/orgs/wasmCloud/packages/container/package/components%2Fqrcode) | | [HTTP Axios](#http-axios) | TypeScript | — | | [HTTP Password Checker](#http-password-checker) | TypeScript | — | | [HTTP Server with Hono](#http-server-with-hono) | TypeScript | — | | [HTTP stdio](#http-stdio) | TypeScript | — | | [HTTP Streaming](#http-streaming) | TypeScript | — | | [WebGPU TensorFlow](#webgpu-tensorflow) | TypeScript | — | | [WASI Config from Kubernetes Env](#wasi-config-from-kubernetes-env) | TypeScript | — | ### Rust #### Blobby A file server component ("Little Blobby Tables") demonstrating CRUD operations on blob storage, served through a web UI. Built with the [`wstd`](https://docs.rs/wstd) async runtime. - **Source**: [`wasmCloud/wasmCloud — examples/blobby`](https://github.com/wasmCloud/wasmCloud/tree/main/examples/blobby) - **OCI artifact**: `ghcr.io/wasmcloud/components/blobby` - **Interfaces**: Imports `wasi:blobstore/blobstore@0.2.0-draft`; exports `wasi:http/incoming-handler` (via the `wstd` `#[http_server]` proc macro) #### gRPC Hello World Two components demonstrating how to make and serve gRPC calls from a wasmCloud component. `component-client` calls an external gRPC server; `component-server` serves gRPC traffic to an external client. Built with [`tonic`](https://github.com/hyperium/tonic) and [`prost`](https://github.com/tokio-rs/prost) over HTTP/2. - **Source**: [`wasmCloud/wasmCloud — examples/grpc-hello-world`](https://github.com/wasmCloud/wasmCloud/tree/main/examples/grpc-hello-world) - **OCI artifacts**: `ghcr.io/wasmcloud/components/grpc-hello-world-client`, `ghcr.io/wasmcloud/components/grpc-hello-world-server` - **`component-client` interfaces**: Imports `wasi:http/outgoing-handler`; exports `wasi:http/incoming-handler` (via `wstd`) - **`component-server` interfaces**: Exports `wasi:http/incoming-handler` (via `wstd`) #### OpenTelemetry HTTP Counter An HTTP counter service instrumented with OpenTelemetry through the `wasi:otel` interfaces. Each request creates a parent server span, emits child spans for downstream HTTP / blobstore / keyvalue operations, records structured log entries correlated with the active trace, and exports request-count and response-size metrics. - **Source**: [`wasmCloud/wasmCloud — examples/otel-http`](https://github.com/wasmCloud/wasmCloud/tree/main/examples/otel-http) - **OCI artifact**: `ghcr.io/wasmcloud/components/otel-http` - **Interfaces**: Imports `wasi:otel/{tracing,logs,metrics,types}@0.2.0-rc.1`, `wasi:http/outgoing-handler@0.2.2`, `wasi:blobstore/{blobstore,container}@0.2.0-draft`, `wasi:keyvalue/{store,atomics}@0.2.0-draft`, `wasi:config/store@0.2.0-rc.1`, `wasi:clocks/wall-clock@0.2.0`, `wasi:logging/logging@0.1.0-draft`; exports `wasi:http/incoming-handler@0.2.2` #### QR Code Generator A QR code generator that accepts text input via HTTP and returns a PNG-encoded QR code, with a web UI. - **Source**: [`wasmCloud/wasmCloud — examples/qrcode`](https://github.com/wasmCloud/wasmCloud/tree/main/examples/qrcode) - **OCI artifact**: `ghcr.io/wasmcloud/components/qrcode` - **Interfaces**: Exports `wasi:http/incoming-handler@0.2.2` ### TypeScript #### HTTP Axios A CLI-style component that uses [`axios`](https://www.npmjs.com/package/axios) to perform an outbound HTTP request and returns the result through a custom WIT interface. The same component can be invoked via `wash call`, `wasmtime run`, or `jco run` through its `wasi:cli/run` entrypoint. Demonstrates pulling in a widely-used npm package and using it inside a component. - **Source**: [`wasmCloud/typescript — examples/components/http-axios`](https://github.com/wasmCloud/typescript/tree/main/examples/components/http-axios) - **Interfaces**: Exports `wasi:cli/run@0.2.3` and a custom `wasmcloud:examples/invoke` interface #### HTTP Password Checker A `wasi:http`-compliant HTTP handler that provides an API for checking password strength. Uses only standard WASI interfaces (no wasmCloud-specific dependencies) and demonstrates pulling in JS ecosystem packages inside a component — notably [`valibot`](https://github.com/fabian-hiller/valibot) for schema validation and [`check-password-strength`](https://www.npmjs.com/package/check-password-strength) for the strength heuristic. - **Source**: [`wasmCloud/typescript — examples/components/http-password-checker`](https://github.com/wasmCloud/typescript/tree/main/examples/components/http-password-checker) - **Interfaces**: Exports `wasi:http/incoming-handler@0.2.2` #### HTTP Server with Hono An HTTP server component built on [Hono](https://hono.dev) via a custom adapter that bridges Hono onto the `wasi:http/incoming-handler` interface. Includes example routes (`/api/data`, `/api/config`) and demonstrates a minimal Hono + `wasi:http` setup. - **Source**: [`wasmCloud/typescript — examples/components/http-server-with-hono`](https://github.com/wasmCloud/typescript/tree/main/examples/components/http-server-with-hono) - **Interfaces**: Exports `wasi:http/incoming-handler@0.2.3` #### HTTP stdio A small HTTP handler that demonstrates writing to stdout and stderr from inside a component via the `wasi:cli` stream interfaces. Responds to `GET /?name=` while logging the request to the host's stdout/stderr. - **Source**: [`wasmCloud/typescript — examples/components/http-stdio`](https://github.com/wasmCloud/typescript/tree/main/examples/components/http-stdio) - **Interfaces**: Imports `wasi:cli/stdout@0.2.6`, `wasi:cli/stderr@0.2.6`; exports `wasi:http/incoming-handler@0.2.6` #### HTTP Streaming An HTTP component that performs a streaming response using `wasi:http` and `wasi:io` primitives, streaming 1 MiB of random bytes on every request. Demonstrates how to stream a response body from a TypeScript component without buffering. - **Source**: [`wasmCloud/typescript — examples/components/http-streaming`](https://github.com/wasmCloud/typescript/tree/main/examples/components/http-streaming) - **Interfaces**: Imports `wasi:random/random@0.2.3`; exports `wasi:http/incoming-handler@0.2.3` #### WebGPU TensorFlow A two-workload TypeScript example demonstrating GPU-accelerated machine learning inside a wasmCloud workload, running [TensorFlow.js](https://www.tensorflow.org/js) with the [WebGPU backend](https://www.tensorflow.org/js/guide/platform_environment) on top of `wasi:webgpu`. A `service` workload loads a style-transfer model into GPU memory at startup and serves a binary protocol on loopback TCP `127.0.0.1:7878`; a `component` workload serves the browser UI and forwards `POST /stylize` requests to the service, returning the stylized JPEG. - **Source**: [`wasmCloud/typescript — examples/components/webgpu-tensorflow`](https://github.com/wasmCloud/typescript/tree/main/examples/components/webgpu-tensorflow) - **`service` interfaces**: Imports `wasi:webgpu/webgpu@0.0.1`, `wasi:graphics-context/graphics-context@0.0.1`, `wasi:sockets/tcp@0.2.3` (and related socket interfaces); exports `wasi:cli/run@0.2.3` - **`component` interfaces**: Imports `wasi:sockets/tcp@0.2.3` (and related socket interfaces); exports `wasi:http/incoming-handler@0.2.3` - **Requirements**: `wash` ≥ 2.1.0 (WebGPU in services landed in 2.1.0) and a GPU on the host machine running `wash dev` #### WASI Config from Kubernetes Env An HTTP component that reads runtime configuration via the `wasi:config/store` interface, demonstrating how wasmCloud sources config from Kubernetes ConfigMaps and Secrets at runtime. Exposes `GET /config` (all values as JSON) and `GET /config/:key` (a single value). The component contains no Kubernetes-specific code — values are injected by the wasmCloud config provider, which can be backed by a ConfigMap or Secret in a Kubernetes deployment. - **Source**: [`wasmCloud/typescript — examples/components/wasi-config-from-k8s-env`](https://github.com/wasmCloud/typescript/tree/main/examples/components/wasi-config-from-k8s-env) - **Interfaces**: Imports `wasi:config/store@0.2.0-rc.1`; exports `wasi:http/incoming-handler@0.2.6` --- ## FAQ This page includes frequently asked questions on wasmCloud v2, including common questions about [migration from wasmCloud v1](#migrating-from-wasmcloud-v1). For a comprehensive migration guide with architecture diagrams and practical instructions, see [Migration to v2](./migration.mdx). ## General questions ### Do I have to use Kubernetes with wasmCloud v2? No. The workload API exposed by the runtime can work with any orchestrator, so you can choose or create the solution that suits your environment. You can also use the operator with a stripped-down version of Kubernetes that includes only the K8s API server, for a straightforward but lightweight Kubernetes-native approach. In real-world implementations, wasmCloud maintainers have found that the vast majority of users are deploying wasmCloud on Kubernetes. For wasmCloud v2, we're aiming to align smoothly to this most common scenario while preserving flexibility for a variety of deployment patterns including distributed edge environments. For now, maintainer time is focused on the prevailing Kubernetes use case, but we'd love to hear from folks who are interested in other deployment scenarios, and we strongly encourage contributors who would like to work on other scheduling implementations to get involved. ## Migrating from wasmCloud v1 :::info For a comprehensive migration guide with architecture comparisons, diagrams, and step-by-step instructions, see the **[Migration to v2](./migration.mdx)** guide. ::: ### What happened to capability providers? In v1, capability providers were external processes that communicated over NATS. In v2, capabilities are provided through [host plugins](./overview/hosts/plugins.mdx) (recommended for shared, performance-critical capabilities), [service components](./overview/workloads/services.mdx) (for workload-specific long-running functionality), or containerized providers (for minimal-change migration of existing providers). For details and examples, see [Migrating capability providers](./migration.mdx#migrating-capability-providers). For guidance on choosing between a plugin and a service, see [Plugin or Service?](./recipes/plugin-or-service.mdx). ### Is `wadm` still a part of wasmCloud v2? No—the [runtime-operator](./kubernetes-operator/index.mdx) and the Kubernetes API now manage deployments. The operator acts as a reconciler, giving hosts the appropriate instructions to match the state specified to the Kubernetes API. Though wasmCloud v2 is designed to operate in a Kubernetes-native way, [it is not restricted to Kubernetes](#do-i-have-to-use-kubernetes-with-wasmcloud-v2). For more on the architectural changes, see [Architecture comparison](./migration.mdx#architecture-comparison). ### What happened to the "Application" abstraction? The v1 "Application" has been replaced by the [Workload](./overview/workloads/index.mdx), which may include one or more components that communicate over interfaces. Components in the same workload are placed on the same host and linked at runtime. For a side-by-side manifest comparison, see [Migrating applications to workloads](./migration.mdx#migrating-applications-to-workloads). ### How do distributed applications work in wasmCloud v2? In v2, distributed networking is [intentionally more explicit](https://github.com/orgs/wasmCloud/projects/7/views/15?pane=issue&itemId=119180761&issue=wasmCloud%7CwasmCloud%7C4640). Components within the same workload communicate in-process (~30,000 RPS). When distributed communication is needed, you'll use interfaces like `wasmcloud:messaging` with manual serialization/deserialization (~5,000 RPS). For more context, see [Migrating distributed networking](./migration.mdx#migrating-distributed-networking). --- ## Glossary wasmCloud uses several terms with distinct meanings in the context of the platform and the WebAssembly ecosystem. ### Capability An abstraction for a given functionality (such as key-value storage or connection over HTTP) utilized by a WebAssembly component. ### Component Components are sandboxed, interoperable [WebAssembly](#webassembly) binaries that can be compiled from various programming languages such as Rust, Go, and JavaScript. Components are `.wasm` files that include [interface](#interface) definitions which define the contracts through which they may relate to other entities. In a wasmCloud workload, we use "component" to refer to the binary that implements stateless logic and typically enacts the core logic of an application, but it is important to note that [services](#service) are implemented as Wasm components as well. For more information, see [Workloads](./overview/workloads/index.mdx). ### Host A wasmCloud host is a process consisting of a WebAssembly runtime ([Wasmtime](#wasmtime)) and additional layers of security and functionality. Multiple hosts may run on a single machine and can be ephemeral. ### Host Group A host group is a set of wasmCloud hosts. ### Host Plugin Host plugins extend a [wasmCloud host](#host) with a specific implementation of a given [capability](#capability) according to an [interface](#interface), and may optionally serve as shared resources, providing functionality to many different components for many different applications. You can find several examples of host plugins in the [`wash-runtime/src/plugin`](https://github.com/wasmCloud/wasmCloud/tree/main/crates/wash-runtime/src/plugin) directory of the `wash` repository. (Note that these plugins are built-in by default.) ### Interface Interfaces are contracts that define the relationships between entities like components and providers, often defining functionalities like HTTP or key-value storage at a high and vendor-agnostic level. Interfaces describe a component or provider's requirements ("imports") and exposed functions that may be used by other entities ("exports"). Interfaces are defined in [WebAssembly Interface Type (WIT)](#webassembly-interface-type-wit) files. See the [WebAssembly System Interface (WASI) API proposals](https://github.com/WebAssembly/WASI/tree/main/proposals) for examples of interfaces defined in WIT. ### NATS NATS is a connective technology for distributed systems that can be used as a transport in production wasmCloud deployments. NATS is governed by the Cloud Native Computing Foundation. For more information, see the [**NATS documentation**](https://docs.nats.io/). ### Service In a wasmCloud workload, "service" refers to a WebAssembly binary that acts as a persistent, stateful companion to stateless components in the same workload. For more information, see [Workloads](./overview/workloads/index.mdx). ### `wash` `wash` is shorthand for **Wasm Shell**, the open source CLI tool for developing and publishing WebAssembly components. Using `wash`, you can build components, view component interfaces, publish components to an OCI registry, and more. For more information, see [**Wasm Shell**](./wash/index.mdx). ### Wasm Wasm is shorthand for [WebAssembly](#webassembly). ### Wasmtime Wasmtime is an open source WebAssembly runtime wrapped by wasmCloud's `wash-runtime` crate. Wasmtime is maintained by the Bytecode Alliance and may be used as a standalone tool. For more information, see the [**Wasmtime documentation**](https://docs.wasmtime.dev/). ### WebAssembly WebAssembly is a bytecode format paired with a virtual instruction set architecture that can serve as a compilation target for any language. Developers can compile code from a given language to a very small binary that runs on what is essentially a tiny virtual machine, enabling that code to run in any environment, including browsers, servers, edge devices, clouds, and more. For more information, see [**WebAssembly.org**](https://webassembly.org/). ### WebAssembly Component Model The WebAssembly Component Model is a specification for WebAssembly binaries that enables them to **interoperate**—meaning that a component written in one language (Rust, for example) can communicate with a component written in another language (such as Go) using shared types and functions. When we talk about [components](#component) in wasmCloud, we are simply talking about standard WebAssembly components conforming to the Component Model, which may run anywhere else that runs components. For more information on components and the Component Model, see [**Components**](/docs/overview/workloads/components) or the [**Component Model documentation**](https://component-model.bytecodealliance.org/). ### WebAssembly Module WebAssembly modules are units of code conforming to the core WebAssembly specification. These are sometimes contrasted with components, but components are modules with an additional layer of specification to the Component Model. For more information, see the [**WebAssembly core specification**](https://webassembly.github.io/spec/core/syntax/modules.html). ### WebAssembly System Interface (WASI) WASI stands for "**WebAssembly System Interface**" and refers to a group of standard APIs for use by WebAssembly binaries. WASI is developed and maintained by the WASI Subgroup in the W3C WebAssembly Community Group. WASI interfaces are sometimes called "proposals" as they move through a multi-stage proposal process before being accepted as a standard. You may see the second major WASI release referred to variously as WASI 0.2, WASI P2, or WASI Preview 2. For more information, see [**WASI.dev**](https://wasi.dev/). ### WebAssembly Interface Type (WIT) WIT stands for "**WebAssembly Interface Type**." WIT is an [interface description language](https://en.wikipedia.org/wiki/Interface_description_language) used to define the high-level interfaces used by components, such as the APIs included in [WASI](#webassembly-system-interface-wasi). In wasmCloud, all interfaces are defined in WIT. For more information on WIT, see the [**Component Model documentation**](https://component-model.bytecodealliance.org/design/wit.html). ### Workload A wasmCloud **workload** is an application-level abstraction that consists of one or more [components](#component) and optionally a [service](#service). For more information, see [Workloads](./overview/workloads/index.mdx). --- ## Governance ### wasmCloud is a community-led open source project Incubating with the Cloud Native Computing Foundation. wasmCloud joined the [Cloud Native Computing Foundation (CNCF)](https://www.cncf.io/) as a Sandbox project in 2021 and was accepted as an [Incubating project](https://www.cncf.io/projects/wasmcloud/) in 2024. As a CNCF project, wasmCloud operates under the [CNCF charter](https://github.com/cncf/foundation/blob/main/charter.md) and is guided by the [CNCF Technical Oversight Committee (TOC)](https://github.com/cncf/toc). All wasmCloud community participants are expected to abide by the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). wasmCloud code is licensed under the [Apache License 2.0](https://github.com/wasmCloud/wasmCloud/blob/main/LICENSE) and documentation is licensed under [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/legalcode). All contributions require a [Developer Certificate of Origin (DCO)](https://developercertificate.org/). The comprehensive and canonical document on wasmCloud project governance is [**GOVERNANCE.md**](https://github.com/wasmCloud/wasmCloud/blob/main/GOVERNANCE.md) in the main repository. This page provides an overview; refer to GOVERNANCE.md for authoritative details. ## Maintainers The wasmCloud project has two levels of maintainers. For detailed responsibilities at each level, see the [Contributor Ladder](https://github.com/wasmCloud/wasmCloud/blob/main/CONTRIBUTION_LADDER.md). **Org maintainers** oversee the overall wasmCloud project and its health. They are responsible for maintaining the project's mission, vision, and scope; refining governance; making project-level decisions; managing the wasmCloud brand and assets; handling code of conduct violations; and overseeing security issues. There are between three and nine org maintainers at any time. **Project maintainers** focus on a single codebase, group of related codebases, or service (such as this documentation site) within the wasmCloud organization. The maintainers for each repository are listed in that repository's `MAINTAINERS.md` file. ### Becoming a maintainer Any project maintainer of an active wasmCloud organization project is eligible to become an org maintainer. When a vacancy opens, any contributor to the wasmCloud org may nominate a project maintainer to fill it. The nomination period is three weeks, after which org maintainers vote; the nomination passes with a super-majority. New project-level maintainers are added at the discretion of existing project maintainers for that repository. ### Stepping down A maintainer steps down by opening a PR to move themselves to the emeritus section of the relevant `MAINTAINERS.md`. Maintainers may also take a leave of absence of up to 6 months (extendable by majority vote of the project's maintainers) without losing their maintainer status; after 6 months without contact, they are moved to emeritus. ## Decision making The default decision-making process at both the org and project level is [lazy consensus](http://communitymgt.wikia.com/wiki/Lazy_consensus): a decision is considered supported as long as no one objects. Silence is implicit agreement. When consensus cannot be reached, a maintainer can call for a vote. Most votes require a simple majority. The following decisions require a **super-majority**: - Enforcing a code of conduct violation - Removing a maintainer for any reason other than inactivity - Changing the governance rules - Licensing and intellectual property changes Simple majority is sufficient for adding, archiving, or removing subprojects and for financial decisions within CNCF guidelines. ## Code of conduct This project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). Possible violations should be reported to the org maintainers. If the possible violation involves an org maintainer, that person is recused from voting and the matter must be escalated to the CNCF. ## Community meetings The wasmCloud community holds **weekly community meetings** open to all. Meetings cover project updates, demos, roadmap discussion, and Q&A. - **Schedule**: Wednesdays at 1:00 PM ET / 10:00 AM PT - **Video**: [wasmCloud YouTube channel](https://www.youtube.com/@wasmCloud) - **Agendas and notes**: Published in the [`community`](https://wasmcloud.com/community) section of this site All are welcome to attend and contribute agenda items. ## Roadmap The wasmCloud roadmap is maintained publicly on [GitHub Projects](https://github.com/orgs/wasmCloud/projects/7). Roadmap items are proposed via GitHub issues and discussed in community meetings. ## Communications The primary communication channels for the wasmCloud project are GitHub Issues, PRs, and Discussions, and the [wasmCloud Slack](https://slack.wasmcloud.com). The org maintainers can also be reached by email at cncf-wasmCloud-maintainers@lists.cncf.io. --- ## What is wasmCloud? # wasmCloud ### wasmCloud is a cloud native platform for running WebAssembly workloads across any cloud, Kubernetes, datacenter, or edge. Using wasmCloud, you can run workloads like microservices, functions, and agents as ultra-dense, deny-by-default **bytecode sandboxes** that are far more secure and efficient than traditional containers, *without* changing your operational model. Those bytecode sandboxes are called [WebAssembly (Wasm) components](./overview/workloads/components.mdx). Measuring in kilobytes to low megabytes and starting in milliseconds, components target the open [WebAssembly System Interface (WASI)](https://wasi.dev/) standard, so they are portable across any conformant runtime, not locked to wasmCloud. Build with any language that compiles to Wasm components, push to an OCI registry, and deploy to wasmCloud on Kubernetes—or directly with the wasmCloud runtime on any host. The wasmCloud project is open source and Incubating with the [Cloud Native Computing Foundation (CNCF)](https://www.cncf.io/). ## Why wasmCloud? Securing modern applications is hard. Containers provide process-level isolation, but their default security posture is **allow-by-default**: a container has broad access to the network, system calls, and environment variables unless something is explicitly blocked. Locking down a container image requires knowing everything it might try to do, then correctly enforcing those restrictions from the outside. ![Sandbox model](./images/containers-components.webp) WebAssembly components take the opposite approach. The WebAssembly sandbox is **deny-by-default**: a component can do *nothing*—no file I/O, no network access, no system calls—unless a capability is explicitly granted. Capabilities are declared as language-agnostic [interfaces](./overview/interfaces.mdx) in the component itself (a typical component imports one or two interfaces rather than inheriting hundreds of available syscalls) making the security surface small, visible, auditable, and enforced at the runtime level rather than bolted on afterward. wasmCloud runs WebAssembly components and manages their capabilities. You decide exactly which interfaces each component can access. Everything else is denied. ## Complementing containers wasmCloud is designed to run alongside your existing container infrastructure, enabling you to start using WebAssembly components immediately without changes to your existing infrastructure and workloads. WebAssembly components are a strong fit for: - **Untrusted or third-party code**: the capability model lets you run code you don't fully control with strict, auditable bounds on what it can access - **New application logic**: business logic, API handlers, data processing pipelines—anything written fresh benefits from the deny-by-default security model and fine-grained capability controls - **Edge and constrained environments**: components are practical on IoT gateways and constrained hosts where container images aren't viable ## Kubernetes-native wasmCloud v2 is built for Kubernetes. The [Kubernetes Operator](./kubernetes-operator/index.mdx) deploys and manages wasmCloud infrastructure as standard Kubernetes resources, so wasmCloud works naturally with the cloud native toolchains, workflows, and policies your team already uses. - **Auto-scaling**: handled by Kubernetes—HPA, KEDA, or whatever your cluster already uses - **Backup and recovery**: your existing Kubernetes backup solution covers wasmCloud infrastructure - **Observability, GitOps, RBAC**: standard Kubernetes patterns apply You don't learn a new operational model. You extend the one you already have. ## Any cloud, datacenter, or edge Beyond Kubernetes, the [wasmCloud runtime](./runtime/index.mdx) can run as a standalone process on any Linux, macOS, or Windows host—bare metal servers, VMs, IoT gateways, or CI runners. Because components are WASI-standard bytecode, the same artifact deploys identically across all of these environments without recompilation. ## The platform The wasmCloud platform consists of three primary parts: * [**Wasm Shell** (`wash`) CLI](./wash/index.mdx): A development tool for building and publishing WebAssembly components with languages including Go, TypeScript, Rust, and more. * [**Runtime** (`wash-runtime`)](./runtime/index.mdx): An easy-to-use runtime and workload API for executing WebAssembly components, with built-in support for WASI interfaces. * [**Kubernetes Operator** (`runtime-operator`)](./kubernetes-operator/index.mdx): An operations tool that runs wasmCloud infrastructure on Kubernetes. ![wasmcloud user contexts](./images/wasmcloud-stack.webp) For an overview of core concepts in wasmCloud and WebAssembly, see the [Platform Overview](./overview/index.mdx). ## Get started - **[Install wasmCloud](./installation.mdx)**: deploy your first WebAssembly component in minutes - **[Platform Overview](./overview/index.mdx)**: understand components, interfaces, and the capability model - **[Kubernetes Operator](./kubernetes-operator/index.mdx)**: run wasmCloud on your existing cluster --- ## Installation import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Install wasmCloud Install the parts of the [wasmCloud platform](./overview/index.mdx) that you need depending on what you want to accomplish: * If you want to **develop and publish Wasm applications**, [install the Wasm Shell (`wash`) CLI](#install-wash). * If you want to **run Wasm workloads on Kubernetes**, [install wasmCloud on Kubernetes](#install-wasmcloud-on-kubernetes). ## Install `wash` In your terminal, run the installation script to download and install the latest version of `wash` (**`2.0.4`**): ```shell curl -fsSL https://wasmcloud.com/sh | bash ``` The script installs `wash` to `~/.wash/bin` and automatically updates your shell profile. Open a new terminal session to use `wash`.
Customizing the install location To install to a different directory, set `INSTALL_DIR`: ```shell curl -fsSL https://wasmcloud.com/sh | INSTALL_DIR=/usr/local/bin bash ``` To skip automatic PATH modification (for example, if you manage your own dotfiles): ```shell curl -fsSL https://wasmcloud.com/sh -o install.sh && bash install.sh --no-modify-path ```
If you have [Homebrew](https://brew.sh/) installed, you can install `wash` with: ```shell brew install wasmcloud/wasmcloud/wash ``` Homebrew updates your PATH automatically. You're ready to use `wash` in any terminal session.
In PowerShell, run the installation script to download and install the latest version of `wash` (**`{{WASMCLOUD_VERSION}}`**): ```powershell iwr -useb https://wasmcloud.com/ps1 | iex ``` The script installs `wash` to `%USERPROFILE%\.wash\bin` and automatically adds it to your user PATH. Open a new terminal session to use `wash`.
Customizing the install location To install to a different directory, set `INSTALL_DIR`: ```powershell $env:INSTALL_DIR = "C:\tools\wash"; iwr -useb https://wasmcloud.com/ps1 | iex ``` To skip automatic PATH modification: ```powershell iwr -useb https://wasmcloud.com/ps1 -OutFile install.ps1; .\install.ps1 -NoModifyPath ```
If you have [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/) installed, you can install `wash` with: ```powershell winget install wasmCloud.wash ``` winget updates your PATH automatically. Open a new terminal session to use `wash`.
You will need [`cargo`](https://doc.rust-lang.org/cargo/getting-started/installation.html) to install from source. ```shell git clone https://github.com/wasmcloud/wasmCloud.git cd wasmCloud cargo install --path crates/wash-cli ``` Pre-built binaries for macOS, Linux, and Windows are [available on GitHub](https://github.com/wasmcloud/wasmCloud/releases).
Verify that `wash` is properly installed and check your version for **`{{WASMCLOUD_VERSION}}`** with: ```bash wash -V ``` Now that `wash` is installed, the next step is to [build and publish a Wasm application](./wash/developer-guide/build-and-publish.mdx). ## Install wasmCloud on Kubernetes Installation requires the following tools: - [`kubectl`](https://kubernetes.io/docs/tasks/tools/#kubectl) - [Helm](https://helm.sh/docs) v3.8.0+ You'll also need a Kubernetes environment. We recommend [`kind`](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) for the best local Kubernetes experience. ### Local Kubernetes environment You can use the one-liner below to start a `kind` cluster with a configuration from the [`wasmCloud/wasmCloud`](https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/deploy/kind/kind-config.yaml) repository. ```shell curl -fLO https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/deploy/kind/kind-config.yaml && kind create cluster --config=kind-config.yaml && rm kind-config.yaml ``` ### Install the wasmCloud operator Use Helm to install the wasmCloud operator from an OCI chart image, using the [values for local installation](https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml). The overlay disables the deprecated Runtime Gateway by default — HTTP traffic is routed by the operator via EndpointSlices tied to standard Kubernetes Services: ```shell helm install wasmcloud --version {{WASMCLOUD_VERSION}} oci://ghcr.io/wasmcloud/charts/runtime-operator \ --namespace wasmcloud --create-namespace \ -f https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml ``` :::warning[Existing wasmCloud state in the cluster] If you've previously installed the wasmCloud operator (in any namespace), `helm install` may fail with: ```text Error: INSTALLATION FAILED: ... ClusterRole "wasmcloud-runtime-operator-..." cannot be imported into the current release ``` Helm 3 does not re-import cluster-scoped resources owned by a previous release. If you no longer need the prior install, fully clean up its cluster-scoped resources: ```shell helm uninstall wasmcloud --namespace kubectl delete clusterrole -l app.kubernetes.io/instance=wasmcloud kubectl delete clusterrolebinding -l app.kubernetes.io/instance=wasmcloud kubectl delete crd -l app.kubernetes.io/instance=wasmcloud ``` **Deleting the CRDs also deletes any existing `WorkloadDeployment`, `Host`, `Workload`, `WorkloadReplicaSet`, and `Artifact` resources in the cluster.** Make sure you have manifests checked in (or otherwise saved) for anything you want to recreate before running the CRD delete. After cleanup, re-run the install command above. ::: Verify the deployment: ```shell kubectl get pods -l app.kubernetes.io/instance=wasmcloud -n wasmcloud ``` Once all pods are running, you're ready to deploy a Wasm workload. ### Deploy a Wasm workload Apply the [wasmCloud-hosted manifest](https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/templates/http-hello-world/manifests/workloaddeployment.yaml), which contains a `NodePort` Service and a `WorkloadDeployment` that references it by name. The operator creates an EndpointSlice for the Service pointing at the host pods running the workload, so the NodePort on port 30950 (mapped to host port 80 by the kind cluster config) reaches the component directly: ```shell kubectl apply -f https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/templates/http-hello-world/manifests/workloaddeployment.yaml ``` Use `curl` to invoke the Wasm workload with an HTTP request: ```shell curl localhost -i ``` ```text Hello from wasmCloud! ``` For more information on each of these steps, see [Kubernetes Operator](./kubernetes-operator/index.mdx). ## Clean up Delete the workload deployment and its Service: ```shell kubectl delete workloaddeployment hello-world kubectl delete service hello-world ``` Uninstall wasmCloud: ```shell helm uninstall wasmcloud --namespace wasmcloud ``` Delete the local Kubernetes environment: ```shell kind delete cluster ``` ## Next steps * Read the [Overview](./overview/index.mdx) for an explanation of core concepts in wasmCloud. * Explore the [`wash/examples` directory](https://github.com/wasmCloud/wasmCloud/tree/main/examples) for more advanced Wasm component examples. * Check out the [Developer Guide](./wash/developer-guide/index.mdx) for information on building Wasm components. --- ## API Reference ## Packages - [runtime.wasmcloud.dev/v1alpha1](#runtimewasmclouddevv1alpha1) ## runtime.wasmcloud.dev/v1alpha1 Package v1alpha1 contains API Schema definitions for the runtime v1alpha1 API group. ### Resource Types - [Artifact](#artifact) - [ArtifactList](#artifactlist) - [Host](#host) - [HostList](#hostlist) - [Workload](#workload) - [WorkloadDeployment](#workloaddeployment) - [WorkloadDeploymentList](#workloaddeploymentlist) - [WorkloadList](#workloadlist) - [WorkloadReplicaSet](#workloadreplicaset) - [WorkloadReplicaSetList](#workloadreplicasetlist) #### Artifact Artifact is the Schema for the artifacts API. _Appears in:_ - [ArtifactList](#artifactlist) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `apiVersion` _string_ | `runtime.wasmcloud.dev/v1alpha1` | | | | `kind` _string_ | `Artifact` | | | | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `spec` _[ArtifactSpec](#artifactspec)_ | | | | #### ArtifactList ArtifactList contains a list of Artifact. | Field | Description | Default | Validation | | --- | --- | --- | --- | | `apiVersion` _string_ | `runtime.wasmcloud.dev/v1alpha1` | | | | `kind` _string_ | `ArtifactList` | | | | `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `items` _[Artifact](#artifact) array_ | | | | #### ArtifactSpec ArtifactSpec defines the desired state of Artifact. _Appears in:_ - [Artifact](#artifact) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `image` _string_ | | | Required: \{\} | | `imagePullSecret` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#localobjectreference-v1-core)_ | | | Optional: \{\} | #### ConfigLayer _Appears in:_ - [HostInterface](#hostinterface) - [LocalResources](#localresources) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `configFrom` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#localobjectreference-v1-core) array_ | ConfigFrom is a list of references to ConfigMaps that will be provided to the workload.The keys and values of all referenced ConfigMaps will be merged. In case of key conflicts,the last ConfigMap in the list wins. | | Optional: \{\} | | `secretFrom` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#localobjectreference-v1-core) array_ | The keys and values of all referenced Secrets will be merged. In case of key conflicts,the last Secret in the list wins.The values of the Secrets will be base64-decoded, utf-8 decoded before being provided to the workload. | | Optional: \{\} | | `config` _object (keys:string, values:string)_ | | | Optional: \{\} | #### EphemeralVolume EphemeralVolume represents a temporary directory that shares a workload's lifetime. _Appears in:_ - [Volume](#volume) #### Host Host is the Schema for the Hosts API. _Appears in:_ - [HostList](#hostlist) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `apiVersion` _string_ | `runtime.wasmcloud.dev/v1alpha1` | | | | `kind` _string_ | `Host` | | | | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `hostId` _string_ | | | Required: \{\} | | `hostname` _string_ | | | Optional: \{\} | | `httpPort` _integer_ | | | Optional: \{\} | | `environment` _string_ | Environment records where the host is running. For Kubernetes hostpods this is the pod's namespace; for out-of-cluster hosts it can beany operator-defined identifier (e.g. a region or data center). | | Optional: \{\} | #### HostInterface _Appears in:_ - [WorkloadSpec](#workloadspec) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `configFrom` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#localobjectreference-v1-core) array_ | ConfigFrom is a list of references to ConfigMaps that will be provided to the workload.The keys and values of all referenced ConfigMaps will be merged. In case of key conflicts,the last ConfigMap in the list wins. | | Optional: \{\} | | `secretFrom` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#localobjectreference-v1-core) array_ | The keys and values of all referenced Secrets will be merged. In case of key conflicts,the last Secret in the list wins.The values of the Secrets will be base64-decoded, utf-8 decoded before being provided to the workload. | | Optional: \{\} | | `config` _object (keys:string, values:string)_ | | | Optional: \{\} | | `name` _string_ | Name uniquely identifies this interface instance when multiple entriesshare the same namespace+package. Components use this name as theidentifier parameter in resource-opening functions (e.g., store::open(name)).Required when multiple entries of the same namespace:package exist. | | Optional: \{\} Pattern: `^[a-z0-9][a-z0-9-]*$` | | `namespace` _string_ | | | Required: \{\} | | `package` _string_ | | | Required: \{\} | | `interfaces` _string array_ | | | MinItems: 1 Required: \{\} | | `version` _string_ | | | Optional: \{\} | #### HostList HostList contains a list of Host. | Field | Description | Default | Validation | | --- | --- | --- | --- | | `apiVersion` _string_ | `runtime.wasmcloud.dev/v1alpha1` | | | | `kind` _string_ | `HostList` | | | | `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `items` _[Host](#host) array_ | | | | #### HostPathVolume HostPathVolume represents a pre-existing file or directory on the host machine. _Appears in:_ - [Volume](#volume) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `path` _string_ | Path of the file or directory on the host. | | Required: \{\} | #### KubernetesServiceRef KubernetesServiceRef references an existing Kubernetes Service that the operator will manage an EndpointSlice for, pointing to the host pods that are running this workload. _Appears in:_ - [KubernetesSpec](#kubernetesspec) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | Name is the name of the Kubernetes Service in the same namespace. | | Required: \{\} | #### KubernetesSpec KubernetesSpec groups Kubernetes-specific configuration for a workload. _Appears in:_ - [WorkloadDeploymentSpec](#workloaddeploymentspec) - [WorkloadSpec](#workloadspec) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `service` _[KubernetesServiceRef](#kubernetesserviceref)_ | Service references an existing Kubernetes Service that the operator willmaintain an EndpointSlice for, pointing to the host pods running thisworkload. When set, the operator also registers DNS aliases for theservice (e.g. service-name, service-name.namespace.svc.cluster.local)with the host so cluster-internal callers can reach the workload viaService DNS without going through an external gateway. | | Optional: \{\} | #### LocalResources LocalResources describes resources that will be made available to a workload component. _Appears in:_ - [WorkloadComponent](#workloadcomponent) - [WorkloadService](#workloadservice) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `volumeMounts` _[VolumeMount](#volumemount) array_ | VolumeMounts is a list of volume mounts that will be mounted into the workload component.The volumes must be defined in the WorkloadSpec.Volumes field. | | Optional: \{\} | | `environment` _[ConfigLayer](#configlayer)_ | | | Optional: \{\} | | `config` _object (keys:string, values:string)_ | | | Optional: \{\} | | `allowedHosts` _string array_ | | | Optional: \{\} | #### ReplicaSetStatus _Appears in:_ - WorkloadDeploymentStatus - WorkloadReplicaSetStatus | Field | Description | Default | Validation | | --- | --- | --- | --- | | `expected` _integer_ | | | Optional: \{\} | | `current` _integer_ | | | Optional: \{\} | | `ready` _integer_ | | | Optional: \{\} | | `unavailable` _integer_ | | | Optional: \{\} | #### Volume Volume represents a named volume that can be mounted by components. _Appears in:_ - [WorkloadSpec](#workloadspec) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | Name of the volume. Must be a DNS_LABEL and unique within the Workload. | | Required: \{\} | | `ephemeral` _[EphemeralVolume](#ephemeralvolume)_ | EphemeralVolume represents a temporary directory that shares a workload's lifetime. | | Optional: \{\} | | `hostPath` _[HostPathVolume](#hostpathvolume)_ | HostPathVolume represents a pre-existing file or directory on the host machine. | | Optional: \{\} | #### VolumeMount VolumeMount describes a mounting of a Volume within a component. _Appears in:_ - [LocalResources](#localresources) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | Name must match the Name of a Volume defined in the WorkloadSpec.Volumes field. | | Required: \{\} | | `mountPath` _string_ | MountPath is the path within the component where the volume should be mounted. | | Required: \{\} | | `readOnly` _boolean_ | ReadOnly indicates whether the volume should be mounted as read-only. | | Optional: \{\} | #### Workload Workload is the Schema for the artifacts API. _Appears in:_ - [WorkloadList](#workloadlist) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `apiVersion` _string_ | `runtime.wasmcloud.dev/v1alpha1` | | | | `kind` _string_ | `Workload` | | | | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `spec` _[WorkloadSpec](#workloadspec)_ | | | | #### WorkloadComponent WorkloadComponent represents a component of a workload. Components are stateless, invocation-driven units of computation. Components are isolated from each other and can be scaled independently. Each Component has a Root WIT World, representing the Components imports/exports. The combined list of all Components' Root WIT Worlds within a workload must be compatible with the Host's WIT World. All components within a workload are guaranteed to be placed on the same Wasm Host. _Appears in:_ - [WorkloadSpec](#workloadspec) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | | | Required: \{\} | | `image` _string_ | | | Required: \{\} | | `imagePullSecret` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#localobjectreference-v1-core)_ | | | Optional: \{\} | | `imagePullPolicy` _[PullPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#pullpolicy-v1-core)_ | | | Optional: \{\} | | `poolSize` _integer_ | | | Optional: \{\} | | `maxInvocations` _integer_ | | | Optional: \{\} | | `localResources` _[LocalResources](#localresources)_ | | | Optional: \{\} | #### WorkloadDeployPolicy _Underlying type:_ _string_ _Appears in:_ - [WorkloadDeploymentSpec](#workloaddeploymentspec) | Field | Description | | --- | --- | | `RollingUpdate` | | | `Recreate` | | #### WorkloadDeployment WorkloadDeployment is the Schema for the artifacts API. _Appears in:_ - [WorkloadDeploymentList](#workloaddeploymentlist) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `apiVersion` _string_ | `runtime.wasmcloud.dev/v1alpha1` | | | | `kind` _string_ | `WorkloadDeployment` | | | | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `spec` _[WorkloadDeploymentSpec](#workloaddeploymentspec)_ | | | | #### WorkloadDeploymentArtifact _Appears in:_ - [WorkloadDeploymentSpec](#workloaddeploymentspec) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | | | Required: \{\} | | `artifactFrom` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#localobjectreference-v1-core)_ | | | Required: \{\} | #### WorkloadDeploymentList WorkloadDeploymentList contains a list of HttpTrigger. | Field | Description | Default | Validation | | --- | --- | --- | --- | | `apiVersion` _string_ | `runtime.wasmcloud.dev/v1alpha1` | | | | `kind` _string_ | `WorkloadDeploymentList` | | | | `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `items` _[WorkloadDeployment](#workloaddeployment) array_ | | | | #### WorkloadDeploymentSpec WorkloadDeploymentSpec defines the desired state of WorkloadDeployment. _Appears in:_ - [WorkloadDeployment](#workloaddeployment) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `replicas` _integer_ | | | Optional: \{\} | | `template` _[WorkloadReplicaTemplate](#workloadreplicatemplate)_ | | | Required: \{\} | | `deployPolicy` _[WorkloadDeployPolicy](#workloaddeploypolicy)_ | | RollingUpdate | Optional: \{\} | | `artifacts` _[WorkloadDeploymentArtifact](#workloaddeploymentartifact) array_ | | | Optional: \{\} | | `kubernetes` _[KubernetesSpec](#kubernetesspec)_ | Kubernetes groups Kubernetes-specific configuration such as Servicereferences and endpoint management. | | Optional: \{\} | #### WorkloadList WorkloadList contains a list of Workload. | Field | Description | Default | Validation | | --- | --- | --- | --- | | `apiVersion` _string_ | `runtime.wasmcloud.dev/v1alpha1` | | | | `kind` _string_ | `WorkloadList` | | | | `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `items` _[Workload](#workload) array_ | | | | #### WorkloadReplicaSet WorkloadReplicaSet is the Schema for the artifacts API. _Appears in:_ - [WorkloadReplicaSetList](#workloadreplicasetlist) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `apiVersion` _string_ | `runtime.wasmcloud.dev/v1alpha1` | | | | `kind` _string_ | `WorkloadReplicaSet` | | | | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `spec` _[WorkloadReplicaSetSpec](#workloadreplicasetspec)_ | | | | #### WorkloadReplicaSetList WorkloadReplicaSetList contains a list of WorkloadReplicaSet. | Field | Description | Default | Validation | | --- | --- | --- | --- | | `apiVersion` _string_ | `runtime.wasmcloud.dev/v1alpha1` | | | | `kind` _string_ | `WorkloadReplicaSetList` | | | | `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `items` _[WorkloadReplicaSet](#workloadreplicaset) array_ | | | | #### WorkloadReplicaSetSpec WorkloadReplicaSetSpec defines the desired state of WorkloadReplicaSet. _Appears in:_ - [WorkloadDeploymentSpec](#workloaddeploymentspec) - [WorkloadReplicaSet](#workloadreplicaset) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `replicas` _integer_ | | | Optional: \{\} | | `template` _[WorkloadReplicaTemplate](#workloadreplicatemplate)_ | | | Required: \{\} | #### WorkloadReplicaTemplate _Appears in:_ - [WorkloadDeploymentSpec](#workloaddeploymentspec) - [WorkloadReplicaSetSpec](#workloadreplicasetspec) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `annotations` _object (keys:string, values:string)_ | | | Optional: \{\} | | `labels` _object (keys:string, values:string)_ | | | Optional: \{\} | | `spec` _[WorkloadSpec](#workloadspec)_ | | | Required: \{\} | #### WorkloadService WorkloadService represents a long-running service that is part of the workload. It is also sometimes referred to as a "sidecar" and is optional. A Service differs from a Component in that it is long-running and represents the Workload's "localhost". Services can bind to TCP & UDP ports, which are accessible by Components within the same workload via "localhost" or "127.0.0.1". Services export a single WIT interface, shaped as wasi:cli/run. Services can import interfaces from any Component within the same workload, or from the Host. _Appears in:_ - [WorkloadSpec](#workloadspec) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `image` _string_ | | | Required: \{\} | | `imagePullSecret` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#localobjectreference-v1-core)_ | | | Optional: \{\} | | `imagePullPolicy` _[PullPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#pullpolicy-v1-core)_ | | | Optional: \{\} | | `maxRestarts` _integer_ | | | Optional: \{\} | | `localResources` _[LocalResources](#localresources)_ | | | Optional: \{\} | #### WorkloadSpec WorkloadSpec defines the desired state of Workload. _Appears in:_ - [Workload](#workload) - [WorkloadReplicaTemplate](#workloadreplicatemplate) | Field | Description | Default | Validation | | --- | --- | --- | --- | | `hostSelector` _object (keys:string, values:string)_ | | | Optional: \{\} | | `hostId` _string_ | | | Optional: \{\} | | `environment` _string_ | Environment, if set, scopes scheduling to Hosts whose Environmentmatches this value, regardless of the Workload's own namespace.The value is matched against Host.Environment — typically aKubernetes namespace for in-cluster host pods, or anyoperator-defined identifier for out-of-cluster hosts (e.g. aregion or data center). Only honored when the operator is startedwith allowSharedHosts=true, or when Environment equals theWorkload's namespace. | | Optional: \{\} | | `components` _[WorkloadComponent](#workloadcomponent) array_ | | | Optional: \{\} | | `hostInterfaces` _[HostInterface](#hostinterface) array_ | | | Optional: \{\} | | `service` _[WorkloadService](#workloadservice)_ | | | Optional: \{\} | | `volumes` _[Volume](#volume) array_ | | | Optional: \{\} | | `kubernetes` _[KubernetesSpec](#kubernetesspec)_ | Kubernetes groups Kubernetes-specific configuration such as Servicereferences and endpoint management. | | Optional: \{\} | --- ## Custom Resource Definitions (CRDs) When deployed to Kubernetes, the core primitives of wasmCloud are represented by [**custom resources definitions (CRDs)**](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/). wasmCloud uses CRDs from the [`runtime.wasmcloud.dev/v1alpha1`](./api-reference.md) API package: - [Artifact](#artifact) - `runtime.wasmcloud.dev/v1alpha1` - [Host](#host) - `runtime.wasmcloud.dev/v1alpha1` - [Workload](#workload) - `runtime.wasmcloud.dev/v1alpha1` - [WorkloadDeployment](#workloaddeployment) - `runtime.wasmcloud.dev/v1alpha1` - [WorkloadReplicaSet](#workloadreplicaset) - `runtime.wasmcloud.dev/v1alpha1` This document explains each of these custom resources at a high level. For a complete API specification, see the [API reference](./api-reference.md). :::note[] [`WorkloadDeployment`](#workloaddeployment) is the resource used to deploy Wasm workloads—if you're looking to quickly deploy a component, [start there](#workloaddeployment). ::: ## Artifact An [Artifact](./api-reference.md#artifact) represents a Wasm component that can be referenced by Workloads. Artifacts define the image location and optional image pull secrets for accessing private registries. This can be used to fetch an OCI image and store its contents in a [NATS JetStream Object Store](https://docs.nats.io/nats-concepts/jetstream/obj_store). The `Artifact` resource tracks individual revisions, publishing the artifact's location under `Status.ArtifactURL`. A [`WorkloadDeployment`](#workloaddeployment) can reference an Artifact as its component image. A new deployment will be automatically rolled out when a new image is detected. Use `Artifact` when you want the operator to watch for new image versions and trigger rolling updates automatically, or to centralize image pull secrets so individual `WorkloadDeployment` manifests don't need to repeat them. Example manifest: ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: Artifact metadata: name: http-hello-world namespace: default spec: image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 imagePullSecret: name: ghcr-secret ``` ## Host A [Host](./api-reference.md#host) resource defines a wasmCloud runtime environment, or **host**, which has a unique ID and can run Wasm workloads. Example manifest: ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: Host metadata: name: host-sample namespace: default labels: hostgroup: default hostId: NABCDEFGHIJKLMNOPQRSTUVWXYZ234567 hostname: host-sample.default httpPort: 4000 ``` Unlike most Kubernetes resources, the `Host` CRD does not use a `spec` wrapper — fields like `hostId`, `hostname`, `httpPort`, and `environment` are set at the resource root. See the [Host API reference](./api-reference.md#host) for the full field list. ## Workload A [Workload](./api-reference.md#workload) represents an application composed of one or more WebAssembly components and optional services. Workloads define the components, their configurations, volume mounts, and host interfaces they consume. Workloads are analogous to Kubernetes Pods in that they typically are not managed individually, but are instead owned by a [WorkloadDeployment](#workloaddeployment), much as a Pod is owned by a Deployment. Example manifest: ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: Workload metadata: name: hello-world namespace: default spec: hostSelector: hostgroup: default components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 poolSize: 10 maxInvocations: 1000 localResources: environment: config: LOG_LEVEL: info allowedHosts: - api.example.com hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: address: '0.0.0.0:8080' volumes: - name: cache ephemeral: {} ``` ## WorkloadDeployment A [WorkloadDeployment](./api-reference.md#workloaddeployment) defines the deployment and scaling of Workloads across hosts. It creates and manages WorkloadReplicaSets to ensure the desired number of workload replicas are running. Example manifest: ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: hello-world namespace: default spec: replicas: 3 deployPolicy: RollingUpdate artifacts: - name: http-component artifactFrom: name: http-hello-world template: metadata: labels: app: hello-world spec: hostSelector: hostgroup: default components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 poolSize: 10 hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: address: '0.0.0.0:8080' ``` ### Host Interfaces Use the `hostInterfaces` field to define host interfaces used by the workload. Each entry requires `namespace`, `package`, and `interfaces`, and may optionally include `config` and `name`. The optional `name` field enables **multi-backend binding**: when a component needs multiple implementations of the same interface type (e.g., two `wasi:keyvalue` backends), each entry can be given a unique name. The name maps directly to the `identifier` argument in resource-opening functions such as `store::open("cache")`. ```yaml hostInterfaces: # Named: NATS-backed keyvalue for caching - name: cache namespace: wasi package: keyvalue interfaces: [store, atomics, batch] config: backend: nats bucket: cache-kv # Named: Redis-backed keyvalue for sessions - name: sessions namespace: wasi package: keyvalue interfaces: [store] config: backend: redis url: redis://redis:6379 # Unnamed: single HTTP interface (name not required) - namespace: wasi package: http interfaces: [incoming-handler] config: address: '0.0.0.0:8080' ``` Naming rules: - `name` is optional. Omitting it preserves existing single-backend behavior. - When two or more entries share the same `namespace`+`package`, **all** of them must have a non-empty `name`. - Names must be unique within a workload's `hostInterfaces` for the same `namespace`+`package`. - Names must match `[a-z0-9][a-z0-9-]*` (DNS label style). ### Runtime Configuration Runtime configuration values (such as environment variables) may be supplied via the optional `localResources` subfield of the `component` field. ```yaml localResources: environment: config: some_key: some_value ``` Environmental values may also come from ConfigMaps or Secrets. The following approaches are also valid: ```yaml localResources: environment: configFrom: - name: my-configmap secretFrom: - name: my-secret config: literal_key: literal_value ``` ### Component resource controls The `poolSize`, `maxInvocations`, and `allowedHosts` fields on a component control how it runs inside the host. They are set under `spec.template.spec.components[*]` in a `WorkloadDeployment` (or directly in a `Workload`). - **`poolSize`**: Sets the maximum number of concurrent instances of this component that the host will pre-allocate in its execution pool. Higher values increase throughput under load; lower values reduce memory consumption. If omitted, the host uses its default pool size. - **`maxInvocations`**: Limits the number of in-flight invocations allowed for this component at any one time. Requests that exceed this limit are queued or rejected, providing back-pressure to protect host resources. - **`allowedHosts`**: A list of hostnames this component is permitted to make outbound HTTP calls to (e.g., `api.example.com`, `*.s3.amazonaws.com`). Entries are matched case-insensitively against the request's host — no scheme, no path. When the list is non-empty, calls to hosts not on the list are blocked, enforcing a least-privilege network policy for the component. An empty list (or omitted field) allows all outbound HTTP. See [Workload Security](./workload-security.mdx#restricting-outbound-http-with-allowedhosts) for details and usage guidance. ## WorkloadReplicaSet A [WorkloadReplicaSet](./api-reference.md#workloadreplicaset) ensures that a given number of Workload replicas are running at once. It is typically managed by a WorkloadDeployment but can be used directly for more granular control. Example manifest: ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadReplicaSet metadata: name: hello-world-v1 namespace: default spec: replicas: 5 template: metadata: labels: app: hello-world version: v1 spec: hostSelector: hostgroup: default components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 poolSize: 10 hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: address: '0.0.0.0:8080' ``` --- ## Introduction import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Kubernetes Operator ### The wasmCloud operator makes it easy to run wasmCloud and WebAssembly workloads on Kubernetes. We use the [operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) to run wasmCloud on Kubernetes, leveraging the orchestrator to schedule wasmCloud infrastructure and workloads. By aligning to Kubernetes, teams can adopt WebAssembly (Wasm) progressively—and integrate wasmCloud with existing tooling for ingress, registries, CI/CD, and other areas of the cloud-native ecosystem. ## The wasmCloud platform on Kubernetes Along with the wasmCloud operator, the wasmCloud platform on Kubernetes consists of these core parts: * [**Custom resource definitions (CRDs)**](crds.mdx) for wasmCloud infrastructure and Wasm workloads. * [**wasmCloud host(s)**](../glossary.mdx#host) - Sandboxed runtime environments for WebAssembly components. (By default, these are `wash` binaries using the `wash host` command to run a [cluster host (washlet)](../runtime/washlet.mdx) that surfaces the `wash-runtime` API over NATS.) * [**NATS with JetStream**](../glossary.mdx#nats) - CNCF project that provides a connective layer for transport between operator and hosts, along with built-in object storage through JetStream. NATS carries all control-plane traffic to the host (the host never communicates directly with the Kubernetes API). The operator sends workload start/stop requests to individual hosts via NATS subjects, and hosts self-register by publishing heartbeat messages the operator subscribes to. The Helm chart bundles NATS automatically—you can also connect an existing NATS cluster by setting `nats.enabled: false` and pointing the operator at your endpoint. HTTP traffic reaches workloads through standard Kubernetes Services: the operator manages an EndpointSlice for each user-defined Service referenced by a workload, so cluster DNS (e.g. `my-svc.default.svc.cluster.local`) resolves directly to the host pods. See [Expose a Workload via Kubernetes Service](../recipes/expose-workload-via-kubernetes-service.mdx) for the full pattern. :::note[Runtime Gateway deprecated in 2.0.3] Earlier releases included a separate Runtime Gateway deployment that proxied HTTP traffic to workloads. The gateway is deprecated as of 2.0.3 and will be removed in a future release; routing is now handled by the operator via EndpointSlices. The chart still installs a gateway pod by default for backwards compatibility — set `gateway.enabled: false` to skip it. ::: The entire platform can be deployed with [Helm](https://helm.sh/docs) using the [**wasmCloud operator Helm chart**](https://github.com/wasmCloud/wasmCloud/tree/main/charts/runtime-operator). (NATS and hosts can also be installed separately, if you wish.) For a detailed breakdown of each component's responsibilities and the request flow, see the [Operator Overview](./operator-manual/overview.mdx). For commonly-overridden chart values, see the [Helm Values Reference](./operator-manual/helm-values.mdx). ## Get started with the wasmCloud operator Installation requires the following tools: - [`kubectl`](https://kubernetes.io/docs/tasks/tools/#kubectl) - [Helm](https://helm.sh/docs) v3.8.0+ Select your Kubernetes environment: If you already have a Kubernetes cluster, skip cluster creation. Verify your `kubectl` context is pointing to the right cluster: ```shell kubectl cluster-info ``` [kind](https://kind.sigs.k8s.io/) runs Kubernetes nodes as Docker containers. **Requirements:** [Docker](https://docs.docker.com/get-started/get-docker/) · [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) The following command downloads a `kind-config.yaml` from the [`wasmCloud/wasmCloud`](https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/deploy/kind/kind-config.yaml) repository, starts a cluster with port 80 mapped for ingress, and deletes the config upon completion: ```shell curl -fLO https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/deploy/kind/kind-config.yaml && kind create cluster --config=kind-config.yaml && rm kind-config.yaml ``` [k3d](https://k3d.io/) runs a lightweight [k3s](https://k3s.io/) cluster inside Docker. **Requirements:** [Docker](https://docs.docker.com/get-started/get-docker/) · [k3d](https://k3d.io/#installation) The `--port` flag maps host port 80 to NodePort 30950 on the server node. The hello-world manifest exposes the workload as a `NodePort` Service on port 30950, so this maps the Wasm component directly to `localhost:80` without going through k3d's bundled Traefik LoadBalancer: ```shell k3d cluster create wasmcloud --port "80:30950@server:0" ``` If you'd prefer the `LoadBalancer` pattern more typical of k8s production deployments, see the [Expose a Workload via Kubernetes Service](../recipes/expose-workload-via-kubernetes-service.mdx#exposing-the-service-externally) recipe. [k3s](https://k3s.io/) is a lightweight Kubernetes distribution. **Linux only.** Install k3s: ```shell curl -sfL https://get.k3s.io | sh - ``` Configure `kubectl` to use the k3s cluster: ```shell mkdir -p ~/.kube sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config sudo chown $USER ~/.kube/config ``` ### Install the wasmCloud operator Use Helm to install the wasmCloud operator from an OCI chart image: ```shell helm install wasmcloud --version {{WASMCLOUD_VERSION}} oci://ghcr.io/wasmcloud/charts/runtime-operator \ --namespace wasmcloud --create-namespace \ -f https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml ``` The [`values.local.yaml`](https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml) file configures the host HTTP port to 80, which the kind cluster config maps to host port 80 via a NodePort Service: ```shell helm install wasmcloud --version {{WASMCLOUD_VERSION}} oci://ghcr.io/wasmcloud/charts/runtime-operator \ --namespace wasmcloud --create-namespace \ -f https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml ``` ```shell helm install wasmcloud --version {{WASMCLOUD_VERSION}} oci://ghcr.io/wasmcloud/charts/runtime-operator \ --namespace wasmcloud --create-namespace \ -f https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml ``` ```shell helm install wasmcloud --version {{WASMCLOUD_VERSION}} oci://ghcr.io/wasmcloud/charts/runtime-operator \ --namespace wasmcloud --create-namespace \ -f https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml ``` Along with the wasmCloud operator, wasmCloud CRDs, and NATS, the Helm chart will deploy **three wasmCloud hosts** using the [`wasmcloud/wash`](https://github.com/wasmCloud/wasmCloud/pkgs/container/wash) container image. You can build your own hosts that provide extended capabilities via [host plugins](../glossary.mdx#host-plugin). :::note[] You can find the full set of configurable values for the chart in [`wasmCloud/wasmCloud/charts/runtime-operator`](https://github.com/wasmCloud/wasmCloud/blob/main/charts/runtime-operator/values.yaml). ::: Verify the deployment: ```shell kubectl get pods -l app.kubernetes.io/instance=wasmcloud -n wasmcloud ``` Once all pods are running, you're ready to deploy a Wasm workload. ### Deploy a Wasm component Deploying a Wasm component that handles HTTP traffic takes two resources: a Kubernetes `Service` that exposes the component, and a [`WorkloadDeployment`](./crds.mdx#workloaddeployment) that references the Service. The operator creates an EndpointSlice for the Service pointing at whichever host pods are running the workload. ```yaml apiVersion: v1 kind: Service metadata: name: hello-world spec: type: ClusterIP ports: - port: 80 targetPort: 80 protocol: TCP --- apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: hello-world spec: replicas: 1 template: spec: hostSelector: hostgroup: default kubernetes: service: name: hello-world components: - name: hello-world image: ghcr.io/wasmcloud/components/hello-world:0.1.0 hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler ``` This manifest deploys a simple "Hello world!" component that uses the `wasi:http` interface, scheduled on the `default` hostgroup and reachable inside the cluster at `hello-world.default.svc.cluster.local`. For a kind-optimized NodePort variant that answers `curl localhost` on port 80, see the [wasmCloud-hosted hello-world manifest](https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/templates/http-hello-world/manifests/workloaddeployment.yaml) used by the [Installation](../installation.mdx#deploy-a-wasm-workload) guide. :::note[] Learn more about `WorkloadDeployments` and other wasmCloud resources in the [Custom Resource Definitions (CRDs) section](./crds.mdx). For the full Service-routing walk-through, see [Expose a Workload via Kubernetes Service](../recipes/expose-workload-via-kubernetes-service.mdx). ::: Verify the component is reachable from inside the cluster: ```shell kubectl run curl --rm -it --image=curlimages/curl --restart=Never -- \ curl -s http://hello-world.default.svc.cluster.local ``` ```text Hello from wasmCloud! ``` ## Clean up Delete the workload deployment and its Service: ```shell kubectl delete workloaddeployment hello-world kubectl delete service hello-world ``` Uninstall wasmCloud: ```shell helm uninstall wasmcloud ``` Delete the local Kubernetes environment: No action needed — your cluster remains running. ```shell kind delete cluster ``` ```shell k3d cluster delete wasmcloud ``` ```shell /usr/local/bin/k3s-uninstall.sh ``` ## Next steps * Explore the [`wash/examples` directory](https://github.com/wasmCloud/wasmCloud/tree/main/examples) for more advanced Wasm component examples. * The [Custom Resource Definitions (CRDs)](./crds.mdx) section explains the custom resources used by wasmCloud. * The [API reference](./api-reference.md) provides a complete API specification. * For securing workloads in production, see [Workload Security](./workload-security.mdx) for the sandbox model, `allowedHosts`, and NetworkPolicy. * For edge or resource-constrained deployments, see [Lightweight Deployments](./operator-manual/lightweight-deployments.mdx) for a lightweight alternative to kind. --- ## CI/CD and GitOps ## Overview wasmCloud components are distributed as [OCI artifacts](https://opencontainers.org/), so a CI/CD pipeline for wasmCloud follows a familiar pattern: build `.wasm` binaries, push them to an OCI registry, and update Kubernetes manifests to reference the new image. The wasmCloud project provides a set of official [GitHub Actions](#wasmcloud-github-actions) that handle building components, publishing to OCI registries, and generating supply-chain attestations. For teams using GitOps, [Argo CD](#gitops-with-argo-cd) or Flux can close the loop by reconciling Kubernetes state with a Git repository. ![Deployment pipeline from developer push through CI, OCI registry, GitOps, and Kubernetes](../../images/deployment-pipeline.webp) ## wasmCloud GitHub Actions wasmCloud provides four GitHub Actions for CI pipelines: | Action | Description | |--------|-------------| | [`wasmCloud/setup-wash-action`](https://github.com/wasmCloud/setup-wash-action) | Installs the `wash` CLI on the runner | | [`wasmcloud/actions/setup-wash-cargo-auditable`](https://github.com/wasmCloud/actions/tree/main/setup-wash-cargo-auditable) | Configures `cargo-auditable` to embed SBOM data in Rust builds | | [`wasmcloud/actions/wash-build`](https://github.com/wasmCloud/actions/tree/main/wash-build) | Builds a Wasm component, outputs the path to the built artifact | | [`wasmcloud/actions/wash-oci-publish`](https://github.com/wasmCloud/actions/tree/main/wash-oci-publish) | Publishes a component to an OCI registry with optional attestation and SBOM | ### setup-wash-action The [`setup-wash-action`](https://github.com/wasmCloud/setup-wash-action) installs `wash`, adds it to `PATH`, caches the binary, and installs the `wasm32-wasip2` Rust target. ```yaml - uses: wasmCloud/setup-wash-action@main with: wash-version: "v{{WASMCLOUD_VERSION}}" # version to install (default: latest) ``` ### setup-wash-cargo-auditable The [`setup-wash-cargo-auditable`](https://github.com/wasmCloud/actions/tree/main/setup-wash-cargo-auditable) action installs `cargo-auditable` and `cargo-audit`, then configures `.wash/config.yaml` so that `wash build` uses `cargo auditable build` under the hood. This embeds dependency metadata in the compiled binary for later SBOM extraction. :::note[] A Cargo project (`Cargo.toml`) must already exist in the working directory before calling this action, as it reads the package name to determine the component output path. ::: ```yaml - uses: wasmcloud/actions/setup-wash-cargo-auditable@main with: working-directory: "." # directory containing the project (default: .) ``` ### wash-build The [`wash-build`](https://github.com/wasmCloud/actions/tree/main/wash-build) action runs `wash build --output json` and exposes the path to the built component as a step output. ```yaml - id: build uses: wasmcloud/actions/wash-build@main with: working-directory: "." # directory containing the project (default: .) ``` **Output:** `steps.build.outputs.component_path` — path to the built `.wasm` file. ### wash-oci-publish The [`wash-oci-publish`](https://github.com/wasmCloud/actions/tree/main/wash-oci-publish) action pushes the built component to an OCI registry. When attestation is enabled, the action generates build provenance and an SBOM (converted from CycloneDX to SPDX format). ```yaml - uses: wasmcloud/actions/wash-oci-publish@main with: component_path: ${{ steps.build.outputs.component_path }} # required registry: ghcr.io # default: ghcr.io attestation: "true" # default: false image_tags: "latest,v1.0.0,${{ github.sha }}" # default: branch name ``` :::note[] When `attestation` is enabled, the workflow needs the following permissions. See [Supply chain security](#supply-chain-security) for details. ```yaml permissions: contents: write packages: write attestations: write id-token: write ``` ::: ## Attestation When attestation is enabled, the `wash-oci-publish` action generates cryptographically signed metadata that links a published artifact back to the source code, build environment, and dependency tree that produced it. This enables consumers to verify that an artifact was built from a specific commit in a trusted CI pipeline. ![Attestation flow](../../images/attestation.webp) The attestation flow works as follows: - A **developer pushes** code to a GitHub repository, triggering a GitHub Actions workflow. - The workflow runs **`wash build`** (with `cargo-auditable`) to compile the component, embedding dependency metadata in the binary. - Two attestations are generated in parallel: - **`attest-sbom`** extracts a CycloneDX SBOM from the binary, converts it to SPDX format, and creates an SBOM attestation. - **`attest-build-provenance`** generates [SLSA](https://slsa.dev/) build provenance, recording *where*, *how*, and *from what source* the artifact was built. - Both attestations are signed via **Sigstore** using keyless OIDC signing—no manual key management required. - The signed attestations are stored in the **GitHub Attestation Store** and associated with the published artifact in the **container registry** (e.g., GHCR). - Consumers can run **`gh attestation verify`** to confirm the artifact's integrity and provenance before deploying it. ## Example: Build and publish pipeline The following GitHub Actions workflow builds a Rust-based Wasm component with auditable dependency metadata, publishes it to GitHub Container Registry, and generates supply-chain attestations: ```yaml name: Build and Publish Component on: push: tags: - "v*" permissions: contents: write packages: write attestations: write id-token: write jobs: build-and-publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup wash CLI uses: wasmCloud/setup-wash-action@main - name: Setup cargo-auditable uses: wasmcloud/actions/setup-wash-cargo-auditable@main - name: Build component id: build uses: wasmcloud/actions/wash-build@main - name: Publish component uses: wasmcloud/actions/wash-oci-publish@main with: component_path: ${{ steps.build.outputs.component_path }} registry: ghcr.io attestation: "true" image_tags: "latest,${{ github.ref_name }}" ``` This pipeline triggers on version tags (e.g. `v1.0.0`). The published image will be tagged with both `latest` and the Git tag. ## GitOps with Argo CD For production deployments, a GitOps workflow keeps Kubernetes state in sync with a Git repository. [Argo CD](https://argo-cd.readthedocs.io/) is a popular GitOps tool for Kubernetes and pairs well with the wasmCloud operator. ### Two-application pattern A common pattern uses two Argo CD Applications: 1. **Infrastructure Application** — deploys the wasmCloud platform (operator, NATS, hosts) from the [Helm chart](../index.mdx). 2. **Workloads Application** — deploys `WorkloadDeployment` manifests from a dedicated Git repository. This separation lets infrastructure and workload teams operate independently, each managing their own deployment cadence. ### Example Argo CD Applications **Infrastructure Application** — installs the wasmCloud operator via Helm: ```yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: wasmcloud-platform namespace: argocd spec: project: default source: chart: runtime-operator repoURL: ghcr.io/wasmcloud/charts targetRevision: v2-canary helm: releaseName: wasmcloud destination: server: https://kubernetes.default.svc namespace: default syncPolicy: automated: prune: true selfHeal: true ``` **Workloads Application** — syncs `WorkloadDeployment` manifests from a Git repository: ```yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: wasmcloud-workloads namespace: argocd spec: project: default source: repoURL: https://github.com//wasmcloud-workloads.git targetRevision: main path: manifests destination: server: https://kubernetes.default.svc namespace: default syncPolicy: automated: prune: true selfHeal: true ``` ### Automating manifest updates After the CI pipeline publishes a new component image, a second workflow job can update the `WorkloadDeployment` manifest in the workloads repository and open a pull request: ```yaml update-manifests: needs: build-and-publish runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: repository: /wasmcloud-workloads token: ${{ secrets.WORKLOADS_REPO_TOKEN }} - name: Update image tag run: | sed -i "s|image: ghcr.io//my-component:.*|image: ghcr.io//my-component:${{ github.ref_name }}|" \ manifests/my-component.yaml - name: Create pull request uses: peter-evans/create-pull-request@v7 with: title: "Update my-component to ${{ github.ref_name }}" commit-message: "chore: update my-component to ${{ github.ref_name }}" branch: "update-my-component-${{ github.ref_name }}" ``` Once merged, Argo CD detects the change and rolls out the new version automatically. :::info[] This pattern works with any GitOps tool that watches a Git repository for changes, including [Flux](https://fluxcd.io/). ::: ## Supply chain security The wasmCloud GitHub Actions support a full supply-chain security pipeline using `cargo-auditable`, CycloneDX, and GitHub's built-in attestation actions. The attestation flow works as follows: 1. **`setup-wash-cargo-auditable`** configures `cargo-auditable` via `.wash/config.yaml` so that dependency metadata is embedded in the compiled binary during `wash build`. 2. **`wash-oci-publish`** (with `attestation: "true"`) extracts the embedded metadata and generates attestations: - Extracts a CycloneDX SBOM from the binary using `auditable2cdx` - Converts the SBOM to SPDX format using `cyclonedx-cli` - Generates an SBOM attestation via [`actions/attest-sbom`](https://github.com/actions/attest-sbom) - Generates build provenance via [`actions/attest-build-provenance`](https://github.com/actions/attest-build-provenance) For attestation to work, the workflow must include the following permissions block: ```yaml permissions: contents: write # required for attestation uploads packages: write # required for OCI registry push attestations: write # required for attestation creation id-token: write # required for OIDC token (provenance signing) ``` :::warning[] Without all four permissions, the attestation steps will fail. If you don't need attestation, you can omit these permissions and set `attestation: "false"` (the default). ::: --- ## Filesystems and Volumes ## Overview For workloads that utilize external filesystem resources via [`wasi:filesystem`](https://github.com/WebAssembly/WASI/tree/main/proposals/filesystem) preopens, you can mount data from a [Kubernetes Volume](https://kubernetes.io/docs/concepts/storage/volumes/). * **Preopens** provide secure access to filesystem resources by mounting them within a component before instantiation. * **Kubernetes Volumes** enable containers to access filesystem data. In a wasmCloud deployment, filesystem data is made available to the containerized wasmCloud host, which may in turn pass data to a component. In this section of the Operator Manual, you'll learn how to: * Deploy volumes that provide filesystem resources to the wasmCloud host * Create a `WorkloadDeployment` manifest for a component that preopens filesystem resources ## Deploying volumes with wasmCloud hosts When deploying components that require filesystem resources, you first need to create the volume and make the volume available to the wasmCloud host. The sample host configuration excerpt below makes a volume called `wasmcloud-data` available to a host with the `default` label. (We use this hostgroup for simple local testing in our [default operator deployment](../index.mdx), but you could use any hostgroup.) ```yaml {20-23} apiVersion: apps/v1 kind: Deployment metadata: labels: wasmcloud.com/hostgroup: default wasmcloud.com/name: hostgroup name: hostgroup-default namespace: default spec: selector: matchLabels: wasmcloud.com/hostgroup: default wasmcloud.com/name: hostgroup template: metadata: labels: wasmcloud.com/hostgroup: default wasmcloud.com/name: hostgroup spec: volumes: - name: wasmcloud-data hostPath: path: "/var/data" ``` * The `name` field assigns a unique name to the volume. * The `hostPath.path` field specifies the target path for the file or directory on the host machine. ## Deploying components with filesystem access To access filesystem resources via the preopens and the wasmCloud host, a component should import `wasi:filesystem`. You can use the Wasm Shell (`wash`) CLI to check your component's imports: ```shell wash inspect ./component.wasm ``` Output should include: ```wit import wasi:filesystem/types@0.2.x; import wasi:filesystem/preopens@0.2.x; ``` The sample WorkloadDeployment manifest below makes the `wasmcloud-data` volume available to the `http-fs-hello` component at the `/assets` path within the component. ```yaml {11-21} apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: http-fs-hello spec: replicas: 1 template: spec: hostSelector: hostgroup: default volumes: - name: wasmcloud-data hostPath: path: /var/data # should match path in host deployment components: - name: http-fs-hello image: ghcr.io//http-fs-hello:0.1.2 localResources: volumeMounts: - name: wasmcloud-data mountPath: /assets # where data is mounted inside component hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: host: localhost ``` * The workload must be assigned to a host with the named volume. * Resources defined under the `localResources` field are made available to the component. * The `volumeMounts.name` field specifies the volume to use. * The `volumeMounts.mountPath` field specifies the path where the data from the volume should be made available within the component. * It is not necessary to define `wasi:filesystem` under `hostInterfaces`. --- ## Helm Values Reference This page documents the configuration values you are most likely to override when installing the [`runtime-operator` Helm chart](https://github.com/wasmCloud/wasmCloud/tree/main/charts/runtime-operator). For the authoritative list of every value the chart supports, run: ```shell helm show values oci://ghcr.io/wasmcloud/charts/runtime-operator --version ``` ## Top-level structure The chart's `values.yaml` is organized into five top-level sections: | Section | Purpose | |---|---| | `global` | Settings that apply across all components (image registry, TLS, image pull secrets) | | `nats` | The bundled NATS server — set `enabled: false` to connect an external NATS cluster instead | | `operator` | The wasmCloud runtime-operator deployment | | `gateway` | **Deprecated in 2.0.3.** Legacy runtime-gateway. Set `enabled: false` to skip installing it | | `runtime` | Host group deployments (pods running the `wash` host binary) | ## `global` ### `global.image.registry` Override the container image registry for all components at once. Useful for air-gapped or mirrored deployments. ```yaml global: image: registry: myregistry.example.com ``` See [Private Registries and Air-Gapped Deployments](./private-registries.mdx) for the full mirroring workflow. ### `global.tls.enabled` Introduced in 2.0.3. Set to `false` to disable TLS for NATS connections and skip certificate generation. Intended for clusters where a service mesh (e.g. Istio, Linkerd) provides mTLS between pods. ```yaml global: tls: enabled: false ``` When `global.tls.enabled` is `false`, the chart ignores `global.certificates.generate` — no self-signed certs are created and NATS runs plaintext. ### `global.certificates.generate` Controls whether the chart generates self-signed TLS certificates for NATS and the control plane. Set to `false` when bringing your own certificate secrets. See the [TLS: bring your own certificates](../../recipes/tls-bring-your-own-certificates.mdx) recipe for the full BYOC flow. ## `operator`, `nats`, `runtime` — pod labels and annotations Introduced in 2.0.3. Each deployment accepts `podLabels` and `podAnnotations` that are merged into the pod template. This is most commonly used for service mesh injection: ```yaml operator: podLabels: sidecar.istio.io/inject: "true" podAnnotations: proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}' nats: podLabels: sidecar.istio.io/inject: "true" runtime: podLabels: sidecar.istio.io/inject: "true" ``` ## `operator` ### `operator.watchNamespaces` By default, the operator watches every namespace in the cluster. Set `watchNamespaces` to a list of namespace names to scope it down: ```yaml operator: watchNamespaces: - team-a - team-b ``` When `watchNamespaces` is populated, the chart generates namespace-scoped `Role` and `RoleBinding` resources for each listed namespace (instead of a single `ClusterRole`). ### `operator.hostNamespaces` Introduced in 2.1. List of namespaces where host pods run. The operator's pod informer cache and per-namespace pod RBAC cover this set so the host-pod controller can manage finalizers on host pods. Leave empty when host pods only run in the operator's own namespace (the chart's default). ```yaml operator: hostNamespaces: - team-a - team-b ``` When you set `runtime.hostGroups[].namespace` to deploy host pods outside the operator's namespace, also include those namespaces here — otherwise the operator can't observe or finalize the host pods running there. ### `operator.allowSharedHosts` Introduced in 2.1. Default: `true`. Controls whether `WorkloadDeployment`s can schedule onto hosts whose `Host.environment` differs from the workload's own namespace, via `spec.template.spec.environment`. ```yaml operator: allowSharedHosts: false ``` The default (`true`) preserves the existing behavior where workloads with no `environment` set may schedule onto any matching host regardless of which tenant namespace the host runs in. This is permissive: in a multi-tenant cluster where each tenant has its own namespace and host pods, a workload in `team-a` can target hosts in `team-b` simply by setting `spec.template.spec.environment: team-b`. Set to `false` when namespace boundaries are part of your tenant isolation model. With `allowSharedHosts: false`: - Scheduling is locked to the workload's own namespace. - Any cross-namespace `environment` value is rejected with a `CrossEnvironmentSchedulingDenied` Warning Event and a `HostSelection=False` condition on the Workload. See [Troubleshooting: Workload stays unscheduled with `allowSharedHosts: false`](../../troubleshooting.mdx#workload-stays-unscheduled-with-allowsharedhosts-false) for the symptom and resolution patterns. ### `operator.image.tag` Defaults to the chart's `appVersion`. Override only when you need to pin to a specific operator build that differs from the chart release: ```yaml operator: image: tag: "{{WASMCLOUD_VERSION}}" ``` The same pattern applies to `gateway.image.tag` and `runtime.image.tag`. ## `gateway` (deprecated) :::warning[Deprecated] The runtime-gateway is deprecated as of 2.0.3. HTTP routing is now handled by the runtime-operator via EndpointSlices tied to user-defined Kubernetes Services. See [Expose a Workload via Kubernetes Service](../../recipes/expose-workload-via-kubernetes-service.mdx) for the replacement pattern. To skip installing the gateway, set `gateway.enabled: false`. ::: ```yaml gateway: enabled: false ``` ## `runtime` ### `runtime.hostGroups` A host group is a `Deployment` of pods running the `wash` host. You can define multiple groups to isolate workloads or provide specialized capabilities (e.g. WebGPU-enabled hosts): ```yaml runtime: hostGroups: - name: default replicas: 3 http: enabled: true port: 80 resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" - name: gpu replicas: 1 webgpu: enabled: true ``` `WorkloadDeployment` manifests target a group via `spec.template.spec.hostSelector.hostgroup`. ### `runtime.hostGroups[].namespace` Introduced in 2.1. Namespace to deploy this host group's `Deployment`, `Service`, generated TLS Secret, and `ServiceAccount` into. Empty (default) deploys to the chart release's namespace. ```yaml runtime: hostGroups: - name: team-a namespace: team-a replicas: 2 ``` When you override this, ensure the namespace exists and is included in [`operator.hostNamespaces`](#operatorhostnamespaces) so the operator has the pod RBAC and informer cache access it needs to manage host pod lifecycle there. Each host's `Host.environment` will reflect the namespace where its pod runs, which is what `allowSharedHosts: false` matches against for namespace-scoped scheduling. ### `runtime.hostGroups[].http.port` Starting in 2.0.3, this value is honored by the host (it was previously hardcoded). This is the port the host's HTTP server listens on inside the pod, and the port the operator populates into each managed EndpointSlice. The upstream chart default is `9191`; the [`values.local.yaml`](https://github.com/wasmCloud/wasmCloud/blob/main/charts/runtime-operator/values.local.yaml) overlay overrides it to `80` for local development. ### `runtime.hostGroups[].webgpu.enabled` Enables the WebGPU plugin on hosts in the group. Requires a host image built with the `wasi-webgpu` feature. ### `runtime.image.tag` Starting in 2.0.3, this value defaults to the chart's `appVersion` (previously defaulted to a hardcoded tag). Leave unset to track the chart release. ## Related documentation - [Kubernetes Operator introduction](../index.mdx) — install and deploy walk-through - [Private Registries](./private-registries.mdx) — mirroring images for air-gapped deployments - [TLS: bring your own certificates](../../recipes/tls-bring-your-own-certificates.mdx) - [Expose a Workload via Kubernetes Service](../../recipes/expose-workload-via-kubernetes-service.mdx) --- ## Lightweight Deployment ### Run a complete, lightweight wasmCloud stack using K3s and Docker Compose. [K3s](https://k3s.io/) is a lightweight, CNCF-certified Kubernetes distribution from Rancher/SUSE that packages the entire control plane into a single ~60MB binary. It starts in seconds, requires around 512MB of RAM, and runs on Linux, macOS, and Windows via Docker, making it practical for development, edge deployments, and CI/CD pipelines where a full Kubernetes cluster would be excessive. The wasmCloud repository includes a ready-to-use K3s setup that spins up the entire wasmCloud platform—Kubernetes, NATS, the operator, gateway, and a host—with a single `docker compose up`. :::note[Runtime Gateway deprecated in 2.0.3] The Compose setup described on this page still uses the **Runtime Gateway** to accept HTTP traffic on port 80. The gateway is deprecated as of 2.0.3 and will be removed in a future release; on a full Kubernetes cluster, HTTP routing is now handled by the operator via EndpointSlices tied to standard Kubernetes Services ([see recipe](../../recipes/expose-workload-via-kubernetes-service.mdx)). The gateway remains in this example because Docker Compose doesn't implement the Kubernetes Service/EndpointSlice model itself. ::: ## When to use K3s | Use case | Why K3s works well | |----------|-------------------| | **Local development** | Run a production-equivalent stack on your laptop without cloud resources | | **Integration testing** | Spin up and tear down a real Kubernetes cluster in CI/CD pipelines | | **Edge deployments** | Deploy full Kubernetes on resource-constrained hardware at the edge | | **Learning** | Experiment with the wasmCloud operator without provisioning cloud infrastructure | For production clusters at scale, we typically recommend using a managed Kubernetes service or a full k8s distribution and deploying via the [Helm chart](https://github.com/wasmCloud/wasmCloud/tree/main/charts/runtime-operator). ## What the example runs The K3s setup (`deploy/k3s` in the `wasmCloud/wasmCloud` repository) runs five containers via Docker Compose: | Service | Image | Description | |---------|-------|-------------| | `kubernetes` | `rancher/k3s` | K3s Kubernetes control plane | | `nats` | `nats:2-alpine` | NATS with JetStream (messaging backbone and object storage) | | `operator` | `ghcr.io/wasmcloud/runtime-operator:{{WASMCLOUD_VERSION}}` | wasmCloud operator (watches CRDs, schedules workloads) | | `gateway` | `ghcr.io/wasmcloud/runtime-gateway:{{WASMCLOUD_VERSION}}` | HTTP ingress — routes requests to workloads (port 80) | | `wash-host` | `ghcr.io/wasmcloud/wash:{{WASMCLOUD_VERSION}}` | A [washlet](../../runtime/washlet.mdx) — the cluster-connected runtime host | The `kubernetes` container automatically loads wasmCloud CRDs from the operator chart on first start, and writes a kubeconfig to `tmp/kubeconfig.yaml` for local use. ## Prerequisites - [Docker](https://docs.docker.com/get-docker/) with Docker Compose v2 - [`kubectl`](https://kubernetes.io/docs/tasks/tools/#kubectl) - [`wash` CLI](../../wash/index.mdx) ## Setup **1. Clone the repository and navigate to the k3s directory:** ```shell git clone https://github.com/wasmCloud/wasmCloud.git cd wasmCloud/deploy/k3s ``` **2. Start the stack:** ```shell docker compose up ``` This starts K3s, NATS, the operator, gateway, and a host. The first run takes a moment as the K3s node initializes and CRDs are applied. When ready, you'll see the operator and wash-host connect to NATS. **3. Export the kubeconfig:** ```shell export KUBECONFIG=$PWD/tmp/kubeconfig.yaml ``` **4. Verify the stack:** ```shell kubectl get host ``` ``` NAME HOSTID HOSTGROUP READY AGE happy-dancer-2971 cf1ee307-cf74-4f69-b92f-a9eb593e478b default True 3m16s ``` A host in the `default` host group with `READY: True` means the washlet registered successfully with the operator. ## Deploying a workload With the stack running, deploy a Wasm workload using a `WorkloadDeployment` manifest: ```yaml # workload.yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: hello-world namespace: default spec: replicas: 1 template: spec: hostSelector: hostgroup: default components: - name: hello-world image: ghcr.io/wasmcloud/components/hello-world:0.1.0 poolSize: 5 hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: host: localhost ``` ```shell kubectl apply -f workload.yaml ``` Check that the workload reaches `READY: True`: ```shell kubectl get workloaddeployment kubectl get workload ``` ``` NAME REPLICAS READY hello-world 1 True ``` **Access the workload via the gateway:** ```shell curl http://localhost ``` The gateway routes all incoming HTTP on port 80 to workloads running on the host. ## Practical considerations ### Port mappings The Docker Compose setup exposes three ports to your local machine: | Port | Service | Use | |------|---------|-----| | `6443` | K3s API server | `kubectl` access | | `4222` | NATS | Direct NATS access (for debugging) | | `80` | Gateway | HTTP requests to workloads | The `wash-host` container's internal HTTP port (8080) is not exposed to the host machine: all external HTTP traffic flows through the gateway on port 80. ### Adding more hosts To scale out, add additional `wash-host` entries to `docker-compose.yml`: ```yaml wash-host-2: image: ghcr.io/wasmcloud/wash:{{WASMCLOUD_VERSION}} command: host --scheduler-nats-url nats://nats:4222 --data-nats-url nats://nats:4222 --http-addr 0.0.0.0:8080 --host-group default --host-name wash-host-2 depends_on: nats: condition: service_healthy ``` Each host registers separately with the operator and is available for workload scheduling. ### Host groups The example assigns all hosts to the `default` host group. WorkloadDeployments target a host group via `spec.template.spec.hostSelector.hostgroup`. To run workloads on specific hosts, create multiple host groups and configure your workloads accordingly. ### Teardown ```shell docker compose down -v ``` The `-v` flag removes the persistent volumes for K3s and NATS state, giving you a clean slate on the next `docker compose up`. ## Keep reading - [CRDs](../crds.mdx) - Full reference for wasmCloud custom resources - [Cluster Hosts (Washlet)](../../runtime/washlet.mdx) - How to run a wasmCloud host as a cluster node connected to the operator mesh - [deploy/k3s source](https://github.com/wasmCloud/wasmCloud/tree/main/deploy/k3s) - The example files in the `wasmCloud/wasmCloud` repository --- ## Operator Overview There are three primary deployments that comprise wasmCloud in a Kubernetes cluster: 1. **wasmCloud Operator** — watches workload CRDs, schedules Wasm workloads onto hosts, and manages EndpointSlices for user-defined Services 2. **Host Group** — a pool of pods running [cluster hosts (washlets)](../../runtime/washlet.mdx) that execute Wasm components 3. **NATS** — message broker providing the control-plane transport between the operator and hosts :::note[Runtime Gateway deprecated in 2.0.3] Earlier releases included a separate **Runtime Gateway** deployment that proxied HTTP traffic to workloads. The gateway is deprecated as of 2.0.3 and is scheduled for removal. HTTP routing is now handled natively by Kubernetes Services: the operator manages an EndpointSlice for each Service referenced by a workload, so requests arriving through standard Kubernetes Service DNS reach the right host pods without any wasmCloud-specific ingress. The gateway pod is still installed by the chart for backwards compatibility; set `gateway.enabled: false` to skip it. ::: ## Architecture ![Workload diagram](../../images/operator-gateway.webp) ## wasmCloud operator The wasmCloud operator (`runtime-operator`) is the control-plane entity that watches for wasmCloud [CRDs](../crds.mdx) and reconciles the desired state. Its responsibilities include: - **Watching CRDs**: Monitors `WorkloadDeployment`, `WorkloadReplicaSet`, `Workload`, `Host`, and `Artifact` resources across all (or configured) namespaces. - **Scheduling workloads**: Reads the `WorkloadDeployment` spec and selects a `Host` that matches the `hostSelector` criteria, then creates the appropriate child resources to run the Wasm component. - **EndpointSlice management**: For workloads that reference a Kubernetes Service via `spec.kubernetes.service.name`, the operator creates and maintains an EndpointSlice pointing to the pod IPs of the hosts running the workload. It also registers Service DNS aliases with the host's HTTP router so requests arriving via cluster DNS reach the correct component. - **Host communication**: Sends workload start/stop requests and polls host health over NATS, using the `runtime.host..` subject pattern (e.g. `runtime.host..workload.start`). - **Status reporting**: Updates the `status` subresource of each CRD to reflect whether scheduling succeeded or failed, and surfaces Kubernetes events for observability. - **Leader election**: When enabled and running with multiple replicas, uses a `coordination.k8s.io/v1` Lease in the operator's own namespace to elect a single active instance and avoid split-brain reconciliation. - **Metrics endpoint**: Exposes a `/metrics` endpoint (Prometheus format) protected by Kubernetes token review and subject access review, suitable for scraping by monitoring tools. The operator runs with the `wasmcloud-runtime-operator` ServiceAccount. See [Roles and Role Bindings](./roles-and-rolebindings.mdx) for the full set of permissions required. ## Host group The host group is a `Deployment` of pods, each running [cluster hosts (washlets)](../../runtime/washlet.mdx). A host group provides the sandboxed execution environment for WebAssembly components. Key characteristics: - **Multiple hosts per group**: Multiple pods can form a single host group, allowing the operator to spread or replicate Wasm workloads across them. - **Host labels**: Each pod is labelled (e.g., `hostgroup: default`) so that `WorkloadDeployment` manifests can use `hostSelector` to target a specific group. - **Isolation**: Each host is an isolated sandbox; components on different hosts do not share memory or state. - **Extensibility**: You can build custom host images that include [host plugins](../../glossary.mdx#host-plugin) to extend the capabilities available to Wasm components. By default, the Helm chart installs three host pods in the `default` host group. ## NATS NATS is the message broker that carries all control-plane traffic between the wasmCloud Operator and wasmCloud hosts. Key roles include: - **Operator ↔ host RPC**: The operator sends workload start, stop, and status requests to individual hosts via NATS subjects (`runtime.host..workload.start`, `runtime.host..workload.stop`, etc.). - **Host self-registration**: Each host pod publishes periodic `HostHeartbeat` messages to `runtime.operator.heartbeat.`. The operator subscribes to these to discover hosts and create or update their `Host` CRDs. - **JetStream**: Provides built-in object storage used by the platform. The Helm chart bundles NATS with JetStream enabled (`nats.enabled: true` by default). To use an existing NATS cluster, set `nats.enabled: false` and configure the operator and hosts to point at your endpoint. ## Request flow When a `WorkloadDeployment` that references a Kubernetes Service is applied: 1. The **wasmCloud operator** detects the new CRD, selects a matching host from the host group, and sends a workload start request to the host **via NATS** (`runtime.host..workload.start`). 2. The operator injects the Service's DNS aliases (e.g. `my-svc`, `my-svc.default`, `my-svc.default.svc`) into the host's HTTP router so incoming requests with a matching `Host` header route to this component. 3. The operator creates and maintains an **EndpointSlice** owned by the referenced Service, populated with the pod IPs of the hosts running the workload. 4. Incoming HTTP requests resolve the Service's ClusterIP via standard Kubernetes DNS and are forwarded by kube-proxy to the host pod IPs in the EndpointSlice, where the **wash runtime** executes the Wasm component. See the [Expose a Workload via Kubernetes Service](../../recipes/expose-workload-via-kubernetes-service.mdx) recipe for a step-by-step walk-through. ## Related documentation - [Workload Security](../workload-security.mdx) — the WebAssembly sandbox model, `allowedHosts`, and Kubernetes NetworkPolicy for host pods - [Custom Resource Definitions (CRDs)](../crds.mdx) — describes the `WorkloadDeployment`, `Host`, `Workload`, and other resources - [Helm Values Reference](./helm-values.mdx) — commonly-overridden configuration values - [Roles and Role Bindings](./roles-and-rolebindings.mdx) — details the RBAC permissions required by the operator - [Filesystems and Volumes](./filesystems-and-volumes.mdx) — how to mount volumes into host pods - [Secrets and Configuration Management](./secrets-and-configuration.mdx) — supplying environment variables, ConfigMaps, and Secrets to components - [Private Registries](./private-registries.mdx) — how to pull Wasm component images from private OCI registries - [CI/CD](./cicd.mdx) — integrating wasmCloud workload deployments into CI/CD pipelines --- ## Private Registries and Air-Gapped Deployments Some environments restrict access to public container registries like `ghcr.io` and `docker.io`. In these cases, you need to mirror the required container images to your private registry before deploying. This guide covers how to mirror the images required by the wasmCloud Helm chart and configure the deployment to use your private registry. ## Required images The wasmCloud `runtime-operator` Helm chart uses the following container images: | Image | Component | |---|---| | `ghcr.io/wasmcloud/runtime-operator:` | Operator | | `ghcr.io/wasmcloud/runtime-gateway:` | Gateway | | `ghcr.io/wasmcloud/wash:` | Runtime host | | `docker.io/nats:` | NATS server (optional) | :::tip To find the exact image tags for a given chart version, run: ```shell helm show values oci://ghcr.io/wasmcloud/charts/runtime-operator --version ``` ::: For example, with version `{{WASMCLOUD_VERSION}}`, the images are: - `ghcr.io/wasmcloud/runtime-operator:{{WASMCLOUD_VERSION}}` - `ghcr.io/wasmcloud/runtime-gateway:{{WASMCLOUD_VERSION}}` - `ghcr.io/wasmcloud/wash:{{WASMCLOUD_VERSION}}` - `docker.io/nats:2.11.3-alpine` If you disable the built-in NATS server (`--set nats.enabled=false`), you do not need to mirror the NATS image. ## Mirror images to your registry ### Prerequisites - [oras](https://oras.land) CLI - [yq](https://github.com/mikefarah/yq) and [jq](https://jqlang.github.io/jq/) - Authenticated to both the source registries (`ghcr.io`, `docker.io`) and your destination registry ### Create an image manifest Create a `mirror.yaml` file listing the source and destination for each image. Replace `myregistry` with your private registry: ```yaml images: # wasmCloud runtime images - source: ghcr.io/wasmcloud/runtime-operator:{{WASMCLOUD_VERSION}} destination: myregistry/wasmcloud/runtime-operator:{{WASMCLOUD_VERSION}} - source: ghcr.io/wasmcloud/runtime-gateway:{{WASMCLOUD_VERSION}} destination: myregistry/wasmcloud/runtime-gateway:{{WASMCLOUD_VERSION}} - source: ghcr.io/wasmcloud/wash:{{WASMCLOUD_VERSION}} destination: myregistry/wasmcloud/wash:{{WASMCLOUD_VERSION}} # Third-party images - source: docker.io/nats:2.11.3-alpine destination: myregistry/nats:2.11.3-alpine ``` ### Run the mirror script Create and run `mirror.sh`: ```bash #!/bin/bash CONFIG_FILE="mirror.yaml" if ! command -v oras >/dev/null 2>&1; then echo "oras not found. Please install: https://oras.land" exit 1 fi echo "Mirroring images from $CONFIG_FILE..." yq -o=json e '.images[]' "$CONFIG_FILE" | jq -c '.' | while read -r line; do src=$(echo "$line" | jq -r '.source') dst=$(echo "$line" | jq -r '.destination') if [[ -n "$src" && -n "$dst" && "$dst" != "null" ]]; then echo "Copying $src -> $dst" oras copy "$src" "$dst" fi done ``` ```shell chmod +x ./mirror.sh ./mirror.sh ``` ## Deploy with your private registry The Helm chart provides a `global.image.registry` value that overrides the registry for all component images at once. ### Basic install with global registry override ```shell helm install wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \ --version {{WASMCLOUD_VERSION}} \ --namespace wasmcloud \ --create-namespace \ --set global.image.registry="myregistry" ``` ### With an image pull secret If your private registry requires authentication, create a Kubernetes pull secret and reference it in the Helm install: ```shell kubectl create namespace wasmcloud kubectl create secret docker-registry my-registry-secret \ --namespace wasmcloud \ --docker-server=myregistry \ --docker-username= \ --docker-password= ``` Then install with the pull secret: ```shell helm install wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \ --version {{WASMCLOUD_VERSION}} \ --namespace wasmcloud \ --create-namespace \ --set global.image.registry="myregistry" \ --set global.image.pullSecrets[0].name="my-registry-secret" ``` ### Per-component registry overrides If you need to mirror images to different registry paths, you can override each component individually: ```shell helm install wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \ --version {{WASMCLOUD_VERSION}} \ --namespace wasmcloud \ --create-namespace \ --set operator.image.registry="myregistry" \ --set gateway.image.registry="myregistry" \ --set runtime.image.registry="myregistry" \ --set nats.image.registry="myregistry" ``` ## Rendering manifests with `helm template` If your organization uses a GitOps workflow or requires manifests to be reviewed before applying, you can use `helm template` to render the Kubernetes manifests locally without installing anything to the cluster. ### Render to stdout ```shell helm template wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \ --version {{WASMCLOUD_VERSION}} \ --namespace wasmcloud \ --set global.image.registry="myregistry" \ --set global.image.pullSecrets[0].name="my-registry-secret" ``` ### Render to a directory Write the rendered manifests to a directory for review or to check into version control: ```shell helm template wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \ --version {{WASMCLOUD_VERSION}} \ --namespace wasmcloud \ --set global.image.registry="myregistry" \ --set global.image.pullSecrets[0].name="my-registry-secret" \ --output-dir ./wasmcloud-manifests ``` This creates a `wasmcloud-manifests/` directory with one file per Kubernetes resource. You can then apply them with: ```shell kubectl apply --recursive -f ./wasmcloud-manifests ``` ### Verify image references After rendering, you can verify that all image references point to your private registry: ```shell helm template wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \ --version {{WASMCLOUD_VERSION}} \ --namespace wasmcloud \ --set global.image.registry="myregistry" \ | grep "image:" ``` You should see all images prefixed with your registry and no references to `ghcr.io` or `docker.io`. ### Use a values file For repeatable deployments, store your overrides in a values file rather than passing `--set` flags: ```yaml title="my-values.yaml" global: image: registry: myregistry pullSecrets: - name: my-registry-secret ``` Then reference it with `-f`: ```shell helm template wasmcloud-runtime oci://ghcr.io/wasmcloud/charts/runtime-operator \ --version {{WASMCLOUD_VERSION}} \ --namespace wasmcloud \ -f my-values.yaml ``` This same values file works with `helm install` and `helm upgrade`. :::note[Using Kustomize?] If your team uses [Kustomize](https://kustomize.io/) to manage Kubernetes manifests, you can combine it with `helm template`. Render the chart to a directory as shown above, then use Kustomize to apply additional patches, labels, or transformations on top of the rendered output. For example, you can use a Kustomize [`images` transformer](https://kubectl.docs.kubernetes.io/references/kustomize/builtins/#_imagetagtransformer_) to rewrite image references, or layer environment-specific overlays on top of a shared base. A minimal setup looks like: ```yaml title="kustomization.yaml" resources: - ./wasmcloud-manifests/runtime-operator/templates images: - name: ghcr.io/wasmcloud/runtime-operator newName: myregistry/wasmcloud/runtime-operator - name: ghcr.io/wasmcloud/runtime-gateway newName: myregistry/wasmcloud/runtime-gateway - name: ghcr.io/wasmcloud/wash newName: myregistry/wasmcloud/wash - name: docker.io/nats newName: myregistry/nats ``` ```shell kubectl apply -k . ``` This approach is particularly useful when you need to manage multiple environments (staging, production) with different registries or configurations. ::: ## Helm values reference These are the image-related values you can configure: | Value | Default | Description | |---|---|---| | `global.image.registry` | `""` | Override registry for all images | | `global.image.pullSecrets` | `[]` | Global image pull secrets | | `operator.image.registry` | `ghcr.io` | Operator image registry | | `operator.image.repository` | `wasmcloud/runtime-operator` | Operator image repository | | `operator.image.tag` | Chart `appVersion` | Operator image tag | | `gateway.image.registry` | `ghcr.io` | Gateway image registry | | `gateway.image.repository` | `wasmcloud/runtime-gateway` | Gateway image repository | | `runtime.image.registry` | `ghcr.io` | Runtime host image registry | | `runtime.image.repository` | `wasmcloud/wash` | Runtime host image repository | | `runtime.image.tag` | Chart `appVersion` | Runtime host image tag (defaulted to the chart's `appVersion` starting in 2.0.3) | | `nats.image.registry` | `docker.io` | NATS image registry | | `nats.image.repository` | `nats` | NATS image repository | | `nats.image.tag` | `2.11.3-alpine` | NATS image tag | --- ## Roles and Role Bindings ## Overview The wasmCloud Operator manages Workload Deployments by interacting with the [CRDs](../crds.mdx) inside the Kubernetes cluster. With WorkloadDeployments being created in various Namespaces, the Operator needs to have permission to update CRDs, as well as permission for interacting with the Kubernetes API. This is accomplished by creating Roles and Role Bindings (for Namespace-level permissions), and Cluster Roles and Cluster Role Bindings (for Cluster-wide permissions). wasmCloud is deployed with two Service Accounts: 1. `wasmcloud-runtime-operator`: Used by the runtime-operator 2. `wasmcloud-runtime-operator-gateway`: Used by the Gateway deployment that manages ingress traffic to the Wasm Workloads Each Service Account has a set of Roles and Role Bindings defined. For the `wasmcloud-runtime-operator` Service Account: ### 1. ClusterRole: `wasmcloud-runtime-operator` The main operator role. Full CRUD on wasmCloud CRDs and read access to core resources. | API Group | Resources | Verbs | |---|---|---| | `runtime.wasmcloud.dev` | `artifacts`, `hosts`, `workloads`, `workloadreplicasets`, `workloaddeployments` | create, delete, get, list, patch, update, watch | | `runtime.wasmcloud.dev` | `artifacts/status`, `hosts/status`, `workloads/status`, `workloadreplicasets/status`, `workloaddeployments/status` | get, update, patch | | `""` (core) | `configmaps`, `secrets`, `namespaces` | get, list, watch | | `""` (core) | `events` | create, get, list, patch, update, watch | ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: wasmcloud-runtime-operator rules: - apiGroups: ["runtime.wasmcloud.dev"] resources: ["artifacts", "hosts", "workloads", "workloadreplicasets", "workloaddeployments"] verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] - apiGroups: ["runtime.wasmcloud.dev"] resources: ["artifacts/status", "hosts/status", "workloads/status", "workloadreplicasets/status", "workloaddeployments/status"] verbs: ["get", "update", "patch"] - apiGroups: [""] resources: ["configmaps", "secrets", "namespaces"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["events"] verbs: ["create", "get", "list", "patch", "update", "watch"] ``` ### 2. ClusterRole: `wasmcloud-runtime-operator-metrics-auth` — Cluster-wide Allows the operator to perform authentication/authorization checks (used to protect the metrics endpoint). | API Group | Resources | Verbs | |---|---|---| | `authentication.k8s.io` | `tokenreviews` | create | | `authorization.k8s.io` | `subjectaccessreviews` | create | ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: wasmcloud-runtime-operator-metrics-auth rules: - apiGroups: ["authentication.k8s.io"] resources: ["tokenreviews"] verbs: ["create"] - apiGroups: ["authorization.k8s.io"] resources: ["subjectaccessreviews"] verbs: ["create"] ``` ### 3. ClusterRole: `wasmcloud-runtime-operator-metrics-reader` — Cluster-wide **Note:** This ClusterRole exists but has **no ClusterRoleBinding** to any ServiceAccount. It grants: | Resource | Verbs | |---|---| | Non-resource URL `/metrics` | get | This is intended to be bound to monitoring tools (e.g., Prometheus) separately. ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: wasmcloud-runtime-operator-metrics-reader rules: - nonResourceURLs: ["/metrics"] verbs: ["get"] ``` ### 4. Role: `wasmcloud-runtime-operator-leader-election` — Namespace-scoped (`default` only) Manages leader election leases so only one operator replica is active at a time. | API Group | Resources | Verbs | |---|---|---| | `coordination.k8s.io` | `leases` | get, list, watch, create, update, patch, delete | ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: wasmcloud-runtime-operator-leader-election namespace: default rules: - apiGroups: ["coordination.k8s.io"] resources: ["leases"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] ``` --- For the `wasmcloud-runtime-operator-gateway` Service Account: ### 1. ClusterRole: `wasmcloud-runtime-operator-gateway` — Cluster-wide Read-only access to wasmCloud hosts and workloads. This is the gateway/API component: it can observe state but cannot modify it. | API Group | Resources | Verbs | |---|---|---| | `runtime.wasmcloud.dev` | `hosts`, `workloads` | get, list, watch | | `runtime.wasmcloud.dev` | `hosts/status`, `workloads/status` | get | ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: wasmcloud-runtime-operator-gateway rules: - apiGroups: ["runtime.wasmcloud.dev"] resources: ["hosts", "workloads"] verbs: ["get", "list", "watch"] - apiGroups: ["runtime.wasmcloud.dev"] resources: ["hosts/status", "workloads/status"] verbs: ["get"] ``` ## Restricting access of the Operator to Namespaces In some cases, it may be desired to restrict the Operator to only processing Workloads deployed in certain namespaces. This can be achieved by modifying the ClusterRole `wasmcloud-runtime-operator` to only have the following permissions: ### ClusterRole: `wasmcloud-runtime-operator` | API Group | Resources | Verbs | Reason | |---|---|---|---| | `""` (core) | `namespaces` | get, list, watch | Required to monitor and schedule Workloads | | `""` (core) | `events` | create, get, list, patch, update, watch | Used to notify of progress | | `runtime.wasmcloud.dev` | `hosts` | create, delete, get, list, patch, update, watch | | `runtime.wasmcloud.dev` | `hosts/status` | get, update, patch | ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: wasmcloud-runtime-operator rules: - apiGroups: [""] resources: ["namespaces"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["events"] verbs: ["create", "get", "list", "patch", "update", "watch"] - apiGroups: ["runtime.wasmcloud.dev"] resources: ["hosts"] verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] - apiGroups: ["runtime.wasmcloud.dev"] resources: ["hosts/status"] verbs: ["get", "update", "patch"] ``` ### Namespace Role: `wasmcloud-runtime-operator` Note: Add this to each namespace a Workload is deployed Next, create a role in each Namespace that has the Workloads deployed, and grant the permissions to the runtime-operator: | API Group | Resources | Verbs | Reason | |---|---|---|---| | `runtime.wasmcloud.dev` | `artifacts`, `hosts`, `workloads`, `workloadreplicasets`, `workloaddeployments` | create, delete, get, list, patch, update, watch | Full life-cycle control of Wasm Workloads | | `runtime.wasmcloud.dev` | `artifacts/status`, `hosts/status`, `workloads/status`, `workloadreplicasets/status`, `workloaddeployments/status` | get, update, patch | Status updates for both Workloads and Hosts | | `""` (core) | `configmaps`, `secrets` | get, list, watch | Needed if Configmaps or Secrets are mounted for a Workload | ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: wasmcloud-runtime-operator namespace: rules: - apiGroups: ["runtime.wasmcloud.dev"] resources: ["artifacts", "hosts", "workloads", "workloadreplicasets", "workloaddeployments"] verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] - apiGroups: ["runtime.wasmcloud.dev"] resources: ["artifacts/status", "hosts/status", "workloads/status", "workloadreplicasets/status", "workloaddeployments/status"] verbs: ["get", "update", "patch"] - apiGroups: [""] resources: ["configmaps", "secrets"] verbs: ["get", "list", "watch"] ``` ### Namespace Role: `wasmcloud-runtime-operator-hosts` Note: Add this to the Namespace where the Wasmcloud Hosts are deployed Finally, a role needs to be created for the namespace that has WasmCloud hosts installed: | API Group | Resources | Verbs | |---|---|---| | `runtime.wasmcloud.dev` | `artifacts`, `hosts`, `workloads`, `workloadreplicasets`, `workloaddeployments` | create, delete, get, list, patch, update, watch | | `runtime.wasmcloud.dev` | `artifacts/status`, `hosts/status`, `workloads/status`, `workloadreplicasets/status`, `workloaddeployments/status` | get, update, patch | | `""` (core) | `configmaps`, `secrets` | get, list, watch | ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: wasmcloud-runtime-operator-hosts namespace: rules: - apiGroups: ["runtime.wasmcloud.dev"] resources: ["artifacts", "hosts", "workloads", "workloadreplicasets", "workloaddeployments"] verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] - apiGroups: ["runtime.wasmcloud.dev"] resources: ["artifacts/status", "hosts/status", "workloads/status", "workloadreplicasets/status", "workloaddeployments/status"] verbs: ["get", "update", "patch"] - apiGroups: [""] resources: ["configmaps", "secrets"] verbs: ["get", "list", "watch"] ``` ## Binding the Restricted Roles to the Operator ServiceAccount Once the roles above are created, they must be bound to the `runtime-operator` ServiceAccount. The examples below assume the ServiceAccount lives in the `wasmcloud` namespace. ### ClusterRoleBinding: `wasmcloud-runtime-operator` Grants the operator cluster-wide access to namespaces and events (as defined in the restricted ClusterRole above): ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: wasmcloud-runtime-operator roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: wasmcloud-runtime-operator subjects: - kind: ServiceAccount name: runtime-operator namespace: wasmcloud ``` ### RoleBinding: `wasmcloud-runtime-operator` (per workload namespace) Repeat this for each namespace where Workloads are deployed, substituting `` with the target namespace: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: wasmcloud-runtime-operator namespace: roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: wasmcloud-runtime-operator subjects: - kind: ServiceAccount name: runtime-operator namespace: wasmcloud ``` ## Related documentation - [Workload Security](../workload-security.mdx) — the WebAssembly sandbox model, `allowedHosts`, and Kubernetes NetworkPolicy for securing what workloads can do - [Secrets and Configuration Management](./secrets-and-configuration.mdx) — RBAC considerations for restricting access to Kubernetes Secrets used by components ### RoleBinding: `wasmcloud-runtime-operator-hosts` (hosts namespace) Apply this once in the namespace where WasmCloud Hosts are deployed, substituting `` with the appropriate namespace: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: wasmcloud-runtime-operator-hosts namespace: roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: wasmcloud-runtime-operator-hosts subjects: - kind: ServiceAccount name: runtime-operator namespace: wasmcloud ``` --- ## Secrets and Configuration Management wasmCloud components receive configuration through the `localResources.environment` field in a `Workload` or `WorkloadDeployment` manifest. This field supports three sources that can be used individually or combined: inline key-value pairs, Kubernetes ConfigMaps, and Kubernetes Secrets. In this section of the Operator Manual, you'll learn how to: - Supply inline configuration to a component - Reference ConfigMaps and Secrets from a workload manifest - Combine multiple configuration sources ## Inline configuration All values supplied through `localResources.environment` — regardless of source — are delivered to the component as POSIX-style environment variables via the [`wasi:cli/environment`](https://github.com/WebAssembly/wasi-cli) interface. Components read them the same way they would read any environment variable in their language of choice. The simplest approach is to supply key-value pairs directly in the manifest under `localResources.environment.config`: ```yaml components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 localResources: environment: config: LOG_LEVEL: info REGION: us-east-1 ``` Inline values are suitable for non-sensitive configuration that is safe to store in version control. ## ConfigMaps To supply configuration from a Kubernetes ConfigMap, reference it by name under `configFrom`: ```yaml components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 localResources: environment: configFrom: - name: app-config ``` Each key-value pair in the referenced ConfigMap is delivered to the component as an environment variable. Multiple ConfigMaps may be listed; if the same key appears in more than one, the last entry in the list takes precedence. ## Secrets To supply sensitive values from a Kubernetes Secret, reference it by name under `secretFrom`: ```yaml components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 localResources: environment: secretFrom: - name: db-credentials ``` Secret values are delivered to the component as plain strings — no decoding is required in your component code. Multiple Secrets may be listed, and the last entry wins on key conflicts. :::tip The `secretFrom` field only accepts Secret names; values are never embedded in the manifest. Use Kubernetes RBAC to restrict which service accounts and users can read the referenced Secret resources. ::: ## Combining sources All three sources can be combined in a single component definition. When the same key appears in multiple sources, the order of precedence (lowest to highest) is: `config` → `configFrom` → `secretFrom`. ```yaml components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 localResources: environment: config: LOG_LEVEL: debug configFrom: - name: app-config secretFrom: - name: db-credentials ``` All keys from `app-config` and `db-credentials` are merged with the inline values. Note that precedence is determined by source type, not by the order fields appear in the YAML: `secretFrom` always wins over `configFrom`, which always wins over `config`. ## Complete example A `WorkloadDeployment` using all three configuration sources: ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: my-app namespace: default spec: replicas: 2 template: spec: hostSelector: hostgroup: default components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 localResources: environment: config: LOG_LEVEL: info # inline values (lowest precedence) configFrom: - name: app-config # non-sensitive config from a ConfigMap secretFrom: - name: db-credentials # sensitive values from a Secret (highest precedence) hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: address: '0.0.0.0:8080' ``` ## Related documentation - [Custom Resource Definitions](../crds.mdx) — field reference for `localResources`, `hostInterfaces` (including backend config and `allowedHosts`), and other CRD fields - [Roles and Role Bindings](./roles-and-rolebindings.mdx) — RBAC configuration for the operator and host pods - [Filesystems and Volumes](./filesystems-and-volumes.mdx) — mounting volume data into components via `localResources.volumeMounts` - [Private Registries](./private-registries.mdx) — managing image pull secrets for private OCI registries --- ## Workload Security wasmCloud workload security operates in two complementary layers: - **The WebAssembly sandbox** — enforced by the [Wasmtime](https://wasmtime.dev/) runtime, which provides strong isolation guarantees for every component by default - **Kubernetes controls** — NetworkPolicy for host pods, and RBAC for the operator and gateway ServiceAccounts This page covers the sandbox model, `allowedHosts`, and NetworkPolicy. For RBAC configuration, see [Roles and Role Bindings](./operator-manual/roles-and-rolebindings.mdx). ## The WebAssembly sandbox Every wasmCloud component runs inside the [Wasmtime](https://wasmtime.dev/) WebAssembly runtime, which provides strong, runtime-enforced isolation regardless of what the component code does: - **Memory isolation** — each component instance has its own linear memory. One component cannot read or write another's memory, and cannot access the host process's memory. - **No implicit system access** — components cannot open files, make network connections, read environment variables, or call system APIs unless the host explicitly provides an implementation of the corresponding WASI interface. - **Capability-based access** — a component can only use a capability if it declares the relevant WIT interface import *and* the host has a matching plugin bound to that interface. Undeclared capabilities are structurally unreachable, not merely blocked at runtime. In practice this means the security posture of a component is determined by what it imports, what the operator mounts via `localResources`, and what `hostInterfaces` are bound to it. A component with no `hostInterfaces` and no `localResources` has no access to anything outside its own computation. :::tip Use `wash inspect ` to see exactly which interfaces a component imports before deploying it. ::: ## Restricting outbound HTTP with `allowedHosts` When a component has the `wasi:http/outgoing-handler` interface bound, it can make outbound HTTP requests. Use `allowedHosts` to restrict which hosts it may call: ```yaml components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 localResources: allowedHosts: - api.example.com - storage.googleapis.com - "*.s3.amazonaws.com" ``` Entries are matched case-insensitively against the request's host (no scheme, no path). A leading wildcard like `*.example.com` matches any subdomain but not the bare `example.com` — list both if you need both. When `allowedHosts` is non-empty, any outbound HTTP request whose host doesn't match an entry is blocked by the host before it leaves the process. This is enforced at the wasmCloud level, independently of any Kubernetes NetworkPolicy. If `allowedHosts` is empty or the field is omitted, all outbound HTTP requests are allowed — the allowlist only takes effect when at least one entry is present. :::note `allowedHosts` controls outbound HTTP calls made via `wasi:http/outgoing-handler`. It does not restrict network access that may be available through other host interfaces that are explicitly bound to the component. ::: ## Kubernetes NetworkPolicy for host pods The wasmCloud Helm chart does not include a `NetworkPolicy` by default. In environments where network isolation is required, you can apply one manually. Host pods carry the labels `wasmcloud.com/hostgroup: ` and `wasmcloud.com/name: hostgroup`. A baseline policy that allows only the traffic the host pods need looks like this: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: wasmcloud-hostgroup namespace: default spec: podSelector: matchLabels: wasmcloud.com/name: hostgroup policyTypes: - Ingress - Egress ingress: # Allow the Runtime Gateway to forward HTTP requests to host pods - from: - podSelector: matchLabels: wasmcloud.com/name: runtime-gateway ports: - port: 80 egress: # Allow outbound to NATS for control-plane communication - to: - podSelector: matchLabels: wasmcloud.com/name: nats ports: - port: 4222 # Allow DNS resolution - ports: - port: 53 protocol: UDP ``` :::note The exact pod label selectors depend on your Helm release name and values. Verify the labels on your gateway and NATS pods before applying this policy: ```shell kubectl get pods --show-labels -n ``` ::: If your components make outbound HTTP calls via `wasi:http/outgoing-handler`, you will also need to add egress rules for ports 80 and 443 to allow those requests to leave the host pod. Combine this with `allowedHosts` for defense in depth — NetworkPolicy enforces the Kubernetes-level boundary, while `allowedHosts` enforces the wasmCloud-level boundary. ## Related documentation - [Roles and Role Bindings](./operator-manual/roles-and-rolebindings.mdx) — RBAC for the operator and gateway ServiceAccounts, including namespace-scoped restrictions - [Secrets and Configuration Management](./operator-manual/secrets-and-configuration.mdx) — supplying sensitive values to components via Kubernetes Secrets - [Custom Resource Definitions](./crds.mdx) — full field reference for `allowedHosts`, `hostInterfaces`, and other workload controls --- ## Migration to v2 ### Migrating to wasmCloud v2 reduces operational complexity and dramatically improves performance. This guide walks through the architectural differences between wasmCloud v1 and v2, the benefits of migrating, and practical steps for bringing your v1 workloads to v2. ## What changed and why wasmCloud v1 introduced a powerful model for distributed WebAssembly, but operational experience revealed several areas for improvement: - **The v1 wasmCloud Application Deployment Manager (`wadm`) required its own StatefulSet and persistent JetStream storage**, adding operational surface area and failure modes to every deployment. - **The `wadm` reconciler had resiliency issues**—eventually-consistent reconciliation could become stale or divergent, requiring manual intervention during degraded NATS scenarios. - **OAM (the Open Application Model) was deprecated**, creating friction for teams using modern OCI supply chain security. - **The v1 host was overburdened**, responsible for reading/writing state to JetStream, fetching OCI artifacts, managing secrets, assembling link graphs, and more. - **Capability providers added network hops for every call**, limiting performance to ~5,000 requests-per-second compared to ~30,000 RPS with in-process calls. wasmCloud v2 addresses all of these issues with a new architecture built on Kubernetes-native primitives and a simplified runtime. ## Architecture comparison ### wasmCloud v1 architecture In v1, `wadm` acted as the workload scheduler, storing state in NATS JetStream and communicating with hosts over the NATS lattice. Capability providers ran as separate external processes, and applications were defined using OAM manifests. ![wasmCloud v1 architecture](./images/v1-architecture.webp) Key characteristics of v1: - **`wadm`** managed application lifecycle using OAM manifests - **NATS JetStream** stored all scheduler state - **Capability providers** ran as separate processes, communicating over NATS - **Actors** (now called components) linked to providers over the network - **All inter-component communication** traversed the NATS lattice ### wasmCloud v2 architecture In v2, the Kubernetes operator (`runtime-operator`) replaces `wadm` as the workload scheduler. State is stored in Kubernetes `etcd`, workloads are defined using Kubernetes CRDs, and capability providers are replaced by [host plugins](./overview/hosts/plugins.mdx) and [services](./overview/workloads/services.mdx). ![wasmCloud v2 architecture](./images/v2-architecture-migration.webp) Key characteristics of v2: - **`runtime-operator`** manages workload lifecycle using Kubernetes CRDs - **Kubernetes `etcd`** stores all desired state - **Host plugins** provide capabilities in-process for dramatically better performance - **Components and services** run within a workload on the same host - NATS serves as the **control plane** between operator and hosts (not the data plane) ## Concept migration map The following diagram maps v1 concepts to their v2 equivalents: ![v1 to v2 concept migration map](./images/concept-mapping.webp) | wasmCloud v1 | wasmCloud v2 | Notes | |---|---|---| | `wadm` (scheduler) | `runtime-operator` (K8s operator) | Kubernetes-native reconciliation replaces custom reconciler | | NATS JetStream (state store) | Kubernetes `etcd` | Data stays in-cluster; no separate StatefulSet required | | OAM manifest (`wadm.yaml`) | Kubernetes CRDs (YAML) | Standard Kubernetes tooling (kubectl, Helm, ArgoCD) | | Application | [Workload](./overview/workloads/index.mdx) | Workloads consist of components and optional services | | Actors | [Components](./overview/workloads/components.mdx) | Same concept, updated terminology | | Capability providers | [Host plugins](./overview/hosts/plugins.mdx) / [Services](./overview/workloads/services.mdx) | In-process instead of over-the-network | | JWT claims (identity) | OCI attestation + SPIFFE/SPIRE | Industry-standard workload identity | | CloudEvents (observability) | K8s Events + OpenTelemetry | Native Kubernetes observability | | `wasmcloud:secrets` | `wasi:config` + K8s Secrets | Standard WASI interfaces with Kubernetes Secrets backend | | Config service | `wasi:config` + K8s ConfigMaps | Standard WASI interfaces with Kubernetes ConfigMaps backend | | Policy service | K8s admission controllers | Standard Kubernetes policy enforcement | ## Benefits of migrating ### Kubernetes-native operations wasmCloud v2 is designed to work with the tools and patterns that platform teams already know: - **`kubectl`** for managing resources - **Helm** for packaging and deploying - **ArgoCD / Flux** for GitOps workflows - **OPA / Gatekeeper** for policy enforcement - **Prometheus / OpenTelemetry** for observability There is no need for wasmCloud-specific deployment tooling. ### Performance improvement Replacing external capability providers with host plugins eliminates network hops for host-provided functionality: ![Capability provider migration paths](./images/capability-migration.webp) In v1, every call from an actor to a capability provider traversed the NATS lattice, adding latency and the possibility of message loss. In v2, host plugins serve capabilities in-process, achieving approximately **6x higher throughput** (~30,000 RPS vs. ~5,000 RPS). ### Simplified operational footprint wasmCloud v2 eliminates several components that v1 required for every deployment: - No `wadm` StatefulSet to provision and maintain - No JetStream persistent storage for scheduler state - No custom reconciler with resiliency concerns - Kubernetes `etcd` provides the state store - Kubernetes controller reconciliation provides mature, battle-tested convergence ### Improved security model wasmCloud v2 leverages Kubernetes-native security boundaries: ![v2 security and permissions model](./images/security-model.webp) - **CRD-scoped RBAC**: The operator's ClusterRole is scoped exclusively to the `runtime.wasmcloud.dev` API group - **Namespace isolation**: Workloads respect Kubernetes namespace boundaries - **Privilege separation**: Only the operator is privileged; hosts hold no Kubernetes API credentials - **Wasm sandboxing**: Components run in Wasmtime's sandbox, accessing only explicitly granted capabilities - **Audit trail**: All state changes flow through the Kubernetes API server audit log - **Network policies**: Hosts are subject to standard Kubernetes NetworkPolicies ### Data locality All desired state remains within the cluster as Kubernetes custom resources backed by `etcd`. No external coordination services, cross-cluster state synchronization, or centralized control planes are required. Each cluster operates as an autonomous unit. ## How the CRD hierarchy works wasmCloud v2 uses five Custom Resource Definitions in the `runtime.wasmcloud.dev/v1alpha1` API group, following patterns familiar from core Kubernetes resources: ![wasmCloud CRD hierarchy](./images/crd-hierarchy.webp) | wasmCloud CRD | Kubernetes Analog | Purpose | |---|---|---| | `WorkloadDeployment` | `Deployment` | Manages rollouts of WorkloadReplicaSets | | `WorkloadReplicaSet` | `ReplicaSet` | Manages replica count of Workloads | | `Workload` | `Pod` | Single schedulable unit (components + optional service) | | `Host` | `Node` | Registers capacity, receives scheduling decisions | | `Artifact` | *(no direct analog)* | OCI image resolution and caching | For most users, [`WorkloadDeployment`](./kubernetes-operator/crds.mdx#workloaddeployment) is the primary resource for deploying Wasm workloads. See the [CRD reference](./kubernetes-operator/crds.mdx) for full details and example manifests. ## How workload scheduling works in v2 The following diagram illustrates the complete flow from building a component to running it on a wasmCloud host: ![v2 workload scheduling flow](./images/scheduling-flow.webp) 1. **Developer compiles** a `.wasm` component from source using `wash build` or native toolchain 2. **Component is pushed** to an OCI registry using `wash oci push` 3. **CRDs are applied** to Kubernetes via `kubectl`, Helm, or GitOps tooling 4. **Kubernetes API server** stores the desired state in `etcd` 5. **`runtime-operator` reconciles** the desired state, creating `WorkloadReplicaSet` and `Workload` CRs 6. **Operator resolves configuration** from ConfigMaps/Secrets and selects a target Host matching the `hostSelector` 7. **Operator sends a `WorkloadStartRequest`** to the selected host via NATS 8. **Host downloads the `.wasm` component** and instantiates it in Wasmtime 9. **Host reports status**; operator updates the Workload CR status conditions ## Migration guide ### Migrating capability providers This is likely the most significant change for v1 users. In v1, capability providers were external processes that communicated over NATS. In v2, there are three approaches depending on your use case: #### Host plugins (recommended) For shared, performance-critical capabilities (HTTP serving, key-value storage, configuration), **host plugins** are the recommended approach. Plugins are built into the host and serve capabilities to many workloads with in-process performance. You can find a [basic example](https://github.com/wasmCloud/wasmCloud/tree/main/crates/wash-runtime#usage) in the `wash-runtime` repository. #### Service components For workload-specific, long-running functionality (cron jobs, connection pools, in-memory caches), use a **service component** within the workload. Services run continuously alongside components and can listen on TCP ports. For an introduction to building services, see the [Creating services](./wash/developer-guide/create-services.mdx) guide. #### Containerized providers For existing provider code that you want to migrate with minimal changes, you can **containerize the provider** and run it outside wasmCloud. This requires manually facilitating communication between your components and the provider via [wRPC](https://github.com/bytecodealliance/wrpc). ### Migrating applications to workloads The v1 "Application" abstraction has been replaced by the [Workload](./overview/workloads/index.mdx). Where a v1 application was defined in an OAM manifest, a v2 workload is defined as a Kubernetes `WorkloadDeployment` CR: **v1 (wadm.yaml using OAM):** ```yaml apiVersion: core.oam.dev/v1beta1 kind: Application metadata: name: hello-world annotations: version: v0.0.1 spec: components: - name: http-component type: component properties: image: ghcr.io/wasmcloud/components/http-hello-world:0.1.0 traits: - type: spreadscaler properties: instances: 3 - type: linkdef properties: target: httpserver values: address: 0.0.0.0:8080 - name: httpserver type: capability properties: image: ghcr.io/wasmcloud/http-server:0.21.0 ``` **v2 (Kubernetes CRD):** ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: hello-world namespace: default spec: replicas: 3 template: spec: hostSelector: hostgroup: default components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 poolSize: 10 hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: address: '0.0.0.0:8080' ``` Key differences: - No separate capability provider component—HTTP serving is provided by a **host plugin** - No link definitions—host interfaces are declared in the workload spec - Replicas are managed by the `WorkloadDeployment`, following the same pattern as Kubernetes `Deployment` - Standard Kubernetes `hostSelector` replaces OAM spread scalers ### Migrating secrets In v1, the `wasmcloud:secrets` interface provided secrets access through a custom protocol. In v2, secrets are managed through standard Kubernetes Secrets and accessed via the `wasi:config` interface: ```yaml # v2: Secrets via Kubernetes Secrets components: - name: my-component image: ghcr.io/my-org/my-component:0.1.0 localResources: environment: secretFrom: - name: my-k8s-secret ``` ### Migrating configuration The v1 config service is replaced by `wasi:config` backed by Kubernetes ConfigMaps: ```yaml # v2: Configuration via ConfigMaps components: - name: my-component image: ghcr.io/my-org/my-component:0.1.0 localResources: environment: configFrom: - name: my-configmap config: LITERAL_KEY: literal_value ``` ### Migrating distributed networking In v1, components communicated over the network automatically through the NATS lattice. In v2, networking is [intentionally more explicit](./faq.mdx#how-do-distributed-applications-work-in-wasmcloud-v2). Components within the same workload communicate in-process. When distributed communication is required, use interfaces like `wasmcloud:messaging` with manual serialization/deserialization. This change reflects a deliberate performance decision: in-process calls achieve ~30,000 RPS while distributed calls achieve ~5,000 RPS. ## Important considerations ### Kubernetes native wasmCloud v2 is primarily designed for Kubernetes deployments. While the `wash-runtime` workload API can work with any orchestrator, and the operator can run with a standalone Kubernetes API server for lightweight scenarios, maintainer efforts are focused on the Kubernetes use case. See the [FAQ](./faq.mdx#do-i-have-to-use-kubernetes-with-wasmcloud-v2) for more details. ### What has been intentionally removed Several v1 subsystems have been replaced by industry standards: - **JWT Claims**: Replaced by OCI artifact attestation and SPIFFE/SPIRE for workload identity - **XKey Encoding**: Replaced with standard ed25519 key handling - **CloudEvents**: Replaced by Kubernetes-native mechanisms (CRD status conditions, Kubernetes Events) and OpenTelemetry - **External Capability Providers**: Replaced by host plugins and services - **`wasmcloud:secrets`**: Replaced by `wasi:config` backed by Kubernetes Secrets - **Config Service**: Replaced by `wasi:config` via ConfigMaps and Secrets - **Policy Service**: Replaced by Kubernetes admission controllers and component-level capability-based security ### Learning curve Teams migrating to v2 will need to learn: - The [wasmCloud CRD schema](./kubernetes-operator/crds.mdx) (similar to core Kubernetes resources) - Host plugin development (for custom capabilities) - The [Workload](./overview/workloads/index.mdx) abstraction (replacing Applications) ## Getting started with v2 If you're ready to begin migrating: 1. **[Install wasmCloud v2](./installation.mdx)** to set up your environment 2. **[Read the Platform Overview](./overview/index.mdx)** to understand v2 concepts 3. **[Follow the Developer Guide](./wash/developer-guide/index.mdx)** to build and publish your first v2 component 4. **[Review the CRD reference](./kubernetes-operator/crds.mdx)** to understand how to define workloads 5. **[Check the FAQ](./faq.mdx)** for answers to common migration questions --- ## Hosts ### The wasmCloud host is a runtime environment for WebAssembly (Wasm) workloads. WebAssembly components require a runtime to execute. The wasmCloud host provides a wrapper around [Wasmtime](https://github.com/bytecodealliance/wasmtime), a fast, secure, and standards-compliant WebAssembly runtime that supports the [Component Model](https://github.com/WebAssembly/component-model). Users can build [**host plugins**](./plugins.mdx) to customize wasmCloud hosts with extended capabilities using the [runtime library](../../runtime/index.mdx). ## Hosts as commodity A core design principle of wasmCloud is that the health of applications should not be dependent on any particular host. Hosts are designed to be operated in **host groups**, such that they are interchangeable and should be able to start and stop without causing downtime. While it is possible to run a single host, it's also possible for workloads to be distributed across many hosts, and for those hosts to dynamically scale in response to changing demand. ## Usage The wasmCloud host is designed for flexibility and may be used in a variety of ways: - **Edge computing**: Run lightweight hosts on edge devices (IoT gateways, retail kiosks, factory equipment) to process data locally with minimal latency. - **Multi-cloud deployments**: Deploy hosts across AWS, Azure, and GCP to run the same workloads without vendor-specific modifications. - **Kubernetes clusters**: Run hosts as pods in Kubernetes, managed by the [wasmCloud operator](../../kubernetes-operator/index.mdx) for automatic scaling and orchestration. - **Development environments**: Run a local host on your laptop for testing and debugging before deploying to production. --- ## Plugins ### Host plugins extend hosts with additional capabilities. Plugins provide capabilities at the host level, extending a wasmCloud host with specific implementations of [interfaces](../interfaces.mdx). Each plugin implements a **WIT world**—a collection of imports and exports that are directly linked to workloads at runtime. Plugins allow you to customize how your host handles common operations like HTTP requests, key-value storage, configuration, and logging. You can use the built-in plugins provided by `wash-runtime`, or implement custom plugins for specialized requirements. ## Built-in plugins The [`wash-runtime`](https://github.com/wasmCloud/wasmCloud/tree/main/crates/wash-runtime) crate includes built-in plugins for common WASI interfaces. These plugins come in two variants: in-memory implementations for local development (used by `wash dev`), and NATS-backed implementations for production deployments. | Plugin | Interface | Description | |--------|-----------|-------------| | **Key-Value** | `wasi:keyvalue` | Key-value storage operations | | **Blobstore** | `wasi:blobstore` | Blob/object storage operations | | **Config** | `wasi:config` | Runtime configuration access | | **Logging** | `wasi:logging` | Structured logging output | | **Messaging** | `wasmcloud:messaging` | Message passing and pub/sub communication | :::note HTTP is handled by `HttpServer`, which implements the `HostHandler` trait and is registered separately via `with_http_handler()` rather than `with_plugin()`. In addition, all [WASI P2 interfaces](https://docs.wasmtime.dev/api/wasmtime_wasi/p2/index.html#wasip2-interfaces) provided by `wasmtime-wasi`—including `wasi:filesystem`, `wasi:clocks`, `wasi:random`, `wasi:io`, `wasi:sockets`, and the `wasi:cli` suite—are built into the host core and always available without registration. ::: Messaging is always available. The remaining plugins can be enabled or disabled via Cargo feature flags when building your host: ```toml [dependencies] wash-runtime = { version = "*", features = ["wasi-keyvalue", "wasi-config", "wasi-logging", "wasi-blobstore"] } ``` ## Using plugins Plugins are registered with the host using the `HostBuilder` API. HTTP is handled separately via `with_http_handler()` because `HttpServer` implements the `HostHandler` trait rather than `HostPlugin`. The following example demonstrates how to configure a host with an HTTP handler and a configuration plugin: ```rust use std::sync::Arc; use std::collections::HashMap; use wash_runtime::{ engine::Engine, host::{HostBuilder, HostApi, http::{HttpServer, DevRouter}}, plugin::wasi_config::DynamicConfig, types::{WorkloadStartRequest, Workload}, }; #[tokio::main] async fn main() -> anyhow::Result<()> { // Create a Wasmtime engine let engine = Engine::builder().build()?; // Configure HTTP handler and plugins // DevRouter routes all requests to the most recently resolved workload let http_handler = HttpServer::new(DevRouter::default(), "127.0.0.1:8080".parse()?).await?; let config_plugin = DynamicConfig::new(false); // Build and start the host let host = HostBuilder::new() .with_engine(engine) .with_http_handler(Arc::new(http_handler)) .with_plugin(Arc::new(config_plugin))? .build()?; let host = host.start().await?; // Start a workload (components will have access to plugin capabilities) let req = WorkloadStartRequest { workload_id: uuid::Uuid::new_v4().to_string(), workload: Workload { namespace: "example".to_string(), name: "my-workload".to_string(), annotations: HashMap::new(), service: None, components: vec![], host_interfaces: vec![], volumes: vec![], }, }; host.workload_start(req).await?; Ok(()) } ``` :::info[Default behavior] If a handler is not provided for a particular capability, a "deny all" implementation is used. This ensures that components cannot access capabilities unless explicitly configured. ::: ## Custom plugins You can create custom plugins by implementing the `HostPlugin` trait. See [Creating Host Plugins](../../runtime/creating-host-plugins.mdx) for more information. Custom plugins are useful when you need to: - **Integrate with proprietary systems**: Connect components to internal APIs, databases, or services that don't have standard WASI interfaces. - **Add security layers**: Implement custom authentication, authorization, or audit logging for capability access. - **Provide specialized hardware access**: Expose GPUs or other specialized hardware to components. ## Keep reading - Learn more about the [wasmCloud runtime](../../runtime/index.mdx). - Choosing between a plugin and a service? See [Plugin or Service?](../../recipes/plugin-or-service.mdx). - See the [wash-runtime source code](https://github.com/wasmCloud/wasmCloud/tree/main/crates/wash-runtime) for implementation details. - Watch a [discussion of host plugin implementation](https://www.youtube.com/live/3DlUjl8gQVs?si=MbTz4MohjYDmE8fI&t=731) from a wasmCloud community call. --- ## Platform Overview ### wasmCloud is a cloud native platform for running WebAssembly workloads across any cloud, Kubernetes, datacenter, or edge. WebAssembly (Wasm) components are a lightweight, secure-by-default unit of compute that is well-suited to sensitive workloads and greenfield applications. Teams can use wasmCloud to deploy these applications in lightweight, universal sandboxes and run them efficiently on existing cloud native infrastructure. ## The platform The wasmCloud platform consists of three primary parts: * [Wasm Shell (`wash`) CLI](../wash/index.mdx): A development tool for building and publishing WebAssembly components with languages including Go, TypeScript, Rust, and more. * [Runtime (`wash-runtime`)](../runtime/index.mdx): An easy-to-use runtime and workload API for executing WebAssembly components, with built-in support for WASI interfaces. * [Kubernetes Operator (`runtime-operator`)](../kubernetes-operator/index.mdx): An operations tool that runs wasmCloud infrastructure on Kubernetes. Component developers use the [Wasm Shell (`wash`) CLI](https://github.com/wasmCloud/wasmCloud/) to develop and publish components, and the [wasmCloud operator](https://github.com/wasmCloud/runtime-operator) integrates wasmCloud with Kubernetes. ![wasmcloud user contexts](../images/wasmcloud-stack.webp) ## The architecture In most deployments, wasmCloud runs on Kubernetes, where the wasmCloud operator communicates over [NATS transport](./transport.mdx) to manage hosts and workloads, mediating between hosts and the Kubernetes API server. ![wasmcloud architecture](../images/wasmcloud-v2-architecture.webp) *Note: The wasmCloud operator can run with a standalone Kubernetes API server for edge use-cases, and the wasmCloud runtime can run with alternative schedulers.* **Workloads** are comprised of **Wasm components** and **services** that make and respond to calls over **interfaces**: * [Components](./workloads/components.mdx) are portable, interoperable WebAssembly binaries that implement stateless logic. * [Services](./workloads/services.mdx) provide long-running processes within the workload boundary. * [Interfaces](./interfaces.mdx) are contracts that define the relationships between entities. {/* ![component using logging and http interfaces]() */} Workloads run on **hosts** that can be extended with **host plugins**: * [Hosts](./hosts/index.mdx) are runtime environments for WebAssembly (Wasm) workloads. * [Plugins](./hosts/plugins.mdx) extend hosts with additional capabilities. --- ## Interfaces ### Interfaces are contracts that define the relationships between entities. In wasmCloud, [components](./workloads/components.mdx) communicate through interfaces, which come in two kinds: * **Well-known interfaces** are common standards (such as [WebAssembly System Interface (WASI) APIs](#wasi-interfaces)) or wasmCloud interfaces (core functionalities like `wasmcloud-messaging`) supported out-of-the-box by wasmCloud hosts. * **Custom interfaces** are user-created contracts that make it possible to extend and tailor how wasmCloud components and providers interact with one another. In all cases, wasmCloud interfaces are defined using the interface description language **WebAssembly Interface Type (WIT)**. ## WebAssembly Interface Type (WIT) WebAssembly Interface Type (WIT) is an [open standard maintained as part of the Component Model](https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md) by the W3C WebAssembly Community Group. WIT enables WebAssembly components to define the functions they expose to external entities ("**exports**") and the functionalities they require ("**imports**") in `.wit` files. ### Packages, namespaces, and versions Interfaces defined in WIT are organized into **packages**. Packages must include a **namespace** and **identifier**. ![Namespace and package in a WIT file](../images/wit-package.webp) Optionally, WIT packages may include a version using [semantic versioning](https://semver.org/). :::info[Why `0.2.0-draft`?] The version for the example above is `0.2.0-draft`. WASI proposals move through three phases. Once a proposal reaches Phase 3, it may be included in the standard API group of WASI 0.2. The `wasi-keyvalue` interface above is at Phase 2. ::: In wasmCloud, you will often see packages belonging to the `wasmcloud` and `wasi` namespaces. You may also create custom interfaces with arbitrary namespaces. Packages with different namespaces may be mixed and matched freely, and the contents of a given package may be spread across multiple files. A common organizational pattern divides a package into: * `types.wit` * `imports.wit` * `world.wit` * `my-interface-name.wit` An interface may also have a `deps` folder holding WIT files for other WIT files used as dependencies in your interface. {/* For more information on organizing an interface, see [Creating an interface](TK). */} ### Worlds The highest-level contract in a WIT interface is called a **world**. A WIT world is akin to a complete description of a component, defining the imports and exports that enable the component to interact other entities. Here is a simple example of a world: ```wit package wasmcloud:demo; world demo { import wasi:logging/logging; export wasi:http/incoming-handler@0.2.0; } ``` This is the top-level world for a hypothetical component that *imports* on the `logging` interface from [WASI Logging](https://github.com/WebAssembly/wasi-logging) and *exports* (or exposes a function on) the `incoming-handler` interface from [WASI HTTP](https://github.com/WebAssembly/wasi-http). This enables the component to be invoked (and respond) via HTTP and to use logging functionality. :::info In addition to using the `wasi:logging` interface, logs printed to STDERR will be output in host logs by default. ::: There are often at least two worlds defined in a package. A common convention is to have an `imports` world and the world components typically target. In a wasmCloud component project, it is conventional to include a top-level WIT world at the root of a `wit` folder in the project directory. ### Interfaces An interface is a collection of **types** and **functions** scoped to a package which can be used within a world. Interfaces are the only place that a type can be defined. Packages may contain multiple interfaces. Interfaces represent the lower-level vocabulary of the contract between entities. Worlds may also refer to other worlds, which themselves may refer to interfaces or still "deeper" worlds. Here is the `incoming-handler` interface imported by the world above: ```wit /// This interface defines a handler of incoming HTTP Requests. It should /// be exported by components which can respond to HTTP Requests. interface incoming-handler { use types.{incoming-request, response-outparam}; /// This function is invoked with an incoming HTTP Request, and a resource /// `response-outparam` which provides the capability to reply with an HTTP /// Response. The response is sent by calling the `response-outparam.set` /// method, which allows execution to continue after the response has been /// sent. This enables both streaming to the response body, and performing other /// work. /// /// The implementor of this function must write a response to the /// `response-outparam` before returning, or else the caller will respond /// with an error on its behalf. handle: func( request: incoming-request, response-out: response-outparam ); } ``` When two entities import and export respectively on the same interface (such as `incoming-handler`), they can be **linked** so that once invoked, they interact according to the contract defined in the interface. ![Interface diagram](../images/import-export.webp) {/* For more information on using WIT, see our Developer Guide page on [**Creating an interface**](/docs/developer/interfaces/creating-an-interface). */} :::info[WIT without WebAssembly] In spite of the name, WIT isn't limited to WebAssembly: wasmCloud also uses WIT to define the interfaces used by providers and host functions written in Rust or Go. It is entirely possible, for example, to create a Rust or Go binary that uses WIT interfaces over the wRPC (WIT over RPC) protocol. ::: ## Well-known interfaces wasmCloud supports interfaces belonging to [WebAssembly System Interface (WASI)](https://wasi.dev/) P2 (also known as WASI 0.2 / P2) in addition to a selection of interfaces proposed for inclusion in WASI, and interfaces belonging to the wasmCloud host. ### WASI interfaces [WASI P2 includes these APIs](https://github.com/WebAssembly/WASI/tree/main/preview2#wasi-preview-2-contents), all available for use with wasmCloud 2.0: | API | Versions | | ----------------------------------------------- | -------- | | https://github.com/WebAssembly/wasi-io | 0.2.0 | | https://github.com/WebAssembly/wasi-clocks | 0.2.0 | | https://github.com/WebAssembly/wasi-random | 0.2.0 | | https://github.com/WebAssembly/wasi-filesystem* | 0.2.0 | | https://github.com/WebAssembly/wasi-sockets* | 0.2.0 | | https://github.com/WebAssembly/wasi-cli | 0.2.0 | | https://github.com/WebAssembly/wasi-http | 0.2.0 | :::info[wasi-filesystem and wasi-sockets] `wasi-filesystem` access is granted through preopens. By default, components have no filesystem access. Directories can be explicitly mounted to a component via volume mounts in the workload manifest — see [Filesystems and Volumes](../kubernetes-operator/operator-manual/filesystems-and-volumes.mdx) for details. [wasi-virt](https://github.com/bytecodealliance/wasi-virt) can be used to embed a virtual filesystem directly into a component binary (for example, to bundle static assets). `wasi-sockets` is supported with host-enforced policy: outbound TCP connections are allowed, services can bind on loopback, and DNS name resolution is disabled by default. The host enforces these restrictions unconditionally—components do not need to implement their own socket access control. For an overview of socket policy and the service model for intra-workload TCP, see [Network Access and Socket Isolation](../wash/developer-guide/network-access-and-socket-isolation.mdx). ::: Additionally, wasmCloud supports proposed WASI APIs that are in the process of implementation and standardization: | API | Versions | | --------------------------------------------- | ----------- | | https://github.com/WebAssembly/wasi-blobstore | 0.2.0-draft | | https://github.com/WebAssembly/wasi-keyvalue | 0.2.0-draft | | https://github.com/WebAssembly/wasi-logging | 0.1.0-draft | ### wasmCloud interfaces Well-known interfaces include two APIs built specifically for wasmCloud: | API | Versions | | -------------------------------------------------------------------------------- | ----------- | | [wasmcloud:bus](https://github.com/wasmCloud/wasmCloud/tree/main/wit/bus) | 1.0.0 | | [wasmcloud:messaging](https://github.com/wasmCloud/messaging) | 1.0.0 | * **`wasmcloud:bus`** provides advanced link configuration only available in wasmCloud. * **`wasmcloud:messaging`** facilitates communication through message brokers. ## Custom interfaces WASI interfaces are ultimately common standards using WIT, but wasmCloud enables you to build custom WIT interfaces and communicate between components in the way best-suited to your requirements. Here is an example of a `greeter` interface defined in WIT: ```wit package local:greeter-demo; // : interface greet { // interface greet: func(name: string) -> string; // a function named "greet" } world greeter { export greet; // make the `greet` function available to other components/the runtime } ``` While reading the [spec][wit-spec] is the best way to learn about WIT, it is also designed to be easy to understand at a glance. WASI interfaces written in WIT contain their own documentation and are useful to consult as examples. :::warning[Compared to gRPC and Smithy...] While similar frameworks and languages like [gRPC][grpc] and [Smithy][smithy] are meant to perform over network boundaries, WIT is _in-process_, and performs at near-native speed. ::: ## Interface-driven development Interface-driven development (IDD) is a development approach that focuses on defining what capabilities components require before the specifics of how you will meet those needs. Systems developed using IDD—especially distributed systems—are loosely coupled, robust, and maintainable. [wit-spec]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md [wiki-idl]: https://en.wikipedia.org/wiki/Interface_description_language [smithy]: https://smithy.io [grpc]: https://grpc.io/ --- ## Packaging ### Components and interfaces are packaged as OCI artifacts. The wasmCloud ecosystem uses the [Open Container Initiative (OCI) image specification](https://github.com/opencontainers/image-spec) to package components and interfaces as OCI artifacts according to the [standard OCI artifact format for Wasm](https://tag-runtime.cncf.io/wgs/wasm/deliverables/wasm-oci-artifact/). While we sometimes refer to them as "images," these artifacts **are not *container* images**—nonetheless, they conform to OCI standards and may be stored on any OCI-compatible registry. You can think of the OCI artifact layer as a thin shell of metadata wrapped around the component or interface. :::tip[Registry compatibility] wasmCloud OCI artifacts work with any registry that supports the OCI artifact spec, including GHCR, Amazon ECR, Docker Hub (as of 2023), Harbor, and Zot. Registries that only accept container images and strip non-image manifests will not work — check your registry's documentation for OCI artifact support before pushing. ::: Using the OCI specification for packaging means: - **wasmCloud can integrate** with organizations' existing cloud native tooling such as registries. - **Components and interfaces are portable** across the WebAssembly (Wasm) ecosystem, which uses OCI as a common packaging standard. ## WebAssembly and OCI artifacts The OCI specification enables users to create "artifacts," providing a standard set of guidelines for packaging objects other than container images. WebAssembly components are a compelling use-case for OCI artifacts because, unlike containers, the same WebAssembly binary can run on any architecture or operating system. WebAssembly components can leverage the existing base of OCI tooling, enabling organizations to easily start packaging, distributing, and running components with OCI artifacts across environments. ## wasmCloud and OCI artifacts In the wasmCloud ecosystem, we use OCI images in two primary contexts: - Packaging components - WIT interface dependency management Both of these use-cases are aligned with the wider [WebAssembly ecosystem's implementation of OCI for component packaging](https://tag-runtime.cncf.io/wgs/wasm/deliverables/wasm-oci-artifact/) and WIT interface dependency management, making both components and interfaces portable across platforms adopting open standards. ### Publishing components as OCI artifacts You can publish components as OCI artifacts using the wasmCloud Shell (`wash`) CLI. You can learn more about publishing component artifacts to registries (and pulling them for local use) [in the Wasm Shell Developer Guide](../wash/developer-guide/build-and-publish.mdx). This page outlines how to authenticate to a registry and push and pull components. ### Running components from OCI artifacts Once a component or provider is published to a registry, you can include it in a [CRD manifest](../kubernetes-operator/crds.mdx), like so: ```yaml ... components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 ... ``` In the manifest excerpt above, the `http-component` is included in a `WorkloadDeployment` manifest via an artifact (or "image") stored on a GitHub Packages registry. You can learn more about CRD manifests on the [Custom Resource Definitions (CRDs)](../kubernetes-operator/crds.mdx) page. ### WIT interface dependency management The wasmCloud (and wider WebAssembly) ecosystem uses OCI artifacts to package and distribute [WebAssembly Interface Type (WIT) interfaces](interfaces.mdx) that define the contracts between entities and typically serve as dependencies for a WebAssembly component. `wash` builds on a set of open source tooling called [Wasm Package Tools](https://github.com/bytecodealliance/wasm-pkg-tools) to handle the following jobs: - Checking for WIT package dependencies - Fetching WIT packages from known registries - Pushing packages to your own registry If you set a [well-known interface](./interfaces.mdx#well-known-interfaces) as an import or export in your specified world file (usually `world.wit`), `wash` will ensure that you have the proper packages in your project directory when you build. The tooling reads the world and downloads the appropriate dependencies automatically into the `wit/deps` directory. With no further configuration, `wash` can download dependencies from the following well-known namespaces: - `wasi`: Interfaces proposed for the common [WebAssembly System Interface (WASI) standard](https://wasi.dev/) - `wasmcloud`: Interfaces maintained as part of the wasmCloud project - `ba`: Interfaces maintained by the [Bytecode Alliance](https://bytecodealliance.org/) --- ## Transport ### NATS provides transport between hosts and the operator. In wasmCloud, transport between hosts and the operator is handled by [**NATS**](https://nats.io/), an open source connective technology hosted by the Cloud Native Computing Foundation (CNCF). NATS enables secure application-layer networking across diverse environments including edge, different vendors' clouds, and on-premise datacenters. NATS is designed to provide seamless connectivity tailored specifically to distributed systems, avoiding the complexities and limitations of 1:1 communication frameworks like HTTP or gRPC in distributed use cases. **Note**: Calls between [services](./workloads/services.mdx) and [components](./workloads/components.mdx) **do not** use NATS. See [Workloads](./workloads/index.mdx) for more information. ## Essential NATS concepts NATS conceptualizes communications as **messages**. Applications send and receive messages identified by **subject** strings. In addition to the subject, messages contain a byte array payload and any number of header fields. :::info[Topics and subjects] What is called a **"subject"** in NATS is often called a **"topic"** in messaging systems more generally, including Apache Kafka, Google Cloud Pub/Sub, and Confluent, as well as the [`wasi-messaging` proposal](https://github.com/WebAssembly/wasi-messaging). In this documentation, we use "subject" in NATS-specific contexts, but you can think of "topic" and "subject" as essentially interchangeable. ::: An entity that sends a message is a **publisher**. When a publisher sends a message, it may be received by one or more **subscribers**. This one-to-many communication pattern is called the [**publish-subscribe**](https://docs.nats.io/nats-concepts/core-nats/pubsub) model. NATS supports a **[request-reply](https://docs.nats.io/nats-concepts/core-nats/reqreply)** pattern built on the publish-subscribe model. A publisher may send a "reply" message (the "request") on a given subject. Entities subscribed to the subject may send replies which are automatically directed back to the original request publisher. In addition to the core publish-subscribe functionality, NATS provides **streaming** and **key-value storage** through its distributed persistence system called **[JetStream](https://docs.nats.io/nats-concepts/jetstream)**: - **[Streams](https://docs.nats.io/nats-concepts/jetstream/streams)** are stores for messages on a given subject. - **[Buckets](https://docs.nats.io/nats-concepts/jetstream/key-value-store)** are immediately consistent key-value stores using strings as keys and byte arrays as values (used in wasmCloud for tasks such as storing application manifests). ## Using the NATS CLI You can interact directly with NATS using the NATS CLI. While it is not necessary to install the NATS CLI to use wasmCloud, it can be useful for troubleshooting. Installation instructions for the CLI are available on the [NATS CLI GitHub repo](https://github.com/nats-io/natscli). To list all streams on your NATS network: ```shell nats stream list ``` You can list all key-value buckets on your NATS instance with: ```shell nats kv list ``` {/* For more information on troubleshooting and NATS, see the [troubleshooting](/docs/developer/troubleshooting/host#clear-nats-streams-and-buckets) section. */} ## Keep reading {/* - See the [NATS Reference](/docs/category/nats/) for complete reference documentation on NATS usage in wasmCloud - See the Operator Guide for instructions on [provisioning NATS for wasmCloud](/docs/category/provisioning-nats) in production environments. - For information on authenticating to NATS with accounts and users, see [Connecting NATS to Wadm](/docs/deployment/wadm/nats-credentials). - You can [use the `wash` CLI to open a WebSocket port](/docs/cli/wash#wash-up) utilizing [WebSocket support in NATS](https://docs.nats.io/running-a-nats-service/configuration/websocket). */} - For more information on NATS, see the [NATS documentation](https://docs.nats.io/). --- ## Components ### WebAssembly (Wasm) components are stateless, sandboxed binaries. **Components** are portable, interoperable binaries (`.wasm` files) that implement stateless logic, typically enacting the core logic of an application (for example, the API for a web application), while leaving stateful or reusable functionality (e.g., key-value storage or HTTP) to [services](./services.mdx), [hosts](../hosts/index.mdx), and [host plugins](../hosts/plugins.mdx). ## Features of components Components may be compiled from a variety of languages including Rust, Go, Python, JavaScript, and more. Because components can be compiled from many different languages and then interact with one another, they enable developers to break down language silos and utilize libraries and tooling in new ways. ![Compilation from a variety of languages](../../images/compile.webp) *Components can compile from a variety of languages and run across architectures. The [`wash build`](../../wash/commands.mdx#wash-build) command can compile components from any language, leveraging language-specific toolchains.* Components are secure-by-default, portable, interoperable, and composable: * **Secure-by-default**: Components can't use capabilities without mediation by the host, so they are incapable of interacting with any operating system functionality on their own. The only way components can affect their external environment is through an explicitly permitted capability facilitated by a host. * **Portable**: Because WebAssembly binaries execute against a virtual instruction set architecture (essentially a tiny VM), they are agnostic to architecture and operating system kernel; they run anywhere there is a WebAssembly runtime. Component binaries are typically much smaller than analogous container images, as well—often measured in kilobytes—enabling them to run in resource-constrained environments where even containers aren't practical. * **Interoperable**: Components can interact with one another over high-level APIs regardless of their respective languages of origin, so that a component written in Rust can utilize the functionality of a library from Go. * **Composable**: Multiple components can be combined into a single binary (or dynamically linked at runtime in wasmCloud). This enables developers to build applications as if with construction bricks, satisfying dependencies with other components as needed. The wasmCloud runtime dynamically links components in the same workload at runtime. Components follow the principles of **[reactive programming](https://en.wikipedia.org/wiki/Reactive_programming)**: they only run when invoked by another entity. An invocation might originate from another component's function call, an extension, or a message from the [wasmCloud host](../hosts/index.mdx). In turn, components can invoke exposed functions on other components or the host. ## Connected by abstractions In wasmCloud, components are loosely coupled with the extensions they use for non-functional requirements. A component typically doesn't communicate with **Redis** or **Cassandra** or **Consul**; instead it communicates with a generalized abstraction over the `keyvalue` interface. An [interface](../interfaces.mdx) represents an abstracted functionality. As long as an extension implements the correct interface, it's considered compatible with your component. A component written using the `keyvalue` interface should be able to work with _any_ key-value store. This decoupling also enables swapping the store at runtime without requiring a rebuild or redeploy. Learn more about wasmCloud's interface support on the [Interfaces](../interfaces.mdx) page. ## Open standards The wasmCloud project is committed to supporting a componentized ecosystem and remains up-to-date with the latest versions of the [Wasmtime](https://wasmtime.dev/) WebAssembly runtime and [WebAssembly System Interface (WASI) P2](https://wasi.dev/interfaces#wasi-02), a set of standard APIs designed to allow WebAssembly components to access external resources in a safe and portable way. Components are defined according to the [Component Model](https://github.com/WebAssembly/component-model), an open standard governed by the [W3C WebAssembly Community Group](https://www.w3.org/community/webassembly/) that describes a layer of specification in addition to that of a core WebAssembly module. In principle, any language can compile code to a component; the maturity of compilers varies by language, and component tooling is developing rapidly since the release of WASI 0.2 in January 2024. You can find practical tooling for working with components in [Useful WebAssembly Tooling](../../wash/developer-guide/useful-webassembly-tools.mdx). We aim to support WASI P3 swiftly upon release. ## Keep reading - [Continue to learn more about services](./services.mdx). - Dig deeper on creating components in the [Developer Guide](../../wash/developer-guide/index.mdx). --- ## Workloads ### Workloads are composed of one or more WebAssembly (Wasm) components and an optional service. A wasmCloud **workload** is an application-level abstraction that consists of one or more [**components**](./components.mdx) and optionally a [**service**](./services.mdx). ![Workload diagram](../../images/workload-diagram.webp) - **Components** are portable, interoperable binaries (`.wasm` files) that implement stateless logic, typically enacting the core logic of an application (for example, the API for a web application), while leaving stateful or reusable functionality (e.g., key-value storage or HTTP) to [services](./services.mdx), [hosts](../hosts/index.mdx), and [host plugins](../hosts/plugins.mdx). * You can find a simple example of a Rust-based "Hello world!" component in [`wasmCloud/templates`](https://github.com/wasmCloud/wasmCloud/tree/main/templates/http-hello-world). - **Services** are persistent, stateful Wasm binaries that can act as `localhost` within the workload boundary and provide long-running processes. See [Creating services](../../wash/developer-guide/create-services.mdx) for usage patterns. The [wasmCloud host](../hosts/index.mdx) runs workloads in cloud and edge environments, while the [Wasm Shell (`wash`) CLI](../../wash/index.mdx) helps developers build and publish components and services. Workloads are defined according to the [wasmCloud runtime's Workload API](https://github.com/wasmCloud/wasmCloud/blob/main/proto/wasmcloud/runtime/v2/workload.proto). For most users, this means defining the workload in a Kubernetes manifest with the [`WorkloadDeployment` custom resource](../../kubernetes-operator/crds.mdx#workloaddeployment). ## Usage Workloads are application-level units that combine components and services. Example use cases include: - **Microservices**: Deploy a workload as a single microservice—for example, an order processing service with a component handling business logic and a service maintaining database connections. - **API backends**: Build HTTP APIs where components handle request routing and response generation while services manage authentication state or connection pools. - **Event processors**: Create workloads that respond to events from message queues, with components processing messages and services maintaining connections to the queue. - **Scheduled jobs**: Run batch processing workloads where a cron service triggers component invocations on a schedule. ## Components vs Services | Feature | Components | Services | |---------|------------|----------| | **Lifecycle** | Invocation-based (stateless) | Long-running (stateful) | | **State** | No state between invocations | Maintains state across lifetime | | **TCP Listening** | ❌ Cannot listen on ports | ✅ Can listen on TCP ports | | **TCP Outbound** | ✅ Can connect out | ✅ Can connect out | | **Execution Model** | Invoked per request | Runs continuously (like traditional binaries) | | **WASI Interface** | Component model | `wasi:cli/run` | | **Restart Policy** | N/A (invoked as needed) | Automatically restarted on crash | **Use a component when you need:** - Stateless request/response patterns - Pure computation or transformation - Scaling with invocation-based concurrency - Standard WIT interface implementations **Use a service when you need:** - Persistent connections (database connection pooling) - Long-running state (in-memory caches) - TCP server capabilities - Cron/scheduled task execution - Protocol adapters and bridges ## Keep reading - [Continue to learn more about WebAssembly (Wasm) components](./components.mdx). - Learn more about [services](./services.mdx). --- ## Services ### Services are persistent, stateful companions to stateless components. Workloads are composed of **components** and optionally a **service**. While services *are* Wasm components, they serve a fundamentally different role than a typical component in the architecture of an application. A service is essentially the "localhost" for a workload, acting as a persistent, stateful companion to stateless components, and providing capabilities that don't make sense for ephemeral invocations. Services are an important primitive for building complete applications that require both stateless, scalable components *and* stateful, long-running processes. By isolating services at the workload level and providing them with TCP capabilities, wasmCloud enables patterns like connection pooling, protocol bridging, and cron scheduling—all while maintaining strong security boundaries. ## Service as localhost Services can open TCP sockets, listen on ports for incoming connections, and act as `127.0.0.1`/`localhost` within the workload boundary. Services run continuously for the lifetime of the workload. When a component opens a TCP connection to `127.0.0.1` or `localhost`, it connects to the service within its own workload—**not** to the underlying host OS. This creates a strong isolation boundary. Multiple workloads on the same [wasmCloud host](../hosts/index.mdx) can each have services listening on the same port (e.g., port 8080) without conflicts, as each workload has its own isolated network namespace. ![services with isolated network namespaces](../../images/multiple-services.webp) ## Communication flow Services can call [interfaces](../interfaces.mdx) exported by components: ![service to component flow](../../images/service-to-component-flow.webp) Components **cannot** call into the service via WIT interfaces. However, components **can** connect to TCP ports the service is listening on: ![component to service flow](../../images/component-to-service-flow.webp) ## How services use interfaces Services are not limited to TCP—they can use your own [custom interfaces](../interfaces.mdx) defined in WIT. The `wasi:sockets` TCP bind permission is simply a special capability that services receive, enabling them to act as TCP listeners within the workload boundary. ### Export requirement A service must export `wasi:cli/run` (the standard run function) **or** exactly one [WIT interface](../interfaces.mdx). This is how the runtime identifies the service's entry point. For example, a service could export `wasi:cli/run` to act as a long-running process with a `main` function, or it could export a single custom interface. ### Imports and host plugins Services can import any host interface that a [plugin](../hosts/plugins.mdx) provides. The `host_interfaces` field on a workload lists WIT interfaces that need to be resolved by host plugins. When the runtime binds plugins, it checks which WIT interfaces the service's world needs and binds matching plugins—treating services the same as components in this regard. The special treatment services receive for `wasi:sockets` only affects whether TCP bind is allowed: services can bind to loopback and unspecified addresses for TCP, while regular components cannot. Beyond this, services interact with host plugins in the same way as components. ### Custom interface exports Services can export arbitrary WIT interfaces. The only constraint is the validation described above: the service must export `wasi:cli/run` or exactly one interface, so that the runtime knows the entry point. This means you can build services that expose domain-specific functionality through a custom WIT interface. ## Use cases :::info[Developing services] For instructions on how to develop services, see the [Wasm Shell (`wash`) Developer Guide](../../wash/developer-guide/create-services.mdx). ::: ### Connection pooling Services are ideal for managing persistent connections to databases or external services. Components don't pay the overhead of establishing new TCP connections per invocation, and this approach enables efficient connection reuse across all component invocations. ![connection pooling diagram](../../images/connection-pool.webp) ### Cron or scheduled tasks Implement cron-like functionality by having a service periodically call component interfaces: ```rust wit_bindgen::generate!({ world: "service" }); // NOTE: This example is a `tokio::main` to show how you can use an async main, but // it can just be a synchronous main as well. #[tokio::main(flavor = "current_thread")] async fn main() { eprintln!("Starting cron-service with 1 second intervals..."); loop { tokio::time::sleep(std::time::Duration::from_secs(1)).await; let _ = wasmcloud::example::cron::invoke(); } } ``` ### Stateful in-memory cache Run a small key-value store that maintains state across invocations. Connections are extremely fast, since they use virtual pipes within the runtime rather than "real" TCP/IP. ![in-memory diagram](../../images/in-memory.webp) ### TCP server applications Build TCP server applications directly in Wasm, such as the following TCP echo server that accepts incoming connections, echoes back any data received, and runs continuously for the workload's lifetime: ```rust use wstd::io; use wstd::iter::AsyncIterator; use wstd::net::TcpListener; #[wstd::main] async fn main() -> io::Result<()> { let listener = TcpListener::bind("127.0.0.1:7070").await?; println!("Listening on {}", listener.local_addr()?); println!("type `nc localhost 7070` to create a TCP client"); let mut incoming = listener.incoming(); while let Some(stream) = incoming.next().await { let stream = stream?; println!("Accepted from: {}", stream.peer_addr()?); wstd::runtime::spawn(async move { // if echo copy fails, we can ignore it. let _ = io::copy(&stream, &stream).await; }) .detach(); } Ok(()) } ``` ## Considerations When developing applications with services, it is important to consider the following: - **One service per workload**: Currently, workloads support a single service. - **Export constraint**: Services must export `wasi:cli/run` or exactly one WIT interface. - **One-way communication**: Components cannot call service exports via WIT (but can use TCP). - **Memory usage**: Services maintain state, so they consume memory continuously. - **WASI P2**: Services currently compile to WASI P2 (`wasm32-wasip2` target). - **Async runtime**: Can use single-threaded async runtimes (e.g., Tokio with single-threaded executor). - **Restarts**: Services are automatically restarted if they crash. - **Isolation**: Service network operations are isolated within the workload boundary. :::note[Looking ahead] As wasmCloud v2 moves from WASI P2 to P3 and beyond, services make it possible to bridge between TCP and WIT (WebAssembly Interface Types) for protocols that don't have native WASI support yet. ::: ## Keep reading - [Continue to learn more about interfaces](../interfaces.mdx). - Learn more about services' role within a component—and how to decide when you should use components or services—in [Workloads](./index.mdx). - Choosing between a plugin and a service? See [Plugin or Service?](../../recipes/plugin-or-service.mdx). - Learn how to develop services in the [Wasm Shell Developer Guide](../../wash/developer-guide/create-services.mdx). --- ## Deploy a Wasm Workload import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import HubspotForm from 'react-hubspot-form'; In our previous tutorials, we started a developer loop and added the [`wasi:keyvalue`](../overview/interfaces.mdx) interface to our application. The `wash dev` process satisfied our application's capability requirements automatically, so we could move quickly and focus on code. Now we'll learn how to: - Compile your application to a WebAssembly component binary - Publish your Wasm binary to an OCI registry - Set up a wasmCloud environment on Kubernetes and deploy your Wasm workload :::info[Prerequisites] This tutorial assumes you're following directly from the previous steps. Make sure to complete [**Quickstart**](./index.mdx) and [**Develop a WebAssembly Component**](./develop-a-webassembly-component.mdx) first. ::: ## Build Wasm binary Use the `wash build` command from the root of a project directory to compile the component into a `.wasm` binary: ```shell wash build ``` By default, the compiled `.wasm` binary for a Rust project is generated at `/target/wasm32-wasip2/release/`. The output path for the compiled `.wasm` binary [can be configured via `wash`'s `config.yaml` file](../wash/config.mdx). {/* Use the `wash build` command from the root of a project directory to compile the component into a `.wasm` binary: ```shell wash build ``` By default, the compiled `.wasm` binary for a TinyGo project is generated at `/build/`. The output path for the compiled `.wasm` binary [can be configured via `wash`'s `config.yaml` file](../wash/config.mdx). */} Use the `wash build` command from the root of a project directory to compile the component into a `.wasm` binary: ```shell wash build ``` By default, the compiled `.wasm` binary for a TypeScript project is generated at `/dist/`. The output path for the compiled `.wasm` binary [can be configured via `wash`'s `config.yaml` file](../wash/config.mdx). If you prefer working in a language that isn't listed here, let us know! {' '} console.log('Submitted form')} onReady={(form) => console.log('Form ready for submit')} region="na1" loading={Loading...} /> ## Publish image Now we can publish a Wasm binary image to any OCI compliant registry that supports **OCI artifacts**. :::note[What is this image?] The wasmCloud ecosystem uses the [OCI image specification](https://github.com/opencontainers/image-spec) to package Wasm components—these images are not container images, but conform to OCI standards and may be stored on any OCI-compatible registry. You can learn more about wasmCloud packaging on the [**Packaging**](../overview/packaging.mdx) page. ::: ### Authenticate to registry `wash` uses Docker credentials for OCI registry authentication. For GitHub Packages (GHCR), you'll need a [GitHub Personal Access Token (PAT)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with `write:packages` scope. Authenticate with the `docker` CLI: ```shell docker login ghcr.io -u ``` Docker will prompt you for your PAT as the password. :::note[] Your GitHub username (and the namespace in image paths) must be **all lowercase**. ::: ### Push image to registry Now we'll push a Wasm component image to a registry. The example below uses GitHub Packages, but you can use any OCI compliant registry that supports OCI artifacts. If you're following along using GitHub Packages, make sure to replace `` in the command below with your own GitHub namespace (lowercase). ```shell wash oci push ghcr.io//http-hello-world:0.1.0 ./target/wasm32-wasip2/release/hello_world.wasm ``` {/* ```shell wash oci push ghcr.io//http-hello-world:0.1.0 ./build/http_hello_world.wasm ``` */} ```shell wash oci push ghcr.io//http-hello-world:0.1.0 ./dist/http_hello_world_hono.wasm ``` If you prefer working in a language that isn't listed here, let us know! {' '} console.log('Submitted form')} onReady={(form) => console.log('Form ready for submit')} region="na1" loading={Loading...} /> * The registry address (including name and tag) is specified for the first option with `wash oci push`. * The second option defines the target path for the component binary to push. :::warning[] If you're using GitHub Packages, remember to [make the image public](https://docs.github.com/en/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility) in order to follow the steps below. ::: ## Install wasmCloud on Kubernetes Installation requires the following tools: - [`kubectl`](https://kubernetes.io/docs/tasks/tools/#kubectl) - [Helm](https://helm.sh/docs) v3.8.0+ For more information on running wasmCloud on Kubernetes, see [Kubernetes Operator](../kubernetes-operator/index.mdx). Select your Kubernetes environment: If you already have a Kubernetes cluster, skip cluster creation. Verify your `kubectl` context is pointing to the right cluster: ```shell kubectl cluster-info ``` [kind](https://kind.sigs.k8s.io/) runs Kubernetes nodes as Docker containers. **Requirements:** [Docker](https://docs.docker.com/get-started/get-docker/) · [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) :::note[Linux] On Linux, Docker may require `sudo` unless you've completed the [post-installation steps](https://docs.docker.com/engine/install/linux-postinstall/) to run Docker as a non-root user. ::: The following command downloads a `kind-config.yaml` from the [`wasmCloud/wasmCloud`](https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/deploy/kind/kind-config.yaml) repository, starts a cluster with port 80 mapped for ingress, and deletes the config upon completion: ```shell curl -fLO https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/deploy/kind/kind-config.yaml && kind create cluster --config=kind-config.yaml && rm kind-config.yaml ``` [k3d](https://k3d.io/) runs a lightweight [k3s](https://k3s.io/) cluster inside Docker. **Requirements:** [Docker](https://docs.docker.com/get-started/get-docker/) · [k3d](https://k3d.io/#installation) :::note[Linux] On Linux, Docker may require `sudo` unless you've completed the [post-installation steps](https://docs.docker.com/engine/install/linux-postinstall/) to run Docker as a non-root user. ::: The `--port` flag maps host port 80 to NodePort 30950 on the server node. The hello-world manifest used later in this tutorial exposes the workload as a `NodePort` Service on port 30950, so this maps the Wasm component directly to `localhost:80` without going through k3d's bundled Traefik LoadBalancer: ```shell k3d cluster create wasmcloud --port "80:30950@server:0" ``` If you'd prefer the `LoadBalancer` pattern more typical of k8s production deployments, see the [Expose a Workload via Kubernetes Service](../recipes/expose-workload-via-kubernetes-service.mdx#exposing-the-service-externally) recipe. [k3s](https://k3s.io/) is a lightweight Kubernetes distribution. **Linux only.** Install k3s: ```shell curl -sfL https://get.k3s.io | sh - ``` Configure `kubectl` to use the k3s cluster: ```shell mkdir -p ~/.kube sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config sudo chown $USER ~/.kube/config ``` Use Helm to install the wasmCloud operator: :::note[Runtime Gateway deprecated in 2.0.3] The tutorial below routes HTTP traffic through a Kubernetes Service whose EndpointSlice the operator manages directly. The earlier **Runtime Gateway** component that proxied traffic to workloads is deprecated as of 2.0.3 and will be removed in a future release; the local-dev overlay (`values.local.yaml`) disables it by default. ::: ```shell helm install wasmcloud --version {{WASMCLOUD_VERSION}} oci://ghcr.io/wasmcloud/charts/runtime-operator \ --namespace wasmcloud --create-namespace \ -f https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml ``` The [`values.local.yaml`](https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml) file configures each host's HTTP listener on port 80. The kind cluster config maps container port 30950 to host port 80, so any NodePort Service on port 30950 will be reachable at `localhost`: ```shell helm install wasmcloud --version {{WASMCLOUD_VERSION}} oci://ghcr.io/wasmcloud/charts/runtime-operator \ --namespace wasmcloud --create-namespace \ -f https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml ``` The k3d cluster was started with host port 80 mapped to NodePort 30950 on the server node. That NodePort matches the `nodePort: 30950` in the hello-world Service applied later in this tutorial, so requests to `localhost:80` reach the workload directly: ```shell helm install wasmcloud --version {{WASMCLOUD_VERSION}} oci://ghcr.io/wasmcloud/charts/runtime-operator \ --namespace wasmcloud --create-namespace \ -f https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml ``` k3s includes a built-in load balancer (Klipper) that binds `LoadBalancer` services to node ports: ```shell helm install wasmcloud --version {{WASMCLOUD_VERSION}} oci://ghcr.io/wasmcloud/charts/runtime-operator \ --namespace wasmcloud --create-namespace \ -f https://raw.githubusercontent.com/wasmCloud/wasmCloud/refs/heads/main/charts/runtime-operator/values.local.yaml ``` Wait for the deployment to be ready: ```shell kubectl rollout status deploy -l app.kubernetes.io/instance=wasmcloud -n wasmcloud ``` ## Deploy Wasm workload We'll deploy the Wasm application with two resources: a Kubernetes `Service` that receives HTTP traffic, and a [`WorkloadDeployment`](../kubernetes-operator/crds.mdx#workloaddeployment) that references the Service and tells the operator to schedule the component onto a host. The operator maintains an EndpointSlice for the Service pointing at the host pods running the workload, so standard Kubernetes DNS and routing reach the component directly. :::info[wasmCloud security model] wasmCloud components are **deny-by-default**: a component cannot use any capability—HTTP, key-value storage, logging, or any other—unless the host is explicitly told to allow it. The `hostInterfaces` field in a workload manifest is the allowlist. Each entry names an interface package that the host will make available to the component. Any interface not listed is silently unavailable, regardless of what the component's WIT file imports. ::: Run the following script in your terminal to create a file called `deployment.yaml` containing both manifests. ```shell cat > deployment.yaml << 'EOF' apiVersion: v1 kind: Service metadata: name: http-hello-world spec: type: ClusterIP ports: - port: 80 protocol: TCP --- apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: http-hello-world spec: replicas: 1 template: spec: hostSelector: hostgroup: default kubernetes: service: name: http-hello-world components: - name: http-hello-world image: ghcr.io//http-hello-world:0.1.0 hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler - namespace: wasi package: keyvalue interfaces: - atomics - store EOF ``` ```shell cat > deployment.yaml << 'EOF' apiVersion: v1 kind: Service metadata: name: http-hello-world spec: type: NodePort ports: - port: 80 protocol: TCP nodePort: 30950 --- apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: http-hello-world spec: replicas: 1 template: spec: hostSelector: hostgroup: default kubernetes: service: name: http-hello-world components: - name: http-hello-world image: ghcr.io//http-hello-world:0.1.0 hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: host: localhost - namespace: wasi package: keyvalue interfaces: - atomics - store EOF ``` ```shell cat > deployment.yaml << 'EOF' apiVersion: v1 kind: Service metadata: name: http-hello-world spec: type: LoadBalancer ports: - port: 80 protocol: TCP --- apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: http-hello-world spec: replicas: 1 template: spec: hostSelector: hostgroup: default kubernetes: service: name: http-hello-world components: - name: http-hello-world image: ghcr.io//http-hello-world:0.1.0 hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: host: localhost - namespace: wasi package: keyvalue interfaces: - atomics - store EOF ``` ```shell cat > deployment.yaml << 'EOF' apiVersion: v1 kind: Service metadata: name: http-hello-world spec: type: LoadBalancer ports: - port: 80 protocol: TCP --- apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: http-hello-world spec: replicas: 1 template: spec: hostSelector: hostgroup: default kubernetes: service: name: http-hello-world components: - name: http-hello-world image: ghcr.io//http-hello-world:0.1.0 hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: host: localhost - namespace: wasi package: keyvalue interfaces: - atomics - store EOF ``` The manifest has two resources: a Kubernetes [custom resource](../kubernetes-operator/crds.mdx) (`WorkloadDeployment`) that describes the workload, and a standard `Service`. The fields to highlight in the `WorkloadDeployment`: - `hostSelector.hostgroup`: Selects which pool of wasmCloud hosts runs this workload. The Helm chart installs three host pods labelled `hostgroup: default`—this is the correct target for most deployments. Custom host groups can be created to isolate workloads or provide specialized capabilities. - `kubernetes.service.name`: References the Service by name. The operator creates and maintains an EndpointSlice for this Service that points at the host pods running the workload, and registers the Service's DNS names (`http-hello-world`, `http-hello-world.default`, `http-hello-world.default.svc`) with the host's HTTP router. For local development, `config.host: localhost` is added so the component also answers requests whose `Host` header is `localhost`. - `components.image`: Defines the registry address we will use to fetch our Wasm image. - `hostInterfaces`: Declares which capability interfaces the host will make available to the component. **Without an explicit entry here, the component cannot access that capability.** Since the HTTP and key-value capabilities use [well-known built-in interfaces](../overview/interfaces.mdx), all we have to do is list them. Open the file and update the image name with the correct registry address. If you're using GitHub Packages, that means replacing `` with your GitHub namespace. Now use `kubectl` to apply the manifest: ```shell kubectl apply -f deployment.yaml ``` Verify that the Wasm component is successfully deployed: ```shell kubectl get workloaddeployments ``` Use `curl` to invoke the Wasm workload with an HTTP request: :::note[macOS with OrbStack] OrbStack manages port 80 on macOS, intercepting traffic before it reaches the cluster. Run a port-forward in a separate terminal: ```shell kubectl port-forward svc/http-hello-world 8080:80 ``` Then substitute `localhost:8080` for `localhost` in the hello-world command, and use `-H "Host: blobby.localhost.direct" http://localhost:8080/...` in place of `http://blobby.localhost.direct/...` in the Blobby commands further below. ::: ```shell curl localhost ``` ```text Hello x1, World! ``` ## Deploy the Blobby file server Now let's deploy a more feature-rich example: **Blobby** ("Little Blobby Tables"), a simple file server that demonstrates the [`wasi:blobstore`](https://github.com/WebAssembly/wasi-blobstore) capability alongside `wasi:http`. Because only one NodePort/LoadBalancer Service can claim the external port at a time, **delete the `http-hello-world` workload and its Service before deploying Blobby**: ```shell kubectl delete workloaddeployment http-hello-world kubectl delete service http-hello-world ``` The Blobby manifest routes HTTP traffic on `blobby.localhost.direct`, a public DNS alias for `127.0.0.1`, so no `/etc/hosts` changes are needed. Apply the manifest below to deploy the [`blobby`](https://github.com/wasmCloud/wasmCloud/tree/main/examples/blobby) component alongside a Service of the appropriate type for your environment: ```shell kubectl apply -f - < ```shell kubectl apply -f - < ```shell kubectl apply -f - < ```shell kubectl apply -f - < Verify the deployment: ```shell kubectl get workloaddeployments ``` Once running, open **http://blobby.localhost.direct** in your browser to use the file manager UI: ![Blobby Storage Manager UI showing an uploaded file with Copy Link, Download, and Delete buttons](../images/blobby-ui.webp) You can also interact with the file server directly from the command line: Upload a file: ```shell echo "Hello, wasmCloud!" > hello.txt curl -H "Content-Type: text/plain" http://blobby.localhost.direct/hello.txt --data-binary @hello.txt ``` List all files: ```shell curl -X POST http://blobby.localhost.direct/ ``` Download a file: ```shell curl http://blobby.localhost.direct/hello.txt ``` Delete a file: ```shell curl -X DELETE http://blobby.localhost.direct/hello.txt ``` ## Clean up To remove the deployments and their Services created in this tutorial: ```shell kubectl delete workloaddeployment http-hello-world blobby --ignore-not-found kubectl delete service http-hello-world blobby --ignore-not-found ``` To tear down the cluster entirely: Remove the wasmCloud Helm release: ```shell helm uninstall wasmcloud ``` ```shell kind delete cluster ``` ```shell k3d cluster delete wasmcloud ``` ```shell k3s-uninstall.sh ``` ## What's next? - **Explore the interfaces:** Learn how [WASI interfaces](../overview/interfaces.mdx) work and browse the full list of built-in capabilities your components can use. - **Go deeper with components:** The [Developer Guide](../wash/developer-guide/index.mdx) covers building, publishing, debugging, and composing components. - **Deploy to production:** The [Operator Manual](../kubernetes-operator/operator-manual/overview.mdx) covers production topics: RBAC, private registries, and CI/CD pipelines. --- ## Develop a Wasm Component import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import HubspotForm from 'react-hubspot-form'; Once you know what your application needs to be able to do, you can add language-agnostic [**interfaces**](../overview/interfaces.mdx) to access capabilities like HTTP, key-value storage, or logging. In this step, we'll discuss how to: - Update our application to accept a name and return a personalized greeting. - Add more features to our application by plugging in **key-value** and **logging** capabilities. :::info[Prerequisites] This tutorial assumes you're following directly from the previous tutorial. If you don't have a "Hello world" application running, complete [**Quickstart**](./index.mdx) first. ::: ## Add functionality Let's extend this application to do more than just say "Hello!" We can check the request for a name provided in a query string, and then return a greeting with that name. If there isn't one or the path isn't in the format we expect, we'll default to saying "Hello, World!" ```rust use wstd::http::{Body, Request, Response, StatusCode}; #[wstd::http_server] async fn main(req: Request) -> Result, wstd::http::Error> { match req.uri().path() { "/" => home(req).await, _ => not_found(req).await, } } async fn home(req: Request) -> Result, wstd::http::Error> { // Parse query string for name parameter // [!code ++] let query = req.uri().query().unwrap_or(""); // [!code ++] let name = match query.split("=").collect::>()[..] { // [!code ++] ["name", name] => name, // [!code ++] _ => "World", // [!code ++] }; // [!code ++] // Return a simple response with a string body Ok(Response::new("Hello from Wasm!\n".into())) // [!code --] Ok(Response::new(format!("Hello, {name}!\n").into())) // [!code ++] } async fn not_found(_req: Request) -> Result, wstd::http::Error> { Ok(Response::builder() .status(StatusCode::NOT_FOUND) .body("Not found\n".into()) .unwrap()) } ``` Let's extend this application to do more than just say "Hello!" Using Hono's built-in query parameter parsing, we can check the request for a name provided in a query string, and then return a greeting with that name. If there isn't one, we'll default to saying "Hello, World!" ```typescript import { Hono } from 'hono'; import { fire } from '@bytecodealliance/jco-std/wasi/0.2.x/http/adapters/hono/server'; const app = new Hono(); app.get('/', (c) => { return c.text('Hello from Wasm!\n'); // [!code --] const name = c.req.query('name') || 'World'; // [!code ++] return c.text(`Hello, ${name}!\n`); // [!code ++] }); app.notFound((c) => { return c.text('Not found\n', 404); }); fire(app); export { incomingHandler } from '@bytecodealliance/jco-std/wasi/0.2.x/http/adapters/hono/server'; ``` {/* Using the FormValue method, check the query string for a name parameter and return a personalized greeting. */} If you prefer working in a language that isn't listed here, let us know! {' '} console.log('Submitted form')} onReady={(form) => console.log('Form ready for submit')} region="na1" loading={Loading...} /> After saving your changes, your toolchain automatically builds and runs the updated application. We can `curl` the application again: ```shell curl localhost:8000 ``` ```text Hello, World! ``` ```shell curl 'localhost:8000?name=Bob' ``` ```text Hello, Bob! ``` ## Add persistent storage Now let's add persistent storage to keep a record of each person that this application greeted. We'll use the **key-value capability** for this. We don't need to pick a library or a specific vendor implementation—all we have to do is add the `wasi:keyvalue` interface to our component. We can use the [`wasi:keyvalue`](https://github.com/WebAssembly/wasi-keyvalue) interface for interacting with a key-value store. Interfaces are declared in **WIT (WebAssembly Interface Types)** files. A WIT file is a language-neutral contract describing what your component imports (capabilities it consumes) and exports (functions it exposes). Adding an `import` line to your component's WIT `world` tells the runtime to provide that capability—without binding you to a specific library or vendor. You can read more about WIT on the [Interfaces](../overview/interfaces.mdx) page. Before we can use the interface, we'll need to update the `wit/world.wit` file: ```wit package wasmcloud:hello; world hello { import wasi:keyvalue/store@0.2.0-draft; // [!code ++] import wasi:keyvalue/atomics@0.2.0-draft; // [!code ++] } ``` Before we can use the interface, we'll need to update the `wit/world.wit` file: ```wit package wasmcloud:templates@0.1.0; world typescript-http-hello-world-hono { import wasi:keyvalue/store@0.2.0-draft; // [!code ++] import wasi:keyvalue/atomics@0.2.0-draft; // [!code ++] export wasi:http/incoming-handler@0.2.6; } ``` {/* Update `wit/world.wit` to import `wasi:keyvalue/atomics` and `wasi:keyvalue/store`. */} If you prefer working in a language that isn't listed here, let us know! {' '} console.log('Submitted form')} onReady={(form) => console.log('Form ready for submit')} region="na1" loading={Loading...} /> We've given our application the ability to perform atomic incrementation and storage operations via the `wasi:keyvalue` interface. Now we need to add `wit-bindgen` to our `Cargo.toml` so we can generate Rust bindings from the WIT world: ```toml [dependencies] wstd = "0.6.3" wit-bindgen = "0.46.0" # [!code ++] ``` Now let's use the atomic increment function to keep track of how many times we've greeted each person. ```rust use wstd::http::{Body, Request, Response, StatusCode}; // Generate keyvalue bindings from our WIT world // [!code ++] // wstd handles the http export via its macro // [!code ++] wit_bindgen::generate!({ // [!code ++] world: "hello", // [!code ++] path: "wit", // [!code ++] generate_all, // [!code ++] }); // [!code ++] use wasi::keyvalue::{atomics, store}; // [!code ++] #[wstd::http_server] async fn main(req: Request) -> Result, wstd::http::Error> { match req.uri().path() { "/" => home(req).await, _ => not_found(req).await, } } async fn home(req: Request) -> Result, wstd::http::Error> { // Parse query string for name parameter let query = req.uri().query().unwrap_or(""); let name = match query.split("=").collect::>()[..] { ["name", name] => name, _ => "World", }; // Open keyvalue bucket and increment counter for this name // [!code ++] // Note: wasmtime's in-memory keyvalue provider requires empty string as identifier // [!code ++] let bucket = store::open("") // [!code ++] .map_err(|e| wstd::http::Error::msg(format!("keyvalue open error: {:?}", e)))?; // [!code ++] let count = atomics::increment(&bucket, &name, 1) // [!code ++] .map_err(|e| wstd::http::Error::msg(format!("keyvalue increment error: {:?}", e)))?; // [!code ++] // Return greeting with count // [!code ++] Ok(Response::new(format!("Hello x{count}, {name}!\n").into())) // [!code ++] // Return a simple response with a string body // [!code --] Ok(Response::new(format!("Hello, {name}!\n").into())) // [!code --] } async fn not_found(_req: Request) -> Result, wstd::http::Error> { Ok(Response::builder() .status(StatusCode::NOT_FOUND) .body("Not found\n".into()) .unwrap()) } ``` We've made changes, so once we save, the toolchain will once again automatically update the running application. We've given our application the ability to perform atomic incrementation and storage operations via the `wasi:keyvalue` interface. Now let's use the atomic increment function to keep track of how many times we've greeted each person. ```typescript import { Hono } from 'hono'; import { fire } from '@bytecodealliance/jco-std/wasi/0.2.x/http/adapters/hono/server'; import { increment } from 'wasi:keyvalue/atomics@0.2.0-draft'; // [!code ++:2] import { open } from 'wasi:keyvalue/store@0.2.0-draft'; const app = new Hono(); app.get('/', (c) => { const name = c.req.query('name') || 'World'; return c.text(`Hello, ${name}!\n`); // [!code --] // Open keyvalue bucket and increment counter for this name // [!code ++:4] const bucket = open(''); const count = increment(bucket, name, 1); return c.text(`Hello x${count}, ${name}!\n`); // [!code ++] }); app.notFound((c) => { return c.text('Not found\n', 404); }); fire(app); export { incomingHandler } from '@bytecodealliance/jco-std/wasi/0.2.x/http/adapters/hono/server'; ``` We've made changes, so once we save, the toolchain will once again automatically update the running application. {/* Use `store.Open`, `atomics.Increment`, and `wasilog` to implement the counter and logging. */} If you prefer working in a language that isn't listed here, let us know! {' '} console.log('Submitted form')} onReady={(form) => console.log('Form ready for submit')} region="na1" loading={Loading...} /> Let's `curl` the component again: ```shell curl 'localhost:8000?name=Bob' ``` ```text Hello x1, Bob! ``` ```shell curl 'localhost:8000?name=Bob' ``` ```text Hello x2, Bob! ``` ```shell curl 'localhost:8000?name=Alice' ``` ```text Hello x1, Alice! ``` ## Next steps In this tutorial, you added a few more features and persistent storage to a simple microservice. You also got to see the process of developing with capabilities, where you can... - Write purely functional code that doesn't require you to pick a library or vendor upfront - Change your application _separately_ from its non-functional requirements So far, `wash` has satisfied our application's capability requirements automatically, so we can move quickly and focus on code. In the [next tutorial](./deploy-a-webassembly-workload.mdx), we'll deploy WebAssembly workloads to wasmCloud on a local Kubernetes cluster. --- ## Quickstart import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import HubspotForm from 'react-hubspot-form'; ### Build WebAssembly (Wasm) applications and run them across any cloud, Kubernetes, datacenter, or edge. In this tutorial: - We'll use the **Wasm Shell (`wash`) CLI** to build a simple "Hello world" HTTP server application as a [WebAssembly (Wasm) component](../overview/workloads/components.mdx)—written in the language of your choice. - Along the way, we'll start a **developer loop** that automatically deploys our application to a local wasmCloud environment. A **WebAssembly (Wasm) component** is a portable, sandboxed binary (`.wasm` file) that implements the core logic of your application. Components are compiled from your language of choice—Rust, TypeScript, Go, and others—and run inside the wasmCloud runtime, which mediates all access to system resources and external capabilities on their behalf. ## Install `wash` First, we need to install the latest version of `wash` (**`{{WASMCLOUD_VERSION}}`**). If you've already installed `wash`, skip ahead to [Choose your language](#choose-your-language). In your terminal, run the installation script to download and install the latest version of `wash` (**`{{WASMCLOUD_VERSION}}`**): ```shell curl -fsSL https://wasmcloud.com/sh | bash ``` The script installs `wash` to `~/.wash/bin` and automatically updates your shell profile. Open a new terminal session to use `wash`.
Customizing the install location To install to a different directory, set `INSTALL_DIR`: ```shell curl -fsSL https://wasmcloud.com/sh | INSTALL_DIR=/usr/local/bin bash ``` To skip automatic PATH modification (for example, if you manage your own dotfiles): ```shell curl -fsSL https://wasmcloud.com/sh -o install.sh && bash install.sh --no-modify-path ```
If you have [Homebrew](https://brew.sh/) installed, you can install `wash` with: ```shell brew install wasmcloud/wasmcloud/wash ``` Homebrew updates your PATH automatically. You're ready to use `wash` in any terminal session.
In PowerShell, run the installation script to download and install the latest version of `wash` (**`{{WASMCLOUD_VERSION}}`**): ```powershell iwr -useb https://wasmcloud.com/ps1 | iex ``` The script installs `wash` to `%USERPROFILE%\.wash\bin` and automatically adds it to your user PATH. Open a new terminal session to use `wash`.
Customizing the install location To install to a different directory, set `INSTALL_DIR`: ```powershell $env:INSTALL_DIR = "C:\tools\wash"; iwr -useb https://wasmcloud.com/ps1 | iex ``` To skip automatic PATH modification: ```powershell iwr -useb https://wasmcloud.com/ps1 -OutFile install.ps1; .\install.ps1 -NoModifyPath ```
If you have [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/) installed, you can install `wash` with: ```powershell winget install wasmCloud.wash ``` winget updates your PATH automatically. Open a new terminal session to use `wash`.
You will need [`cargo`](https://doc.rust-lang.org/cargo/getting-started/installation.html) to install from source. ```shell git clone https://github.com/wasmcloud/wasmCloud.git cd wasmCloud cargo install --path crates/wash-cli ``` Pre-built binaries for macOS, Linux, and Windows are [available on GitHub](https://github.com/wasmcloud/wasmCloud/releases).
In a new terminal, verify that `wash` is properly installed and check your version for **`{{WASMCLOUD_VERSION}}`** with: ```bash wash -V ``` If `wash` is installed correctly, you will see the version string printed, for example `wash {{WASMCLOUD_VERSION}}`. Run `wash --help` to see all available commands and short descriptions for each. ## Choose your language wasmCloud supports building WebAssembly components from any language that supports the [WASI P2 (also called WASI 0.2) target](../overview/interfaces.mdx#well-known-interfaces), including Go, Rust, TypeScript, and others. `wash` depends on your local language toolchain for your language of choice. - **Choose the language** you would like to use. - **Install the toolchain** or verify that you have the correct versions. **Install the Rust toolchain** Requirements: - [The Rust toolchain](https://www.rust-lang.org/tools/install) (always use the latest version) - The `wasm32-wasip2` target for Rust On macOS or Ubuntu/Debian, you can use the official install script to download `rustup`, which will automatically install the Rust toolchain: ```shell curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` Once you've installed the Rust toolchain, use `rustup` to add the `wasm32-wasip2` target: ```shell rustup target add wasm32-wasip2 ``` **Install the TypeScript toolchain** Requirements: - [Node.js](https://nodejs.org/en/learn/getting-started/how-to-install-nodejs) v18+ - [TypeScript](https://www.typescriptlang.org/) v5.6+ On macOS, you can use [Homebrew](https://brew.sh/) to install `npm` and Node.js. (See the [official Node.js documentation](https://nodejs.org/en/learn/getting-started/how-to-install-nodejs) for other options.) ```shell brew install node ``` On Ubuntu, you can install `npm` (and Node.js) using the [Node Version Manager (`nvm`)](https://github.com/nvm-sh/nvm). (See the [official Node.js documentation](https://nodejs.org/en/learn/getting-started/how-to-install-nodejs) for other options.) You can download and install `nvm` using the official installation script: ```shell curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash ``` Once the script completes, you may need to start a new shell session. You can use `nvm` to install the latest Long-Term Service version of `node`: ```shell nvm install --lts ``` If you prefer working in a language that isn't listed here, let us know! {' '} console.log('Submitted form')} onReady={(form) => console.log('Form ready for submit')} region="na1" loading={Loading...} /> Once you've installed Node.js and `npm`, you can use `npm` to install TypeScript: ```shell npm install -g typescript ``` {/* **Install the Go toolchain** Requirements: - [Go](https://go.dev/doc/install) 1.23.0+ - [TinyGo](https://tinygo.org/getting-started/install/) (always use the latest version): - [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools#installation) **v1.225.0** :::warning[] Due to incompatibilities introduced in `wasm-tools` v1.226.0 and higher, **we strongly recommend using `wasm-tools` v1.225.0** when building Go projects. The easiest way to download `wasm-tools` v1.225.0 is via [cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html): `cargo install --locked wasm-tools@1.225.0` ::: On macOS, you can use [Homebrew](https://brew.sh/) to install the Go toolchain. (See the [official Go documentation](https://go.dev/doc/install) for other options.) ```shell brew install go ``` ```shell brew install tinygo-org/tools/tinygo ``` **You will also need the `wasm-tools` utility.** On Ubuntu/Debian, Go 1.23+ isn't currently available via major package managers, but you can use `curl` or `wget` to download the Go toolchain from the [official Go download page](https://go.dev/dl/). (See the [official Go documentation](https://go.dev/doc/install) for other options.) Before downloading, ensure that you do not have a previous version of Go installed. You can find an existing installation in the `/usr/local/go` directory. Consult the [download page](https://go.dev/dl/) and set environment variables specifying the appropriate version and architecture for your system. (The values below are examples.) ```shell export GO_VERSION="1.23.4" ``` ```shell export GO_ARCH="amd64" ``` Download your file: ```shell curl -O -L "https://golang.org/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" ``` Find the appropriate checksum value for your file on the [download page](https://go.dev/dl/) and validate the file. (The example below uses the checksum value for the v1.23.4 on `amd64` download.) ```shell echo -n "6924efde5de86fe277676e929dc9917d466efa02fb934197bc2eba35d5680971 *go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" | shasum -a 256 --check ``` Extract the file: ```shell tar -xf "go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" ``` You may need to change ownership on the new `go` directory: ```shell sudo chown -R root:root ./go ``` Finally, move the `go` directory to `/usr/local`: ```shell mv -v go /usr/local ``` On Ubuntu/Debian, you can download TinyGo similarly. Refer to the [TinyGo GitHub releases page](https://github.com/tinygo-org/tinygo/releases/) to find the correct version and architecture and set environment variables accordingly. (The values below are examples—**remember to always use the latest version of TinyGo**.) ```shell export TINYGO_VERSION="0.34.0" ``` ```shell export TINYGO_ARCH="amd64" ``` Now download the `.deb` file: ```shell curl -O -L "https://github.com/tinygo-org/tinygo/releases/download/v${TINYGO_VERSION}/tinygo_${TINYGO_VERSION}_${TINYGO_ARCH}.deb" ``` Install TinyGo from the `.deb` file: ```shell sudo dpkg -i tinygo_${TINYGO_VERSION}_${TINYGO_ARCH}.deb ``` Add TinyGo to your PATH: ```shell export PATH=$PATH:/usr/local/bin ``` Validate the installation: ```shell tinygo version ``` **You will also need the `wasm-tools` utility.** For the best experience, we strongly recommend using `wasm-tools` v1.225.0 and installing with [cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html): ```shell cargo install --locked wasm-tools@1.225.0 ``` Otherwise, [download the binary for `wasm-tools` v1.225.0 for your architecture](https://github.com/bytecodealliance/wasm-tools/releases/tag/v1.225.0) and add it to your PATH. */} If you prefer working in a language that isn't listed here, let us know! {' '} console.log('Submitted form')} onReady={(form) => console.log('Form ready for submit')} region="na1" loading={Loading...} /> ## Create a new component Now that we've installed `wash` and our language toolchain, it's time to create a new component project. In your terminal, run: ```shell wash new https://github.com/wasmCloud/wasmCloud.git --name http-hello-world --subfolder templates/http-hello-world ``` This command... * Creates a new project named `http-hello-world`... * Based on a template found in the [`wasmCloud/wasmCloud`](https://github.com/wasmCloud/wasmCloud) Git repository... * In the subfolder `templates/http-hello-world`. Navigate to the `hello` project directory: ```sh cd http-hello-world ``` ### Start your developer loop We'll use [`watchexec`](https://github.com/watchexec/watchexec) to rebuild and redeploy our component whenever we save a source file. Install it once (see the [watchexec install guide](https://github.com/watchexec/watchexec#install) for other options): ```shell cargo install watchexec-cli ``` From our project directory, run: ```shell watchexec -c -r 'wash dev' ``` `watchexec` watches the project directory and restarts `wash dev` on every save (`-r` restarts the running command, `-c` clears the screen). `wash dev` **builds your WebAssembly component** and **deploys it to a local wasmCloud environment**. The terminal output should look like this: ```text [Running: wash dev] INFO starting development session for project path="." INFO HTTP server listening addr=0.0.0.0:8000 INFO development session started, building and deploying component... INFO listening for HTTP requests address=http://0.0.0.0:8000 ``` By default, the application will run on our local port 8000. In a new terminal tab, we can `curl` our application: ```shell curl localhost:8000 ``` The application should return: ```text Hello from wasmCloud! ``` In your terminal, run: ```shell wash new https://github.com/wasmCloud/typescript.git --name http-hello-world --subfolder templates/http-hello-world-hono ``` This command... * Creates a new project named `http-hello-world`... * Based on a template found in the [`wasmCloud/typescript`](https://github.com/wasmCloud/typescript) Git repository... * In the subfolder `templates/http-hello-world-hono`. This template uses the [Hono](https://hono.dev/) web framework for routing. Navigate to the project directory: ```sh cd http-hello-world ``` ### Start your developer loop From our project directory, we'll run: ```shell npm run dev ``` The `npm run dev` command automatically **builds your WebAssembly component** and **deploys it to a local wasmCloud environment**. The last lines of the terminal output should look like this: ```text OK Successfully written dist/http_hello_world_hono.wasm. 2026-02-06T15:35:03.588693Z INFO listening for HTTP requests address=http://0.0.0.0:8000 ``` By default, the application will run on our local port 8000. In a new terminal tab, we can `curl` our application: ```shell curl localhost:8000 ``` The application should return: ```text Hello from TypeScript! ``` You just ran a WebAssembly application in wasmCloud! If you prefer working in a language that isn't listed here, let us know! {' '} console.log('Submitted form')} onReady={(form) => console.log('Form ready for submit')} region="na1" loading={Loading...} /> ## Make the application yours Now we'll make a change to our code and see how the running application updates automatically. The Rust code for our component is found in `src/lib.rs`. Open the file in your code editor and change the "Hello" message on line 13. You might modify the message to say hello from WebAssembly, your own name, or whatever else you like. ```rust use wstd::http::{Body, Request, Response, StatusCode}; #[wstd::http_server] async fn main(req: Request) -> Result, wstd::http::Error> { match req.uri().path() { "/" => home(req).await, _ => not_found(req).await, } } async fn home(_req: Request) -> Result, wstd::http::Error> { // Return a simple response with a string body Ok(Response::new("Hello from wasmCloud!\n".into())) // [!code --] Ok(Response::new("Hello from Wasm!\n".into())) // [!code ++] } async fn not_found(_req: Request) -> Result, wstd::http::Error> { Ok(Response::builder() .status(StatusCode::NOT_FOUND) .body("Not found\n".into()) .unwrap()) } ``` The TypeScript code for our component is found in `src/component.ts`. Open the file in your code editor and change the "Hello" message on line 7. You might change the message into a greeting from WebAssembly, your own name, or whatever else you like. ```typescript import { Hono } from 'hono'; import { fire } from '@bytecodealliance/jco-std/wasi/0.2.x/http/adapters/hono/server'; const app = new Hono(); app.get('/', (c) => { return c.text('Hello from TypeScript!\n'); // [!code --] return c.text('Hello from Wasm!\n'); // [!code ++] }); app.notFound((c) => { return c.text('Not found\n', 404); }); fire(app); export { incomingHandler } from '@bytecodealliance/jco-std/wasi/0.2.x/http/adapters/hono/server'; ``` {/* The Go code for our component is found in `main.go`. Change the "Hello" message in the `handleRequest` function. */} If you prefer working in a language that isn't listed here, let us know! {' '} console.log('Submitted form')} onReady={(form) => console.log('Form ready for submit')} region="na1" loading={Loading...} /> Save the modified file, then rebuild and redeploy the updated component: `watchexec` detects the save and restarts `wash dev`, which rebuilds and redeploys the component. No manual restart needed. The TypeScript template uses [`nodemon`](https://nodemon.io/) to watch your `src/` directory and automatically rebuild and redeploy on save. No restart needed. In the terminal, `curl` the application again: ```shell curl localhost:8000 ``` ```text Hello from Wasm! ``` ## Next steps We installed `wash`, built our first component, and ran our application locally. In our next tutorial, we'll **add pluggable, reusable capabilities** like key-value storage to our application. --- ## Expose a Workload via Kubernetes Service This recipe shows how to expose a wasmCloud workload to other services in your Kubernetes cluster using a standard [Kubernetes Service](https://kubernetes.io/docs/concepts/services-networking/service). The operator manages [EndpointSlices](https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/) that point to the host pods running the workload, so cluster-internal DNS (e.g. `my-service.default.svc.cluster.local`) resolves to the correct pods automatically. By the end, you will have a wasmCloud workload reachable by DNS name from anywhere in the cluster. ## Prerequisites - A Kubernetes cluster with wasmCloud installed via the [Helm chart](../kubernetes-operator/index.mdx) (runtime-operator **v2.0.3 or later** — the `kubernetes.service` field was added in 2.0.3) - [`kubectl`](https://kubernetes.io/docs/tasks/tools/#kubectl) ## Overview When you set `spec.kubernetes.service.name` (or `spec.template.spec.kubernetes.service.name` on a `WorkloadDeployment`) to the name of an existing Kubernetes Service, the operator: 1. Creates and maintains an EndpointSlice for that Service, pointing to the pod IPs of the hosts running the workload. 2. Registers Service DNS aliases (`my-service`, `my-service.default`, `my-service.default.svc`) with the host, so the host's HTTP router accepts requests whose `Host` header matches any of these forms. This means you can call the workload from other pods by resolving its Service DNS name (short name or fully-qualified, in-cluster or cross-namespace), as long as the request's `Host` header is one of the registered aliases. Note that the fully-qualified `.cluster.local` form is **not** a registered alias — use it for DNS resolution only, and set the `Host` header to one of the shorter forms. ## Step 1: Create the Kubernetes Service Create a Service before deploying the workload. The operator will reference this Service by name and manage its EndpointSlices. ```yaml apiVersion: v1 kind: Service metadata: name: hello-service namespace: default spec: ports: - name: http port: 8080 protocol: TCP ``` Note that the Service does not include a `selector`. The operator manages the EndpointSlice directly, so no label matching is needed. ```shell kubectl apply -f - < std::collections::HashMap { wasi::cli::environment::get_environment() .into_iter() .collect() } ``` ### TypeScript Import `getEnvironment` from `wasi:cli/environment`: ```typescript import { getEnvironment } from 'wasi:cli/environment@0.2.3'; const config = Object.fromEntries(getEnvironment()); const region = config['REGION'] ?? 'us-west-2'; const apiKey = config['API_KEY']; ``` Call `getEnvironment()` inside route handlers rather than at module scope. See the [Configuration guide](../wash/developer-guide/language-support/typescript/configuration.mdx) for a full TypeScript walkthrough including Hono integration. ## Multiple ConfigMaps and Secrets You can reference multiple ConfigMaps and multiple Secrets. Within each list, the last entry wins on key conflicts: ```yaml localResources: environment: configFrom: - name: base-config # loaded first - name: override-config # wins on conflicts secretFrom: - name: shared-secrets # loaded first - name: app-secrets # wins on conflicts ``` This is useful for layering a shared base configuration with per-application overrides. ## Development workflow During local development with `wash dev`, `localResources.environment` defaults to empty. Provide fallback defaults in your component code so the component runs correctly without configuration: ```typescript const config = Object.fromEntries(getEnvironment()); const region = config['REGION'] ?? 'us-west-2'; const logLevel = config['LOG_LEVEL'] ?? 'debug'; ``` Test configuration-dependent behavior by deploying to a Kubernetes environment with the full manifest. ## Clean up ```shell kubectl delete workloaddeployment configured-app kubectl delete configmap app-config kubectl delete secret app-secrets ``` ## Related documentation - [Secrets and Configuration Management](../kubernetes-operator/operator-manual/secrets-and-configuration.mdx) for a complete reference on `localResources.environment` - [Configuration (TypeScript)](../wash/developer-guide/language-support/typescript/configuration.mdx) for a full walkthrough of reading configuration in TypeScript - [Custom Resource Definitions](../kubernetes-operator/crds.mdx) for the full CRD field reference - [Roles and Role Bindings](../kubernetes-operator/operator-manual/roles-and-rolebindings.mdx) for RBAC considerations when referencing Secrets --- ## Plugin or Service? This recipe gives you a framework for choosing between two ways of providing capabilities to a wasmCloud application: extending the host with a [plugin](../overview/hosts/plugins.mdx), or shipping a [service](../overview/workloads/services.mdx) inside the workload. The two are not interchangeable—each has hard constraints that rule it out in some situations, and softer trade-offs in the rest. ## A quick introduction to plugins and services A **host plugin** is a Rust implementation of the `HostPlugin` trait, compiled into the host binary. A host plugin is native Rust code that is available to and executed by components via calling into the host. More concretely, when workloads are bound, the runtime adds the plugin's WIT imports directly to the wasmtime linker for each component or service in the workload, so calls from guest code dispatch in-process. For example, `wash-runtime` ships with built-in host plugins for interfaces like `wasi:keyvalue`, `wasi:blobstore`, `wasi:config`, `wasi:logging`, `wasi:otel`, `wasmcloud:messaging`, and more. See the [Plugins overview](../overview/hosts/plugins.mdx). A **wasmCloud service** is a Wasm component that runs for the lifetime of a workload, deployed as part of a [`WorkloadDeployment`](../kubernetes-operator/crds.mdx#workloaddeployment). Unlike other components in a `WorkloadDeployment`, services receive the `wasi:sockets` TCP bind permission, can listen on loopback and unspecified addresses to interact with other components in the workload, and are automatically restarted if they crash. A service exports either `wasi:cli/run` or exactly one WIT interface so the runtime has an unambiguous entry point. See the [Services overview](../overview/workloads/services.mdx). ## Plugin and service comparison | | Plugin | Service | |---|---|---| | **Form** | Rust trait impl linked into the host binary | Wasm component shipped with the workload | | **Author language** | Rust | Any language that targets `wasm32-wasip2` | | **Decision point** | Host build time | Workload deploy time | | **Scope** | All workloads on the host | One workload | | **Execution** | In-process (added to the Wasmtime linker) | Separate Wasm instance | | **Shared state across workloads** | Yes—e.g. one connection pool or cache | No—sandboxed per workload | | **TCP listen()** | No | Yes, on loopback and unspecified addresses | | **Auto-restart on crash** | N/A | Yes | | **Declared by workload as** | `host_interfaces: [...]` | `service: { ... }` | | **Operator-facing toggle** | Cargo features on the host crate | Manifest field on the workload | ## Decision tree ![Decision tree for choosing a plugin or a service](./images/plugin-or-service.svg) The first two questions are hard filters: needing to listen on a TCP port rules plugins out, and needing in-process sharing across workloads rules services out. The third question is a tiebreaker that maps the choice to where the code should live in your deploy: with the platform (plugin) or with the application (service). ## When to choose a plugin Pick a plugin when: - The capability is **infrastructure**—storage, telemetry, secrets, messaging, a custom database driver—and should behave the same across every workload running on the host. - You want **one resource** (a connection pool, a shared cache, a hardware handle) serving many workloads at once. Built-in plugins like `InMemoryKeyValue` do exactly this, keyed by workload ID for isolation. - The capability needs to be **available to components in any language**, not just Rust. Plugins expose a WIT interface, so guests written in Rust, TypeScript, Go, or any other Wasm-targeting language consume them identically. - You **control the host build** and can roll a new host image to ship any required changes. ## When to choose a service Pick a service when: - The work is **specific to one application** and shouldn't bleed across deployments—a per-tenant connection, a request batcher, an in-memory cache that only one workload uses. - You need to **listen on a TCP port** inside the workload, whether as a real protocol server or as a way to bridge component invocations into a streaming connection. - The behavior ships **with the application artifact**, not with the platform. Updating it means redeploying the workload, not rebuilding the host. - The implementation **isn't Rust**, or the team writing it doesn't own the host binary. ## Best-practice examples **Per-app configuration from Kubernetes ConfigMaps and Secrets** → **Plugin.** The built-in `wasi:config` plugin reads runtime configuration uniformly for every workload on the host. This is platform-level concern, language-agnostic, and benefits from a single code path. See the [config-injection recipe](./inject-configuration-from-configmaps-and-secrets.mdx). **Cron-driven invocation of components in the same workload** → **Service.** A long-running loop that periodically calls a component interface is application logic, lives with the workload, and benefits from automatic restart. See the canonical [`cron-service` example](https://github.com/wasmCloud/wasmCloud/tree/main/examples/cron-service). **Connecting to a proprietary database with no WASI binding** → **Plugin.** If multiple workloads should share one driver and one connection pool, build a custom plugin against the `HostPlugin` trait. Components stay portable—they import a WIT interface, not a Rust SDK. See [Creating Host Plugins](../runtime/creating-host-plugins.mdx). **A TCP protocol adapter that translates between an external system and your components** → **Service.** Listening on a TCP port inside the workload is a hard requirement here. A service can bind to `127.0.0.1`, accept incoming connections, and call into companion components over WIT. Plugins cannot listen on ports. ## Keep reading - [Plugins overview](../overview/hosts/plugins.mdx)—built-in plugins and the registration API. - [Services overview](../overview/workloads/services.mdx)—the service model, TCP isolation, and export requirements. - [Creating Host Plugins](../runtime/creating-host-plugins.mdx)—the `HostPlugin` trait reference for plugin authors. - [Creating Services](../wash/developer-guide/create-services.mdx)—the developer-guide walkthrough for service authors. --- ## TLS with Bring-Your-Own Certificates This recipe shows how to configure a wasmCloud host group to serve HTTPS using TLS certificates you provide. By the end, you will have a kind cluster running wasmCloud with a host group that terminates TLS using a certificate stored in a Kubernetes Secret. ## Prerequisites - A Kubernetes cluster (this recipe uses [kind](https://kind.sigs.k8s.io/)) - [`kubectl`](https://kubernetes.io/docs/tasks/tools/#kubectl) - [Helm](https://helm.sh/docs) v3.8.0+ - A TLS certificate and private key (self-signed or CA-issued) If you do not already have a certificate, you can generate a self-signed one for testing: ```shell openssl req -x509 -newkey rsa:2048 -keyout tls.key -out tls.crt -days 365 -nodes \ -subj "/CN=example.com" \ -addext "subjectAltName=DNS:example.com,DNS:localhost,DNS:*.default.svc.cluster.local" ``` You will also need a CA certificate. For self-signed certificates, the certificate itself serves as the CA: ```shell cp tls.crt ca.crt ``` ## Step 1: Create the cluster Create a kind cluster with port 30443 mapped for HTTPS ingress: ```yaml kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane extraPortMappings: - containerPort: 30443 hostPort: 8443 protocol: TCP ``` Save this as `kind-config.yaml` and create the cluster: ```shell kind create cluster --config kind-config.yaml ``` ## Step 2: Create the TLS Secret Store your certificate, key, and CA certificate in a Kubernetes Secret: ```shell kubectl create secret generic byoc-cert \ --from-file=tls.crt=tls.crt \ --from-file=tls.key=tls.key \ --from-file=ca.crt=ca.crt ``` The Secret must contain three keys: `tls.crt`, `tls.key`, and `ca.crt`. The wasmCloud host reads all three when starting its HTTPS listener. ## Step 3: Install wasmCloud with TLS enabled Create a `values.yaml` file that configures the host group to use your certificate: ```yaml global: tls: enabled: true runtime: hostGroups: - name: default replicas: 1 service: type: ClusterIP http: enabled: true port: 8443 tls: enabled: true certificate: secretName: "byoc-cert" generate: enabled: false resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" ``` Key fields: - `global.tls.enabled: true` enables TLS for the platform's NATS connections and certificate infrastructure. - `http.tls.enabled: true` tells the host group to serve HTTPS instead of HTTP. - `http.tls.certificate.secretName` points to the Secret created in Step 2. - `http.tls.certificate.generate.enabled: false` disables auto-generated certificates so the host uses your Secret. - `http.port: 8443` sets the HTTPS listen port on the host pod. Install with Helm: ```shell helm install wasmcloud --version {{WASMCLOUD_VERSION}} oci://ghcr.io/wasmcloud/charts/runtime-operator \ -f values.yaml ``` Wait for the pods to start: ```shell kubectl rollout status deploy -l app.kubernetes.io/instance=wasmcloud -n wasmcloud ``` ## Step 4: Expose the host group with a NodePort Service Create a Service that routes external traffic to the host group on port 8443: ```yaml apiVersion: v1 kind: Service metadata: name: wasmcloud-https namespace: default spec: type: NodePort selector: wasmcloud.com/hostgroup: default wasmcloud.com/name: hostgroup ports: - name: https port: 8443 targetPort: 8443 nodePort: 30443 protocol: TCP ``` ```shell kubectl apply -f - <)` | Register the HTTP handler (`HttpServer` implements `HostHandler`, not `HostPlugin`) | | `with_plugin(Arc)` | Register a plugin (can be called multiple times; IDs must be unique) | ## Managing workloads Once the host is running, use the `HostApi` trait to manage workloads. Each `WorkloadStartRequest` requires a caller-supplied `workload_id` string. ### Starting a workload ```rust use std::collections::HashMap; use wash_runtime::types::{Workload, WorkloadStartRequest, Component, LocalResources}; let component_bytes = std::fs::read("./my-component.wasm")?; let request = WorkloadStartRequest { workload_id: uuid::Uuid::new_v4().to_string(), workload: Workload { namespace: "my-app".to_string(), name: "http-handler".to_string(), annotations: HashMap::from([ ("version".to_string(), "1.0.0".to_string()), ]), service: None, components: vec![ Component { name: "my-handler".to_string(), bytes: component_bytes.into(), digest: None, local_resources: LocalResources { memory_limit_mb: 64, // In megabytes; -1 = unlimited cpu_limit: -1, // In millicores; -1 = unlimited config: HashMap::new(), environment: HashMap::from([ ("LOG_LEVEL".to_string(), "info".to_string()), ]), volume_mounts: vec![], allowed_hosts: vec!["api.example.com".to_string()], }, pool_size: 10, max_invocations: 0, // 0 = unlimited }, ], host_interfaces: vec![], volumes: vec![], }, }; let response = host.workload_start(request).await?; let workload_id = response.workload_status.workload_id; println!("Workload started with ID: {}", workload_id); ``` ### Checking workload status ```rust use wash_runtime::types::{WorkloadStatusRequest, WorkloadState}; let status = host.workload_status(WorkloadStatusRequest { workload_id: workload_id.clone(), }).await?; match status.workload_status.workload_state { WorkloadState::Running => println!("Workload is running"), WorkloadState::Error => println!("Workload encountered an error"), WorkloadState::Completed => println!("Workload completed"), state => println!("Workload state: {:?}", state), } ``` ### Stopping a workload ```rust use wash_runtime::types::WorkloadStopRequest; host.workload_stop(WorkloadStopRequest { workload_id: workload_id.clone(), }).await?; ``` ### Host heartbeat You can query the host for system information and current workload counts: ```rust let heartbeat = host.heartbeat().await?; println!("Host: {} ({})", heartbeat.friendly_name, heartbeat.id); println!("CPU: {:.1}%, Memory: {} MB free / {} MB total", heartbeat.system_cpu_usage, heartbeat.system_memory_free / 1024 / 1024, heartbeat.system_memory_total / 1024 / 1024, ); println!("Workloads: {}, Components: {}", heartbeat.workload_count, heartbeat.component_count); ``` ## Complete example Here's a complete example that creates a custom host with HTTP and config plugins: ```rust use std::sync::Arc; use std::collections::HashMap; use wash_runtime::{ engine::Engine, host::{HostBuilder, HostApi, http::{HttpServer, DevRouter}}, plugin::wasi_config::DynamicConfig, types::{Workload, WorkloadStartRequest, Component, LocalResources}, }; #[tokio::main] async fn main() -> anyhow::Result<()> { // Initialize tracing for observability tracing_subscriber::fmt::init(); // Create the engine with pooling enabled let engine = Engine::builder() .with_pooling_allocator(true) .build()?; // Configure HTTP handler and plugins let http_handler = HttpServer::new(DevRouter::default(), "0.0.0.0:8080".parse()?).await?; let config_plugin = DynamicConfig::new(false); // Build and start the host let host = HostBuilder::new() .with_engine(engine) .with_friendly_name("my-custom-host") .with_http_handler(Arc::new(http_handler)) .with_plugin(Arc::new(config_plugin))? .build()?; let host = host.start().await?; println!("Host started: {}", host.friendly_name()); // Load a component from disk let component_bytes = std::fs::read("./my-component.wasm")?; // Create and start a workload let request = WorkloadStartRequest { workload_id: uuid::Uuid::new_v4().to_string(), workload: Workload { namespace: "default".to_string(), name: "my-component".to_string(), annotations: HashMap::new(), service: None, components: vec![Component { name: "my-component".to_string(), bytes: component_bytes.into(), digest: None, local_resources: LocalResources::default(), pool_size: 5, max_invocations: 0, }], host_interfaces: vec![], volumes: vec![], }, }; let response = host.workload_start(request).await?; let workload_id = response.workload_status.workload_id.clone(); println!("Workload started: {}", workload_id); // Keep the host running println!("Host listening on http://0.0.0.0:8080"); tokio::signal::ctrl_c().await?; // Clean shutdown host.workload_stop(wash_runtime::types::WorkloadStopRequest { workload_id, }).await?; host.stop().await?; println!("Host shutdown complete"); Ok(()) } ``` ## Adding custom plugins For specialized requirements, you can create custom plugins that implement the `HostPlugin` trait. See [Creating Host Plugins](./creating-host-plugins.mdx) for detailed instructions. ```rust use std::sync::Arc; use wash_runtime::host::HostBuilder; // Create your custom plugin let my_plugin = MyCustomPlugin::new(); // Register it with the host let host = HostBuilder::new() .with_engine(engine) .with_plugin(Arc::new(my_plugin))? .build()?; ``` ## Error handling The `wash-runtime` APIs return `anyhow::Result` for flexible error handling. Common error scenarios include: - **Engine build failures**: Invalid Wasmtime configuration - **Plugin registration**: Duplicate plugin IDs - **Workload start**: Invalid component bytes or missing interface dependencies - **Resource limits**: Exceeded memory or instance limits ```rust match host.workload_start(request).await { Ok(response) => { println!("Started workload: {}", response.workload_status.workload_id); } Err(e) => { eprintln!("Failed to start workload: {}", e); } } ``` ## Keep reading - [Cluster Hosts (Washlet)](./washlet.mdx) - Run a cluster-connected host managed by the operator - [Creating Host Plugins](./creating-host-plugins.mdx) - Build custom plugins for your host - [Host Plugins Overview](../overview/hosts/plugins.mdx) - Understand the plugin architecture - [wash-runtime source code](https://github.com/wasmCloud/wasmCloud/tree/main/crates/wash-runtime) - Explore the implementation --- ## Creating Host Plugins ### Extend wasmCloud hosts with additional capabilities. The `wash-runtime` crate includes [built-in support](../overview/hosts/plugins.mdx) for common WASI interfaces like HTTP, key-value, and configuration. When you need to provide capabilities that aren't covered by the built-ins—such as integrating with a proprietary database, exposing specialized hardware, or implementing a custom protocol—you can create a custom plugin. A host plugin is primarily responsible for implementing a specific WIT world as a collection of imports and exports that will be directly linked to the workload's `wasmtime::component::Linker`. ### The `HostPlugin` trait ```rust #[async_trait] pub trait HostPlugin: Any + Send + Sync + 'static { /// Returns a unique identifier for the plugin. /// Plugin IDs must be unique across all registered plugins. fn id(&self) -> &'static str; /// Returns the WIT world this plugin implements. /// The binding process only occurs if workloads require these interfaces. fn world(&self) -> WitWorld; /// Called during host initialization before accepting workloads. /// Use this for setup tasks like establishing connections. async fn start(&self) -> anyhow::Result<()> { Ok(()) } /// Called when a workload begins binding to the plugin. /// Enables pre-binding validation and setup. async fn on_workload_bind( &self, workload: &UnresolvedWorkload, interfaces: HashSet, ) -> anyhow::Result<()> { Ok(()) } /// Called when configuring a specific component or service's linker. /// `item` is an enum — match on `WorkloadItem::Component` or `WorkloadItem::Service`. /// This is where you add your implementations to the linker. async fn on_workload_item_bind<'a>( &self, item: &mut WorkloadItem<'a>, interfaces: HashSet, ) -> anyhow::Result<()> { Ok(()) } /// Called after successful workload binding and resolution. async fn on_workload_resolved( &self, workload: &ResolvedWorkload, component_id: &str, ) -> anyhow::Result<()> { Ok(()) } /// Called during unbinding or shutdown for cleanup. async fn on_workload_unbind( &self, workload_id: &str, interfaces: HashSet, ) -> anyhow::Result<()> { Ok(()) } /// Called during host shutdown for final cleanup. async fn stop(&self) -> anyhow::Result<()> { Ok(()) } } ``` ### Plugin lifecycle Plugins follow a defined lifecycle within the host: 1. **Registration**: Plugins are registered via `HostBuilder::with_plugin()`. Each plugin must have a unique ID. 2. **Start**: When the host starts, all plugins' `start()` methods are called. If any plugin fails to start, the host startup fails. 3. **Workload binding**: When a workload starts, the host calls `on_workload_bind()` once per plugin, then calls `on_workload_item_bind()` for each component and service in that workload. 4. **Resolution**: After successful binding, `on_workload_resolved()` is called. 5. **Unbinding**: When workloads stop, `on_workload_unbind()` is called for cleanup. 6. **Stop**: During host shutdown, all plugins' `stop()` methods are called (with a 3-second timeout per plugin). ### Key types The `HostPlugin` trait uses several types from the `wash_runtime` crate: - **`WitWorld`** - Contains `imports` and `exports` as `HashSet`, representing the WIT interfaces the plugin provides. - **`WitInterface`** - Describes a specific interface with `namespace`, `package`, `interfaces` (a set of interface names), an optional `version`, an optional `name` (used for multi-backend binding), and a `config` map. - **`UnresolvedWorkload`** - A workload that has been initialized but not yet bound to plugins. - **`WorkloadItem<'a>`** - An enum passed to `on_workload_item_bind`. Variants are `WorkloadItem::Component(&mut WorkloadComponent)` and `WorkloadItem::Service(&mut WorkloadService)`. Implements `Deref`, so `item.linker()` is accessible directly without pattern matching. Use `item.is_component()` / `item.is_service()` when you need to handle the two variants differently. - **`ResolvedWorkload`** - A fully bound workload ready for execution. ### Example: Custom logging plugin Here's a simplified example of a custom plugin that implements logging: ```rust use std::collections::HashSet; use async_trait::async_trait; use wash_runtime::{ engine::workload::WorkloadItem, plugin::HostPlugin, wit::{WitInterface, WitWorld}, }; pub struct CustomLogger { prefix: String, } impl CustomLogger { pub fn new(prefix: impl Into) -> Self { Self { prefix: prefix.into() } } } #[async_trait] impl HostPlugin for CustomLogger { fn id(&self) -> &'static str { "custom-logger" } fn world(&self) -> WitWorld { WitWorld { imports: HashSet::new(), exports: HashSet::from([WitInterface { namespace: "wasi".to_string(), package: "logging".to_string(), interfaces: HashSet::from(["logging".to_string()]), version: None, name: None, config: std::collections::HashMap::new(), }]), } } async fn start(&self) -> anyhow::Result<()> { println!("[{}] Logger plugin started", self.prefix); Ok(()) } async fn on_workload_item_bind<'a>( &self, item: &mut WorkloadItem<'a>, interfaces: HashSet, ) -> anyhow::Result<()> { // WorkloadItem implements Deref, so linker() // is accessible directly without pattern matching: // item.linker().func_wrap(...)?; // // Use item.is_component() / item.is_service() if you need to // handle components and services differently. Ok(()) } async fn stop(&self) -> anyhow::Result<()> { println!("[{}] Logger plugin stopped", self.prefix); Ok(()) } } ``` Register the custom plugin with your host: ```rust let logger = CustomLogger::new("my-app"); let host = HostBuilder::new() .with_engine(engine) .with_plugin(Arc::new(logger))? .build()?; ``` ## Keep reading - [Building Custom Hosts](./building-custom-hosts.mdx) - Embed the wasmCloud runtime in your own application - [Host Plugins Overview](../overview/hosts/plugins.mdx) - Built-in plugins and plugin architecture - [wash-runtime source code](https://github.com/wasmCloud/wasmCloud/tree/main/crates/wash-runtime/src/plugin) - Reference implementations of built-in plugins --- ## Introduction(Runtime) # Runtime ### wasmCloud's runtime (`wash-runtime`) is a simple, flexible runtime environment for WebAssembly workloads. [`wash-runtime`](https://github.com/wasmCloud/wasmCloud/tree/main/crates/wash-runtime) is a Rust crate that includes Wasmtime as the underlying runtime engine, as well as the wasmCloud host, which serves as a runtime environment for WebAssembly components. A plugin system allows for extension of the host with new and custom capabilities. The runtime provides easy-to-use abstractions over the low-level [Wasmtime API](https://wasmtime.dev/), as well as wasmCloud-specific APIs for managing workloads, handling NATS subscriptions, managing and utilizing host plugins, and more. For a step-by-step guide to embedding the runtime, see [Building Custom Hosts](./building-custom-hosts.mdx). To run a cluster-connected host managed by the operator, see [Cluster Hosts (Washlet)](./washlet.mdx). To extend a host with new capabilities, see [Creating Host Plugins](./creating-host-plugins.mdx). ## Architecture The runtime provides three primary abstractions: 1. **Engine**: Wasmtime configuration and component compilation 2. **Host**: Runtime environment with plugin management 3. **Workload**: High-level API for managing component lifecycles ![architecture diagram](../images/wasmcloud-architecture.webp) ## WASI Interface Support The runtime provides WASI interface support through three distinct mechanisms: **Built-in via `wasmtime-wasi`** (always available, no registration needed): all [WASI P2 interfaces](https://docs.wasmtime.dev/api/wasmtime_wasi/p2/index.html#wasip2-interfaces) including `wasi:filesystem`, `wasi:clocks`, `wasi:random`, `wasi:io`, `wasi:sockets`, and the `wasi:cli` suite. **HTTP handler** (`HttpServer`, registered via `with_http_handler()`): `wasi:http` — HTTP client and server. Uses the `HostHandler` trait, not `HostPlugin`. **Host plugins** (registered via `with_plugin()`, feature-flag controlled): two sets are included — in-memory implementations for development with `wash dev`, and NATS-backed implementations for production. Supported interfaces: - `wasi:keyvalue` - `wasi:blobstore` - `wasi:config` - `wasi:logging` - `wasmcloud:messaging` Hosts can be extended with additional custom plugins at build-time. :::info[Declaring plugins in Kubernetes] On Kubernetes, host plugins are *available* on the host but must be *declared per workload* under `spec.hostInterfaces` (in a `Workload`) or `spec.template.spec.hostInterfaces` (in a `WorkloadDeployment`). The runtime links interfaces declaratively from the workload spec; an undeclared import surfaces as a linker error at startup. See [Host Interfaces](../kubernetes-operator/crds.mdx#host-interfaces) for the field reference and [Troubleshooting](../troubleshooting.mdx#missing-host-interface-implementation-in-the-linker) for the specific error. ::: ## Usage ```rust use std::sync::Arc; use std::collections::HashMap; use wash_runtime::{ engine::Engine, host::{HostBuilder, HostApi, http::{HttpServer, DevRouter}}, plugin::wasi_config::DynamicConfig, types::{WorkloadStartRequest, Workload}, }; #[tokio::main] async fn main() -> anyhow::Result<()> { // Create a Wasmtime engine let engine = Engine::builder().build()?; // Configure HTTP handler and plugins let http_handler = HttpServer::new(DevRouter::default(), "127.0.0.1:8080".parse()?).await?; let config_plugin = DynamicConfig::new(false); // Build and start the host let host = HostBuilder::new() .with_engine(engine) .with_http_handler(Arc::new(http_handler)) .with_plugin(Arc::new(config_plugin))? .build()?; let host = host.start().await?; // Start a workload let req = WorkloadStartRequest { workload_id: uuid::Uuid::new_v4().to_string(), workload: Workload { namespace: "test".to_string(), name: "test-workload".to_string(), annotations: HashMap::new(), service: None, components: vec![], host_interfaces: vec![], volumes: vec![], }, }; host.workload_start(req).await?; Ok(()) } ``` ## `cargo` Features The crate supports the following `cargo` features: - `wasi-config` (default): Runtime configuration interface - `wasi-logging` (default): Logging interface - `wasi-blobstore` (default): Blob/object storage interface - `wasi-keyvalue` (default): Key-value storage interface - `wasmcloud-postgres` (default): PostgreSQL-backed implementations for keyvalue and blobstore - `washlet` (default): Washlet support (depends on `oci`) - `wasi-webgpu`: WebGPU interface - `oci`: OCI registry integration for pulling components - `wasip3`: Experimental WASI Preview 3 support (see below) ## WASI Preview 3 (experimental) As of wasmCloud 2.0.3, the runtime includes an experimental implementation of [WASI Preview 3](https://github.com/WebAssembly/WASI/blob/main/wasip2/README.md) behind the `wasip3` Cargo feature. WASI P3 adds native `async` support to WASI interfaces, replacing the poll-based concurrency model in P2 with futures and streams. Enable the feature when building `wash-runtime`: ```shell cargo build --features wasip3 ``` With `wasip3` enabled, the host registers P3 implementations for the following WASI interfaces alongside the P2 versions: - `wasi:http` (incoming-handler, outgoing-handler, types) - `wasi:sockets/ip-name-lookup` - `wasi:sockets/tcp`, `wasi:sockets/udp` Components targeting either P2 or P3 worlds are compatible with a host built with this flag. The P2 interfaces remain the stable default — P3 support is exposed for early integration testing and will become the default in a future release. --- ## Cluster Hosts (Washlet) ### Run a wasmCloud host as a cluster node connected to the operator mesh. A **washlet** is a cluster-connected host: a `ClusterHost` from the `wash_runtime::washlet` module. Unlike a standalone `Host` (covered in [Building Custom Hosts](./building-custom-hosts.mdx)), a washlet connects to the wasmCloud operator mesh via NATS and receives workload commands from the scheduler. This is how the [Kubernetes Operator](../kubernetes-operator/index.mdx) deploys and manages workloads across a fleet of hosts. ## Standalone host vs. cluster host | | Standalone `Host` | `ClusterHost` (washlet) | |---|---|---| | Managed by | Your application code | wasmCloud operator via NATS | | Workload commands | Programmatic API (`HostApi`) | NATS subjects | | Plugins | In-memory (dev) or custom | NATS-backed for production | | Entry point | `HostBuilder` | `ClusterHostBuilder` | | Use case | Embedding, custom integration | Operator-managed fleet | ## Running with `wash host` The simplest way to run a washlet is the `wash host` CLI command. It connects to NATS and joins a host group, ready to receive workload commands from the operator. ```shell wash host --host-group my-cluster --scheduler-nats-url nats://nats:4222 ``` ### `wash host` flags | Flag | Default | Description | |------|---------|-------------| | `--host-group` | `default` | Host group label used for workload scheduling | | `--host-name` | (auto-generated) | Human-readable name for this host | | `--scheduler-nats-url` | `nats://localhost:4222` | NATS URL for the control plane (workload commands) | | `--data-nats-url` | `nats://localhost:4222` | NATS URL for the data plane (plugin backends) | | `--http-addr` | (disabled) | If set, enables the HTTP handler on this address | | `--postgres-url` | (disabled) | If set, enables PostgreSQL-backed keyvalue and blobstore | | `--wasi-webgpu` | false | Enable the WebGPU interface | | `--allow-insecure-registries` | false | Allow pulling from insecure OCI registries | | `--registry-pull-timeout` | `30s` | Timeout for OCI pulls | | `--scheduler-tls-ca` | — | TLS CA for scheduler NATS | | `--data-tls-ca` | — | TLS CA for data plane NATS | ### Environment variables `WASH_POSTGRES_URL` can be used in place of `--postgres-url`. ## Embedding programmatically Use `ClusterHostBuilder` to embed a washlet in your own application. The builder mirrors `HostBuilder` but adds NATS-specific configuration: ```rust use std::sync::Arc; use wash_runtime::{ host::http::{HttpServer, DynamicRouter}, plugin::{ wasi_blobstore::NatsBlobstore, wasi_config::DynamicConfig, wasi_keyvalue::NatsKeyValue, wasi_logging::TracingLogger, wasmcloud_messaging::NatsMessaging, }, washlet::{ClusterHostBuilder, run_cluster_host}, }; #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); // Connect to NATS (scheduler and data plane can be separate connections) let scheduler_nats = async_nats::connect("nats://localhost:4222").await?; let data_nats = async_nats::connect("nats://localhost:4222").await?; // Build the cluster host with NATS-backed plugins let mut builder = ClusterHostBuilder::default() .with_host_group("my-cluster") .with_nats_client(Arc::new(scheduler_nats)) .with_plugin(Arc::new(DynamicConfig::new(true)))? // true = NATS-backed .with_plugin(Arc::new(TracingLogger::default()))? .with_plugin(Arc::new(NatsBlobstore::new(&data_nats)))? .with_plugin(Arc::new(NatsMessaging::new(data_nats.clone())))? .with_plugin(Arc::new(NatsKeyValue::new(&data_nats)))?; // Optionally enable HTTP let http_router = DynamicRouter::default(); let http_server = HttpServer::new(http_router, "0.0.0.0:8080".parse()?).await?; builder = builder.with_http_handler(Arc::new(http_server)); let cluster_host = builder.build()?; // Start the host and run until shutdown let shutdown = run_cluster_host(cluster_host).await?; println!("Cluster host running. Press Ctrl+C to stop."); tokio::signal::ctrl_c().await?; // Graceful shutdown shutdown.await?; println!("Host stopped."); Ok(()) } ``` ### `ClusterHostBuilder` methods | Method | Description | |--------|-------------| | `with_host_group(str)` | Host group label for workload scheduling | | `with_host_name(str)` | Human-readable name (auto-generated if not set) | | `with_nats_client(Arc)` | NATS client for the control plane | | `with_plugin(Arc)` | Register a plugin (IDs must be unique) | | `with_http_handler(Arc)` | Register the HTTP handler | | `with_host_builder(HostBuilder)` | Provide a pre-configured `HostBuilder` for advanced use | | `with_artifact_cleaner(frequency, max_age)` | Clean up stale cached artifacts on a schedule | | `build()` | Construct the `ClusterHost` | ### NATS-backed plugins In cluster mode, use NATS-backed plugin implementations so that workloads can access shared state across hosts: | Plugin | Type | Description | |--------|------|-------------| | Config | `DynamicConfig::new(true)` | NATS-backed runtime config (pass `false` for dev/in-memory) | | Logging | `TracingLogger::default()` | Structured logging via `tracing` | | Key-value | `NatsKeyValue::new(&nats_client)` | NATS-backed key-value store | | Blobstore | `NatsBlobstore::new(&nats_client)` | NATS-backed blob storage | | Messaging | `NatsMessaging::new(nats_client)` | NATS pub/sub messaging | ## How the operator communicates with washlets The operator sends commands to washlets by publishing to NATS subjects: ``` runtime.host..workload.start runtime.host..workload.stop runtime.host..workload.status runtime.host..heartbeat ``` Washlets publish heartbeats to: ``` runtime.operator.heartbeat. ``` Messages are JSON-encoded. You typically don't interact with these subjects directly—the operator handles scheduling and the washlet handles command processing. ## Keep reading - [Building Custom Hosts](./building-custom-hosts.mdx) - Embed a standalone host without NATS - [Creating Host Plugins](./creating-host-plugins.mdx) - Build custom plugins for your host - [Kubernetes Operator](../kubernetes-operator/index.mdx) - Deploy washlets managed by the operator - [wash-runtime source code](https://github.com/wasmCloud/wasmCloud/tree/main/crates/wash-runtime/src/washlet) - `ClusterHost` implementation --- ## Troubleshooting This page collects common errors you may encounter when running wasmCloud workloads and the resolutions for each. Entries include the literal error message, the underlying cause, and the fix. If you hit an error that isn't listed here, please open an issue on [github.com/wasmCloud/wasmCloud](https://github.com/wasmCloud/wasmCloud/issues) or ask in the [wasmCloud Slack](https://slack.wasmcloud.com/) so we can document it. ## Workload deployment ### Missing host interface implementation in the linker You see an error similar to the following when a `WorkloadDeployment` fails to start: ``` component imports instance `wasi:config/store@0.2.0-rc.1`, but a matching implementation was not found in the linker ``` **Cause:** A wasmCloud host advertises the set of [host plugins](./overview/hosts/plugins.mdx) it provides (for example, `wasi:keyvalue`, `wasi:blobstore`, `wasi:config`, `wasi:logging`, `wasmcloud:messaging`), but a workload must declare *which* of those interfaces its components import. The runtime resolves and links interfaces declaratively from the workload spec; if the import isn't declared, the linker has no matching implementation to wire in and the component fails to instantiate. **Fix:** Add the missing interface under `spec.hostInterfaces` (in a `Workload`) or `spec.template.spec.hostInterfaces` (in a `WorkloadDeployment`). For the error above, the relevant entry is: ```yaml hostInterfaces: - namespace: wasi package: config version: "0.2.0-rc.1" interfaces: - store config: example.greeting: "hello from wasi:config" example.environment: "dev" ``` Each entry requires `namespace`, `package`, and `interfaces`; `version` and `config` are optional. The same pattern applies to other host plugins: ```yaml hostInterfaces: - namespace: wasi package: keyvalue interfaces: [store, atomics, batch] config: backend: nats bucket: my-bucket - namespace: wasi package: blobstore interfaces: [blobstore] - namespace: wasmcloud package: messaging interfaces: [consumer, producer] ``` **See also:** [Host Interfaces](./kubernetes-operator/crds.mdx#host-interfaces) for the full field reference (including the `name` field for multi-backend binding) and the [`HostInterface` API reference](./kubernetes-operator/api-reference.md#hostinterface). If you're hitting this error in `wash dev` rather than on Kubernetes, see [Debugging Components: Unknown import errors](./wash/developer-guide/debugging-components.mdx#unknown-import-errors). ### Outbound HTTP call from a component is blocked A component that imports `wasi:http/outgoing-handler` and is bound to the corresponding host interface still cannot reach an external URL — the call fails or returns an error to the component, even though the network path appears clear. **Cause:** The host enforces a per-component allowlist of outbound destinations via `localResources.allowedHosts`. When the list is non-empty, any outbound HTTP request whose host doesn't match an entry is blocked at the wasmCloud layer before the request leaves the process — independently of any Kubernetes `NetworkPolicy`. If `allowedHosts` is set but does not include the host the component is trying to reach, the call is rejected. **Fix:** Add the host to `allowedHosts` for the component: ```yaml components: - name: http-component image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 localResources: allowedHosts: - api.example.com - storage.googleapis.com - "*.s3.amazonaws.com" ``` Entries are matched against the request's host (no scheme, no path) using case-insensitive comparison. A leading wildcard like `*.example.com` matches any subdomain but **not** the bare `example.com` — list both if you need both. If `allowedHosts` is empty (or the field is omitted), **all outbound HTTP requests are allowed** — the allowlist only takes effect when at least one entry is present. **See also:** [Workload Security — Restricting outbound HTTP with `allowedHosts`](./kubernetes-operator/workload-security.mdx#restricting-outbound-http-with-allowedhosts). If outbound HTTP is blocked at the cluster level rather than the wasmCloud level, see the same page for [NetworkPolicy](./kubernetes-operator/workload-security.mdx#kubernetes-networkpolicy-for-host-pods) guidance. ### Workload image fails to pull from a private registry A `WorkloadDeployment` never reaches its desired replica count, and either the underlying `Artifact` fails to resolve or the host records a registry authentication error when fetching the component image. The pull URL points to a private registry (private GHCR, ECR, ACR, a self-hosted registry, etc.). **Cause:** wasmCloud references component images by registry URL. When the image is private, the resource that references it needs registry credentials in the form of a Kubernetes [`docker-registry` Secret](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-from-private-registry/), pointed to via the `imagePullSecret` field. If the field is omitted, or the Secret doesn't grant pull access to the image, the pull fails. **Fix:** 1. Create a `docker-registry` Secret in the workload's namespace: ```shell kubectl create secret docker-registry ghcr-secret \ --namespace default \ --docker-server=ghcr.io \ --docker-username= \ --docker-password= ``` 2. Reference the Secret from the resource that fetches the image. On an `Artifact` (recommended for automatic rollout on new image versions): ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: Artifact metadata: name: my-component namespace: default spec: image: ghcr.io/my-org/my-component:0.1.0 imagePullSecret: name: ghcr-secret ``` Or directly on a `WorkloadComponent` (or `WorkloadService`) when not using an Artifact: ```yaml components: - name: my-component image: ghcr.io/my-org/my-component:0.1.0 imagePullSecret: name: ghcr-secret ``` 3. The Secret must live in the **same namespace** as the resource that references it. **See also:** [`ArtifactSpec`](./kubernetes-operator/api-reference.md#artifactspec) and the [CRDs guide](./kubernetes-operator/crds.mdx#artifact). For mirroring the wasmCloud chart's own images to a private registry (air-gapped installs), see [Private Registries and Air-Gapped Deployments](./kubernetes-operator/operator-manual/private-registries.mdx). ## Scheduling ### Workload stays unscheduled — no host matches `hostSelector` A `WorkloadDeployment` reports zero ready replicas indefinitely. The image pulls cleanly, there are no runtime errors, and `kubectl describe workloaddeployment` shows the workload as pending without recent placement events — the operator has simply found no host to put it on. **Cause:** `hostSelector` is a label selector matched against `Host` metadata labels. If no `Host` object carries labels matching the selector, the workload has nowhere to land. The most common cause is a typo or mismatch between the workload's selector (commonly `hostgroup: default`) and the labels on the host pool defined in Helm values (`runtime.hostGroups[]`). **Fix:** Compare the selector to your host labels. ```shell # List host labels across namespaces kubectl get hosts -A --show-labels # Inspect the selector on the workload kubectl get workloaddeployment -n \ -o jsonpath='{.spec.template.spec.hostSelector}' ``` Either update the workload's selector to match an existing host group: ```yaml spec: template: spec: hostSelector: hostgroup: my-team ``` …or add a matching host group in Helm values. The host group's `name` becomes the value of the `hostgroup` label on `Host` resources it produces: ```yaml runtime: hostGroups: - name: my-team replicas: 3 ``` If a host group exists but the workload still doesn't schedule, also check `allowSharedHosts` — see the next entry. **See also:** [`runtime.hostGroups`](./kubernetes-operator/operator-manual/helm-values.mdx#runtimehostgroups) for host group configuration, [`WorkloadSpec`](./kubernetes-operator/api-reference.md#workloadspec) for the selector field. ### Workload stays unscheduled with `allowSharedHosts: false` After enabling `operator.allowSharedHosts: false`, workloads that previously scheduled successfully now stay unscheduled, even though `hostSelector` still matches host labels in another namespace. **Cause:** As of [wasmCloud 2.1](/blog/wasmcloud-2-1-0-release/), `allowSharedHosts: false` enforces namespace-level scheduling isolation. A workload is only placed onto hosts whose `Host.environment` matches the workload's own namespace (or a value explicitly set via `WorkloadDeployment.spec.template.spec.environment`). Hosts in other namespaces are skipped regardless of label matches. This is the intended security boundary, but it can surprise teams who relied on cross-namespace host sharing before the upgrade. **Fix:** Choose the model that matches your isolation requirements. - **Run a host pool in the workload's namespace.** Add a host group to Helm values that the operator creates in the workload's namespace: ```yaml runtime: hostGroups: - name: team-a namespace: team-a replicas: 2 ``` - **Opt in to a specific shared namespace** by setting `environment` on the WorkloadDeployment, pointing to a namespace where a host pool is intentionally shared: ```yaml spec: template: spec: environment: shared-infra hostSelector: hostgroup: shared ``` Cross-namespace placement requires that sharing is allowed for the target — if `allowSharedHosts: false` is set globally, the workload's namespace must match the host's `environment` for placement to succeed. - **Re-enable host sharing** if your isolation requirements allow it, returning to a cluster-wide host pool model: ```yaml operator: allowSharedHosts: true ``` **See also:** [wasmCloud 2.1.0 release notes](/blog/wasmcloud-2-1-0-release/) for the full namespace-scoped scheduling model. ## Hosts and namespaces ### `Host` objects don't appear in the expected namespace After upgrading to wasmCloud 2.1, `kubectl get hosts` in your workload's namespace returns no results, or `Host` objects you expected to find are missing. **Cause:** As of [wasmCloud 2.1](/blog/wasmcloud-2-1-0-release/), the `Host` CRD is **namespace-scoped** rather than cluster-scoped. The runtime-operator creates every `Host` object in its own namespace — not in the namespace where the underlying host pod actually runs. Each `Host` records the pod's location in the new `Host.environment` field (a top-level field on the `Host` resource, not nested under `spec`), populated automatically from the downward API for in-cluster hosts (or supplied via `--environment` for external ones). **Fix:** Look in the operator's namespace, or list across all namespaces: ```shell # List every host you have permission to see kubectl get hosts -A # List hosts in the operator's namespace kubectl get hosts -n ``` The `ENVIRONMENT` column in the default `kubectl get hosts` output shows each host's environment. To filter by environment, use a jsonpath query against the top-level `environment` field: ```shell kubectl get hosts -A -o jsonpath='{range .items[?(@.environment=="team-a")]}{.metadata.name}{"\n"}{end}' ``` If you were relying on `ClusterRole` bindings to grant host visibility, note that user-facing host roles in 2.1 are generated as namespaced `Role`s. A namespace admin can now grant host visibility within their own namespace without cluster-admin involvement. **See also:** [Migration to v2](./migration.mdx) for upgrade notes, and the [Host API reference](./kubernetes-operator/api-reference.md#host). --- ## Commands # Wasm Shell CLI Command Reference ## Commands * [`wash build`↴](#wash-build) - Build a Wasm component * [`wash completion`↴](#wash-completion) - Generate shell completions * [`wash config`↴](#wash-config) - View configuration for `wash` * [`wash dev`↴](#wash-dev) - Start a development server for a Wasm component * [`wash host`↴](#wash-host) - Act as a wasmCloud host * [`wash inspect`↴](#wash-inspect) - Inspect a Wasm component's embedded WIT * [`wash new`↴](#wash-new) - Create a new project from a Git repository * [`wash oci`↴](#wash-oci) - Push or pull Wasm components to/from an OCI registry - [`wash oci push`↴](#wash-oci-push) - [`wash oci pull`↴](#wash-oci-pull) * [`wash update`↴](#wash-update) - Update `wash` to the latest version * [`wash wit`↴](#wash-wit) - Manage WIT dependencies - [`wash wit add`](#wash-wit-add) - [`wash wit build`](#wash-wit-build) - [`wash wit clean`](#wash-wit-clean) - [`wash wit fetch`](#wash-wit-fetch) - [`wash wit remove`](#wash-wit-remove) - [`wash wit update`](#wash-wit-update) ###### **Options:** * `-C ` - Path to the project to run a command against (when outside a project directory) * `-h`, `--help` - Print help * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (text or json) [default: text] * `--verbose` - Enable verbose output * `-V`, `--version` - Print version ## Directory Context The `wash` CLI uses automatic project detection similar to `git` and other GNU tools. This allows you to run `wash` commands from anywhere within your project without specifying the project path. ```shell ~/myproject$ wash dev ~/myproject/wit$ wash build ~/myproject/src$ wash build ``` When you're **outside** a project directory, you can use the `-C` flag to specify the project path. This is useful in CI environments and situations where users can't change directories prior to calling `wash`. From your home directory, targeting a project in `/tmp`: ```shell wash -C /tmp/myproject build ``` ## `wash build` Compile a project to a WebAssembly component artifact—i.e., a WebAssembly (`.wasm`) binary that conforms to the WebAssembly Component Model. **Usage:** `wash build [OPTIONS]` ###### **Options:** * `-C ` - Path to the project to build (when outside a project directory) * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (text or json) [default: text] * `--skip-fetch` - Skip fetching WIT dependencies, useful for offline builds * `--user-config ` - Path to user configuration file. * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash completion` Generate shell completions. **Usage:** `wash completion [OPTIONS] ` * `` - The shell to generate completions for [possible values: `bash`, `elvish`, `fish`, `powershell`, `zsh`] ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (text or json) [default: text] * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash config` View configuration for wash. **Usage:** `wash config [OPTIONS] ` **Subcommands**: * `init` - Initialize a new configuration file for wash * `info` - Print the current version and local directories used by wash * `show` - Print the current configuration file for wash ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: text] * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash config init` Initialize a new configuration file for wash. **Usage:** `wash config init [OPTIONS]` ###### **Options:** * `--force` - Overwrite existing configuration * `--global` - Overwrite global configuration instead of project * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--user-config ` - Path to user configuration file. * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash config info` Print the current version and local directories used by wash. **Usage:** `wash config info [OPTIONS]` ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--user-config ` - Path to user configuration file. * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash config show` Print the current configuration file for wash. **Usage:** `wash config show [OPTIONS]` ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--user-config ` - Path to user configuration file. * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash dev` Start a development server for a Wasm component. **Usage:** `wash dev [OPTIONS]` ###### **Options:** * `-C ` - Path to the project to build (when outside a project directory) * `-l`, `--log-level ` — Set the log level (trace, debug, info, warn, error) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (text or json) [default: `text`] * `--user-config ` - Path to user configuration file. * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash host` Run a cluster-connected wasmCloud host ([washlet](../runtime/washlet.mdx)) that joins the operator mesh via NATS. **Usage:** `wash host [OPTIONS]` ###### **Options:** * `--allow-insecure-registries` - Allow insecure OCI registries * `--data-nats-url ` - NATS URL for data plane communications [default: `nats://localhost:4222`] * `--host-group ` - The host group label to assign to the host [default: `default`] * `--host-name ` - The host name to assign to the host * `--http-addr ` - The address on which the HTTP server will listen * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--registry-pull-timeout ` - Timeout for pulling artifacts from OCI registries [default: `30s`] * `--scheduler-nats-url ` - NATS URL for control plane communications [default: `nats://localhost:4222`] * `--user-config ` - Path to user configuration file. * `--verbose` - Enable verbose output * `--wasi-webgpu` - Enable WASI WebGPU support * `-h`, `--help` - Print help ## `wash inspect` Inspect a Wasm component's embedded WIT. **Usage:** `wash inspect [OPTIONS] ` * `` - Inspect a component by its reference, which can be a local file path, project directory, or remote OCI reference. If omitted or pointing to a directory, attempts to build and inspect a component from that directory. ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash new` Create a new project from a Git repository. **Usage:** `wash new [OPTIONS] ` * `` - Git repository URL to use as project template. ###### **Options:** * `-C ` - Target path for project * `--git-ref ` - Git reference (branch, tag, or commit) to checkout * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--name` - Project name and local directory to create [default: repository/subfolder name] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--subfolder ` - Subdirectory within the git repository to use (only valid with ``) * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash oci` Push or pull Wasm components to or from an OCI registry. **Usage:**: `wash oci [OPTIONS] ` **Subcommands**: * `pull` - Pull a component artifact from an OCI registry. * `push` - Push a component artifact to an OCI registry. ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash oci pull` Pull a component artifact from an OCI registry. **Usage:** `wash oci pull [OPTIONS] ` * `` - The OCI reference to pull. * `` - The path to write the pulled component to [default: `component.wasm`] ###### **Options:** * `-C ` - Target path for pulled artifact * `--insecure` - Allow HTTP protocol [default: `false`] * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash oci push` Push a component artifact to an OCI registry. **Usage:** `wash oci push [OPTIONS] ` * `` - The OCI reference to push. * `` - The path to the component to push. ###### **Options:** * `--insecure` - Allow HTTP protocol [default: `false`] * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash update` Update wash to the latest version. **Usage:** `wash update [OPTIONS]` ###### **Options:** * `-d`, `--dry-run` - Check for updates without applying them * `-f`, `--force` - Force update even if already on the latest version * `--git ` - Point at a different repository for updates [default: `wasmCloud/wasmCloud`] * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--major` - Allow major version updates (breaking changes) * `--minor` - Allow minor version updates (new features, no breaking changes) * `--patch` - Allow only patch updates (bug fixes only) * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--token ` - GitHub token for private repository access. Can also be set via `GITHUB_TOKEN`, `GH_TOKEN`, or `GITHUB_ACCESS_TOKEN` environment variables [env: `WASH_GITHUB_TOKEN=`]. * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash wit` Manage WIT dependencies. **Usage**: `wash wit [OPTIONS] ` **Subcommands**: * `add` - Add a new WIT dependency * `build` - Build a WIT package into a Wasm binary * `clean` - Remove fetched dependencies at `wit/deps/` * `fetch` - Fetch WIT dependencies, reading from `wit/world.wit` imports * `remove` - Remove a WIT dependency * `update` - Update dependencies to latest compatible versions ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--verbose` - Enable verbose output * `-h`, `--help` - Print help ## `wash wit add` Add a new WIT dependency. **Usage**: `wash wit add [OPTIONS] ` **Arguments**: `` - Package to add (e.g., `wasi:keyvalue` or `wasi:keyvalue@0.2.0-draft`) ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--verbose` - Enable verbose output * `--wit-dir ` - Path to the WIT directory * `-h`, `--help` - Print help ## `wash wit build` Add a new WIT dependency. **Usage**: `wash wit build [OPTIONS]` ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--output-file ` - Output file path for the built Wasm package * `--verbose` - Enable verbose output * `--wit-dir ` - Path to the WIT directory * `-h`, `--help` - Print help ## `wash wit clean` Remove fetched dependencies at `wit/deps/`. **Usage**: `wash wit clean [OPTIONS]` ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--verbose` - Enable verbose output * `--wit-dir ` - Path to the WIT directory * `-h`, `--help` - Print help ## `wash wit fetch` Fetch WIT dependencies, reading from `wit/world.wit` imports. **Usage**: `wash wit fetch [OPTIONS]` ###### **Options:** * `--clean` - Remove existing dependencies before fetching * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--verbose` - Enable verbose output * `--wit-dir ` - Path to the WIT directory * `-h`, `--help` - Print help ## `wash wit remove` Remove a WIT dependency. **Usage**: `wash wit remove [OPTIONS] ` **Arguments**: `` - Package to remove (e.g., `wasi:keyvalue`) ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--verbose` - Enable verbose output * `--wit-dir ` - Path to the WIT directory * `-h`, `--help` - Print help ## `wash wit update` Update dependencies to latest compatible versions. **Usage**: `wash wit update [OPTIONS] ` **Arguments**: `` - Specific package to update (e.g., `wasi:http`). If not specified, updates all packages. ###### **Options:** * `-l`, `--log-level ` — Set the log level (`trace`, `debug`, `info`, `warn`, `error`) [default: `info`] * `--non-interactive` - Run in non-interactive mode (skip terminal checks for host exec). Automatically enabled when stdin is not a TTY * `-o`, `--output ` — Specify output format (`text` or `json`) [default: `text`] * `--verbose` - Enable verbose output * `--wit-dir ` - Path to the WIT directory * `-h`, `--help` - Print help --- ## Configuration import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Wasm Shell Configuration This document describes the configuration options available for the `wash` CLI. Configuration can be specified in YAML, JSON, or TOML format (`.json`, `.yaml`, `.yml`, or `.toml`). Wasm Shell uses two configuration files: * [**User configuration**](#user-configuration) that is local to the machine * [**Project configuration**](#project-configuration) in the `.wash` directory at the root of each project ## Configuration hierarchy Configuration is merged with the following precedence (highest to lowest): 1. CLI arguments 2. Environment variables (`WASH_ prefix`) 3. Project config (`.wash/config.yaml`) 4. User config (`$HOME/.config/wash/config.yaml`) 5. Default values ## User configuration By default, user configuration files are found at `$HOME/.config/wash/config.yaml`. :::warning[Under construction] This section is under active development—please check back soon for a more complete reference. ::: ## Project configuration Projects can be configured via a project config file that resides in the `.wash` directory at the project root, and which may be expressed in JSON, YAML, or TOML format. Top-level project configuration options include: | Option | Type | Description | |--------|------|-------------| | `version` | string | Configuration schema version | | `build` | object | Build configuration options | | `dev` | object | Development server configuration options | | `wit` | object | WIT dependency management configuration | ### `version` configuration Specify the `wash` version for this project. Example: ```yaml version: 2.0.1 ``` ### `build` configuration Wasm Shell builds WebAssembly component binaries by wrapping language-specific utilities like `cargo`, `go`, `npm`, etc, and project configuration files are used to define configuration for builds. Wasm Shell executes the build command for your language toolchain specified in your project configuration file. #### `build` options | Option | Type | Default | Description | |--------|------|---------|-------------| | `command` | string | - | Build command to execute | | `env` | map | `{}` | Environment variables to set during the build process | | `component_path` | string | - | Path to the output Wasm component file | #### `build` examples ```yaml build: command: cargo build --target wasm32-wasip2 --release component_path: target/wasm32-wasip2/release/project_name.wasm ``` ```yaml build: command: go generate ./... && tinygo build -target wasip2 -wit-package ./wit -wit-world example -o build/output.wasm ./ component_path: build/output.wasm ``` ```yaml build: command: npm run install-and-build component_path: dist/output.wasm ``` This build command will execute the `install-and-build` script defined in your TypeScript project's `package.json` file. Make sure the value for `component_path` matches the path defined in your build script. ### `dev` configuration `wash dev` builds a component according to the specified build options (unless otherwise specified in the `dev` options) and then starts a local development server. There are a variety of options available for local development. #### `dev` options | Option | Type | Default | Description | |--------|------|---------|-------------| | `command` | string | - | Custom command to run for building during development | | `address` | string | - | Address for the development server to listen on | | `service` | boolean | `false` | Whether to run as a long-running service with port binding | | `service_file` | string | - | Path to a service definition file | | `components` | array | `[]` | Additional components to load during development | | `volumes` | array | `[]` | Volumes to mount from host to guest | | `host_interfaces` | array | `[]` | WIT interfaces to expose from the host | | `tls_cert_path` | string | - | Path to TLS certificate file for HTTPS | | `tls_key_path` | string | - | Path to TLS private key file for HTTPS | | `tls_ca_path` | string | - | Path to TLS CA certificate file | | `wasi_webgpu` | boolean | `false` | Enable WASI WebGPU support | | `wasi_keyvalue_path` | string | - | Path for WASI key-value store data | | `wasi_blobstore_path` | string | - | Path for WASI blob store data | ##### `dev.components[]` Additional components to load alongside your main component. | Field | Type | Description | |-------|------|-------------| | `name` | string | Name identifier for the component | | `file` | string | Path to the component Wasm file | ##### `dev.volumes[]` Volume mounts for filesystem access from within the Wasm component. | Field | Type | Description | |-------|------|-------------| | `host_path` | string | Path on the host filesystem | | `guest_path` | string | Path as seen from within the Wasm component | #### `dev` example ```yaml dev: command: cargo build --target wasm32-wasip2 --debug address: 127.0.0.1:8080 service: true wasi_webgpu: true tls_cert_path: "./certs/cert.pem" tls_key_path: "./certs/key.pem" ``` ### `wit` configuration Configuration for WIT (WebAssembly Interface Type) dependency management. #### `wit` options | Option | Type | Default | Description | |--------|------|---------|-------------| | `registries` | array | wasm.pkg registry | Registries for WIT package fetching | | `skip_fetch` | boolean | `false` | Skip fetching WIT dependencies | | `wit_dir` | string | `./wit` | Directory where WIT files are stored | | `sources` | map | `{}` | Source overrides for WIT dependencies (target → source mapping) | #### `wit.registries[]` Registry configuration for fetching WIT packages. | Field | Type | Description | |-------|------|-------------| | `url` | string | Registry URL endpoint | | `token` | string | Optional authentication token | #### `wit` example ```yaml wit: wit_dir: "./wit" skip_fetch: false registries: - url: "https://private-registry.example.com" token: "${REGISTRY_TOKEN}" sources: "wasi:http": "https://github.com/WebAssembly/wasi-http/archive/refs/tags/v0.2.0.tar.gz" ``` --- ## Building and Publishing Components import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; **Wasm Shell** (`wash`) is the comprehensive command-line tool for developing, building, and publishing WebAssembly components. In this section of the Developer Guide, you'll learn how to: * Compile your project to a WebAssembly component binary * Publish your component to an OCI registry :::info[Prerequisites] If you haven't completed the previous section on installing `wash`, creating a project, and starting a developer loop, we recommend [starting there](./index.mdx). ::: ## Building WebAssembly components Use the `wash build` command from the root of a project directory to compile the component into a `.wasm` binary: ```shell wash build ``` By default, the compiled `.wasm` binary for a Rust project is generated at `/target/wasm32-wasip2/debug/`. The output path for the compiled `.wasm` binary [can be configured via `wash`'s configuration file](../config.mdx). Use the `wash build` command from the root of a project directory to compile the component into a `.wasm` binary: ```shell wash build --skip-fetch ``` By default, the compiled `.wasm` binary for a TinyGo project is generated at `/build/`. The output path for the compiled `.wasm` binary [can be configured via `wash`'s configuration file](../config.mdx). Use the `wash build` command from the root of a project directory to compile the component into a `.wasm` binary: ```shell wash build ``` By default, the compiled `.wasm` binary for a TypeScript project is generated at `/dist/`. The output path for the compiled `.wasm` binary [can be configured via `wash`'s configuration file](../config.mdx). :::warning[Directory context in `wash`] You can run `wash build` from anywhere inside your project directory. From outside, you can use the `-C` flag to target a project directory. See the [Command Reference](../commands.mdx) for more information, as well as more options for the `wash build` command. ::: ### Build commands Wasm Shell executes the build command for your language toolchain specified in the project's [`wash`'s configuration file](../config.mdx). For example: ```yaml build: command: cargo build --target wasm32-wasip2 --release component_path: target/wasm32-wasip2/release/output.wasm ``` ```yaml build: command: go generate ./... && tinygo build -target wasip2 -wit-package ./wit -wit-world example -o build/output.wasm ./ component_path: build/output.wasm ``` ```yaml build: command: npm run install-and-build component_path: dist/output.wasm ``` This configuration is used to build the component when you run `wash dev`, so it must be in place and properly configured for `wash dev` to work. ### Interface dependencies and `wash build` When you run `wash build`, `wash` reads the specified world in your WIT files and downloads the appropriate dependencies automatically into the `wit/deps` directory. As such, in most cases you should add `wit/deps` to your `.gitignore` file. Without any additional configuration, `wash` can download dependencies from the following namespaces (i.e. the `wasi` in `wasi:http@0.2.1`): - `wasi`: Interfaces proposed for the common [WebAssembly System Interface (WASI) standard](https://wasi.dev/) - `wasmcloud`: Interfaces maintained as part of the wasmCloud project - `wrpc`: Interfaces maintained as part of the WIT-over-RPC ([wRPC](https://github.com/bytecodealliance/wrpc)) project - `ba`: Interfaces maintained by the [Bytecode Alliance](https://bytecodealliance.org/) ## Components and OCI registries Once you're finished iterating on your component, you can publish it to any OCI compliant registry that supports **OCI artifacts**. These artifacts are not container images, but conform to OCI standards and may be stored on any OCI-compatible registry. ### Authenticating to OCI registries `wash` supports the usage of Docker credentials for authentication to registries. There are multiple [ways to authenticate with Docker credentials](https://docs.docker.com/reference/cli/docker/login/), including the `docker login` command with the `docker` CLI: ```shell docker login -u -p ``` ### Publishing to OCI registries Push the component to your registry: ```shell wash oci push ghcr.io//hello:0.2.0 ./dist/http-hello-world.wasm ``` * The target registry address (including artifact name and tag) are specified for the first option with `wash oci push`. * The second option defines the target path for the component binary to push. --- ## Creating Services **Services** are WebAssembly (Wasm) binaries that can provide persistent, stateful processes within the [workload](../../overview/workloads/index.mdx) boundary for one or more companion Wasm components. In this section of the Developer Guide, you'll learn how to: * Structure a service project * Use custom interfaces with services * Develop and iterate on a service with `wash dev` :::info[Prerequisites] If you haven't read the [Workload](../../overview/workloads/index.mdx) and [Service Overviews](../../overview/workloads/services.mdx) or completed the [previous section](./index.mdx) on installing `wash`, creating a project, and starting a developer loop, we recommend starting there. ::: ## How services differ from components Services and components are both Wasm Components, but they serve fundamentally different roles: * **Components** are invocation-based and stateless—they export interfaces like `wasi:http/incoming-handler` and are called per-request. * **Services** are long-running and stateful—they run continuously for the lifetime of the workload. Services can listen on TCP ports, maintain persistent connections, and call interfaces exported by companion components. ## Service export requirement The wasmCloud runtime needs to know how to invoke a service. A service must export one of the following: * **`wasi:cli/run`**—the standard WASI run function, which gives the service a `main` entry point. This is the most common pattern for services that act as long-running processes. * **Exactly one WIT interface**—a single custom interface export, which the runtime uses as the entry point. This constraint ensures that the runtime has an unambiguous way to start the service. ### Example: Service with `wasi:cli/run` Most services export `wasi:cli/run` and use a `main` function as their entry point. For example, a cron-style service might look like this: ```wit package wasmcloud:example; world service { // Importing an interface exported by a companion component import cron; // The run export gives this service a main() entry point export wasi:cli/run@0.2.0; } ``` The corresponding Rust code uses `main` as the entry point: ```rust wit_bindgen::generate!({ world: "service" }); #[tokio::main(flavor = "current_thread")] async fn main() { eprintln!("Starting cron-service with 1 second intervals..."); loop { tokio::time::sleep(std::time::Duration::from_secs(1)).await; let _ = wasmcloud::example::cron::invoke(); } } ``` ### Example: Service with a custom interface export Alternatively, a service can export exactly one custom WIT interface instead of `wasi:cli/run`: ```wit package mycompany:cache; interface store { get: func(key: string) -> option; set: func(key: string, value: string); } world service { export store; } ``` ## Using custom interfaces You can use your own custom [WIT interfaces](../../overview/interfaces.mdx) to define how services interact with companion components. A service can **import** interfaces exported by components in the same workload to call into component logic (for example, triggering a scheduled task). Conversely, while components cannot call service exports via WIT, they can connect to TCP ports the service is listening on. For a complete walkthrough of defining custom WIT interfaces, see the [Interfaces overview](../../overview/interfaces.mdx). ## Host plugin interfaces Services can import any host interface that a [host plugin](../../overview/hosts/plugins.mdx) provides. The workload's `host_interfaces` field lists WIT interfaces that need to be resolved by host plugins. When the runtime binds plugins to a workload, it checks which WIT interfaces the service's world needs and binds matching plugins. The runtime treats services and components the same when it comes to plugin binding—the only special treatment services receive is the `wasi:sockets` TCP bind permission, which allows services to bind to loopback and unspecified addresses for TCP listening. ## Service development Services are developed using the `wash dev` command. From the project directory, start a development loop: ```shell wash dev ``` The `wash dev` command compiles your service, starts the runtime, and watches for changes. ## Considerations When developing services, keep the following in mind: - **Export constraint**: Services must export `wasi:cli/run` or exactly one WIT interface. - **One service per workload**: Currently, workloads support a single service. - **TCP capabilities**: Services can bind to loopback/unspecified TCP addresses; regular components cannot. - **WASI P2**: Services compile to WASI P2 (`wasm32-wasip2` target). - **Async runtime**: Services can use single-threaded async runtimes (e.g., Tokio with `current_thread`). - **Restarts**: Services are automatically restarted if they crash. ## Keep reading - Learn more about services in the [Services overview](../../overview/workloads/services.mdx). - Learn more about [interfaces](../../overview/interfaces.mdx) and how to define custom WIT interfaces. - Learn more about [host plugins](../../overview/hosts/plugins.mdx) and the interfaces they provide. --- ## Debugging Components Debugging WebAssembly components is different from debugging native applications. You can't attach a traditional debugger to a Wasm component, and error messages from the build toolchain can be cryptic if you're not familiar with the Component Model. This page covers the diagnostic tools available to you, common errors you'll encounter, and how to resolve them. :::info[AI-assisted debugging] [Claude Code](https://docs.anthropic.com/en/docs/claude-code) users can install skills that give Claude specialized information on WebAssembly component debugging. See the [Useful WebAssembly Tools](./useful-webassembly-tools.mdx#claude-code-skills-for-webassembly-development) page for installation instructions for the `webassembly-component-development` and `wash` skills. ::: ## Diagnostic tools ### `wash inspect` Use `wash inspect` to view a compiled component's WIT world — its imports and exports: ```shell wash inspect my_component.wasm ``` This shows you exactly which interfaces the component declares. If a component isn't behaving as expected at runtime, you can use `wash inspect` to confirm whether the component was built with the right interfaces. You can also use `wasm-tools component wit` for the same purpose: ```shell wasm-tools component wit ./build/output.wasm ``` ### `wasm-tools validate` Validate that a `.wasm` binary is a well-formed component: ```shell wasm-tools validate ./build/output.wasm ``` If this fails, the binary isn't a valid Component Model component. This can happen when your build produces a core Wasm module instead of a component (for example, using a wrong compiler target). ### Verbose build output When `wash build` or `wash dev` fails with an unclear error, add `--verbose` or `--log-level debug` for detailed output: ```shell wash build --verbose wash build --log-level debug ``` The verbose output shows each step of the build pipeline — WIT dependency fetching, binding generation, compilation, and component encoding — so you can identify where the failure occurs. ## Common build errors ### WIT dependency errors **Symptom:** ``` error: failed to read path for WIT [./wit] Caused by: No such file or directory (os error 2) ``` **Cause:** Your WIT files aren't at the expected path. **Fix:** Ensure your `wit/` directory exists and contains a `world.wit` file. If you're starting a new project, run `wkg wit fetch` (or `wash wit fetch`) to populate `wit/deps/` with your interface dependencies. ### Version mismatches **Symptom (Rust):** ``` failed to find export of interface `wasi:http/incoming-handler@0.2.2` function `handle` ``` **Symptom (TinyGo / TypeScript):** Build errors mentioning a WASI interface version that doesn't match your WIT world. **Cause:** The `wasi:http/incoming-handler` version in your `wit/world.wit` doesn't match the version your toolchain provides. **Fix:** Check the version alignment for your language: - **Rust:** Run `cargo tree -p wasip2` and match the WASI HTTP version in your WIT world. See the [Rust Language Guide: Version alignment](./language-support/rust/index.mdx#version-alignment) for the version table. - **TinyGo:** Ensure the `wasi:http` version in your WIT world matches what `wit-bindgen-go` and TinyGo expect. See the [Go Language Guide](./language-support/go/index.mdx). - **TypeScript:** Match the version to your `jco` / `componentize-js` version. See the [TypeScript Language Guide](./language-support/typescript/index.mdx). After updating your WIT world, re-fetch dependencies: ```shell rm -rf wit/deps wkg.lock && wkg wit fetch ``` ### Missing function exports **Symptom:** ``` error: failed to encode a component from module Caused by: 0: failed to decode world from module 1: module was not valid 2: failed to find export of interface `wasi:http/incoming-handler@0.2.0` function `handle` ``` **Cause:** Your code doesn't export all functions required by your WIT world. **Fix:** Check your WIT world definition and ensure your code implements every exported interface. For example, if your world exports `wasi:http/incoming-handler`, your code must provide a handler function for incoming HTTP requests. ### Missing import resolution **Symptom:** ``` error: failed to encode a component from module Caused by: 0: failed to decode world from module 1: module was not valid 2: failed to resolve import `wasi:http/outgoing-handler@0.2.0::handle` 3: module requires an import interface named `wasi:http/outgoing-handler@0.2.0` ``` **Cause:** Your code uses an import that isn't declared in your WIT world, or bindings weren't generated correctly. **Fix:** 1. Add the import to your `wit/world.wit`. 2. Re-fetch dependencies: `wkg wit fetch` 3. Regenerate bindings (for Go: `go generate ./...`; for TypeScript: `jco types wit/ -o generated/types`) 4. Rebuild. ### Target and toolchain errors **Symptom (Rust):** ``` error[E0463]: can't find crate for `std` ``` **Cause:** Building without the Wasm target. **Fix:** Install and specify the target: ```shell rustup target add wasm32-wasip2 cargo build --target wasm32-wasip2 ``` **Symptom (TinyGo):** Build errors referencing WIT world or package names. **Cause:** The `-wit-package` or `-wit-world` flags in your Makefile don't match your `wit/world.wit`. **Fix:** Ensure the `-wit-world` flag in your Makefile matches the world name in `wit/world.wit`, and `-wit-package` points to the correct WIT directory. See the [Go Language Guide](./language-support/go/index.mdx) for the recommended Makefile setup. ## Runtime errors ### "Unknown import" errors **Symptom:** ``` component imports instance `wasi:http/types@0.2.0`, but a matching implementation was not found in the linker ``` **Cause:** The runtime doesn't implement an interface your component imports. This happens when you use an import that the host runtime hasn't wired up. **Fix:** - Check that the runtime you're using supports the interface. For `wash dev`, supported interfaces include `wasi:http`, `wasi:cli`, `wasi:io`, `wasi:clocks`, `wasi:random`, and others. - If you're using a draft or experimental interface (like `wasi:keyvalue` or `wasi:config`), ensure your runtime is configured to provide it. - Use `wash inspect` to verify which interfaces your component imports, then check your runtime's documentation for supported interfaces. If you're seeing this error when deploying a workload to Kubernetes rather than in `wash dev`, the fix is different — see [Troubleshooting: Missing host interface implementation in the linker](../../troubleshooting.mdx#missing-host-interface-implementation-in-the-linker). ### Threading errors **Symptom:** ``` operation not supported on this platform ``` Or panics related to `std::thread`, `std::sync::Mutex`, or similar concurrency primitives. **Cause:** WASI 0.2 does not support threads. Standard library threading APIs (`std::thread` in Rust, goroutines with shared state in Go) may compile but fail at runtime. **Fix:** - **Rust:** Use `wstd`'s async runtime instead of `std::thread`. Mainstream async runtimes (`tokio`, `async-std`) don't support WASI 0.2 yet. - **Go:** TinyGo supports goroutines via the `asyncify` scheduler, but some synchronization patterns may not work. Test concurrent code in the Wasm target specifically. - **TypeScript:** JavaScript is single-threaded by nature, so this is less likely to be an issue. ### Unexpected behavior from stdlib modules Some standard library modules compile to the `wasip2` target but don't behave as expected: - **`net/http` in Go** — does not work directly. Use `wasihttp.HandleFunc` and `wasihttp.Transport` from `go.wasmcloud.dev/component`. - **`os` in Go** — limited; filesystem and process operations require WASI filesystem configuration. - **`std::net` and `std::fs` in Rust** — require OS-level syscalls not available in the Wasm sandbox. Use `wstd::net` and WASI filesystem interfaces. - **Node.js APIs in TypeScript** — `fs`, `path`, `os`, `child_process`, `Buffer`, `process.env`, `require()` are not available. Use Web Standards APIs and WASI interfaces instead. For a complete list of what works and what doesn't, see the compatibility sections of each language guide: - [Rust: Crate compatibility](./language-support/rust/index.mdx#crate-compatibility) - [Go: Standard library compatibility](./language-support/go/index.mdx#standard-library-compatibility) - [TypeScript: Library compatibility](./language-support/typescript/index.mdx#library-compatibility) ## Language-specific debugging ### Rust **Check compilation before building:** ```shell cargo check --target wasm32-wasip2 ``` This is faster than a full build and catches most type errors and missing imports. **Type conflicts with `wit-bindgen`:** If you use both `wstd` and `wit-bindgen` for custom WASI interfaces, you may get type conflicts when an interface shares types with `wasi:io` or other standard interfaces. Use `wit-bindgen`'s `with` option to point to `wasip2`'s types: ```rust wit_bindgen::generate!({ world: "my-world", path: "wit", with: { "wasi:io/streams@0.2.9": wasip2::wasi::io::streams, "wasi:io/poll@0.2.9": wasip2::wasi::io::poll, }, generate_all, }); ``` See the [Rust Language Guide: Handling type conflicts](./language-support/rust/index.mdx#handling-type-conflicts) for details. ### Go (TinyGo) **World not found — defaults to CLI.** If TinyGo can't find the specified world, it silently targets `wasi:cli/run` instead of your intended world. Use `wash inspect` to check: ```shell wash inspect ./build/output.wasm ``` If you see `wasi:cli` imports and `export wasi:cli/run@0.2.0` instead of your expected world, verify: 1. The `-wit-world` flag in your Makefile matches the world name in `wit/world.wit`. 2. The `-wit-package` flag points to the correct WIT directory (typically `./wit`). 3. Your WIT files are at the expected path. **Common TinyGo build errors** are cataloged in the [FAQ: Common issues with TinyGo builds](../faq.mdx#common-issues-with-tinygo-builds). ### TypeScript **`bigint` pitfalls.** WASI interfaces use 64-bit integers, which map to JavaScript's `bigint` type. Watch for implicit `number`-to-`bigint` conversion issues and use `BigInt()` explicitly when needed. **StarlingMonkey limitations.** TypeScript components run inside the [StarlingMonkey](https://github.com/bytecodealliance/StarlingMonkey) engine (SpiderMonkey compiled to Wasm). Not all JavaScript APIs are available — notably, WebSocket support is missing, `require()` doesn't work (ESM only), and `Buffer` should be replaced with `Uint8Array`. See the [TypeScript Language Guide: Library compatibility](./language-support/typescript/index.mdx#library-compatibility) for details. ## Logging Adding log output to your component is one of the most effective debugging techniques. ### Rust Use the [`wasi:logging`](https://github.com/WebAssembly/wasi-logging) interface. The `wstd` crate doesn't include logging directly, but you can add it via `wit-bindgen`. For simple debugging, `eprintln!` writes to stderr and is visible in `wash dev` output. ### Go (TinyGo) Use the `wasilog` package from the wasmCloud Go component library: ```go import "go.wasmcloud.dev/component/log/wasilog" func handleRequest(w http.ResponseWriter, r *http.Request) { logger := wasilog.ContextLogger("handler") logger.Info("request received", "path", r.URL.Path) } ``` ### TypeScript Use `console.log` and `console.error` — StarlingMonkey routes these to WASI stderr: ```typescript console.log('Request received:', req.url); console.error('Something went wrong:', error); ``` ## Further reading - [FAQ](../faq.mdx) — common errors and troubleshooting, including a detailed [TinyGo error catalog](../faq.mdx#common-issues-with-tinygo-builds) - [Rust Language Guide](./language-support/rust/index.mdx) — crate compatibility, version alignment, type conflict resolution - [Go (TinyGo) Language Guide](./language-support/go/index.mdx) — stdlib compatibility, `wasm-tools` pinning, project configuration - [TypeScript Language Guide](./language-support/typescript/index.mdx) — library compatibility, StarlingMonkey APIs, build pipeline - [Useful WebAssembly Tools](./useful-webassembly-tools.mdx) — `wasm-tools`, Wasmtime, WASI Virt, and other ecosystem tools - [Command Reference](../commands.mdx) — full `wash` CLI reference including `--verbose` and `--log-level` flags --- ## Developer Guide import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import HubspotForm from 'react-hubspot-form'; **Wasm Shell** (`wash`) is the comprehensive command-line tool for developing, building, and publishing WebAssembly components. In this guide, you'll learn how to: * Create a new WebAssembly component project in Rust, Go (TinyGo), or JavaScript (TypeScript) * Start a development loop for your component project * Compile your project to a WebAssembly component binary * Publish your component to an OCI registry ## Prerequisites This guide requires Wasm Shell (`wash`) and the language toolchain for your language of choice. To get started: * [Install Wasm Shell (`wash`)](../index.mdx). ## Create a new component project Let's create a component that accepts an HTTP request and responds with "Hello from Rust!" Use `wash new` to create a new component project from an example in a Git repository: ```shell wash new https://github.com/wasmCloud/wasmCloud.git --name hello --subfolder templates/http-hello-world ``` This command... * Creates a new project named `hello`... * Based on a template found in the [`wasmCloud/wasmCloud`](https://github.com/wasmCloud/wasmCloud) Git repository... * In the subfolder `templates/http-hello-world` {/* TinyGo tab disabled: no Go template currently published in wasmCloud/wasmCloud. Re-enable once a Go template is available. */} Let's create a component that accepts an HTTP request and responds with "Hello from TypeScript!" Use `wash new` to create a new component project from a template: ```shell wash new https://github.com/wasmCloud/typescript.git --name hello --subfolder templates/http-hello-world-hono ``` This command... * Creates a new project named `hello`... * Based on a template found in the `wasmCloud/typescript` Git repository... * In the subfolder `templates/http-hello-world-hono` on the `v2` branch We're looking to add more examples in languages that support Wasm components. If you prefer working in a language that isn't listed here, let us know! {' '} console.log('Submitted form')} onReady={(form) => console.log('Form ready for submit')} region="na1" loading={Loading...} /> Navigate to the new `hello` directory and take a look at the generated project. ```shell cd hello ``` ## Anatomy of a component project Component projects are made up of three primary parts: * **Application code** in your language of choice * **Interfaces**: language-agnostic APIs that enable components to interact * **Bindings** that translate your interfaces to the language of your application code ![Anatomy of a component](../images/component-anatomy.webp) ### Interfaces Interfaces are APIs written in [WebAssembly Interface Type (WIT)](https://component-model.bytecodealliance.org/design/wit.html). The WIT files (`.wit`) that make up an interface are typically stored in a `/wit` folder at the root of a project. Interfaces define contracts between entities that ultimately express a piece of functionality in terms of **imports** and **exports**: * **Imports** express a dependency: "I need another entity to fulfill this functionality." * **Exports** express a function exposed to other entities: "I can take care of this functionality." For example, a component *exporting* on an **HTTP Incoming Handler** interface is exposing a function with an assertion that it can handle incoming HTTP requests. By contrast, a component *importing* a **Key-Value Storage** interface is expressing that it requires another entity to expose key-value functionality on the same interface. The **WebAssembly System Interface (WASI)** is a group of standards-track interface specifications under development by the WASI Subgroup in the W3C WebAssembly Community Group. WASI interfaces provide standard, namespaced APIs for common functionality, such as [`wasi:http`](https://github.com/WebAssembly/wasi-http). We'll be using `wasi:http` throughout the rest of this tutorial. **Note**: In the definitions above, "entities" often means other WebAssembly components, but not always—any piece of software could theoretically interact over a WIT interface, and common imports like `wasi:io` or `wasi:logging` are often fulfilled by WebAssembly runtime environments. :::info[Further reading] * Learn more about **WebAssembly Interface Type (WIT)** in the [Component Model documentation](https://component-model.bytecodealliance.org/design/wit.html). * Learn more about the **WebAssembly System Interface (WASI)** at [WASI.dev](https://wasi.dev/). ::: ### Bindings Interfaces defined in WIT are language-agnostic, so they must be translated to a given language via **bindings**. Bindings are generated a bit differently across different languages, but ultimately `wash` and the underlying language toolchain will handle binding generation automatically when you run `wash dev` or `wash build`. :::info If you use an IDE that comes with code completion and hover-tooltips, you'll be able to see documentation and get strongly-typed guidance as you develop code to interact with WASI interfaces and language-specific bindings. For more Wasm developer tooling, see [Useful WebAssembly Tools](./useful-webassembly-tools.mdx). ::: ## Explore the code A component's imports and exports are defined in a **WIT world**. You can find this project's WIT world at `./wit/world.wit`: ```wit package wasmcloud:hello; world hello { export wasi:http/incoming-handler@0.2.2; } ``` This component exports on one interface: `wasi:http/incoming-handler`. This means it can *only* interact with other entities by handling incoming HTTP requests, according to contracts defined in v0.2.2 of the `wasi:http` interface. It also means that the component *must* export on this interface in order to compile successfully. Now let's take a look at the application code. The file `src/lib.rs` imports the `wstd` crate and consists of three simple async functions. We'll walk through these sections in detail. ```rust use wstd::http::{Body, Request, Response, StatusCode}; ``` The [`wstd`](https://crates.io/crates/wstd) crate is an async Rust standard library for Wasm components and WASI 0.2 [hosted by the Bytecode Alliance](https://github.com/bytecodealliance/wstd). Importing `wstd` means that we can use `wstd::http` rather than working directly with Rust bindings of the `wasi:http` interface. :::info You do not need to use `wstd` to build components with Rust, but it is standard ecosystem tooling that can help simplify development. This example will work directly with any WebAssembly runtime that supports the Wasm Component Model. ::: Whenever an incoming HTTP request is received, the main function returns a response depending on whether the endpoint is `/` or not found. ```rust #[wstd::http_server] async fn main(req: Request) -> Result, wstd::http::Error> { match req.uri().path_and_query().unwrap().as_str() { "/" => home(req).await, _ => not_found(req).await, } } async fn home(_req: Request) -> Result, wstd::http::Error> { // Return a simple response with a string body Ok(Response::new("Hello from wasmCloud!\n".into())) } async fn not_found(_req: Request) -> Result, wstd::http::Error> { Ok(Response::builder() .status(StatusCode::NOT_FOUND) .body("Not found\n".into()) .unwrap()) } ``` Within the `home` and `not_found` functions, the application creates appropriate responses, and the component returns those responses back to the requesting HTTP client (such as a `curl` command or a web browser). The file `main.go` includes imports, `init` and `handleRequest` functions, and an empty `main` function. We'll walk through these pieces in depth. ```go //go:generate go tool wit-bindgen-go generate --world hello --out gen ./wit package main import ( "fmt" "net/http" "go.wasmcloud.dev/component/net/wasihttp" ) func init() { // Register the handleRequest function as the handler for all incoming requests. wasihttp.HandleFunc(handleRequest) } func handleRequest(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from Go!\n") } // Since we don't run this program like a CLI, the `main` function is empty. Instead, // we call the `handleRequest` function when an HTTP request is received. func main() {} ``` * **The Go directive at the very top** ensures that when we build our project with `tinygo build`, the `wit-bindgen-go` tool will be run to generate the bindings for our component from the `hello` WIT world found in the `./wit` directory. Bindings will be generated in the `gen` directory. * **In the import section**, we import Go's standard `fmt` and `net/http` libraries. We also import the `wasihttp` package, which provides an implementation of `http.Handler` backed by `wasi:http`—enabling us to write idiomatic Go using a language-agnostic WASI interface. * **In the `init` section**, we set the `handleRequest` function to handle incoming HTTP requests. * **In the `handleRequest` function**, we respond to an HTTP request by printing `Hello from Go!` * **The main function is empty**, since the component doesn't run like a CLI, but instead runs the `handleRequest` function when triggered by an HTTP request. ## Add a configuration file and Makefile Go projects use a `Makefile` to wrap the build steps and a [configuration file](../config.mdx) at `.wash/config.yaml` to tell `wash` how to invoke the build. Create the config file: ```shell mkdir .wash && touch ./.wash/config.yaml ``` Add the contents below to `config.yaml`: ```yaml dev: command: make build build: command: make bindgen build ``` Create a `Makefile` at the project root: ```makefile all: build .PHONY: build build: tinygo build -target wasip2 -wit-package ./wit -wit-world hello -o hello.wasm ./ .PHONY: bindgen bindgen: go generate ./... ``` For more on the Makefile-based build workflow, see the [Go Language Guide](./language-support/go/index.mdx). The file `src/component.ts` uses [Hono](https://hono.dev/), a lightweight web framework built on Web Standards. Let's walk through the pieces in depth. ```typescript import { Hono } from 'hono'; import { fire } from '@bytecodealliance/jco-std/wasi/0.2.6/http/adapters/hono/server'; ``` The import section brings in the Hono framework and `fire` from [`@bytecodealliance/jco-std`](https://www.npmjs.com/package/@bytecodealliance/jco-std), a Bytecode Alliance library that adapts Hono to the WASI HTTP incoming handler interface. ```typescript const app = new Hono(); app.get('/', (c) => { return c.text('Hello from TypeScript!\n'); }); app.notFound((c) => { return c.text('Not found\n', 404); }); fire(app); ``` We create a Hono app with a route for `/` and a fallback for unmatched routes. The `fire()` function wires the Hono app to the Service Worker `fetch` event listener, which StarlingMonkey (the embedded JavaScript engine) maps to `wasi:http/incoming-handler`. When a request arrives, Hono routes it and returns the appropriate response. ```typescript export { incomingHandler } from '@bytecodealliance/jco-std/wasi/0.2.6/http/adapters/hono/server'; ``` This re-export provides the `incomingHandler` that the Wasm component must export to satisfy the `wasi:http/incoming-handler` interface declared in the WIT world. The `jco-std` adapter handles all the conversion between WASI HTTP types and the Web Standard `Request`/`Response` objects that Hono uses. We're looking to add more examples in languages that support Wasm components. If you prefer working in a language that isn't listed here, let us know! {' '} console.log('Submitted form')} onReady={(form) => console.log('Form ready for submit')} region="na1" loading={Loading...} /> ## Start a development loop Now we'll start a development loop that runs the component, watches for modifications to the code, and refreshes when we make changes. ```shell wash dev ``` :::warning[Directory context in `wash`] You can run `wash dev` from anywhere inside your project directory. From outside, you can use the `-C` flag to target a project directory. See the [Command Reference](../commands.mdx) for more information. ::: Now we can send a request to `localhost:8000` with `curl` (in a new tab), or by visiting the address in our browser. ```shell curl localhost:8000 ``` You should see: ```text Hello from wasmCloud! ``` ```text Hello from Go! ``` ```text Hello from TypeScript! ``` You can stop the development loop with CTRL+C. ## Next steps In the [next section](./build-and-publish.mdx), we'll compile a component to a `.wasm` binary and publish the artifact to an OCI registry. --- ## Go (TinyGo) Language Guide wasmCloud uses [TinyGo](https://tinygo.org/) to build WebAssembly components from Go source code. TinyGo compiles a large subset of Go to the `wasip2` target, producing Wasm components that work with the Component Model and WASI 0.2. This guide covers the toolchain, the wasmCloud Go component library, bindings generation, and practical guidance for building Go components on wasmCloud. If you're looking for a quick walkthrough of creating, building, and running a Go component, see the [Developer Guide](../../index.mdx?lang=tinygo). ## Why TinyGo? The standard Go compiler does not yet support the WebAssembly Component Model or WASI P2. TinyGo targets `wasip2` directly using LLVM, producing much smaller Wasm binaries. TinyGo supports a large subset of Go — including goroutines, interfaces, closures, and most of the standard library — making it the current path for Go developers building Wasm components. Since the TinyGo project moves quickly, we recommend using the latest version. ## Toolchain overview ### Build pipeline ![Go build pipeline](../../../images/build-pipeline-go.svg) Building a Go component has two steps: generating Go bindings from WIT interfaces, then compiling to a Wasm component with TinyGo. We recommend using a `Makefile` to wrap these steps. When you run `wash build`, it executes the build command from your `.wash/config.yaml`, which calls the Makefile targets. ### TinyGo [TinyGo](https://tinygo.org/) is a Go compiler for microcontrollers, WebAssembly, and other constrained environments. For wasmCloud, the key feature is native Component Model support via the `-target wasip2` flag. Install TinyGo following the [official installation guide](https://tinygo.org/getting-started/install/). Key compiler flags for Wasm components: | Flag | Value | Description | |---|---|---| | `-target` | `wasip2` | Compile to a WASI P2 component | | `-wit-package` | `./wit` | Path to the WIT package directory | | `-wit-world` | `` | Name of the WIT world to target | | `-o` | `.wasm` | Output file path | Optional optimization flags: | Flag | Value | Description | |---|---|---| | `-scheduler` | `asyncify` | Enable goroutine support via Binaryen's Asyncify | | `-gc` | `conservative` | Mark/sweep garbage collector suitable for Wasm | | `-opt` | `z` | Aggressive code size optimization | | `-no-debug` | | Remove debug info (reduces binary size significantly) | ### `wit-bindgen-go` [`wit-bindgen-go`](https://github.com/bytecodealliance/go-modules) generates Go code from WIT interface definitions. It produces typed Go packages that map to your component's imports and exports. `wit-bindgen-go` is invoked via a `//go:generate` directive at the top of your `main.go`: ```go //go:generate go tool wit-bindgen-go generate --world wasmcloud:hello/hello --out gen ./wit ``` This reads the WIT files in `./wit`, generates Go packages for the `hello` world in the `wasmcloud:hello` package, and writes them to the `gen/` directory. The `--world` flag uses the fully-qualified world name in the format `/`. The generated packages depend on the [`cm`](https://pkg.go.dev/go.bytecodealliance.org/cm) package for Component Model types like `option`, `result`, `list`, and `resource`. Declare `wit-bindgen-go` as a tool dependency in your `go.mod` using the Go 1.24+ `tool` directive: ```go title="go.mod" tool go.bytecodealliance.org/cmd/wit-bindgen-go ``` Then run `go mod tidy` to resolve the dependency. ## Handling HTTP requests The [`go.wasmcloud.dev/component`](https://github.com/wasmCloud/go) package provides idiomatic Go adapters for WASI interfaces. The `wasihttp` package bridges Go's standard `net/http` library with WASI HTTP, so you can write familiar Go HTTP handlers. ### Basic HTTP server ```go //go:generate go tool wit-bindgen-go generate --world wasmcloud:hello/hello --out gen ./wit package main import ( "fmt" "net/http" "go.wasmcloud.dev/component/net/wasihttp" ) func init() { wasihttp.HandleFunc(handleRequest) } func handleRequest(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from Go!\n") } func main() {} ``` Key points: - **`wasihttp.HandleFunc`** accepts a standard `http.HandlerFunc` and registers it as the WASI HTTP incoming handler. All conversion between WASI HTTP types and Go's `net/http` types happens behind the scenes. - **Handlers are registered in `init()`**, not `main()`. The component doesn't run like a CLI — it responds to incoming HTTP requests. - **`main()` is empty.** This is required but does nothing for component-based programs. - You can use Go's standard `http.ResponseWriter` and `*http.Request` types exactly as you would in a normal Go HTTP server. ### Routing `wasihttp.Handle` accepts any `http.Handler`, so Go's standard `http.ServeMux`, third-party routers, and middleware chains all work. With Go's standard library: ```go func init() { mux := http.NewServeMux() mux.HandleFunc("/", handleHome) mux.HandleFunc("/api/data", handleData) wasihttp.Handle(mux) } ``` With [`httprouter`](https://github.com/julienschmidt/httprouter): ```go import "github.com/julienschmidt/httprouter" func init() { router := httprouter.New() router.HandlerFunc(http.MethodGet, "/", indexHandler) router.HandlerFunc(http.MethodGet, "/headers", headersHandler) router.HandlerFunc(http.MethodPost, "/post", postHandler) wasihttp.Handle(router) } ``` ### Outgoing HTTP requests The `wasihttp` package provides a `Transport` that implements `http.RoundTripper`, enabling outgoing HTTP requests via WASI: ```go import ( "io" "net/http" "go.wasmcloud.dev/component/net/wasihttp" ) func handleRequest(w http.ResponseWriter, r *http.Request) { client := http.Client{Transport: &wasihttp.Transport{}} resp, err := client.Get("https://api.example.com/data") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer resp.Body.Close() io.Copy(w, resp.Body) } ``` To make outgoing requests, your WIT world must import `wasi:http/outgoing-handler`. ### Logging The `wasilog` package provides structured logging backed by WASI interfaces: ```go import "go.wasmcloud.dev/component/log/wasilog" func handleRequest(w http.ResponseWriter, r *http.Request) { logger := wasilog.ContextLogger("handler") logger.Info("request received", "path", r.URL.Path, "method", r.Method) fmt.Fprintf(w, "Hello from Go!\n") } ``` ## Adding custom WASI interfaces The `wasmcloud:component-go/imports` include pulls in standard imports for the wasmCloud Go component library (logging, config, etc.). For additional interfaces like `wasi:keyvalue`, `wasi:config`, or custom interfaces, add them to your WIT world and regenerate bindings. ### Example: Adding `wasi:keyvalue` **1. Declare the imports in your WIT world:** ```wit title="wit/world.wit" package wasmcloud:hello; world hello { include wasmcloud:component-go/imports@0.1.0; import wasi:keyvalue/store@0.2.0-draft; import wasi:keyvalue/atomics@0.2.0-draft; export wasi:http/incoming-handler@0.2.0; } ``` **2. Fetch dependencies and regenerate bindings:** ```shell wkg wit fetch go generate ./... ``` This downloads the WIT definitions for `wasi:keyvalue` into `wit/deps/` and generates Go packages in `gen/`. **3. Use the generated bindings:** ```go import ( "fmt" "net/http" "go.wasmcloud.dev/component/net/wasihttp" "/gen/wasi/keyvalue/store" "/gen/wasi/keyvalue/atomics" ) func handleRequest(w http.ResponseWriter, r *http.Request) { bucket := store.Open("") count := atomics.Increment(bucket, "visitor-count", 1) fmt.Fprintf(w, "Visit count: %d\n", count) } ``` Generated bindings follow Go naming conventions — WIT function names are converted to PascalCase. The generated packages are in `gen/` under the WIT namespace path (e.g., `gen/wasi/keyvalue/store`). Replace `` with your Go module path from `go.mod`. ### WIT dependency management with `wkg` `wash` bundles the [`wkg`](https://github.com/bytecodealliance/wasm-pkg-tools) WebAssembly package manager, so you can fetch WIT dependencies directly: ```shell wash wit fetch ``` This populates `wit/deps/` with downloaded interface definitions and creates a `wkg.lock` file. You should: - Add `wit/deps/` to `.gitignore` — these are fetched dependencies - Commit `wkg.lock` — this ensures reproducible builds When you run `wash build`, it calls `wash wit fetch` automatically, so you typically don't need to run it manually. For a detailed explanation of `wkg` and auto-resolved namespaces, see the [Rust Language Guide's WIT dependency management section](../rust/index.mdx#wit-dependency-management-with-wkg). ## Project structure A typical Go component project: ``` my-component/ ├── main.go # Application code ├── Makefile # Build recipes (bindgen, build) ├── go.mod # Go module definition ├── go.sum # Go dependency checksums ├── gen/ # Generated bindings (gitignored) ├── wit/ │ ├── world.wit # WIT world definition │ └── deps/ # Fetched WIT dependencies (gitignored) ├── wkg.lock # WIT dependency lock file └── .wash/ └── config.yaml # wash project configuration ``` ### `Makefile` A `Makefile` wraps the two-step build process (bindings generation + TinyGo compilation) so that `wash build` and `wash dev` can invoke them with simple commands: ```makefile title="Makefile" all: build .PHONY: build build: tinygo build -target wasip2 -wit-package ./wit -wit-world hello -o my-component.wasm ./ .PHONY: bindgen bindgen: go generate ./... ``` The `build` target invokes TinyGo with: - `-target wasip2` — compile to a WASI P2 component - `-wit-package ./wit` — path to the WIT package directory - `-wit-world hello` — name of the WIT world to target - `-o my-component.wasm` — output file path The `bindgen` target runs `go generate`, which invokes `wit-bindgen-go` via the directive in `main.go`. ### `go.mod` ```go title="go.mod" module github.com/myorg/my-component go 1.24 require ( go.bytecodealliance.org/cm v0.3.0 go.wasmcloud.dev/component v0.0.10 ) tool go.bytecodealliance.org/cmd/wit-bindgen-go ``` The `tool` directive (Go 1.24+) declares `wit-bindgen-go` as a build tool dependency. This replaces the older `tools.go` file pattern. You should add both `gen/` and `wit/deps/` to your `.gitignore` — these are generated or fetched during the build and should not be committed. ### WIT world ```wit title="wit/world.wit" package wasmcloud:hello; world hello { include wasmcloud:component-go/imports@0.1.0; export wasi:http/incoming-handler@0.2.0; } ``` The `include wasmcloud:component-go/imports` line pulls in standard imports used by the wasmCloud Go component library (logging, config, etc.). You can also declare imports individually if you prefer. :::info[WASI HTTP version] The `wasi:http/incoming-handler` version in your WIT world (here `@0.2.0`) must match the version supported by your TinyGo and `wit-bindgen-go` toolchain. The version shown here matches the wasmCloud Go project template. If you see build errors about missing exports, check that the version in your `world.wit` aligns with your installed toolchain versions. ::: ### `.wash/config.yaml` The [project configuration file](../../../config.mdx) at `.wash/config.yaml` tells `wash` how to build the component: ```yaml title=".wash/config.yaml" build: command: make bindgen build ``` The `build.command` is a shell command that `wash build` executes. Here it runs the Makefile's `bindgen` and `build` targets in sequence — first generating Go bindings from WIT, then compiling with TinyGo. You can also specify a separate command for `wash dev` that skips binding generation for faster iteration: ```yaml title=".wash/config.yaml" dev: command: make build build: command: make bindgen build ``` With this configuration, `wash dev` runs only `make build` (recompiling Go code), while `wash build` runs `make bindgen build` (regenerating bindings and recompiling). This speeds up the development loop when you're only changing Go code, not WIT interfaces. For a full reference of configuration options, see the [Configuration](../../../config.mdx) page. ## Standard library compatibility TinyGo supports most of the Go standard library, but some packages have limitations in the `wasip2` target. ### What works - **`fmt`** — formatted I/O (adds ~100 KB to binary size) - **`io`** — I/O primitives - **`strings`**, **`bytes`** — string and byte manipulation - **`strconv`** — string conversions - **`encoding/json`** — JSON encoding/decoding (may have edge-case panics — test thoroughly) - **`encoding/base64`** — base64 encoding - **`math`** — mathematical functions - **`sort`** — sorting - **`sync`** — synchronization primitives - **`time`** — time operations - **`net/http`** — via `wasihttp` adapter (not directly — see below) ### What does not work - **`net/http` directly** — Go's standard HTTP server/client does not work in `wasip2`. Use `wasihttp.HandleFunc` for incoming requests and `wasihttp.Transport` for outgoing requests. - **`os`** — limited; filesystem and process operations are not available unless WASI filesystem is configured - **`net`** — raw networking is not available; use WASI HTTP interfaces - **Packages requiring cgo** — TinyGo does not support cgo in the `wasip2` target - **`plugin`**, **`net/smtp`**, **`debug/buildinfo`** — cannot be imported ### Practical guidance - Use the `wasihttp` package instead of `net/http` directly — it provides `http.Handler` and `http.RoundTripper` implementations backed by WASI - Prefer pure Go libraries; anything requiring cgo will not compile - Test your dependencies against the `wasip2` target early — some packages compile but have runtime issues - Be mindful of binary size: packages like `fmt` add meaningful overhead to Wasm components ## Building and running ### Development loop Start a development loop that builds, runs, and watches for changes: ```shell wash dev ``` Send a request to test: ```shell curl localhost:8000 ``` ### Build a component Compile your project to a `.wasm` binary: ```shell wash build ``` `wash build` executes the `build.command` from your `.wash/config.yaml`. With the recommended Makefile setup, this runs `make bindgen build` — generating bindings from WIT, then compiling with TinyGo. The compiled component is output to the path specified by `-o` in your Makefile (e.g., `my-component.wasm`). You can also run the Makefile targets directly: ```shell make bindgen # Regenerate Go bindings from WIT make build # Compile to .wasm with TinyGo ``` ## Further reading - [Developer Guide](../../index.mdx?lang=tinygo) — quickstart tutorial for creating, building, and running a Go component - [wasmCloud Go component library](https://github.com/wasmCloud/go) — `wasihttp`, `wasilog`, and other Go adapters for WASI interfaces - [TinyGo documentation](https://tinygo.org/docs/) — compiler reference and standard library support - [TinyGo package support](https://tinygo.org/docs/reference/lang-support/stdlib/) — standard library compatibility matrix - [`wit-bindgen-go`](https://github.com/bytecodealliance/go-modules) — Bytecode Alliance Go modules for WIT bindings - [Component Model: Go](https://component-model.bytecodealliance.org/language-support/go.html) — Component Model documentation for Go - [Language Support overview](../index.mdx) — summary of all supported languages and toolchains --- ## Language Support wasmCloud runs standard [WebAssembly components](https://component-model.bytecodealliance.org/) targeting [WASI P2](https://wasi.dev/). Any language toolchain that can produce a Wasm component with WASI P2 support can build components that run on wasmCloud. This page provides a reference for various languages' support for compiling WebAssembly components, as well as wasmCloud ecosystem support for building and running components developed in a given language. ## Component Support This chart summarizes the current state of language support for WebAssembly components, from languages with built-in WASI P2 targets to toolchains that are still in progress. | Language | Toolchain | Status | WASI version | |---|---|---|---| | **Rust** | `cargo` + `wasm32-wasip2` target | Stable | P2 | | **Go** | TinyGo | Stable | P2 | | **TypeScript / JavaScript** | `jco` + `componentize-js` | Stable | P2 | | **C# / .NET** | `componentize-dotnet` | Stable | P2 | | **Python** | `componentize-py` | Stable | P2 | | **C** | `wasi-sdk` + `wit-bindgen-c` | Stable | P2 | | **C++** | `wit-bindgen-cpp` | Experimental | P2 | | **Ruby** | `ruby.wasm` | In progress | P2 (partial) | | **Kotlin** | Kotlin/Wasm (native) | Planned | — | | **Swift** | SwiftWasm | Roadmap accepted | — | | **Java** | GraalWasm | Planned | — | ## Tier 1: Well-supported toolchains These languages are supported by wasmCloud component project templates, `wash dev` integration, and documentation in the [Developer Guide](../index.mdx). ### Rust Rust has first-class WebAssembly support via the `wasm32-wasip2` compiler target, which ships with the standard Rust toolchain. - **Toolchain:** `cargo` with target `wasm32-wasip2` (install via `rustup target add wasm32-wasip2`) - **Bindings:** The [`wstd`](https://crates.io/crates/wstd) crate provides an async Rust standard library for Wasm components and WASI 0.2, hosted by the [Bytecode Alliance](https://github.com/bytecodealliance/wstd). You can also use [`wit-bindgen`](https://github.com/bytecodealliance/wit-bindgen) directly for lower-level control. - **Get started:** [Developer Guide](../index.mdx?lang=rust) | [Rust Language Guide](./rust/index.mdx) ### JavaScript (TypeScript) TypeScript components are built using the [`jco`](https://github.com/bytecodealliance/jco) toolchain (part of the Bytecode Alliance), which compiles JS/TS to Wasm components via [`componentize-js`](https://github.com/bytecodealliance/ComponentizeJS). - **Toolchain:** `jco` + `componentize-js` - **Bindings:** `jco` generates TypeScript types from WIT files automatically - **Get started:** [Developer Guide](../index.mdx?lang=typescript) | [TypeScript Language Guide](./typescript/index.mdx) ### Go (TinyGo) wasmCloud uses [TinyGo](https://tinygo.org/) (not the standard Go compiler) for WebAssembly component support. TinyGo compiles a large subset of Go to WebAssembly with a much smaller binary footprint. - **Toolchain:** TinyGo with `-target wasip2` - **Bindings:** [`wit-bindgen-go`](https://github.com/bytecodealliance/go-modules) generates Go bindings from WIT files. The [`go.wasmcloud.dev/component`](https://github.com/wasmCloud/go/tree/main/component) package provides idiomatic Go helpers (e.g. `wasihttp.HandleFunc` for `net/http`-style request handling). - **`wash` support:** `wash new`, `wash dev`, `wash build` — requires a [`.wash/config.yaml` configuration file](../../config.mdx) and a `Makefile` - **Get started:** [Developer Guide](../index.mdx?lang=tinygo) | [Go Language Guide](./go/index.mdx) :::note[Why TinyGo?] The standard Go compiler does not yet support the Wasm Component Model. TinyGo targets `wasip2` directly and produces much smaller binaries, making it the current path for Go developers building Wasm components. ::: ## Tier 2: Stable toolchains These languages have stable Component Model toolchains that can produce components compatible with wasmCloud. You can build components manually and run them with `wash`, but there are no `wash` project templates or `wash dev` integrations yet. ### C# / .NET [`componentize-dotnet`](https://github.com/bytecodealliance/componentize-dotnet) is a Bytecode Alliance project that compiles .NET applications to Wasm components using NativeAOT. - **Toolchain:** `componentize-dotnet` NuGet package - **Status:** Stable; actively maintained by the Bytecode Alliance - **Build:** Components are built with `dotnet build` and the `componentize-dotnet` MSBuild integration - **Repository:** [bytecodealliance/componentize-dotnet](https://github.com/bytecodealliance/componentize-dotnet) ### Python [`componentize-py`](https://github.com/bytecodealliance/componentize-py) is a Bytecode Alliance project that compiles Python applications to Wasm components. - **Toolchain:** `componentize-py` CLI - **Status:** Stable; actively maintained by the Bytecode Alliance - **Build:** `componentize-py componentize --wit-path wit --world -o output.wasm app.py` - **Repository:** [bytecodealliance/componentize-py](https://github.com/bytecodealliance/componentize-py) ## Tier 3: In progress or planned These toolchains are actively working toward Component Model support. Components built with these tools may not yet be fully compatible with wasmCloud. | Language | Toolchain | Status | More information | |---|---|---|---| | **C** | [`wasi-sdk`](https://github.com/WebAssembly/wasi-sdk) + [`wit-bindgen-c`](https://github.com/bytecodealliance/wit-bindgen) | WASI P2 + Component Model support complete | [wasi-sdk releases](https://github.com/WebAssembly/wasi-sdk/releases) | | **C++** | [`wit-bindgen-cpp`](https://github.com/aspect-build/wit-bindgen-cpp) | Experimental | [wit-bindgen-cpp repository](https://github.com/aspect-build/wit-bindgen-cpp) | | **Ruby** | [`ruby.wasm`](https://github.com/ruby/ruby.wasm) | Component Model support in progress | [ruby.wasm repository](https://github.com/ruby/ruby.wasm) | | **Kotlin** | Kotlin/Wasm | Native Wasm Component Model support planned by JetBrains | [JetBrains tracking issue](https://youtrack.jetbrains.com/issue/KT-56492) | | **Swift** | SwiftWasm | Component Model roadmap accepted | [WebAssembly vision](https://github.com/swiftlang/swift-evolution/blob/main/visions/webassembly.md) | | **Java** | [GraalWasm](https://www.graalvm.org/latest/reference-manual/wasm/) | WASI support planned | [GraalVM documentation](https://www.graalvm.org/latest/reference-manual/wasm/) | ## Binding generators Most languages use [`wit-bindgen`](https://github.com/bytecodealliance/wit-bindgen) (or a language-specific tool built on the same foundation) to generate typed bindings from WIT interface definitions. `wit-bindgen` currently supports: - **Rust** (`wit-bindgen-rust`) - **Go** (`wit-bindgen-go`) - **C** (`wit-bindgen-c`) - **C#** (via `componentize-dotnet`) - **Java** (`wit-bindgen-java`, early stage) For TypeScript/JavaScript, [`jco`](https://github.com/bytecodealliance/jco) handles binding generation as part of its build pipeline. ## Further reading - [WebAssembly Component Model specification](https://github.com/WebAssembly/component-model) — Official Component Model repository and specification - [Component Model documentation](https://component-model.bytecodealliance.org/) — Specification and guides for the Wasm Component Model - [WASI.dev](https://wasi.dev/) — WebAssembly System Interface specifications - [Developer Guide](../index.mdx) — `wash` tutorial for creating, building, and publishing components - [Useful WebAssembly Tools](../useful-webassembly-tools.mdx) — Additional tooling for Wasm development --- ## Configuration(Rust) You can read runtime configuration values in a Rust component with the [`wasi:cli/environment`](https://github.com/WebAssembly/wasi-cli) interface. Configuration values can be supplied from inline key-value pairs, Kubernetes ConfigMaps, and Kubernetes Secrets. All arrive through the same interface regardless of source. This guide walks through reading `wasi:cli/environment` from a Rust component and shows how to provide values via a Kubernetes workload manifest. ## Overview `wasi:cli/environment` is part of the standard WASI P2 suite and is always available in a wasmCloud host. It exposes a `get-environment` function that returns all configuration key-value pairs available to the component. In production, values come from `localResources.environment` in a `Workload` or `WorkloadDeployment` manifest, which accepts inline key-value pairs, Kubernetes ConfigMaps, and Kubernetes Secrets. :::tip[Guidance for operators] The [Secrets and Configuration Management](../../../../kubernetes-operator/operator-manual/secrets-and-configuration.mdx) page in the Operator Manual describes how the Kubernetes operator injects values via `wasi:cli/environment`. This guide covers the component author's side: how to read those values in Rust. ::: `wasi:cli/environment` is provided by the [`wasip2`](https://crates.io/crates/wasip2) crate maintained by the Bytecode Alliance. `wstd` already depends on `wasip2` internally, but does not re-export it publicly, so you add `wasip2` as a direct dependency and call `wasip2::cli::environment::get_environment` from your handler. No `wit-bindgen` setup is required. ## Step 1: Add the dependency In `Cargo.toml`: ```toml [dependencies] wasip2 = "1.0" wstd = "0.6" ``` ## Step 2: Read configuration values In `src/lib.rs`, add a helper that reads all key-value pairs into a `HashMap` for convenient lookup: ```rust use std::collections::HashMap; use wstd::http::{Body, Request, Response}; fn load_config() -> HashMap { wasip2::cli::environment::get_environment() .into_iter() .collect() } #[wstd::http_server] async fn main(_req: Request) -> Result, wstd::http::Error> { let config = load_config(); let app_name = config .get("APP_NAME") .cloned() .unwrap_or_else(|| "My App (dev)".to_string()); let upstream_url = config .get("UPSTREAM_URL") .cloned() .unwrap_or_else(|| "http://localhost:9090".to_string()); let body = format!("app={app_name}\nupstream={upstream_url}\n"); Ok(Response::new(Body::from(body))) } ``` Call `get_environment()` (or your `load_config` helper) inside each request handler rather than at module scope. Configuration is stable for the lifetime of an invocation, but reading at module scope can run before the host has finished injecting values. ### Synchronous API `get_environment` is synchronous in WIT. It returns `Vec<(String, String)>` directly without an `async` wrapper. The call works the same way whether your handler is synchronous or asynchronous. ## Step 3: Provide configuration values ### In production (Kubernetes manifests) All configuration values are delivered to the component through the `localResources.environment` field in a `Workload` or `WorkloadDeployment` manifest. Three sources are supported and can be combined. #### Inline values The simplest approach: provide key-value pairs directly in the manifest: ```yaml components: - name: http-component image: ghcr.io/your-org/your-component:latest localResources: environment: config: APP_NAME: My Service UPSTREAM_URL: https://api.example.com ``` Inline values are suitable for non-sensitive configuration that is safe to store in version control. #### From a Kubernetes ConfigMap Reference a ConfigMap by name. Each key in the ConfigMap becomes a configuration value in the component: ```yaml components: - name: http-component image: ghcr.io/your-org/your-component:latest localResources: environment: configFrom: - name: app-config ``` Create the ConfigMap separately: ```yaml apiVersion: v1 kind: ConfigMap metadata: name: app-config namespace: default data: APP_NAME: My Service UPSTREAM_URL: https://api.example.com ``` #### From a Kubernetes Secret Reference a Secret by name for sensitive values such as API keys or credentials: ```yaml components: - name: http-component image: ghcr.io/your-org/your-component:latest localResources: environment: secretFrom: - name: app-secrets ``` Secret values are delivered to the component as plain strings. No decoding is required in your component code. #### Combining sources All three sources can be used together. When the same key appears in multiple sources, the order of precedence (lowest to highest) is: `config` → `configFrom` → `secretFrom`. ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: my-app namespace: default spec: replicas: 1 template: spec: hostSelector: hostgroup: default components: - name: http-component image: ghcr.io/your-org/your-component:latest localResources: environment: config: APP_NAME: My Service # inline (lowest precedence) configFrom: - name: app-config # non-sensitive config from ConfigMap secretFrom: - name: app-secrets # sensitive values from Secret (highest precedence) hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: host: my-app.example.com ``` The `host:` value is the HTTP `Host` header the wasmCloud host uses to route requests to this workload. If you want to override the listen address instead, use `address: '0.0.0.0:8080'`. See [Secrets and Configuration Management](../../../../kubernetes-operator/operator-manual/secrets-and-configuration.mdx) for the full `hostInterfaces` reference. :::tip For a complete reference to `localResources.environment` fields, including precedence rules and RBAC considerations, see [Secrets and Configuration Management](../../../../kubernetes-operator/operator-manual/secrets-and-configuration.mdx). ::: ### During development (`wash dev`) `wash dev` provides the `wasi:cli/environment` interface, but `localResources.environment` defaults to empty. There is currently no mechanism in `.wash/config.yaml` or the `wash dev` CLI to inject test configuration values into a component locally. The practical approach for development is to supply fallback defaults in your component code, as shown in the example above. This lets your component run correctly during `wash dev` while reading real values in production. ## Build and verify ```shell wash dev ``` `wash dev` builds the component and serves it on `http://localhost:8000`. With no configuration injected, your fallback defaults take effect: ```shell curl http://localhost:8000 ``` ```text app=My App (dev) upstream=http://localhost:9090 ``` To verify the import is correctly declared, inspect the built component: ```shell wasm-tools component wit target/wasm32-wasip2/release/.wasm | grep environment ``` You should see a line like `import wasi:cli/environment@0.2.x`. The exact patch version is governed by the `wasip2` crate version that `wstd` resolves to. ## Alternative: the `wit-bindgen` path If you would rather not depend on `wasip2` directly, you can declare the import in your WIT world and generate bindings with `wit-bindgen`. In `wit/world.wit`: ```wit package wasmcloud:my-component; world my-component { import wasi:cli/environment@0.2.2; export wasi:http/incoming-handler@0.2.2; } ``` In `src/lib.rs`: ```rust mod bindings { wit_bindgen::generate!({ generate_all }); } use bindings::wasi::cli::environment::get_environment; let config: std::collections::HashMap = get_environment().into_iter().collect(); ``` The function name and signature are identical to the `wasip2` crate path. Choose whichever fits your existing setup. ## Summary: checklist for adding configuration 1. **Add `wasip2 = "1.0"` to `Cargo.toml`** (or use `wit-bindgen` if you prefer to declare the import in your WIT world). 2. **Call `wasip2::cli::environment::get_environment()` inside request handlers**, not at module scope. 3. **Collect into a `HashMap`** for ergonomic key lookup: `get_environment().into_iter().collect::>()`. 4. **Provide fallback defaults** with `unwrap_or_else` for every value your component reads. This keeps the component functional during `wash dev` when no configuration is injected. 5. **In production**, provide values via `localResources.environment` in your Kubernetes manifest, using `config:`, `configFrom:`, or `secretFrom:` as appropriate. ## API reference: `wasi:cli/environment` functions For the upstream WIT definitions, see the [`wasi:cli` 0.2.x interfaces](https://github.com/WebAssembly/wasi-cli/tree/main/wit) in the `WebAssembly/wasi-cli` repository. | Function | Signature | Description | |---|---|---| | `get_environment` | `fn() -> Vec<(String, String)>` | Returns all configuration key-value pairs. Returns an empty `Vec` if no configuration has been provided to the component. | | `get_arguments` | `fn() -> Vec` | Returns POSIX-style command-line arguments. Not used for configuration. | | `initial_cwd` | `fn() -> Option` | Returns the initial working directory. Not used for configuration. | ## Further reading - [Secrets and Configuration Management](../../../../kubernetes-operator/operator-manual/secrets-and-configuration.mdx) — operator reference for `localResources.environment`, ConfigMaps, and Secrets - [Rust Language Guide](./index.mdx) — toolchain overview, HTTP patterns, async runtime guidance, and crate compatibility - [Key-Value Storage](./key-value-storage.mdx) — persistent key-value storage for component state - [Language Support overview](../index.mdx) — summary of all supported languages and toolchains --- ## Filesystem You can add filesystem capabilities to a Rust component with the [`wasi:filesystem`](https://github.com/WebAssembly/WASI/tree/main/proposals/filesystem) interface. This guide walks through adding filesystem access to a Rust component using `wit-bindgen` alongside `wstd`. ## Overview WebAssembly components run in a sandboxed environment with no default access to the host filesystem. The `wasi:filesystem` interface provides secure, capability-based filesystem access through **preopens**: directories that are explicitly mounted into a component before instantiation. This is different from key-value or blob storage. Instead of storing data in an abstract store, your component reads and writes files in mounted directories, just as a traditional application would. `wasi:filesystem` is particularly useful for: - Serving static assets (HTML, CSS, images) - Reading data files - Writing logs or output files - Any workload that needs to interact with files on disk :::info[Security model] Components have **no filesystem access by default**. Directories must be explicitly mounted via volume configuration in `wash dev` or via `volumeMounts` in a WorkloadDeployment manifest. A component can only access the directories that have been preopened for it; it cannot traverse outside those mount points. For details on deploying with volumes, see [Filesystems and Volumes](../../../../kubernetes-operator/operator-manual/filesystems-and-volumes.mdx). ::: `wasi:filesystem` is provided by the [`wasip2`](https://crates.io/crates/wasip2) crate maintained by the Bytecode Alliance. `wstd` already depends on `wasip2` internally but does not re-export it publicly, so you add `wasip2` as a direct dependency. The examples below use this path. A `wit-bindgen` variant is described at the end. In `Cargo.toml`: ```toml [dependencies] wasip2 = "1.0" wstd = "0.6" ``` ## Step 1: Read preopens `get_directories` returns a `Vec<(Descriptor, String)>`. Each tuple pairs a directory descriptor with its mount path inside the component: In `src/lib.rs`: ```rust use wasip2::filesystem::preopens::get_directories; use wasip2::filesystem::types::Descriptor; fn open_preopen(path: &str) -> Option { get_directories() .into_iter() .find(|(_, mount)| mount == path) .map(|(desc, _)| desc) } ``` In the rest of this guide, `open_preopen("/data")` returns the `Descriptor` for the `/data` mount point. ## Step 2: Read and write files A `Descriptor` is a handle to an open file or directory. Directory descriptors expose: - `read_directory()` returns a `DirectoryEntryStream` - `open_at(...)` opens a file relative to the directory and returns a new `Descriptor` - `create_directory_at(name)` creates a subdirectory File descriptors expose: - `read(length, offset)` returns `Result<(Vec, bool), ErrorCode>` (data and EOF flag) - `write(buffer, offset)` returns `Result` (bytes written) ### Reading a file ```rust use wasip2::filesystem::types::{DescriptorFlags, OpenFlags, PathFlags}; fn read_file(dir: &Descriptor, name: &str) -> Result { let file = dir .open_at( PathFlags::SYMLINK_FOLLOW, name, OpenFlags::empty(), DescriptorFlags::READ, ) .map_err(|e| format!("open: {e:?}"))?; let (bytes, _eof) = file .read(1024 * 1024, 0) .map_err(|e| format!("read: {e:?}"))?; Ok(String::from_utf8_lossy(&bytes).into_owned()) } ``` The first parameter to `open_at`, `PathFlags::SYMLINK_FOLLOW`, controls how symbolic links in the path are resolved. The third and fourth parameters are bitflags that control file creation behavior and access mode. ### Writing a file ```rust fn write_file(dir: &Descriptor, name: &str, data: &[u8]) -> Result { let file = dir .open_at( PathFlags::SYMLINK_FOLLOW, name, OpenFlags::CREATE | OpenFlags::TRUNCATE, DescriptorFlags::WRITE, ) .map_err(|e| format!("open: {e:?}"))?; file.write(data, 0).map_err(|e| format!("write: {e:?}")) } ``` `OpenFlags::CREATE | OpenFlags::TRUNCATE` overwrites an existing file or creates a new one. Use `OpenFlags::CREATE | OpenFlags::EXCLUSIVE` to fail if the file already exists. ### Listing entries ```rust use wasip2::filesystem::types::DescriptorType; struct Entry { name: String, kind: DescriptorType, } fn list_entries(dir: &Descriptor) -> Result, String> { let stream = dir .read_directory() .map_err(|e| format!("read_directory: {e:?}"))?; let mut out = Vec::new(); while let Some(entry) = stream .read_directory_entry() .map_err(|e| format!("read_directory_entry: {e:?}"))? { out.push(Entry { name: entry.name, kind: entry.type_, }); } Ok(out) } ``` `read_directory_entry` returns `Result, ErrorCode>`. `Ok(None)` signals end of stream. ## Step 3: A complete file server component Putting it together with `wstd-axum`: ```rust use axum::{ Json, Router, extract::Path, http::StatusCode, response::IntoResponse, routing::get, }; use serde::Serialize; use wasip2::filesystem::preopens::get_directories; use wasip2::filesystem::types::{ Descriptor, DescriptorFlags, DescriptorType, OpenFlags, PathFlags, }; const PREOPEN: &str = "/data"; #[wstd_axum::http_server] fn main() -> Router { Router::new() .route("/", get(list)) .route("/files/{name}", get(read).put(write)) } #[derive(Serialize)] struct Listing { path: String, entries: Vec, } #[derive(Serialize)] struct EntryInfo { name: String, kind: String, } fn entry_kind(t: DescriptorType) -> &'static str { match t { DescriptorType::Directory => "directory", DescriptorType::RegularFile => "regular-file", DescriptorType::SymbolicLink => "symlink", _ => "other", } } fn open_preopen(path: &str) -> Option { get_directories() .into_iter() .find(|(_, mount)| mount == path) .map(|(desc, _)| desc) } async fn list() -> Result, AppError> { let dir = open_preopen(PREOPEN).ok_or(AppError::no_preopen())?; let stream = dir .read_directory() .map_err(|e| AppError::internal(format!("read_directory: {e:?}")))?; let mut entries = Vec::new(); while let Some(entry) = stream .read_directory_entry() .map_err(|e| AppError::internal(format!("read_directory_entry: {e:?}")))? { entries.push(EntryInfo { name: entry.name, kind: entry_kind(entry.type_).to_string(), }); } Ok(Json(Listing { path: PREOPEN.to_string(), entries, })) } async fn read(Path(name): Path) -> Result, AppError> { let dir = open_preopen(PREOPEN).ok_or(AppError::no_preopen())?; let file = dir .open_at( PathFlags::SYMLINK_FOLLOW, &name, OpenFlags::empty(), DescriptorFlags::READ, ) .map_err(|e| AppError::internal(format!("open_at: {e:?}")))?; let (bytes, _eof) = file .read(1024 * 1024, 0) .map_err(|e| AppError::internal(format!("read: {e:?}")))?; Ok(bytes) } async fn write(Path(name): Path, body: axum::body::Bytes) -> Result { let dir = open_preopen(PREOPEN).ok_or(AppError::no_preopen())?; let file = dir .open_at( PathFlags::SYMLINK_FOLLOW, &name, OpenFlags::CREATE | OpenFlags::TRUNCATE, DescriptorFlags::WRITE, ) .map_err(|e| AppError::internal(format!("open_at: {e:?}")))?; file.write(&body, 0) .map_err(|e| AppError::internal(format!("write: {e:?}")))?; Ok(StatusCode::CREATED) } struct AppError { status: StatusCode, message: String, } impl AppError { fn internal(msg: impl Into) -> Self { Self { status: StatusCode::INTERNAL_SERVER_ERROR, message: msg.into(), } } fn no_preopen() -> Self { Self::internal(format!("no {PREOPEN} preopen mounted")) } } impl IntoResponse for AppError { fn into_response(self) -> axum::response::Response { (self.status, format!("{}\n", self.message)).into_response() } } ``` The `Cargo.toml` for this pattern: ```toml [dependencies] axum = { version = "0.8", default-features = false, features = ["json", "matched-path"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" wasip2 = "1.0" wstd = "0.6" wstd-axum = "0.6" ``` ## Configure volumes for `wash dev` To mount a host directory into the component during development, add a `volumes` entry to `.wash/config.yaml`: ```yaml build: command: cargo build --target wasm32-wasip2 --release component_path: target/wasm32-wasip2/release/.wasm dev: volumes: - host_path: ./testdata guest_path: /data ``` `host_path` is a directory on your local machine, `guest_path` is where the component sees it. The component's `get_directories()` call will include an entry whose mount string is `/data` and whose descriptor is rooted at `testdata/` in your project. Create some test data: ```shell mkdir -p testdata echo "Hello from the filesystem!" > testdata/hello.txt ``` ## Build and verify ```shell wash dev ``` ```shell # List files in the /data directory curl http://localhost:8000/ # Read a file curl http://localhost:8000/files/hello.txt # Write a new file curl -X PUT --data-binary 'Hello, filesystem!' http://localhost:8000/files/greeting.txt # Read the written file back curl http://localhost:8000/files/greeting.txt ``` Expected output: ```text {"path":"/data","entries":[{"name":"hello.txt","kind":"regular-file"}]} Hello from the filesystem! Hello, filesystem! ``` Written files persist on the host at `testdata/greeting.txt`. ## Production deployment In a production deployment, filesystem data is provided through Kubernetes Volumes. The volume is first made available to the wasmCloud host, then mounted into the component via a WorkloadDeployment manifest. For a full walkthrough of deploying with volumes, including host deployments, WorkloadDeployment manifests, and `volumeMounts` configuration, see [Filesystems and Volumes](../../../../kubernetes-operator/operator-manual/filesystems-and-volumes.mdx). Important notes: - Volumes are defined at the host level and mounted into components via `localResources.volumeMounts`. - The `mountPath` in the manifest corresponds to the guest path the component sees in `get_directories()`. - You do not need to declare `wasi:filesystem` under `hostInterfaces`. ## Alternative: the `wit-bindgen` path If you would rather declare the imports in your WIT world and generate bindings with `wit-bindgen`: In `wit/world.wit`: ```wit package wasmcloud:my-component; world my-component { import wasi:filesystem/types@0.2.2; import wasi:filesystem/preopens@0.2.2; export wasi:http/incoming-handler@0.2.2; } ``` In `src/lib.rs`: ```rust mod bindings { wit_bindgen::generate!({ generate_all }); } use bindings::wasi::filesystem::preopens::get_directories; use bindings::wasi::filesystem::types::{Descriptor, DescriptorFlags, OpenFlags, PathFlags}; ``` The function and type names are identical between paths. ## Summary: checklist for adding filesystem access 1. **Decide on the binding path**. The simplest option is to add `wasip2 = "1.0"` to `Cargo.toml` and read filesystem types from `wasip2::filesystem::*`. Otherwise, declare the imports in `wit/world.wit` and use `wit_bindgen::generate!` (any 0.4x release works). 2. **Use `get_directories()`** to discover preopened mounts. Look up the descriptor whose mount path matches the directory you need. 3. **Open files with `open_at`** using the appropriate `OpenFlags` and `DescriptorFlags` bitflags. 4. **Read with `file.read(length, offset)`** and write with `file.write(buffer, offset)`. Both return `Result` types. 5. **Configure volumes** in `.wash/config.yaml` for local development or in a WorkloadDeployment manifest for production. ## API reference: `wasi:filesystem` operations used | Operation | Signature | Description | |-----------|-----------|-------------| | `get_directories` | `fn() -> Vec<(Descriptor, String)>` | Returns all preopened directories with their mount paths | | `dir.read_directory` | `fn() -> Result` | Open a stream over the directory's entries | | `stream.read_directory_entry` | `fn() -> Result, ErrorCode>` | Read the next entry; `None` at end | | `dir.open_at(path_flags, name, open_flags, descriptor_flags)` | `fn(...) -> Result` | Open a file or directory relative to a descriptor | | `file.read(length, offset)` | `fn(u64, u64) -> Result<(Vec, bool), ErrorCode>` | Read bytes from a file; bool is EOF | | `file.write(buffer, offset)` | `fn(&[u8], u64) -> Result` | Write bytes to a file at offset; returns bytes written | | `dir.create_directory_at(name)` | `fn(&str) -> Result<(), ErrorCode>` | Create a subdirectory | | `dir.stat_at(path_flags, name)` | `fn(...) -> Result` | Get file attributes (type, size, timestamps) | | `dir.unlink_file_at(name)` | `fn(&str) -> Result<(), ErrorCode>` | Delete a file | | `dir.remove_directory_at(name)` | `fn(&str) -> Result<(), ErrorCode>` | Delete an empty directory | ## Further reading - [Rust Language Guide](./index.mdx) — toolchain overview, HTTP patterns, async runtime guidance, and crate compatibility - [Filesystems and Volumes](../../../../kubernetes-operator/operator-manual/filesystems-and-volumes.mdx) — deploying components with volume mounts in Kubernetes - [Key-Value Storage](./key-value-storage.mdx) — persistent key-value storage for structured data - [Configuration](./configuration.mdx) — read configuration values from ConfigMaps, Secrets, and inline environment variables - [Language Support overview](../index.mdx) — summary of all supported languages and toolchains --- ## Rust Language Guide Rust has first-class support for building WebAssembly components. The `wasm32-wasip2` compiler target ships with the standard Rust toolchain, and the [`wstd`](https://crates.io/crates/wstd) crate provides an async standard library purpose-built for WASI 0.2 components. This guide covers the toolchain, the `wstd` library, adding custom WASI interfaces, and practical guidance for building Rust components on wasmCloud. If you're looking for a quick walkthrough of creating, building, and running a Rust component, see the [Developer Guide](../../index.mdx?lang=rust). ## Toolchain overview ### Build pipeline ![Rust build pipeline](../../../images/build-pipeline-rust.svg) Unlike TypeScript or Go, Rust compiles directly to a Wasm component in a single step. No post-processing, bundling, or external tools are required. ### `wasm32-wasip2` target The `wasm32-wasip2` target has been a [Tier 2 target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-wasip2.html) since Rust 1.82. It compiles Rust code to a WebAssembly component targeting WASI 0.2 (Preview 2), which includes the Component Model. Install the target: ```shell rustup target add wasm32-wasip2 ``` Build a component: ```shell cargo build --target wasm32-wasip2 --release ``` The compiled component is written to `target/wasm32-wasip2/release/.wasm`. ### `wstd` [`wstd`](https://github.com/bytecodealliance/wstd) is a minimal async Rust standard library for Wasm components, maintained by the Bytecode Alliance. It provides high-level APIs for HTTP, networking, I/O, timers, and randomness — all backed by WASI 0.2 interfaces. `wstd` handles WASI bindings internally through its dependency on the [`wasip2`](https://crates.io/crates/wasip2) crate. For standard HTTP components, `wstd` is the only dependency you need. :::note[`wstd` is a transitional library] `wstd` exists because mainstream async runtimes like `tokio` and `async-std` do not yet provide a complete WASI 0.2 story for HTTP-export components. As of `tokio` 1.51, `tokio` supports `wasm32-wasip2` for raw socket networking via `wasi:sockets`, but it does not yet drive the `wasi:http/incoming-handler` export that wasmCloud HTTP components rely on. Once mainstream runtimes cover that path, the `wstd` project recommends migrating to them. The API is designed to make that migration straightforward. ::: Available modules: | Module | Description | |---|---| | `wstd::http` | HTTP request/response types, `#[http_server]` macro | | `wstd::io` | Async I/O abstractions | | `wstd::net` | Async networking (`TcpListener`, `TcpStream`) | | `wstd::time` | Async timers and durations | | `wstd::rand` | Random number generation (backed by `wasi:random`) | | `wstd::task` | Async task types | | `wstd::runtime` | Async event loop (`block_on()` executor) | ### `wit-bindgen` [`wit-bindgen`](https://github.com/bytecodealliance/wit-bindgen) is a lower-level tool that generates Rust code from WIT interface definitions. You don't need `wit-bindgen` for standard HTTP components — `wstd` handles those bindings internally. However, `wit-bindgen` is essential when you need to use WASI interfaces that aren't included in `wstd`, such as `wasi:keyvalue` or `wasi:config`. See [Adding custom WASI interfaces](#adding-custom-wasi-interfaces) for details on using `wstd` and `wit-bindgen` together. ## Async runtime: when to use `wstd` vs `tokio` Both `wstd` and `tokio` 1.51+ compile against `wasm32-wasip2`. Choosing between them comes down to what your component exports, since the two runtimes solve different parts of the problem. | Use case | Runtime | |---|---| | Component exports `wasi:http/incoming-handler` (HTTP service) | `wstd` (or [`wstd-axum`](https://docs.rs/wstd-axum) for routing) | | Component is a long-running TCP server or client over `wasi:sockets` | `tokio` 1.51+ with `RUSTFLAGS="--cfg tokio_unstable"` | | Component uses tokio sync primitives (`Mutex`, `mpsc`, `oneshot`) as in-process utilities | `wstd` for the runtime, with `tokio = { version = "1.51", features = ["sync"] }` as a passive dependency | ### Why HTTP components stay on `wstd` Tokio's wasip2 work covers `wasi:sockets` networking. It does not provide a bridge to `wasi:http/incoming-handler`. The `wstd-axum` crate, which adapts an `axum::Router` onto the wasi-http export, explicitly disables `tokio` and `hyper` integrations: a Wasm component receives HTTP through host-imported interfaces, not raw sockets, so the standard tokio-and-hyper path doesn't apply. Pulling tokio's runtime into an HTTP-export component also expands the WASI capability surface the component demands. The diff below shows the imports of two functionally equivalent components that build cleanly. The first uses `wstd` only; the second initializes a `tokio` current-thread runtime inside an `incoming-handler` implementation. A `wstd`-only component declares only the imports it needs: ```text import wasi:io/poll@0.2.9 import wasi:io/streams@0.2.9 import wasi:io/error@0.2.9 import wasi:http/types@0.2.9 import wasi:cli/{stderr, environment, exit}@0.2.9 import wasi:clocks/monotonic-clock@0.2.9 import wasi:random/insecure-seed@0.2.9 export wasi:http/incoming-handler@0.2.9 ``` A component that initializes a `tokio` runtime inside its `incoming-handler` ends up importing a much broader surface, including sockets and filesystem interfaces it never uses: ```text import wasi:http/types@0.2.4 import wasi:io/{poll, streams, error}@0.2.6 import wasi:sockets/{network, tcp}@0.2.6 import wasi:filesystem/{types, preopens}@0.2.6 import wasi:cli/{environment, exit, stdin, stdout, stderr}@0.2.6 import wasi:clocks/{monotonic-clock, wall-clock}@0.2.6 import wasi:random/insecure-seed@0.2.6 export wasi:http/incoming-handler@0.2.2 ``` The second component imports `wasi:sockets/*` and `wasi:filesystem/*` it never uses, and the version skew between `wasi:http/types@0.2.4` and the `0.2.6` cluster makes it fragile to link against most hosts. Stick to `wstd` (or `wstd-axum`) for any component that exports HTTP. ### When tokio is the right answer If your component opens raw TCP listeners or connects out over TCP, tokio's wasip2 path is the supported runtime. The wasmCloud [`service-tcp`](https://github.com/wasmCloud/wasmCloud/tree/main/templates/service-tcp) template is the canonical example. Cargo features that work on `wasm32-wasip2`: - `sync`, `macros`, `rt`, `time`, `io-util`, `net` Cargo features that do not work on `wasm32-wasip2` today: - `fs` (no `tokio::fs`) - DNS lookups (`tokio::net::lookup_host`) - Multi-threading (`rt-multi-thread`) - Signal handling Use `#[tokio::main(flavor = "current_thread")]`. wasip2 is single-threaded. ### Tokio sync primitives inside a `wstd` component If a library you depend on wants `tokio::sync::Mutex` or you want `mpsc` for in-component fan-out, you can pull tokio in as a passive utility crate without contaminating the WASI import surface. In `Cargo.toml`: ```toml [dependencies] wstd = "0.6" tokio = { version = "1.51", features = ["sync"] } ``` Verified component imports match a `wstd`-only build, byte for byte. Avoid widening to `features = ["full"]` or enabling `rt`/`net`/`fs` in this configuration: that pulls mio into the dep tree and the surface expands as in the second example above. ## Handling HTTP requests The `#[wstd::http_server]` macro is the standard way to build an HTTP component in Rust. It wires an async function to the `wasi:http/incoming-handler` export: ```rust use wstd::http::{Body, Request, Response, StatusCode}; #[wstd::http_server] async fn main(req: Request) -> Result, wstd::http::Error> { match req.uri().path() { "/" => home().await, _ => not_found().await, } } async fn home() -> Result, wstd::http::Error> { Ok(Response::new(Body::from("Hello from wasmCloud!\n"))) } async fn not_found() -> Result, wstd::http::Error> { Ok(Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::from("Not found\n"))?) } ``` Key points: - The function must be `async` and accept a `Request`, returning `Result, wstd::http::Error>`. - Route matching is manual (pattern match on `req.uri().path()`). For complex routing, you can use helper functions or a lightweight router crate. - `Response::new()` creates a 200 OK response. Use `Response::builder()` for custom status codes or headers. - Handler functions can be `async`, enabling outgoing HTTP calls, key-value operations, or other async work within a request. ### Routing with `wstd-axum` For anything more involved than a single handler, [`wstd-axum`](https://docs.rs/wstd-axum) lets you write the component using the [`axum`](https://docs.rs/axum) framework. `wstd-axum` adapts an `axum::Router` onto `wasi:http/incoming-handler`: ```rust use axum::{Router, routing::get}; #[wstd_axum::http_server] fn main() -> Router { Router::new() .route("/", get(|| async { "Hello from wasmCloud!\n" })) .route("/api/greet/{name}", get(|axum::extract::Path(name): axum::extract::Path| async move { format!("Hello, {name}!\n") })) } ``` `Cargo.toml` must opt out of axum's default features and only enable feature flags that don't require `tokio` or `hyper`: ```toml [dependencies] axum = { version = "0.8", default-features = false, features = ["json", "query", "matched-path"] } wstd = "0.6" wstd-axum = "0.6" ``` The wasmCloud [`http-handler`](https://github.com/wasmCloud/wasmCloud/tree/main/templates/http-handler) template demonstrates this pattern end to end. ### Outgoing HTTP requests Components can make outgoing HTTP requests using `wstd::http::Client`: ```rust use wstd::http::{Body, Client, Request, Response}; #[wstd::http_server] async fn main(req: Request) -> Result, wstd::http::Error> { let client = Client::new(); let upstream_resp = client.send( Request::get("https://api.example.com/data").body(Body::empty())? ).await?; Ok(Response::new(upstream_resp.into_body())) } ``` To make outgoing HTTP requests, your WIT world must import `wasi:http/outgoing-handler`: ```wit world hello { import wasi:http/outgoing-handler@0.2.2; export wasi:http/incoming-handler@0.2.2; } ``` ## Adding custom WASI interfaces `wstd` (through its `wasip2` dependency) provides bindings for standard WASI interfaces: `wasi:http`, `wasi:io`, `wasi:clocks`, `wasi:random`, and others. For draft or experimental interfaces like `wasi:keyvalue`, `wasi:config`, or custom interfaces, you need `wit-bindgen` to generate bindings for the additional interfaces. ### How `wstd` and `wit-bindgen` coexist - **`wstd`** handles the HTTP export via the `#[wstd::http_server]` macro - **`wasip2`** (bundled with `wstd`) provides bindings for standard WASI imports - **`wit-bindgen`** generates bindings for your custom imports only Many draft interfaces like `wasi:keyvalue` are self-contained — they don't share types with `wasi:io` or other standard interfaces. This means there are no type conflicts between what `wasip2` provides and what `wit-bindgen` generates. ### Example: Adding `wasi:keyvalue` **1. Add `wit-bindgen` to `Cargo.toml`:** ```toml [package] name = "my-component" edition = "2024" version = "0.1.0" [lib] crate-type = ["cdylib"] [dependencies] wstd = "0.6" wit-bindgen = "0.46.0" ``` **2. Declare the imports in `wit/world.wit`:** ```wit package myorg:mycomponent; world my-world { import wasi:keyvalue/store@0.2.0-draft; import wasi:keyvalue/atomics@0.2.0-draft; export wasi:http/incoming-handler@0.2.2; } ``` **3. Fetch the WIT dependencies:** ```shell wkg wit fetch ``` This populates `wit/deps/` with the interface definitions for `wasi:keyvalue`, `wasi:http`, and their transitive dependencies. **4. Generate bindings and use the interface in `src/lib.rs`:** ```rust use wstd::http::{Body, Request, Response}; // Generate bindings for all interfaces in the WIT world. // wstd handles the HTTP export; wit-bindgen generates the keyvalue imports. wit_bindgen::generate!({ world: "my-world", path: "wit", generate_all, }); use wasi::keyvalue::{atomics, store}; #[wstd::http_server] async fn main(req: Request) -> Result, wstd::http::Error> { let bucket = store::open("") .map_err(|e| wstd::http::Error::msg(format!("keyvalue error: {:?}", e)))?; let count = atomics::increment(&bucket, "visitor-count", 1) .map_err(|e| wstd::http::Error::msg(format!("increment error: {:?}", e)))?; Ok(Response::new(Body::from(format!("Visit count: {count}\n")))) } ``` The `generate_all` option tells `wit-bindgen` to generate bindings for all imports in the world. Since `wasi:keyvalue` isn't in `wasip2`, `wit-bindgen` generates it. The HTTP export is still handled by `wstd`, not `wit-bindgen`. Generated bindings are available under `wasi::keyvalue::*`, matching the WIT package namespace. ### Version alignment The `wasi:http/incoming-handler` version in your WIT world **must match** the version provided by your `wasip2` dependency. Check your resolved version with: ```shell cargo tree -p wasip2 ``` | `wasip2` version | WASI HTTP version | |---|---| | 1.0.0, 1.0.1 | 0.2.4 | | 1.0.2+ | 0.2.9 | If versions mismatch, you'll get a linker error: `failed to find export of interface wasi:http/incoming-handler@... function handle`. To fix this, update the version in `wit/world.wit` and re-fetch dependencies: ```shell rm -rf wit/deps wkg.lock && wkg wit fetch ``` ### Handling type conflicts If a custom interface imports types from `wasi:io` or other standard interfaces (creating overlap with `wasip2`), use `wit-bindgen`'s `with` option to point to `wasip2`'s types: ```rust wit_bindgen::generate!({ world: "my-world", path: "wit", with: { "wasi:io/streams@0.2.9": wasip2::wasi::io::streams, "wasi:io/poll@0.2.9": wasip2::wasi::io::poll, }, generate_all, }); ``` Self-contained interfaces like `wasi:keyvalue@0.2.0-draft` don't share types with standard interfaces, so this isn't needed in most cases. ## Project structure A typical Rust component project: ``` my-component/ ├── .wash/ │ └── config.yaml # wash project configuration ├── src/ │ └── lib.rs # Application code ├── wit/ │ ├── world.wit # WIT world definition │ └── deps/ # Fetched WIT dependencies (gitignored) ├── Cargo.toml ├── Cargo.lock └── wkg.lock # WIT dependency lock file ``` ### `Cargo.toml` ```toml [package] name = "my-component" version = "0.1.0" edition = "2024" [lib] crate-type = ["cdylib"] [dependencies] wstd = "0.6" ``` - **`crate-type = ["cdylib"]`** is required — it tells Cargo to produce a C-compatible dynamic library, which is the format used by the `wasm32-wasip2` target to emit a `.wasm` component. - For a standard HTTP component, `wstd` is the only dependency needed. ### WIT world In `wit/world.wit`: ```wit package wasmcloud:hello; world hello { export wasi:http/incoming-handler@0.2.2; } ``` The WIT world declares your component's imports and exports. A component exporting `wasi:http/incoming-handler` can handle incoming HTTP requests. Adding imports (like `wasi:http/outgoing-handler` or `wasi:keyvalue/store`) declares the capabilities your component requires from the runtime. :::info[WASI HTTP version] The `wasi:http/incoming-handler` version in your WIT world must match the version provided by your `wasip2` dependency. The version shown here (`@0.2.2`) matches the wasmCloud project template. See [Version alignment](#version-alignment) for details on matching versions. ::: ### WIT dependency management with `wkg` `wash` bundles the [`wkg`](https://github.com/bytecodealliance/wasm-pkg-tools) WebAssembly package manager, so you don't need to install a separate tool. You can fetch WIT dependencies directly with `wash`: ```shell wash wit fetch ``` This reads your WIT world, populates `wit/deps/` with downloaded interface definitions, and creates a `wkg.lock` file. You should: - Add `wit/deps/` to `.gitignore` — these are fetched dependencies - Commit `wkg.lock` — this ensures reproducible builds When you run `wash build`, it calls `wash wit fetch` automatically, so you typically don't need to run it manually. If you have `wkg` installed separately, it works the same way — `wash` and `wkg` share the same underlying crate and produce the same `wkg.lock` file. The following namespaces are resolved automatically without configuration: - `wasi` — standard [WASI](https://wasi.dev/) interfaces - `wasmcloud` — wasmCloud project interfaces - `wrpc` — [wRPC](https://github.com/wrpc) interfaces - `ba` — [Bytecode Alliance](https://bytecodealliance.org/) interfaces ### `.wash/config.yaml` Rust projects use a [project configuration file](../../../config.mdx) at `.wash/config.yaml` that tells `wash` how to build the component: ```yaml build: command: cargo build --target wasm32-wasip2 --release component_path: target/wasm32-wasip2/release/hello_world.wasm ``` The `command` field specifies the build command and `component_path` points to the compiled `.wasm` binary. For a full reference of configuration options, see the [Configuration](../../../config.mdx) page. ### Optimizing binary size Wasm component binaries can be reduced with standard Cargo release profile settings. Add the following to `Cargo.toml`: ```toml [profile.release] opt-level = "z" # Optimize for size lto = true # Link-time optimization codegen-units = 1 # Better optimization (slower compile) strip = true # Remove debug symbols ``` ## Crate compatibility ### What works Any Rust crate that compiles to `wasm32-wasip2` works inside a Wasm component. This includes: - **Pure computation** — `serde`, `serde_json`, `regex`, `uuid`, `base64`, `chrono` (without `std` time), etc. - **Error handling** — `anyhow`, `thiserror` - **Async** — `wstd`'s own async runtime (not `tokio` or `async-std` — see below) - **HTTP types** — `wstd::http` provides `Request`, `Response`, `Body`, `StatusCode`, etc. ### What does not work - **`tokio` for `wasi:http/incoming-handler` components** — `tokio` 1.51+ supports `wasm32-wasip2` for raw socket networking, but it does not bridge to `wasi:http/incoming-handler`. Use `wstd` (or `wstd-axum`) for HTTP-export components. See [Async runtime: when to use `wstd` vs `tokio`](#async-runtime-when-to-use-wstd-vs-tokio). - **`async-std`, `smol`** — these mainstream async runtimes do not yet support WASI 0.2. - **Crates that use `std::net` or `std::fs` directly** — these require OS-level syscalls not available in the Wasm sandbox. Use `wstd::net` and WASI filesystem interfaces instead. - **Crates with native C dependencies** — anything that links to a C library via `build.rs` (e.g., `openssl-sys`, `libsqlite3-sys`) will not compile for `wasm32-wasip2`. - **Crates using threads** — `std::thread` is not available in WASI 0.2. Async concurrency through `wstd` is the alternative. ### Practical guidance - Check a crate's compatibility by attempting `cargo check --target wasm32-wasip2` before committing to it - Prefer crates with `no_std` support or explicit Wasm compatibility - For JSON handling, `wstd` has optional `serde` and `serde_json` feature flags: `wstd = { version = "0.6", features = ["serde", "serde_json"] }` ## Building and running ### Create a new project Use `wash new` to scaffold a Rust component project: ```shell wash new https://github.com/wasmCloud/wasmCloud.git --name hello --subfolder templates/http-hello-world ``` ### Development loop Start a development loop that builds, runs, and watches for changes: ```shell wash dev ``` Send a request to test: ```shell curl localhost:8000 ``` ### Build a component Compile your project to a `.wasm` binary using standard Cargo: ```shell cargo build --target wasm32-wasip2 --release ``` The compiled component is written to `target/wasm32-wasip2/release/.wasm`. If your WIT world references external interfaces (e.g. `wasi:keyvalue`), fetch the definitions first: ```shell wkg wit fetch ``` :::tip[`wash build` as an alternative] `wash build` wraps both steps — it runs `wkg wit fetch` and then `cargo build` using the command from your `.wash/config.yaml`. Either approach produces the same output. This guide shows `cargo` directly because it's the standard Rust build command and works without any wasmCloud-specific configuration. ::: ### Inspect a component Verify your component's imports and exports with `wasm-tools`: ```shell wasm-tools component wit target/wasm32-wasip2/release/my_component.wasm ``` This prints the resolved WIT world, showing exactly which interfaces the component imports and exports. ## Working with WASI interfaces These guides walk through adding WASI interfaces to a Rust component: - [Key-Value Storage](./key-value-storage.mdx) — replace an in-memory data store with persistent key-value storage - [Messaging](./messaging.mdx) — implement communication between components over a messaging interface - [Filesystem](./filesystem.mdx) — read and write files from preopened directories - [Configuration](./configuration.mdx) — read configuration values injected from Kubernetes ConfigMaps, Secrets, and inline environment variables The patterns in these guides (declaring WIT imports, generating bindings via `wit-bindgen` alongside `wstd`, handling serialization) apply to any WASI interface you add to a Rust component. ## Further reading - [Developer Guide](../../index.mdx?lang=rust) — quickstart tutorial for creating, building, and running a Rust component - [wasmCloud Rust templates](https://github.com/wasmCloud/wasmCloud/tree/main/templates) — project templates for the most common starting points - [wasmCloud Rust examples](https://github.com/wasmCloud/wasmCloud/tree/main/examples) — example projects in the `wash` repository - [`wstd` documentation](https://docs.rs/wstd) — API reference for the async Wasm standard library - [`wstd-axum` documentation](https://docs.rs/wstd-axum) — `axum` adapter for `wasi:http/incoming-handler` - [`wstd` repository](https://github.com/bytecodealliance/wstd) — source code and examples - [`wit-bindgen` documentation](https://docs.rs/wit-bindgen) — API reference for the WIT bindings generator - [`wasm32-wasip2` platform support](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-wasip2.html) — Rust target documentation - [Component Model: Rust](https://component-model.bytecodealliance.org/language-support/rust.html) — Component Model documentation for Rust - [Language Support overview](../index.mdx) — summary of all supported languages and toolchains --- ## Key-Value Storage You can add key-value capabilities to a Rust component with the [`wasi:keyvalue`](https://github.com/WebAssembly/wasi-keyvalue/) interface. This guide walks through adding `wasi:keyvalue` to a Rust component using `wit-bindgen` alongside `wstd`. You can use it as a model for adding any WASI interface that isn't bundled into `wstd` directly. ## Overview `wasi:keyvalue` is a draft WASI interface that gives a component access to a bucket-based key-value store: `open` a bucket, then `get`, `set`, `delete`, `exists`, and `list-keys` against it. The underlying storage is provided by the host runtime, so the same component code runs against any supported backend (in-memory, filesystem, NATS, Redis, and others) without modification. :::info[] By default, a wasmCloud deployment uses NATS JetStream for storage via the built-in [WASI Key-Value NATS plugin](https://github.com/wasmCloud/wasmCloud/blob/main/crates/wash-runtime/src/plugin/wasi_keyvalue/nats.rs). You can use [host plugins](../../../../runtime/index.mdx) to back `wasi:keyvalue` with a different storage solution. ::: `wasi:keyvalue` is a draft interface and is not bundled into `wstd`. To use it, your component generates Rust bindings with `wit-bindgen` while still relying on `wstd` to drive the HTTP export. The two coexist because draft interfaces like `wasi:keyvalue` are self-contained: they don't share types with `wasi:io` or other standard interfaces, so there are no type conflicts between what `wasip2` (the crate `wstd` is built on) provides and what `wit-bindgen` generates. :::tip[Complete example] A full working example is available as a template: ```shell wash new https://github.com/wasmCloud/wasmCloud.git \ --name http-kv-handler \ --subfolder templates/http-kv-handler ``` ::: ## Step 1: Declare the WIT import Every capability your component uses must be declared in its WIT world. The `wstd::http_server` macro provides the `wasi:http/incoming-handler` export, so the world only needs to declare the `wasi:keyvalue/store` import. In `wit/world.wit`: ```wit package wasmcloud:http-kv-handler; // wasi:http/incoming-handler is exported via wstd's #[http_server] proc macro. world http-kv-handler { import wasi:keyvalue/store@0.2.0-draft; } ``` If you also need atomic operations like `increment`, add `wasi:keyvalue/atomics@0.2.0-draft` as a second import. When you run `wash build`, the build pipeline calls `wash wit fetch`, which resolves the WIT package references and downloads their definitions into `wit/deps/`. If you have not run `wash wit fetch` before, you may need to run it manually the first time before `cargo build` works. ## Step 2: Add the dependencies In `Cargo.toml`: ```toml [package] name = "http-kv-handler" edition = "2024" version = "0.1.0" [lib] crate-type = ["cdylib"] [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" wit-bindgen = "0.46" wstd = "0.6" ``` Any `wit-bindgen` 0.4x release works; `0.46` is what current `wstd` releases pair cleanly with. ## Step 3: Generate bindings and use the interface In `src/lib.rs`, generate bindings for the `wasi:keyvalue/store` import and call them from inside a `#[wstd::http_server]` handler: ```rust wit_bindgen::generate!({ world: "http-kv-handler", path: "wit", generate_all, }); use serde::Deserialize; use wasi::keyvalue::store::open; use wstd::http::{Body, Method, Request, Response, StatusCode}; const BACKEND: &str = "in_memory"; #[derive(Deserialize)] struct KvPayload { key: String, value: String, } #[wstd::http_server] async fn main(mut req: Request) -> Result, wstd::http::Error> { match *req.method() { Method::POST => post(&mut req).await, Method::GET => get(&req).await, _ => Response::builder() .status(StatusCode::METHOD_NOT_ALLOWED) .body(Body::from("Method Not Allowed\n")) .map_err(Into::into), } } async fn post(req: &mut Request) -> Result, wstd::http::Error> { let body_bytes = req.body_mut().contents().await?.to_vec(); let payload: KvPayload = serde_json::from_slice(&body_bytes) .map_err(|e| wstd::http::Error::msg(format!("invalid JSON: {e}")))?; let bucket = open(BACKEND) .map_err(|e| wstd::http::Error::msg(format!("open bucket: {e:?}")))?; bucket .set(&payload.key, payload.value.as_bytes()) .map_err(|e| wstd::http::Error::msg(format!("set: {e:?}")))?; Ok(Response::new(Body::from(format!("[{BACKEND}] Stored '{}'\n", payload.key)))) } async fn get(req: &Request) -> Result, wstd::http::Error> { let path_and_query = req.uri().path_and_query().map(|p| p.as_str()).unwrap_or(""); let Some(key) = parse_query_param(path_and_query, "key") else { return Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::from("Missing required query parameter: key\n")) .map_err(Into::into); }; let bucket = open(BACKEND) .map_err(|e| wstd::http::Error::msg(format!("open bucket: {e:?}")))?; match bucket .get(&key) .map_err(|e| wstd::http::Error::msg(format!("get: {e:?}")))? { Some(bytes) => Ok(Response::new(Body::from(format!( "[{BACKEND}] {}\n", String::from_utf8_lossy(&bytes) )))), None => Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::from(format!("[{BACKEND}] Key '{key}' not found\n"))) .map_err(Into::into), } } fn parse_query_param(path_and_query: &str, param: &str) -> Option { let query = path_and_query.splitn(2, '?').nth(1)?; for pair in query.split('&') { let mut parts = pair.splitn(2, '='); if parts.next()? == param { return parts.next().map(|v| v.to_string()); } } None } ``` The `generate_all` option tells `wit-bindgen` to generate bindings for every interface in the world. The `wasi:keyvalue/store` interface lands at `wasi::keyvalue::store`. The HTTP export is still owned by `wstd`'s `#[http_server]` macro, not by `wit-bindgen`. ### Opening a bucket and choosing a backend Every keyvalue operation goes through a `Bucket` resource returned by `store::open`. The string you pass to `open` is a logical bucket name. The wasmCloud runtime maps it to a physical store based on `.wash/config.yaml` (or the host's `wasi:keyvalue` plugin configuration in production), so the same component code runs against any supported backend without modification. The shipped [`http-kv-handler`](https://github.com/wasmCloud/wasmCloud/tree/main/templates/http-kv-handler) template uses the bucket name as a logical "backend" identifier. Set `BACKEND` in `src/lib.rs` to one of the values below and uncomment the matching block in `.wash/config.yaml`: | `BACKEND` | Required config key | Example value | |---------------|--------------------------------|----------------------------------| | `in_memory` | *(none, default in `wash dev`)* | — | | `filesystem` | `wasi_keyvalue_path` | `/tmp/keyvalue-store` | | `nats` | `wasi_keyvalue_nats_url` | `nats://127.0.0.1:4222` | | `redis` | `wasi_keyvalue_redis_url` | `redis://127.0.0.1:6379` | The component code is unchanged across backends. Treat the bucket as a per-request resource and open it inside each handler rather than caching it at module scope. ### Serialization: values are `list` `wasi:keyvalue/store` uses `list` (`Vec` in Rust) for values. To store structured data, serialize before `set` and deserialize after `get`: ```rust let item_bytes = serde_json::to_vec(&item).map_err(|e| format!("serialize: {e}"))?; bucket.set(key, &item_bytes).map_err(|e| format!("set: {e:?}"))?; if let Some(bytes) = bucket.get(key).map_err(|e| format!("get: {e:?}"))? { let item: Item = serde_json::from_slice(&bytes).map_err(|e| format!("deserialize: {e}"))?; } ``` ### Synchronous API All `wasi:keyvalue/store` calls (`open`, `get`, `set`, `delete`, `exists`, `list-keys`) are synchronous in WIT. The `wit-bindgen`-generated bindings expose them as plain functions, not `async fn`. They work just as well from a synchronous handler if your component does not need `async`. ## Alternative: `wit-bindgen`-only (no `wstd`) If you want full control over the HTTP export and prefer not to bring in `wstd`, you can let `wit-bindgen` generate the `wasi:http/incoming-handler` export too. Declare both the keyvalue import and the HTTP export in your WIT world: ```wit package wasmcloud:http-kv-handler; world http-kv-handler { import wasi:keyvalue/store@0.2.0-draft; export wasi:http/incoming-handler@0.2.2; } ``` Then implement the `Guest` trait directly: ```rust mod bindings { wit_bindgen::generate!({ generate_all, }); } use bindings::{ exports::wasi::http::incoming_handler::Guest, wasi::{ http::types::{Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam}, keyvalue::store::open, }, }; struct Component; impl Guest for Component { fn handle(request: IncomingRequest, response_out: ResponseOutparam) { // ... assemble the response by hand } } bindings::export!(Component with_types_in bindings); ``` This gives you the lowest-level WIT API at the cost of more boilerplate. See [Adding custom WASI interfaces](./index.mdx#adding-custom-wasi-interfaces) in the language guide for the full pattern, including handling type conflicts when imports overlap with `wasi:io`. ## Build and verify ```shell wash dev ``` `wash dev` builds the component, starts an HTTP server on `http://localhost:8000`, and provisions the configured `wasi:keyvalue` backend automatically. ```shell # Store a value curl -X POST http://localhost:8000 \ -H "Content-Type: application/json" \ -d '{"key":"mykey","value":"myvalue"}' # Retrieve it curl "http://localhost:8000?key=mykey" # Missing key returns 404 curl "http://localhost:8000?key=missing" ``` To verify persistence with a non-default backend, pick a backend in `.wash/config.yaml` (filesystem, NATS, Redis), restart the component, and confirm previously stored keys are still readable. ## Summary: checklist for adding any WIT interface 1. **Add `import` lines to `wit/world.wit`** for the interfaces you need. 2. **Add `wit-bindgen` to `Cargo.toml`** if it is not already a dependency. Any 0.4x release works. 3. **Generate bindings** with `wit_bindgen::generate!({ generate_all })`. For interfaces that share types with `wasi:io` (like `wasi:blobstore`), use the `with` option to point shared types at `wasip2`'s definitions to avoid duplicates. 4. **Use the generated functions** at `wasi::::::*` (or `bindings::wasi::::::*` if you wrap the macro in a module). 5. **Handle serialization.** Most WIT interfaces use `list` (`Vec`) for binary data. Use `serde_json` for structured data. 6. **Stable WASI 0.2 interfaces** like `wasi:cli/environment` and `wasi:filesystem` are available via the `wasip2` crate without `wit-bindgen`. Draft interfaces like `wasi:keyvalue` need `wit-bindgen`. See [Adding custom WASI interfaces](./index.mdx#adding-custom-wasi-interfaces) for the distinction. ## API reference: `wasi:keyvalue/store` operations used | Operation | Signature | Description | |-----------|-----------|-------------| | `open(name)` | `fn(&str) -> Result` | Open a named bucket | | `bucket.get(key)` | `fn(&str) -> Result>, Error>` | Get a value by key, `None` if missing | | `bucket.set(key, value)` | `fn(&str, &[u8]) -> Result<(), Error>` | Set a key to a value | | `bucket.delete(key)` | `fn(&str) -> Result<(), Error>` | Delete a key | | `bucket.exists(key)` | `fn(&str) -> Result` | Check if a key exists | | `bucket.list_keys(cursor)` | `fn(Option) -> Result` | List keys with pagination cursor | ## Further reading - [Rust Language Guide](./index.mdx) — toolchain overview, HTTP patterns, framework integration, and library compatibility - [Configuration](./configuration.mdx) — read configuration values from ConfigMaps, Secrets, and inline environment variables - [Filesystem](./filesystem.mdx) — read and write files from preopened directories - [Language Support overview](../index.mdx) — summary of all supported languages and toolchains --- ## Messaging You can add messaging capabilities to a Rust component with the [`wasmcloud:messaging`](https://github.com/wasmCloud/wasmCloud/tree/main/wit/messaging.wit) interface. This guide walks through the wasmCloud [`http-api-with-distributed-workloads`](https://github.com/wasmCloud/wasmCloud/tree/main/templates/http-api-with-distributed-workloads) Rust template, which dispatches work from an HTTP API to a background component over a message broker. You can use it as a model for adding `wasmcloud:messaging` to your own components. ## Overview Messaging typically spans **at least two components**. A component can import `consumer` to publish events without a handler in the same deployment; for example, the component might publish audit events that an external NATS subscriber consumes. A handler can also receive messages from multiple senders. The example template demonstrates a two-component request-reply pattern that may serve as a starting point. The template consists of: - **`http-api`** — An HTTP component that accepts a `POST /task` request and uses `wasmcloud:messaging/consumer` to dispatch a request message and await its reply. - **`task-leet`** — A headless component that exports `wasmcloud:messaging/handler` to receive messages, process them (in this case, leet-speak the payload), and publish a reply. ![Messaging flow](../../../images/messaging-flow.svg) :::info[NATS in production] `wasmcloud:messaging` uses NATS as its production transport, built into the wasmCloud runtime. During development with `wash dev`, messaging is handled in-process and no NATS server is required. In production, the runtime connects to NATS automatically when the host starts with a NATS URL. ::: The template is organized as a Cargo workspace with two crates and a shared `wit/world.wit`: ``` http-api-with-distributed-workloads/ ├── Cargo.toml # workspace, declares both crates ├── wit/ │ └── world.wit # shared WIT worlds for both crates ├── http-api/ │ ├── Cargo.toml │ └── src/lib.rs # imports consumer, exports HTTP handler └── task-leet/ ├── Cargo.toml └── src/lib.rs # exports handler, imports consumer for replies ``` `wasmcloud:messaging` is **not** bundled into `wstd`. Both crates use `wit-bindgen` to generate bindings for the messaging interfaces. The `http-api` crate combines `wit-bindgen`-generated `consumer` bindings with `wstd`'s `#[http_server]` macro for the HTTP export. The `task-leet` crate uses `wit-bindgen` only. :::tip[Complete example] A full working example is available as a template: ```shell wash new https://github.com/wasmCloud/wasmCloud.git \ --name http-api-with-distributed-workloads \ --subfolder templates/http-api-with-distributed-workloads ``` ::: ## Step 1: Declare the WIT worlds Both components share a single `wit/world.wit`. The HTTP API only sends messages, so it imports `consumer` only. The worker receives messages and replies, so it both imports `consumer` and exports `handler`. In `wit/world.wit`: ```wit package wasmcloud:template@0.1.0; // wasi:http/incoming-handler is exported via wstd's #[http_server] proc macro, // so it does not appear explicitly in the http-api world. world http-api { import wasmcloud:messaging/consumer@0.2.0; } world task { import wasmcloud:messaging/consumer@0.2.0; export wasmcloud:messaging/handler@0.2.0; } ``` ### What these interfaces provide | Interface | Direction | Purpose | |---|---|---| | [`wasmcloud:messaging/consumer`](https://github.com/wasmCloud/wasmCloud/blob/main/wit/messaging.wit) | import | Send messages (`request`, `publish`) | | [`wasmcloud:messaging/handler`](https://github.com/wasmCloud/wasmCloud/blob/main/wit/messaging.wit) | export | Receive messages (`handle-message`) | The underlying message type used by both interfaces: ```wit record broker-message { subject: string, body: list, // Vec in Rust reply-to: option, } ``` ### How dependency resolution works When you run `wash dev` or `wash build`, the build pipeline calls `wash wit fetch` as a setup step. `wasmcloud:messaging` is hosted at the `wasmcloud.com` package registry, distinct from the `wasi.dev` registry used for standard WASI interfaces. Both registries are resolved automatically; no extra configuration is required. ## Step 2: Use `consumer::request` from the HTTP API The `http-api` crate combines `#[wstd::http_server]` for the HTTP export with `wit-bindgen`-generated bindings for the `wasmcloud:messaging/consumer` import. In `http-api/src/lib.rs`, generate bindings against the `http-api` world: ```rust mod bindings { wit_bindgen::generate!({ path: "../wit", world: "http-api", generate_all, }); } use anyhow::Context as _; use bindings::wasmcloud::messaging::consumer; use serde::Deserialize; use wstd::{ http::{Body, Request, Response, StatusCode}, time::Duration, }; #[derive(Deserialize)] struct TaskRequest { worker: Option, payload: String, } #[wstd::http_server] async fn main(req: Request) -> anyhow::Result> { match req.uri().path() { "/task" => create_task(req).await, _ => Response::builder() .status(StatusCode::NOT_FOUND) .body("Not found\n".into()) .map_err(Into::into), } } async fn create_task(mut req: Request) -> anyhow::Result> { let task: TaskRequest = req .body_mut() .json() .await .context("failed to parse body")?; let subject = format!("tasks.{}", task.worker.unwrap_or_else(|| "default".into())); let body = task.payload.into_bytes(); let timeout_ms = Duration::from_secs(5).as_millis() as u32; match consumer::request(&subject, &body, timeout_ms) { Ok(reply) => Response::builder() .status(StatusCode::OK) .body(reply.body.into()) .map_err(Into::into), Err(err) => Response::builder() .status(StatusCode::BAD_GATEWAY) .body(err.into()) .map_err(Into::into), } } ``` `consumer::request(subject, body, timeout_ms)` implements the request-reply pattern. It publishes `body` to `subject`, blocks until a reply arrives (or until `timeout_ms` elapses), and returns the reply as a `BrokerMessage`. It is **synchronous** in WIT, even though we call it from an `async fn` here. There is no `await` to add. `consumer::request` returns `Result`. On error (timeout, no subscriber, delivery failure), the `Err(String)` describes what went wrong. :::warning[Timeout behavior] If no component is subscribed to `subject` within `timeout_ms` milliseconds, `consumer::request` returns `Err`. During `wash dev`, the wasmCloud runtime routes calls in-process, so timeouts only occur if the handler itself errors or takes too long. In production, a missing NATS subscription or an unreachable server both surface as timeout errors. ::: In `http-api/Cargo.toml`: ```toml [package] name = "http-api" version = "0.1.0" edition = "2024" [lib] crate-type = ["cdylib"] [dependencies] anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } wit-bindgen = "0.41" wstd = "0.6" ``` Any `wit-bindgen` 0.4x release works. The template pins `0.41` because it is the version the rest of the wasmCloud Rust template fleet uses; newer 0.4x releases are interchangeable. ### Subject naming Subjects are arbitrary dot-separated strings following NATS subject conventions. The template uses `tasks.{worker}` so multiple worker types can each subscribe to their own sub-subject: ```text tasks.task-worker → handled by the task-worker component tasks.summarizer → would be handled by a hypothetical summarizer component ``` In development with `wash dev`, all messages are routed in-process regardless of subject. Subjects only become meaningful in production where NATS subscription patterns control routing. ## Step 3: Implement the handler in the worker The worker exports `wasmcloud:messaging/handler@0.2.0` and imports `consumer` so it can publish replies. In `task-leet/src/lib.rs`, generate bindings against the `task` world: ```rust wit_bindgen::generate!({ path: "../wit", world: "task", generate_all, }); use crate::wasmcloud::messaging::types::BrokerMessage; use wasmcloud::messaging::consumer; struct Component; export!(Component); impl exports::wasmcloud::messaging::handler::Guest for Component { fn handle_message(msg: BrokerMessage) -> Result<(), String> { let Some(subject) = msg.reply_to else { return Err("missing reply_to".to_string()); }; let payload = String::from_utf8(msg.body.to_vec()) .map_err(|e| format!("failed to decode body: {e}"))?; consumer::publish(&BrokerMessage { subject, body: to_leet_speak(&payload).into(), reply_to: None, }) } } fn to_leet_speak(input: &str) -> String { input .chars() .map(|c| match c.to_ascii_lowercase() { 'a' => '4', 'e' => '3', 'i' => '1', 'o' => '0', 's' => '5', 't' => '7', 'l' => '1', _ => c, }) .collect() } ``` `exports::wasmcloud::messaging::handler::Guest::handle_message` is the export shape `wit-bindgen` generates for `export wasmcloud:messaging/handler@0.2.0`. Implement it on a unit struct and call `export!` to register the implementation. `handle_message` returns `Result<(), String>`. Returning `Err` propagates back to the caller; if the sender called `consumer::request`, the `Err` surfaces there as the request's `Err` variant. `consumer::publish(&msg)` is fire-and-forget delivery. It returns `Result<(), String>`. Because the worker is the last hop in this template, the snippet returns the call's `Result` directly as `handle_message`'s return value, propagating any delivery failure to the runtime. In `task-leet/Cargo.toml`: ```toml [package] name = "task-leet" version = "0.1.0" edition = "2024" [lib] crate-type = ["cdylib"] [dependencies] wit-bindgen = "0.41" wstd = "0.6" ``` ### Fire-and-forget handlers If a handler does not need to reply (one-way notification pattern), omit the `consumer::publish` call: ```rust impl exports::wasmcloud::messaging::handler::Guest for Component { fn handle_message(msg: BrokerMessage) -> Result<(), String> { let event = serde_json::from_slice::(&msg.body) .map_err(|e| format!("decode: {e}"))?; process_event(event); // side effects only, no reply Ok(()) } } ``` ## Step 4: Configure multi-component development A convenient layout uses a Cargo workspace with a root `Cargo.toml` that lists each crate as a workspace member, plus a root `.wash/config.yaml` that points `component_path` at the primary component (the HTTP API) and lists the additional component under `dev: components:`. A standalone two-crate layout works too; the workspace just makes the shared dependencies easier to manage. In the workspace root `Cargo.toml`: ```toml [workspace] resolver = "3" members = ["http-api", "task-leet"] [workspace.package] edition = "2024" [workspace.dependencies] anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } wit-bindgen = "0.41" wstd = "0.6" ``` In `.wash/config.yaml`: ```yaml build: command: cargo build --target wasm32-wasip2 --release component_path: target/wasm32-wasip2/release/http_api.wasm dev: components: - name: task-leet file: target/wasm32-wasip2/release/task_leet.wasm ``` The `dev: components:` list tells `wash dev` to deploy the worker alongside the main component. `wash dev` automatically links both components through the wasmCloud runtime; no NATS server is required during development. ## Build and verify ```shell wash dev ``` `wash dev` builds both crates, starts an HTTP server on port 8000, and routes messaging calls between the components in-process. Send a task. The worker converts the payload to leet speak: ```shell curl -s -X POST http://localhost:8000/task \ -H 'Content-Type: application/json' \ -d '{"worker": "task-leet", "payload": "Hello World"}' ``` ```text H3110 W0r1d ``` A request without a payload returns `400`: ```shell curl -s -X POST http://localhost:8000/task \ -H 'Content-Type: application/json' \ -d '{}' ``` ## Production deployment Both components run on the same wasmCloud host. Start the host with a NATS URL; the runtime's built-in messaging plugin connects to NATS automatically. Deploy both components together with a `WorkloadDeployment` manifest: ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: http-api-with-distributed-workloads spec: replicas: 1 template: spec: hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: host: your-domain.example.com - namespace: wasmcloud package: messaging interfaces: - consumer - handler config: subscriptions: "tasks.>" components: - name: http-api image: /http-api:latest - name: task-leet image: /task-leet:latest ``` Key points about the manifest: - **`hostInterfaces`** declares which built-in host capabilities the workload needs. No separate HTTP server or NATS component is listed; both are provided by the runtime. - **`interfaces: [consumer, handler]`** declares both the sending and receiving sides of the messaging interface. The runtime automatically wires subscriptions to the component that exports `handler` (the worker), not to the HTTP API which only imports `consumer`. - **`subscriptions`** is a comma-separated list of NATS subject patterns. `tasks.>` matches any subject starting with `tasks.`, covering any number of worker types. - **HTTP `host` config** is the Host header the runtime uses to route incoming HTTP requests to this workload. The actual listen address is set at host startup (`--http-addr`). :::info[Subject-based routing in production] Because NATS subscriptions are configured per-workload at deploy time, subject naming becomes a contract between sender and handler. The `tasks.{worker}` convention in this template means each worker type can be independently scaled and replaced without modifying the HTTP API. Deploy a new worker with its own subscription and the API keeps working unchanged. ::: ## Summary: checklist for adding `wasmcloud:messaging` **For a component that sends messages (consumer):** 1. **Add `import wasmcloud:messaging/consumer@0.2.0` to its WIT world**. 2. **Add `wit-bindgen` to `Cargo.toml`** and generate bindings with `wit_bindgen::generate!`. 3. **Call `consumer::request(subject, body, timeout_ms)`** for request-reply; it returns `Result`. 4. **Call `consumer::publish(&msg)`** for fire-and-forget delivery; it returns `Result<(), String>`. **For a component that handles messages (handler):** 1. **Add both `import wasmcloud:messaging/consumer@0.2.0` and `export wasmcloud:messaging/handler@0.2.0` to its WIT world**. The import is needed for `publish()` (replies); the export declares the handler. 2. **Implement `exports::wasmcloud::messaging::handler::Guest::handle_message`** on a unit struct and call `export!`. 3. **Use `msg.reply_to`** to find the reply subject for request-reply patterns; call `consumer::publish` to respond. 4. **Return `Err(String)` to signal failure** — the value propagates back to the caller. **For multi-component development:** 1. **Use a Cargo workspace** with each component as a workspace member. 2. **Declare `dev: components:` in root `.wash/config.yaml`** with the path to each additional component's `.wasm` file. 3. **`wash dev` routes messages in-process** — no NATS server required during development. 4. **In production**, use a `WorkloadDeployment` manifest with a `wasmcloud:messaging` `hostInterface` entry. Include `handler` in the interfaces list and set `subscriptions` in the `config` block. ## API reference: `wasmcloud:messaging` functions used | Function | Signature | Description | |---|---|---| | `consumer::request(subject, body, timeout_ms)` | `fn(&str, &[u8], u32) -> Result` | Publish to `subject` and block until a reply arrives or `timeout_ms` elapses. | | `consumer::publish(msg)` | `fn(&BrokerMessage) -> Result<(), String>` | Fire-and-forget publish. | | `handler::Guest::handle_message(msg)` | `fn(BrokerMessage) -> Result<(), String>` | Called by the runtime for each delivered message. Return `Err` to signal failure. | **`BrokerMessage` fields:** | Field | Type | Description | |---|---|---| | `subject` | `String` | The message subject (NATS topic) | | `body` | `Vec` | Raw message payload | | `reply_to` | `Option` | Reply subject set automatically by `consumer::request` | ## Further reading - [Rust Language Guide](./index.mdx) — toolchain overview, HTTP patterns, async runtime guidance, and crate compatibility - [Key-Value Storage](./key-value-storage.mdx) — persistent state for a single component - [Configuration](./configuration.mdx) — read configuration values from ConfigMaps, Secrets, and inline environment variables - [Language Support overview](../index.mdx) — summary of all supported languages and toolchains --- ## Blob Storage You can add blob storage capabilities to a component with the [`wasi:blobstore`](https://github.com/WebAssembly/wasi-blobstore/) interface. This guide walks through replacing the in-memory mock data store in the wasmCloud [`http-handler-hono`](https://github.com/wasmCloud/typescript/tree/v2/templates/http-handler-hono) template with the `wasi:blobstore` interface. You can use this guide as a model for implementing `wasi:blobstore` in your own components or adding any new WIT interface to an existing component. ## Overview The wasmCloud [`http-handler-hono`](https://github.com/wasmCloud/typescript/tree/v2/templates/http-handler-hono) template includes an in-memory `Map` that acts as a simulated database. This means all data is lost whenever the component restarts. In this guide, we'll replace the in-memory map with `wasi:blobstore`, which gives the component persistent object storage backed by a real blob store. :::info[] By default, a wasmCloud deployment uses NATS JetStream for storage via the built-in [WASI Blobstore NATS plugin](https://github.com/wasmCloud/wasmCloud/blob/main/crates/wash-runtime/src/plugin/wasi_blobstore/nats.rs). You can use [host plugins](../../../../runtime/index.mdx) to back `wasi:blobstore` with a different storage solution. ::: `wasi:blobstore` models storage as **containers** (analogous to S3 buckets) that hold named **objects** (blobs of bytes). Unlike a simple key-value store, blobstore uses a streaming I/O model with `OutgoingValue`/`IncomingValue` resources, designed to handle potentially large objects. Our changes will focus on two files: * `wit/world.wit` - Declare the new WIT imports * `src/routes/items.ts` - Use the imported interfaces in application code No changes are needed to `rolldown.config.mjs`, `tsconfig.json`, `package.json`, or any other file. :::tip[Complete example] A full working example is available as a template: ```bash wash new https://github.com/wasmCloud/typescript.git \ --name http-blobstore-handler-hono \ --subfolder templates/http-blobstore-handler-hono \ ``` ::: ## Step 1: Declare the WIT imports in `wit/world.wit` Every capability your component uses must be declared in its WIT world. Open `wit/world.wit` and add an `import` line for the blobstore interface. ```wit {4} package wasmcloud:templates@0.1.0; world typescript-http-blobstore-handler-hono { import wasi:blobstore/blobstore@0.2.0-draft; export wasi:http/incoming-handler@0.2.6; } ``` ### What these interfaces provide - **`wasi:blobstore/blobstore`** -- Top-level container management: `createContainer`, `getContainer`, `containerExists`, `deleteContainer`. - **`wasi:blobstore/container`** -- Object operations within a container: `getData`, `writeData`, `listObjects`, `deleteObject`, `hasObject`, `objectInfo`. Pulled in transitively by `blobstore`. - **`wasi:blobstore/types`** -- Streaming resource types: `OutgoingValue` (for writes), `IncomingValue` (for reads), and `ObjectMetadata`. Pulled in transitively by `blobstore`. Unlike interfaces with multiple sub-imports (e.g. `wasi:keyvalue/store` and `wasi:keyvalue/atomics`), blobstore only needs the single `wasi:blobstore/blobstore` import — `container` and `types` are available automatically. ### How dependency resolution works When you run `npm run build`, the build pipeline calls `wkg wit fetch` as a setup step. This resolves the WIT package references (like `wasi:blobstore`) and downloads their definitions into the `wit/deps/` directory automatically. You don't need to manually download any WIT files. The bundler (`rolldown`) is already configured with `external: /wasi:.*/` in `rolldown.config.mjs`, which tells it to leave all `wasi:*` imports as external -- they'll be resolved at component instantiation time, not at bundle time. This covers any new `wasi:` interface you add. ## Step 2: Import and use the interface in TypeScript With the WIT world updated, you can now import functions from the new interfaces in your TypeScript code. The import paths match the WIT interface names exactly. ### Importing WIT interfaces Add these imports at the top of `src/routes/items.ts`: ```typescript import { getContainer, createContainer, containerExists } from 'wasi:blobstore/blobstore@0.2.0-draft'; import { OutgoingValue, IncomingValue } from 'wasi:blobstore/types@0.2.0-draft'; ``` The `jco guest-types` command generates `.d.ts` files for your WIT interfaces into `generated/types/`, and `tsconfig.json` references them through its `types` field. TypeScript resolves the bare `wasi:` specifier imports against these generated ambient module declarations, so you get full type checking and IDE autocompletion. At runtime, `rolldown` leaves these imports external (via `external: /wasi:.*/`) and `jco componentize` wires them up during Wasm component creation. ### Key pattern: import path = WIT interface name The import path always matches the fully-qualified WIT interface name: ``` wasi:blobstore/blobstore@0.2.0-draft --> import { getContainer, ... } from 'wasi:blobstore/blobstore@0.2.0-draft' wasi:blobstore/types@0.2.0-draft --> import { OutgoingValue, ... } from 'wasi:blobstore/types@0.2.0-draft' ``` To discover which functions are available on an interface, look at the generated type file (e.g. `generated/types/interfaces/wasi-blobstore-blobstore.d.ts`) after running `npm run build` once, or read the WIT definition in `wit/deps/`. ### Serialization: values are `Uint8Array` Blobstore reads and writes blobs as bytes. To store structured data, you need to serialize and deserialize: ```typescript const encoder = new TextEncoder(); const decoder = new TextDecoder(); function serializeItem(item: Item): Uint8Array { return encoder.encode(JSON.stringify(item)); } function deserializeItem(bytes: Uint8Array): Item { return JSON.parse(decoder.decode(bytes)); } ``` ### Ensuring a container exists All blobstore operations happen on a **container**. Unlike a simple key-value bucket, you must ensure the container exists before using it: ```typescript const CONTAINER_NAME = 'default'; function ensureContainer() { if (!containerExists(CONTAINER_NAME)) { createContainer(CONTAINER_NAME); } return getContainer(CONTAINER_NAME); } ``` Call `ensureContainer()` inside each route handler rather than at module scope -- the container handle should be obtained per-request. ### Key namespacing Use a prefix to separate different kinds of data in the same container: ```typescript const ITEM_PREFIX = 'item:'; // Item keys: "item:", "item:", ... ``` This prevents collisions if you store other kinds of objects in the same container. ### Writing blobs Writing to blobstore uses a streaming pattern with `OutgoingValue`. This is more involved than a simple `set(key, bytes)` call: ```typescript function writeBlob(container: any, name: string, bytes: Uint8Array): void { const ov = OutgoingValue.newOutgoingValue(); const stream = ov.outgoingValueWriteBody(); stream.blockingWriteAndFlush(bytes); container.writeData(name, ov); OutgoingValue.finish(ov); } ``` :::warning[Operation ordering] You must write bytes to the stream **before** calling `writeData`. Calling `writeData` first commits an empty value — subsequent writes to the stream won't be associated with the object. This produces no error; the object simply has no data. You must also call `OutgoingValue.finish(ov)` **after** `writeData`. Dropping an `OutgoingValue` without finishing it signals corruption to the runtime. ::: ### Reading blobs Reading uses `IncomingValue` with inclusive byte offsets: ```typescript function readBlob(container: any, name: string): Uint8Array { const metadata = container.objectInfo(name); const iv = container.getData(name, 0n, metadata.size - 1n); return IncomingValue.incomingValueConsumeSync(iv); } ``` :::warning[Inclusive offsets] `getData` uses **inclusive** start and end offsets as **`bigint`** values. For a 100-byte object, the valid range is `(0n, 99n)`, which is `(0n, metadata.size - 1n)`. Using `metadata.size` as the end offset reads one byte past the object. ::: ### Listing objects Listing returns a paginated stream that you read in batches: ```typescript function listObjectNames(container: any): string[] { const stream = container.listObjects(); const names: string[] = []; while (true) { const [batch, done] = stream.readStreamObjectNames(100n); names.push(...batch); if (done) break; } return names; } ``` :::warning[`bigint` and tuple return type] `readStreamObjectNames` takes a **`bigint`**, not a `number`. Passing `100` instead of `100n` causes a runtime type error. The method returns `[string[], boolean]` — a tuple of names and a done flag. Always destructure it: `const [batch, done] = ...`. If you assign the return value to a single variable, `batch.length` will always be `2` (the tuple length). ::: ### ID generation Blobstore has no atomic increment operation. Use `crypto.randomUUID()` to generate unique IDs: ```typescript const id = crypto.randomUUID(); ``` This means IDs are UUIDs rather than sequential integers. ### Synchronous API All blobstore operations (`getData`, `writeData`, `deleteObject`, `hasObject`, `listObjects`) are **synchronous** in the JCO bindings. The WebAssembly component model uses blocking calls, so these don't return Promises. Your existing synchronous Hono route handlers work without modification. ## Step 3: Route-by-route changes Here is exactly what changed in each CRUD route handler, showing the before (in-memory `Map`) and after (`wasi:blobstore` container). ### Removed code The entire mock data store and its initialization were removed: ```typescript // REMOVED: Simulated database const items = new Map(); let nextId = 1; // REMOVED: Sample data initialization items.set('1', { id: '1', name: 'Sample Item', description: 'This is a sample item', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); nextId = 2; ``` With persistent storage, the store starts empty and users create items via the API. Data survives component restarts. ### GET `/` -- List all items **Before:** ```typescript itemsRouter.get('/', (c) => { const itemList = Array.from(items.values()); // ... filtering and pagination ... }); ``` **After:** ```typescript itemsRouter.get('/', (c) => { const container = ensureContainer(); const allNames = listObjectNames(container); const itemNames = allNames.filter((n: string) => n.startsWith(ITEM_PREFIX)); const itemList: Item[] = []; for (const name of itemNames) { const bytes = readBlob(container, name); itemList.push(deserializeItem(bytes)); } // ... filtering and pagination unchanged ... }); ``` `listObjectNames` reads the paginated stream and returns all object names. We filter for the `item:` prefix, then fetch and deserialize each item. ### GET `/:id` -- Get single item **Before:** ```typescript const item = items.get(id); if (!item) { /* 404 */ } return c.json(item); ``` **After:** ```typescript const container = ensureContainer(); if (!container.hasObject(`${ITEM_PREFIX}${id}`)) { /* 404 */ } const bytes = readBlob(container, `${ITEM_PREFIX}${id}`); return c.json(deserializeItem(bytes)); ``` `container.hasObject()` returns a boolean. `readBlob` handles the streaming read and inclusive offset calculation. ### POST `/` -- Create new item **Before:** ```typescript const id = String(nextId++); // ... build newItem ... items.set(id, newItem); ``` **After:** ```typescript const container = ensureContainer(); const id = crypto.randomUUID(); // ... build newItem ... writeBlob(container, `${ITEM_PREFIX}${id}`, serializeItem(newItem)); ``` `crypto.randomUUID()` generates a unique ID. `writeBlob` handles the `OutgoingValue` lifecycle (create, write to stream, commit, finish). ### PUT `/:id` -- Update item **Before:** ```typescript const existing = items.get(id); if (!existing) { /* 404 */ } // ... build updated ... items.set(id, updated); ``` **After:** ```typescript const container = ensureContainer(); if (!container.hasObject(`${ITEM_PREFIX}${id}`)) { /* 404 */ } const bytes = readBlob(container, `${ITEM_PREFIX}${id}`); const existing = deserializeItem(bytes); // ... build updated ... writeBlob(container, `${ITEM_PREFIX}${id}`, serializeItem(updated)); ``` ### DELETE `/:id` -- Delete item **Before:** ```typescript if (!items.has(id)) { /* 404 */ } items.delete(id); ``` **After:** ```typescript const container = ensureContainer(); if (!container.hasObject(`${ITEM_PREFIX}${id}`)) { /* 404 */ } container.deleteObject(`${ITEM_PREFIX}${id}`); ``` ## Build and verify ### Build ```bash npm run build ``` This runs the full pipeline: 1. `wash wit fetch` -- downloads `wasi:blobstore` WIT definitions into `wit/deps/` 2. `jco types` -- generates TypeScript type definitions in `generated/types/` 3. `rolldown` -- bundles TypeScript into `dist/component.js` (leaving `wasi:` imports external) 4. `jco componentize` -- compiles `dist/component.js` into a `.wasm` component ### Configure `wash dev` To get persistent blob storage during development, add `wasi_blobstore_path` to your `.wash/config.yaml`: ```yaml dev: wasi_blobstore_path: tmp/blobstore ``` - **Without `wasi_blobstore_path`**: `wash dev` uses an in-memory provider. State is lost when the dev session ends. - **With `wasi_blobstore_path`**: `wash dev` uses a filesystem provider. Blobs persist as files in the specified directory across restarts. Container names map to subdirectories, and object names map to files within them. ### Run ```bash npm run dev ``` This starts `wash dev`, which automatically provisions a blobstore provider to satisfy the `wasi:blobstore` imports. ### Test ```bash # Create an item curl -X POST http://localhost:8000/api/items \ -H "Content-Type: application/json" \ -d '{"name":"Test Item","description":"Testing blobstore"}' # List items curl http://localhost:8000/api/items # Get by ID (replace with a real ID from the create response) curl http://localhost:8000/api/items/ # Update curl -X PUT http://localhost:8000/api/items/ \ -H "Content-Type: application/json" \ -d '{"name":"Updated Item"}' # Delete curl -X DELETE http://localhost:8000/api/items/ ``` To verify persistence, restart the component and confirm previously created items are still returned by `GET /api/items`. ## Summary: checklist for adding any WIT interface 1. **Add `import` lines to `wit/world.wit`** for the interfaces you need. 2. **Run `npm run build` once** so `wash wit fetch` downloads the WIT definitions and `jco types` generates TypeScript types you can reference. 3. **Import functions in TypeScript** using the exact WIT interface name as the import path. TypeScript resolves the import against the ambient module declarations in `generated/types/`. 4. **Use the imported functions** in your route handlers. Remember that Wasm component model calls are synchronous. 5. **Handle serialization** -- most WIT interfaces use `Uint8Array` for binary data; use `TextEncoder`/`TextDecoder` and `JSON.stringify`/`JSON.parse` for structured data. 6. **No bundler changes needed** -- `rolldown.config.mjs` already externalizes all `wasi:*` imports. 7. **No new npm dependencies needed** -- WIT interfaces are provided by the runtime, not by npm packages. --- ## API reference: `wasi:blobstore` operations used | Operation | Signature | Description | |-----------|-----------|-------------| | `createContainer(name)` | `(string) => Container` | Create a named container | | `getContainer(name)` | `(string) => Container` | Get an existing container by name | | `containerExists(name)` | `(string) => boolean` | Check if a container exists | | `container.writeData(name, ov)` | `(string, OutgoingValue) => void` | Write an object to the container | | `container.getData(name, start, end)` | `(string, bigint, bigint) => IncomingValue` | Read an object (inclusive offsets) | | `container.deleteObject(name)` | `(string) => void` | Delete an object | | `container.hasObject(name)` | `(string) => boolean` | Check if an object exists | | `container.objectInfo(name)` | `(string) => ObjectMetadata` | Get object metadata (name, size, timestamps) | | `container.listObjects()` | `() => StreamObjectNames` | Get a paginated stream of object names | | `stream.readStreamObjectNames(len)` | `(bigint) => [string[], boolean]` | Read a batch of names; returns `[names, done]` | | `OutgoingValue.newOutgoingValue()` | `() => OutgoingValue` | Create a new outgoing value for writing | | `ov.outgoingValueWriteBody()` | `() => OutputStream` | Get the write stream for an outgoing value | | `OutgoingValue.finish(ov)` | `(OutgoingValue) => void` | Finalize the outgoing value (required after write) | | `IncomingValue.incomingValueConsumeSync(iv)` | `(IncomingValue) => Uint8Array` | Read all bytes from an incoming value | ## Further reading - [TypeScript Language Guide](../index.mdx) — toolchain overview, HTTP patterns, framework integration, and library compatibility - [Key-Value Storage](./key-value-storage.mdx) — simpler key-value storage for small values and counters - [Language Support overview](../../index.mdx) — summary of all supported languages and toolchains --- ## Configuration(Typescript) You can read runtime configuration values in a TypeScript component with the [`wasi:cli/environment`](https://github.com/WebAssembly/wasi-cli) interface. Configuration values can be supplied from inline key-value pairs, Kubernetes ConfigMaps, and Kubernetes Secrets. All arrive through the same interface regardless of source. This guide walks through adding `wasi:cli/environment` to the wasmCloud [`http-handler-hono`](https://github.com/wasmCloud/typescript/tree/v2/templates/http-handler-hono) template and shows how to provide values via a Kubernetes workload manifest. ## Overview `wasi:cli/environment` is part of the standard WASI P2 suite and is always available in a wasmCloud host. It exposes a `getEnvironment` function that returns all configuration key-value pairs available to the component. In production, values come from `localResources.environment` in a `Workload` or `WorkloadDeployment` manifest, which accepts inline key-value pairs, Kubernetes ConfigMaps, and Kubernetes Secrets. :::tip[Guidance for operators] The [Secrets and Configuration Management](../../../../kubernetes-operator/operator-manual/secrets-and-configuration.mdx) page in the Operator Manual describes how the Kubernetes operator injects values via `wasi:cli/environment`. This guide covers the component author's side: how to declare and read those values in TypeScript. ::: Our changes focus on two files: * `wit/world.wit` — Declare the `wasi:cli/environment` import * `src/component.ts` (or your main handler file) — Read configuration values in application code No changes are needed to `rolldown.config.mjs`: the default `external: /wasi:.*/` pattern already covers `wasi:cli` imports. ## Step 1: Declare the WIT import in `wit/world.wit` Every capability your component uses must be declared in its WIT world. Open `wit/world.wit` and add an `import` line for the cli environment interface. ```wit {4} package wasmcloud:templates@0.1.0; world typescript-http-handler-hono { import wasi:cli/environment@0.2.3; export wasi:http/incoming-handler@0.2.6; } ``` ### What this interface provides **`wasi:cli/environment`** exposes one function for reading configuration: - **`getEnvironment()`** — Returns all configuration key-value pairs as an array of `[string, string]` tuples. After adding the import and running `npm run build`, JCO generates a typed definition for the interface in `generated/types/interfaces/wasi-cli-environment.d.ts`: ```typescript declare module 'wasi:cli/environment@0.2.3' { export function getEnvironment(): Array<[string, string]>; export function getArguments(): Array; export function initialCwd(): string | undefined; } ``` ### How dependency resolution works When you run `npm run build`, the build pipeline calls `wkg wit fetch` as a setup step. This resolves the WIT package reference and downloads the definition into `wit/deps/` automatically. You don't need to manually download any WIT files. ## Step 2: Import and use the interface in TypeScript With the WIT world updated, you can import `getEnvironment` in your TypeScript code. Because JCO generates types for `wasi:cli/environment`, no `@ts-expect-error` directive is needed. ### Importing WIT interfaces Add this import at the top of your handler file: ```typescript import { getEnvironment } from 'wasi:cli/environment@0.2.3'; ``` ### Reading configuration values `getEnvironment()` returns all key-value pairs as an array. Convert it to a plain object with `Object.fromEntries` for convenient key-based access: ```typescript import { getEnvironment } from 'wasi:cli/environment@0.2.3'; app.get('/', (c) => { const config = Object.fromEntries(getEnvironment()); const appName = config['APP_NAME'] ?? 'My App'; const upstreamUrl = config['UPSTREAM_URL']; if (!upstreamUrl) { return c.text('UPSTREAM_URL is not configured', 500); } return c.json({ app: appName, upstream: upstreamUrl }); }); ``` Call `getEnvironment()` inside your route handlers rather than at module scope. Configuration is stable for the lifetime of each invocation, but calling it at module scope may execute before the host has provided the values. ### Hono env pattern When using Hono, you can load all configuration once in the fetch event listener and pass it as the `env` argument to `app.fetch`. This gives every route and middleware access to config through `c.env`, and keeps configuration loading in one place: ```typescript title="src/component.ts" /// declare const self: ServiceWorkerGlobalScope; import { Hono } from 'hono'; import { getEnvironment } from 'wasi:cli/environment@0.2.3'; type AppConfig = { APP_NAME?: string; UPSTREAM_URL?: string; }; const app = new Hono<{ Bindings: AppConfig }>(); app.get('/', (c) => { const appName = c.env.APP_NAME ?? 'My App'; const upstreamUrl = c.env.UPSTREAM_URL; if (!upstreamUrl) { return c.text('UPSTREAM_URL is not configured', 500); } return c.json({ app: appName, upstream: upstreamUrl }); }); self.addEventListener('fetch', (event) => { const config = Object.fromEntries(getEnvironment()) as AppConfig; event.respondWith(app.fetch(event.request, config)); }); ``` :::note[Using this pattern with jco-std] The fetch event listener shown above also works when you're using `@bytecodealliance/jco-std` — [jco-std's `fetch-hono` fixture](https://github.com/bytecodealliance/jco/blob/main/packages/jco-std/test/fixtures/apps/fetch-hono/app.js) uses the same `self.addEventListener('fetch', ...)` approach. If you'd rather keep using the `fire()` helper, read config with `getEnvironment()` inside each route handler instead. jco-std also ships a Hono middleware, `wasiEnvMiddleware`, that injects an environment helper into the Hono context. See the [`fetch-hono-config-use`](https://github.com/bytecodealliance/jco/blob/main/packages/jco-std/test/fixtures/apps/fetch-hono-config-use/app.js) fixture for an example. The minimal pattern above keeps the WASI plumbing visible; the middleware is a convenience wrapper on top of the same interface. ::: ### Synchronous API `getEnvironment()` is **synchronous**. It doesn't return a Promise and requires no `await`. This is consistent with the WebAssembly component model's blocking call convention. ## Step 3: Provide configuration values ### In production (Kubernetes manifests) All configuration values are delivered to the component through the `localResources.environment` field in a `Workload` or `WorkloadDeployment` manifest. Three sources are supported and can be combined. #### Inline values The simplest approach — provide key-value pairs directly in the manifest: ```yaml components: - name: http-component image: ghcr.io/your-org/your-component:latest localResources: environment: config: APP_NAME: My Service UPSTREAM_URL: https://api.example.com ``` Inline values are suitable for non-sensitive configuration that is safe to store in version control. #### From a Kubernetes ConfigMap Reference a ConfigMap by name. Each key in the ConfigMap becomes a configuration value in the component: ```yaml components: - name: http-component image: ghcr.io/your-org/your-component:latest localResources: environment: configFrom: - name: app-config ``` Create the ConfigMap separately: ```yaml apiVersion: v1 kind: ConfigMap metadata: name: app-config namespace: default data: APP_NAME: My Service UPSTREAM_URL: https://api.example.com ``` #### From a Kubernetes Secret Reference a Secret by name for sensitive values such as API keys or credentials: ```yaml components: - name: http-component image: ghcr.io/your-org/your-component:latest localResources: environment: secretFrom: - name: app-secrets ``` Secret values are delivered to the component as plain strings. No decoding is required in your component code. #### Combining sources All three sources can be used together. When the same key appears in multiple sources, the order of precedence (lowest to highest) is: `config` → `configFrom` → `secretFrom`. ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: my-app namespace: default spec: replicas: 1 template: spec: hostSelector: hostgroup: default components: - name: http-component image: ghcr.io/your-org/your-component:latest localResources: environment: config: APP_NAME: My Service # inline (lowest precedence) configFrom: - name: app-config # non-sensitive config from ConfigMap secretFrom: - name: app-secrets # sensitive values from Secret (highest precedence) hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: address: '0.0.0.0:8080' ``` :::tip For a complete reference to `localResources.environment` fields, including precedence rules and RBAC considerations, see [Secrets and Configuration Management](../../../../kubernetes-operator/operator-manual/secrets-and-configuration.mdx). ::: ### During development (`wash dev`) `wash dev` provides the `wasi:cli/environment` interface, but `localResources.environment` defaults to empty — there is currently no mechanism in `.wash/config.yaml` or the `wash dev` CLI to inject test configuration values into a component locally. The practical approach for development is to supply fallback defaults in your component code: ```typescript const config = Object.fromEntries(getEnvironment()); // Use ?? to supply development defaults when values aren't set const appName = config['APP_NAME'] ?? 'My App (dev)'; const upstreamUrl = config['UPSTREAM_URL'] ?? 'http://localhost:9090'; ``` This lets your component run correctly in development while reading real values in production. Test configuration-dependent behavior by deploying to a Kubernetes environment with the full manifest. ## Build and verify ### Build ```bash npm run build ``` This runs the full pipeline: 1. `wash wit fetch` — downloads `wasi:cli/environment` WIT definitions into `wit/deps/` 2. `jco guest-types` — generates TypeScript type definitions in `generated/types/` 3. `rolldown` — bundles TypeScript into `dist/component.js` (leaving `wasi:` imports external) 4. `jco componentize` — compiles `dist/component.js` into a `.wasm` component ### Verify the WIT import Use `wash inspect` to confirm that your component correctly declares a `wasi:cli/environment` import: ```bash wash inspect dist/http_handler_hono.wasm ``` You should see a line like: ``` import wasi:cli/environment@0.2.x; ``` The exact patch version may differ from what you declared in `wit/world.wit` — this is normal. The JavaScript engine embedded by ComponentizeJS has its own WASI version requirements, and `jco componentize` resolves them automatically. ### Run ```bash npm run dev ``` The component loads and serves requests. In development, `getEnvironment()` returns an empty list — your component will use whatever fallback defaults you've provided. Deploy to Kubernetes to test configuration behavior end-to-end. ## Summary: checklist for adding configuration 1. **Add `import wasi:cli/environment@0.2.3` to `wit/world.wit`**. 2. **Run `npm run build` once** so `wash wit fetch` downloads the WIT definition and `jco guest-types` generates the TypeScript type stubs in `generated/types/`. 3. **Import `getEnvironment` in TypeScript** — no `@ts-expect-error` directive is needed because JCO generates types for this interface. 4. **Call `getEnvironment()` inside route handlers**, not at module scope. 5. **Convert with `Object.fromEntries(getEnvironment())`** for plain-object key-based access. 6. **Provide fallback defaults with `??`** for every value your component reads — this keeps the component functional during `wash dev` when no configuration is injected. 7. **No rolldown changes needed** — `wasi:cli` is already covered by the default `external: /wasi:.*/` pattern. 8. **In production**, provide values via `localResources.environment` in your Kubernetes manifest, using `config:`, `configFrom:`, or `secretFrom:` as appropriate. --- ## API reference: `wasi:cli/environment` functions For the upstream WIT definitions, see the [`wasi:cli` 0.2.x interfaces](https://github.com/WebAssembly/wasi-cli/tree/main/wit) in the `WebAssembly/wasi-cli` repository. | Function | Signature | Description | |---|---|---| | `getEnvironment()` | `() => Array<[string, string]>` | Returns all configuration key-value pairs. Returns an empty array if no configuration has been provided to the component. | | `getArguments()` | `() => Array` | Returns POSIX-style command-line arguments. Not used for configuration. | | `initialCwd()` | `() => string \| undefined` | Returns the initial working directory. Not used for configuration. | **Converting `getEnvironment()` output:** | Expression | Result type | Use when | |---|---|---| | `getEnvironment()` | `Array<[string, string]>` | Iterating over entries | | `Object.fromEntries(getEnvironment())` | `Record` | Key-based access in route handlers | | `new Map(getEnvironment())` | `Map` | When you need Map-specific methods | ## Further reading - [Secrets and Configuration Management](../../../../kubernetes-operator/operator-manual/secrets-and-configuration.mdx) — operator reference for `localResources.environment`, ConfigMaps, and Secrets - [TypeScript Language Guide](../index.mdx) — toolchain overview, HTTP patterns, framework integration, and library compatibility - [Key-Value Storage](./key-value-storage.mdx) — persistent key-value storage for component state - [Language Support overview](../../index.mdx) — summary of all supported languages and toolchains --- ## Filesystem(Typescript) You can add filesystem capabilities to a component with the [`wasi:filesystem`](https://github.com/WebAssembly/WASI/tree/main/proposals/filesystem) interface. This guide walks through adding filesystem access to a wasmCloud TypeScript component, starting from the [`http-hello-world-hono`](https://github.com/wasmCloud/typescript/tree/v2/templates/http-hello-world-hono) template. You can use this guide as a model for implementing `wasi:filesystem` in your own components. ## Overview WebAssembly components run in a sandboxed environment with no default access to the host filesystem. The `wasi:filesystem` interface provides secure, capability-based filesystem access through **preopens**: directories that are explicitly mounted into a component before instantiation. This is different from key-value and blob storage: instead of storing data in an abstract store, your component reads and writes files in mounted directories, just as a traditional application would. This makes `wasi:filesystem` particularly useful for: - Serving static assets (HTML, CSS, images) - Reading data files - Writing logs or output files - Any workload that needs to interact with files on disk :::info[Security model] Components have **no filesystem access by default**. Directories must be explicitly mounted via volume configuration in `wash dev` or via `volumeMounts` in a WorkloadDeployment manifest. A component can only access the directories that have been preopened for it—it cannot traverse outside those mount points. For details on deploying with volumes, see [Filesystems and Volumes](../../../../kubernetes-operator/operator-manual/filesystems-and-volumes.mdx). ::: Our changes will focus on two files: * `wit/world.wit` — Declare the new WIT imports * `src/component.ts` — Use the imported interfaces in application code No changes are needed to `rolldown.config.mjs`, `tsconfig.json`, `package.json`, or any other file. ## Step 1: Declare the WIT imports in `wit/world.wit` Every capability your component uses must be declared in its WIT world. Open `wit/world.wit` and add `import` lines for the filesystem interfaces: ```wit {4-5} package wasmcloud:templates@0.1.0; world typescript-http-hello-world-hono { import wasi:filesystem/types@0.2.2; import wasi:filesystem/preopens@0.2.2; export wasi:http/incoming-handler@0.2.6; } ``` ### What these interfaces provide - **`wasi:filesystem/preopens`** — Provides `getDirectories()`, which returns the set of preopened directories and their mount paths. - **`wasi:filesystem/types`** — Provides the `Descriptor` resource for file and directory operations (read, write, list, open, create, delete) and supporting types like `DirectoryEntryStream`, `DescriptorFlags`, and `OpenFlags`. ### How dependency resolution works When you run `npm run build`, the build pipeline calls `wash wit fetch` as a setup step. This resolves the WIT package references (like `wasi:filesystem`) and downloads their definitions into the `wit/deps/` directory automatically. You don't need to manually download any WIT files. The bundler (`rolldown`) is already configured with `external: /wasi:.*/` in `rolldown.config.mjs`, which tells it to leave all `wasi:*` imports as external — they'll be resolved at component instantiation time, not at bundle time. This covers any new `wasi:` interface you add. ## Step 2: Import and use the interface in TypeScript With the WIT world updated, you can now import functions from the new interfaces in your TypeScript code. The import paths match the WIT interface names exactly. ### Importing WIT interfaces Add these imports at the top of `src/component.ts`: ```typescript import { getDirectories } from 'wasi:filesystem/preopens@0.2.2'; ``` The `jco guest-types` command generates `.d.ts` files for your WIT interfaces into `generated/types/`, and `tsconfig.json` references them through its `types` field. TypeScript resolves the bare `wasi:` specifier imports against these generated ambient module declarations, so you get full type checking and IDE autocompletion. At runtime, `rolldown` leaves these imports external (via `external: /wasi:.*/`) and `jco componentize` wires them up during Wasm component creation. ### Key concepts #### Preopens The `getDirectories()` function returns an array of `[Descriptor, string]` tuples — each tuple pairs a directory descriptor with its mount path (e.g., `"/data"`). This is how your component discovers which directories are available: ```typescript function getPreopenDir(path: string) { const dirs = getDirectories(); for (const [descriptor, dirPath] of dirs) { if (dirPath === path) { return descriptor; } } return null; } ``` #### Descriptors A `Descriptor` is a handle to an open file or directory. Directory descriptors (from preopens) can: - **List entries** with `readDirectory()`, which returns a `DirectoryEntryStream` - **Open files** with `openAt()`, which returns a new `Descriptor` for the file - **Create directories** with `createDirectoryAt()` File descriptors can: - **Read data** with `read(length, offset)`, which returns `[Uint8Array, boolean]` (data and end-of-file flag) - **Write data** with `write(buffer, offset)`, which returns the number of bytes written as a `bigint` #### Synchronous API All filesystem operations are **synchronous** in the Jco bindings. The WebAssembly component model uses blocking calls, so these don't return Promises. Your existing synchronous Hono route handlers work without modification. #### `bigint` for sizes and offsets The `wasi:filesystem` interface uses `bigint` values for file sizes and offsets (mapped from WIT's `u64` type). Use `BigInt()` to create these values: ```typescript // Read up to 1MB starting at offset 0 const [data, eof] = file.read(BigInt(1024 * 1024), BigInt(0)); // Write at offset 0 const bytesWritten = file.write(encoded, BigInt(0)); ``` ## Step 3: Build an HTTP file server Here's a complete component that serves files from a preopened `/data` directory, with endpoints to list files, read a file, and write a file: ```typescript title="src/component.ts" import { Hono } from 'hono'; import { fire } from '@bytecodealliance/jco-std/wasi/0.2.x/http/adapters/hono/server'; import { getDirectories } from 'wasi:filesystem/preopens@0.2.2'; const app = new Hono(); // Helper: find a preopened directory by its guest path function getPreopenDir(path: string) { const dirs = getDirectories(); for (const [descriptor, dirPath] of dirs) { if (dirPath === path) { return descriptor; } } return null; } // List files in the preopened /data directory app.get('/', (c) => { const dir = getPreopenDir('/data'); if (!dir) { return c.text('No /data directory available\n', 500); } const entries = []; const stream = dir.readDirectory(); let entry = stream.readDirectoryEntry(); while (entry !== undefined) { entries.push({ name: entry.name, type: entry.type }); entry = stream.readDirectoryEntry(); } return c.json({ path: '/data', entries }); }); // Read a file from the /data directory app.get('/read/:filename', (c) => { const dir = getPreopenDir('/data'); if (!dir) { return c.text('No /data directory available\n', 500); } const filename = c.req.param('filename'); const file = dir.openAt( { symlinkFollow: true }, filename, {}, { read: true } ); const [data, _eof] = file.read(BigInt(1024 * 1024), BigInt(0)); const text = new TextDecoder().decode(data); return c.text(text); }); // Write a file to the /data directory app.post('/write/:filename', async (c) => { const dir = getPreopenDir('/data'); if (!dir) { return c.text('No /data directory available\n', 500); } const filename = c.req.param('filename'); const body = await c.req.text(); const encoded = new TextEncoder().encode(body); const file = dir.openAt( { symlinkFollow: true }, filename, { create: true, truncate: true }, { write: true } ); const bytesWritten = file.write(encoded, BigInt(0)); return c.json({ filename, bytesWritten: Number(bytesWritten) }); }); app.notFound((c) => { return c.text('Not found\n', 404); }); fire(app); export { incomingHandler } from '@bytecodealliance/jco-std/wasi/0.2.x/http/adapters/hono/server'; ``` ### How `openAt` works The `openAt` method opens a file relative to a directory descriptor: ```typescript dir.openAt(pathFlags, path, openFlags, descriptorFlags) ``` | Parameter | Type | Description | |-----------|------|-------------| | `pathFlags` | `{ symlinkFollow?: boolean }` | Whether to follow symlinks | | `path` | `string` | File path relative to the directory | | `openFlags` | `{ create?: boolean, truncate?: boolean, exclusive?: boolean, directory?: boolean }` | Controls file creation behavior | | `descriptorFlags` | `{ read?: boolean, write?: boolean, mutateDirectory?: boolean }` | Access mode for the opened file | Common patterns: ```typescript // Open existing file for reading dir.openAt({ symlinkFollow: true }, 'data.txt', {}, { read: true }); // Create or overwrite a file dir.openAt({ symlinkFollow: true }, 'output.txt', { create: true, truncate: true }, { write: true }); // Create a new file (fail if it exists) dir.openAt({ symlinkFollow: true }, 'new.txt', { create: true, exclusive: true }, { write: true }); ``` ## Configure volumes for `wash dev` To mount a host directory into the component during development, add a `volumes` entry to `.wash/config.yaml`: ```yaml title=".wash/config.yaml" build: command: npm run build component_path: dist/http_hello_world_hono.wasm dev: volumes: - host_path: ./testdata guest_path: /data ``` The `host_path` specifies a directory on your local machine and `guest_path` specifies the mount path as seen by the component. The component's `getDirectories()` call will include an entry for `/data` that maps to the `testdata/` directory in your project. Create some test data: ```bash mkdir -p testdata echo "Hello from the filesystem!" > testdata/hello.txt ``` ## Build and verify ### Build ```bash npm run build ``` This runs the full pipeline: 1. `wash wit fetch` — downloads `wasi:filesystem` WIT definitions into `wit/deps/` 2. `jco guest-types` — generates TypeScript type definitions in `generated/types/` 3. `rolldown` — bundles TypeScript into `dist/component.js` (leaving `wasi:` imports external) 4. `jco componentize` — compiles `dist/component.js` into a `.wasm` component ### Run ```bash npm run dev ``` This starts `wash dev`, which reads the volume configuration from `.wash/config.yaml` and mounts the specified directory. ### Test ```bash # List files in the /data directory curl http://localhost:8000/ # Read a file curl http://localhost:8000/read/hello.txt # Write a new file curl -X POST http://localhost:8000/write/greeting.txt -d "Hello, filesystem!" # Read the written file back curl http://localhost:8000/read/greeting.txt ``` Expected output: ``` {"path":"/data","entries":[{"name":"hello.txt","type":"regular-file"}]} Hello from the filesystem! {"filename":"greeting.txt","bytesWritten":18} Hello, filesystem! ``` Written files persist on the host at `testdata/greeting.txt` — you can verify with `cat testdata/greeting.txt`. ## Production deployment In a production deployment, filesystem data is provided through Kubernetes Volumes. The volume is first made available to the wasmCloud host, then mounted into the component via a WorkloadDeployment manifest. For a full walkthrough of deploying with volumes, including host deployments, WorkloadDeployment manifests, and `volumeMounts` configuration, see [Filesystems and Volumes](../../../../kubernetes-operator/operator-manual/filesystems-and-volumes.mdx). Important notes: - Volumes are defined at the host level and mounted into components via `localResources.volumeMounts` - The `mountPath` in the manifest corresponds to the guest path the component uses in `getDirectories()` - It is **not** necessary to define `wasi:filesystem` under `hostInterfaces` ## Summary: checklist for adding filesystem access 1. **Add `import` lines to `wit/world.wit`** for `wasi:filesystem/types` and `wasi:filesystem/preopens`. 2. **Run `npm run build` once** so `wash wit fetch` downloads the WIT definitions and `jco guest-types` generates TypeScript types you can reference. 3. **Import `getDirectories`** from `wasi:filesystem/preopens`. TypeScript resolves the import against the generated ambient module declarations in `generated/types/`. 4. **Use `getDirectories()`** to find preopened directories by path, then use `Descriptor` methods to read, write, and list files. 5. **Configure volumes** in `.wash/config.yaml` for local development or in a WorkloadDeployment manifest for production. 6. **No bundler changes needed** — `rolldown.config.mjs` already externalizes all `wasi:*` imports. 7. **No new npm dependencies needed** — WIT interfaces are provided by the runtime, not by npm packages. --- ## API reference: `wasi:filesystem` operations used | Operation | Signature | Description | |-----------|-----------|-------------| | `getDirectories()` | `() => [Descriptor, string][]` | Return all preopened directories with their mount paths | | `dir.readDirectory()` | `() => DirectoryEntryStream` | Open a stream to read directory entries | | `stream.readDirectoryEntry()` | `() => DirectoryEntry \| undefined` | Read the next entry (returns `undefined` at end) | | `dir.openAt(pathFlags, path, openFlags, flags)` | `(...) => Descriptor` | Open a file or directory relative to a descriptor | | `file.read(length, offset)` | `(bigint, bigint) => [Uint8Array, boolean]` | Read bytes from a file at offset; boolean is EOF flag | | `file.write(buffer, offset)` | `(Uint8Array, bigint) => bigint` | Write bytes to a file at offset; returns bytes written | | `dir.createDirectoryAt(path)` | `(string) => void` | Create a subdirectory | | `dir.statAt(pathFlags, path)` | `(...) => DescriptorStat` | Get file attributes (type, size, timestamps) | | `dir.unlinkFileAt(path)` | `(string) => void` | Delete a file | | `dir.removeDirectoryAt(path)` | `(string) => void` | Delete an empty directory | ## Further reading - [TypeScript Language Guide](../index.mdx) — toolchain overview, HTTP patterns, framework integration, and library compatibility - [Filesystems and Volumes](../../../../kubernetes-operator/operator-manual/filesystems-and-volumes.mdx) — deploying components with volume mounts in Kubernetes - [Key-Value Storage](./key-value-storage.mdx) — persistent key-value storage for structured data - [Blob Storage](./blob-storage.mdx) — streaming blob storage for larger objects - [Language Support overview](../../index.mdx) — summary of all supported languages and toolchains --- ## TypeScript Language Guide TypeScript and JavaScript are first-class languages for building WebAssembly components on wasmCloud. This guide covers the toolchain, patterns for handling HTTP requests, framework integration with Hono, and practical guidance for library compatibility. If you're looking for a quick walkthrough of creating, building, and running a TypeScript component, see the [Developer Guide](../../index.mdx?lang=typescript). ## Toolchain overview Building a TypeScript Wasm component involves a pipeline of tools that compile your code from TypeScript source to a `.wasm` binary. Understanding what each tool does will help you debug build issues and make informed decisions about your project setup. ### Build pipeline ![TypeScript build pipeline](../../../images/build-pipeline-typescript.svg) ### jco [`jco`](https://github.com/bytecodealliance/jco) is the JavaScript component toolchain maintained by the Bytecode Alliance. It is the primary CLI tool for building and working with JavaScript Wasm components. Key commands: | Command | Description | |---|---| | `jco componentize` | Compile a JavaScript file into a Wasm component | | `jco guest-types` | Generate TypeScript type definitions from WIT interface files | | `jco transpile` | Convert a Wasm component (from any language) into ES modules for Node.js or browsers | | `jco serve` | Transpile and run a component as an HTTP server | When you run `wash build` on a TypeScript project, `wash` executes the build script defined in your `package.json`, which typically calls `jco guest-types`, `tsc`, and `jco componentize` in sequence. ### ComponentizeJS [ComponentizeJS](https://github.com/bytecodealliance/ComponentizeJS) is the library behind `jco componentize`. It takes a JavaScript source file and a WIT world definition as input and produces a Wasm component binary. Because JavaScript cannot be compiled ahead-of-time to raw WebAssembly instructions, ComponentizeJS embeds a JavaScript engine inside each component. It uses [Wizer](https://github.com/bytecodealliance/wizer) to pre-initialize the engine, parse your source code, and snapshot the result. At runtime, only the pre-compiled bytecode executes — there is no parsing or compilation overhead at startup. ### StarlingMonkey [StarlingMonkey](https://github.com/bytecodealliance/StarlingMonkey) is the JavaScript engine embedded by ComponentizeJS. It is Mozilla's SpiderMonkey engine (the same engine used by Firefox) compiled to WebAssembly and targeting WASI 0.2. StarlingMonkey provides a subset of standard Web APIs inside the Wasm sandbox: - **`fetch()`** — outgoing HTTP requests (backed by `wasi:http`) - **`Request` / `Response` / `Headers`** — standard Web request and response objects - **`URL` / `URLSearchParams`** — URL parsing - **`TextEncoder` / `TextDecoder`** — text encoding - **`ReadableStream` / `WritableStream` / `TransformStream`** — WHATWG Streams - **`addEventListener('fetch', ...)`** — incoming HTTP request handling (Service Worker pattern) - **`setTimeout` / `setInterval`** — timers - **`console.log` / `console.error`** — logging (routed to WASI stderr) - **`crypto.getRandomValues()`** — cryptographic randomness (backed by `wasi:random`) These Web APIs are what make framework compatibility possible — libraries written against Web Standards (rather than Node.js APIs) can run inside a Wasm component without modification. :::note[Binary size] Each JavaScript Wasm component includes the StarlingMonkey runtime, which adds approximately 8 MB to the component binary. This is a fixed cost — it does not grow with the size of your application code. ::: ## Handling HTTP requests There are two patterns for handling incoming HTTP requests in a TypeScript component. They are **mutually exclusive** — you must choose one or the other for a given component. ### Direct `incomingHandler` export The direct approach works with the raw WASI HTTP types. You export an `incomingHandler` object with a `handle` method that receives an `IncomingRequest` and a `ResponseOutparam`: ```typescript import { IncomingRequest, ResponseOutparam, OutgoingBody, OutgoingResponse, Fields, } from 'wasi:http/types@0.2.3'; function handle(req: IncomingRequest, resp: ResponseOutparam) { const outgoingResponse = new OutgoingResponse(new Fields()); let outgoingBody = outgoingResponse.body(); { let outputStream = outgoingBody.write(); outputStream.blockingWriteAndFlush( new Uint8Array(new TextEncoder().encode('Hello from TypeScript!\n')), ); // @ts-ignore: dispose the stream before returning outputStream[Symbol.dispose](); } outgoingResponse.setStatusCode(200); OutgoingBody.finish(outgoingBody, undefined); ResponseOutparam.set(resp, { tag: 'ok', val: outgoingResponse }); } export const incomingHandler = { handle, }; ``` This pattern gives you full control over the WASI HTTP types but requires more boilerplate. ### Service Worker fetch pattern The fetch pattern uses `addEventListener('fetch', ...)` — the same API used by browser Service Workers and platforms like Cloudflare Workers. StarlingMonkey automatically maps this event listener to the `wasi:http/incoming-handler` export: ```typescript addEventListener('fetch', (event) => { const url = new URL(event.request.url); if (url.pathname === '/hello') { event.respondWith(new Response('Hello from TypeScript!\n')); } else { event.respondWith(new Response('Not found\n', { status: 404 })); } }); ``` The fetch pattern lets you work with standard Web `Request` and `Response` objects instead of low-level WASI types. This is the pattern that makes web frameworks like Hono work — they expect `Request` in and `Response` out. ### When to use which | | Direct `incomingHandler` | Service Worker fetch | |---|---|---| | **Best for** | Simple handlers, learning WASI HTTP types | Framework integration, complex routing | | **API surface** | WASI HTTP types (`IncomingRequest`, `ResponseOutparam`, etc.) | Web Standards (`Request`, `Response`, `Headers`) | | **Framework compatible** | No | Yes (Hono, itty-router, etc.) | | **Boilerplate** | More | Less | ### Outgoing HTTP requests Components can make outgoing HTTP requests using the standard `fetch()` API, which is provided by [StarlingMonkey](#starlingmonkey) and backed by `wasi:http/outgoing-handler`: ```typescript addEventListener('fetch', async (event) => { const upstream = await fetch('https://api.example.com/data'); const data = await upstream.json(); event.respondWith( new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' }, }), ); }); ``` To make outgoing requests, your WIT world must import `wasi:http/outgoing-handler`: ```wit world hello { import wasi:http/outgoing-handler@0.2.3; export wasi:http/incoming-handler@0.2.3; } ``` ## Using Hono [Hono](https://hono.dev/) is a lightweight web framework built on Web Standards. Because it uses standard `Request`/`Response` objects rather than Node.js APIs, it works naturally inside Wasm components. ### With `jco-std` (recommended) [`@bytecodealliance/jco-std`](https://www.npmjs.com/package/@bytecodealliance/jco-std) is a Bytecode Alliance library that provides standard adapters for building Wasm components in JavaScript, including a Hono adapter. This is the simplest way to use Hono with wasmCloud. ```typescript title="src/index.ts" import { Hono } from 'hono'; import { fire } from '@bytecodealliance/jco-std/wasi/0.2.6/http/adapters/hono/server'; const app = new Hono(); app.get('/', (c) => c.text('Hello from wasmCloud!')); app.get('/api/data', (c) => c.json({ message: 'This is some JSON data.' })); app.notFound((c) => c.json({ error: 'Not found' }, 404)); fire(app); export { incomingHandler } from '@bytecodealliance/jco-std/wasi/0.2.6/http/adapters/hono/server'; ``` The `fire()` function wires your Hono app to the Service Worker fetch event listener under the hood. The re-exported `incomingHandler` provides the Wasm component export that `jco componentize` expects. Install the dependencies: ```shell npm install hono npm install -D @bytecodealliance/jco @bytecodealliance/componentize-js @bytecodealliance/jco-std ``` Because `jco-std` and Hono are npm packages with multiple modules, you need a bundler to produce a single JavaScript file for componentization. [Rolldown](https://rolldown.rs/), [Rollup](https://rollupjs.org/), [esbuild](https://esbuild.github.io/), and [Rsbuild](https://rsbuild.dev/) all work. ### With the Service Worker pattern directly You can also wire Hono to the fetch event listener yourself, without `jco-std`: ```typescript title="src/index.ts" /// declare const self: ServiceWorkerGlobalScope; import { Hono } from 'hono'; const app = new Hono(); app.get('/', (c) => c.text('Hello from wasmCloud!')); app.get('/api/data', (c) => c.json({ message: 'This is some JSON data.' })); self.addEventListener('fetch', (event) => { event.respondWith(app.fetch(event.request)); }); ``` This approach gives you more control — for example, you can inject WASI configuration values into the Hono context or add custom middleware. The wasmCloud TypeScript examples repository includes a [full http-handler example built with Hono](https://github.com/wasmCloud/typescript/tree/v2/templates/http-handler-hono). ## Project structure A typical TypeScript component project looks like this: ``` my-component/ ├── .wash/ │ └── config.yaml # wash project configuration ├── generated/ │ └── types/ # Generated by `jco guest-types` — TypeScript definitions from WIT ├── src/ │ └── index.ts # Your application code ├── wit/ │ └── world.wit # WIT world definition ├── dist/ # Build output (JS and .wasm) ├── package.json └── tsconfig.json ``` ### WIT world The `wit/world.wit` file declares the interfaces your component exports and imports: ```wit title="wit/world.wit" package wasmcloud:hello; world hello { export wasi:http/incoming-handler@0.2.3; } ``` A component that exports `wasi:http/incoming-handler` is declaring that it can handle incoming HTTP requests. When you add imports (like `wasi:http/outgoing-handler` for outgoing `fetch()` calls, or `wasi:config/runtime` for configuration), `wash` automatically resolves the dependencies. :::info[WASI HTTP version] The `wasi:http/incoming-handler` version in your WIT world (here `@0.2.3`) must match the version supported by your `jco`/`componentize-js` toolchain. The version shown here matches the wasmCloud TypeScript project template. If you see build errors about missing exports, check that the version in your `world.wit` aligns with your installed `jco` version. ::: ### `.wash/config.yaml` TypeScript projects use a [project configuration file](../../../config.mdx) at `.wash/config.yaml` that tells `wash` how to build the component: ```yaml title=".wash/config.yaml" build: command: npm run install-and-build component_path: dist/http-hello-world.wasm ``` The `command` field specifies the build command (which runs the scripts in your `package.json`) and `component_path` points to the compiled `.wasm` binary. For a full reference of configuration options, see the [Configuration](../../../config.mdx) page. ### `package.json` build scripts A standard build pipeline has three steps: ```json title="package.json" { "scripts": { "generate:types": "jco guest-types wit/ -o generated/types", "build:ts": "tsc", "build:js": "jco componentize -w wit -o dist/my-component.wasm dist/my-component.js", "build": "npm run generate:types && npm run build:ts && npm run build:js", "install-and-build": "npm install && npm run build" }, "devDependencies": { "@bytecodealliance/jco": "^1.16.0", "typescript": "^5.9.0" } } ``` 1. **`generate:types`** — `jco guest-types` reads your WIT files and generates TypeScript type definitions into `generated/types/`. This gives you IDE autocompletion and type checking for WASI interfaces. 2. **`build:ts`** — `tsc` compiles TypeScript to JavaScript. 3. **`build:js`** — `jco componentize` takes the compiled JavaScript, embeds it with StarlingMonkey, and produces a `.wasm` component binary. If you use a bundler (for framework projects with npm dependencies), add a bundling step between `build:ts` and `build:js`. ### `tsconfig.json` The key configuration is the `paths` mapping, which tells TypeScript how to resolve WASI import specifiers: ```json title="tsconfig.json" { "compilerOptions": { "target": "es2022", "module": "es2022", "paths": { "wasi:http/types@0.2.3": ["./generated/types/interfaces/wasi-http-types.d.ts"] }, "outDir": "./dist/", "strict": true, "skipLibCheck": true } } ``` The `paths` entry maps `wasi:http/types@0.2.3` (the import specifier you use in your TypeScript code) to the generated type definition file. Without this, TypeScript cannot resolve the WASI imports at compile time. ## Library compatibility ### What works Libraries that target **Web Standards** — meaning they use `fetch()`, `Request`/`Response`, `URL`, `Headers`, `TextEncoder`, Streams, and similar APIs — generally work inside a Wasm component. Known compatible libraries include: - **[Hono](https://hono.dev/)** — web framework (officially supports WASI) - **[itty-router](https://github.com/kwhitley/itty-router)** — lightweight router - **[Axios](https://axios-http.com/)** — HTTP client (uses `fetch()` when available) - Pure computation libraries (parsers, validators, formatters) that don't depend on platform APIs ### What does not work Currently, **Node.js APIs are not available** inside a Wasm component. Libraries that depend on Node.js built-in modules will not work: - `fs`, `path`, `os`, `child_process` — no Node.js built-ins - `require()` — no CommonJS (ESM only) - `Buffer` — use `Uint8Array` and `TextEncoder` instead - `process.env` — use `wasi:config/runtime` instead - `net`, `http`, `https` — use `fetch()` instead This rules out frameworks like Express.js (which depends on the Node.js `http` module), most database drivers, and anything that uses native addons. ### Practical guidance - Prefer libraries designed for edge or serverless environments — they tend to use Web Standards rather than Node.js APIs - Use a bundler to combine your dependencies into a single JavaScript file before componentization - All imports must be statically resolvable — dynamic `import()` is not supported - WebSocket support is not yet available in StarlingMonkey ## Building and running ### Create a new project Use `wash new` to scaffold a TypeScript component project: ```shell wash new https://github.com/wasmCloud/typescript.git --subfolder templates/http-hello-world-fetch ``` For an HTTP handler project with Hono: ```shell wash new https://github.com/wasmCloud/typescript.git --subfolder templates/http-handler-hono ``` TypeScript templates for wasmCloud v2.x use standard `npm` commands for development loops and builds. ### Development loop Start a development loop that watches for source changes, rebuilds, and re-runs the component: ```shell npm run dev ``` TypeScript templates designed for wasmCloud v2 use [`nodemon`](https://nodemon.io/) to watch your `src/` directory and automatically re-run the build pipeline when files change. Under the hood, `npm run dev` runs `nodemon` which triggers `npm run build` on each change and serves the resulting component with `wash dev`. Send a request to test: ```shell curl localhost:8000 ``` ### Build a component Compile your project to a `.wasm` binary: ```shell npm run build ``` The compiled component is generated in the `dist/` directory by default. The output path is configured in your `package.json` build scripts. ## Working with WASI interfaces These guides walk through adding WASI interfaces to a TypeScript component: - [Key-Value Storage](./key-value-storage.mdx) — replace an in-memory data store with persistent key-value storage - [Blob Storage](./blob-storage.mdx) — use streaming blob storage for larger objects - [Messaging](./messaging.mdx) — implement communication between components over a messaging interface - [Filesystem](./filesystem.mdx) — read and write files from preopened directories - [Configuration](./configuration.mdx) — read configuration values injected from Kubernetes ConfigMaps, Secrets, and inline environment variables The patterns in these guides (declaring WIT imports, importing interfaces against ambient module declarations generated by `jco guest-types`, handling serialization) apply to any WASI interface you add to a TypeScript component. ## Further reading - [Developer Guide](../../index.mdx?lang=typescript) — quickstart tutorial for creating, building, and running a TypeScript component - [wasmCloud TypeScript templates](https://github.com/wasmCloud/typescript/tree/main/templates) — project templates including http-client, http-handler-hono, and more - [jco documentation](https://github.com/bytecodealliance/jco) — full reference for the JavaScript Component toolchain - [ComponentizeJS](https://github.com/bytecodealliance/ComponentizeJS) — details on how JavaScript is compiled to Wasm components - [StarlingMonkey](https://github.com/bytecodealliance/StarlingMonkey) — the embedded JavaScript engine and its supported Web APIs - [Hono WASI guide](https://hono.dev/docs/getting-started/webassembly-wasi) — Hono's official documentation for WebAssembly/WASI - [Component Model: JavaScript](https://component-model.bytecodealliance.org/language-support/javascript.html) — Component Model documentation for JavaScript - [Language Support overview](../index.mdx) — summary of all supported languages and toolchains --- ## Key-Value Storage(Typescript) You can add key-value capabilities to a component with the [`wasi:keyvalue`](https://github.com/WebAssembly/wasi-keyvalue/) interface. This guide walks through replacing the in-memory mock data store in the wasmCloud [`http-handler-hono`](https://github.com/wasmCloud/typescript/tree/v2/templates/http-handler-hono) template with the `wasi:keyvalue` interface. You can use this guide as a model for implementing `wasi:keyvalue` in your own components or adding any new WIT interface to an existing component. ## Overview The wasmCloud [`http-handler-hono`](https://github.com/wasmCloud/typescript/tree/main/templates/http-handler-hono) template includes an in-memory `Map` that acts as a simulated database. This means all data is lost whenever the component restarts. In this guide, we'll replace it with `wasi:keyvalue`, which gives the component persistent storage backed by a real key-value store. :::info[] By default, a wasmCloud deployment uses NATS JetStream for storage via the built-in [WASI Key-Value NATS plugin](https://github.com/wasmCloud/wasmCloud/blob/main/crates/wash-runtime/src/plugin/wasi_keyvalue/nats.rs). You can use [host plugins](../../../../runtime/index.mdx) to back `wasi:keyvalue` with a different storage solution. ::: Our changes will focus on two files: * `wit/world.wit` - Declare the new WIT imports * `src/routes/items.ts` - Use the imported interfaces in application code No changes are needed to `rolldown.config.mjs`, `tsconfig.json`, `package.json`, or any other file. :::tip[Complete example] A full working example is available as a template: ```bash wash new https://github.com/wasmCloud/typescript.git \ --name http-kv-handler-hono \ --subfolder templates/http-kv-handler-hono \ ``` ::: ## Step 1: Declare the WIT imports in `wit/world.wit` Every capability your component uses must be declared in its WIT world. Open `wit/world.wit` and add `import` lines for the interfaces you need. ```wit {4-5} package wasmcloud:templates@0.1.0; world typescript-http-handler-hono { import wasi:keyvalue/store@0.2.0-draft; import wasi:keyvalue/atomics@0.2.0-draft; export wasi:http/incoming-handler@0.2.6; } ``` ### What these interfaces provide - **`wasi:keyvalue/store`** -- Bucket-based key-value storage with `open`, `get`, `set`, `delete`, `exists`, and `list-keys` operations. - **`wasi:keyvalue/atomics`** -- Atomic operations like `increment` on numeric values in a bucket. ### How dependency resolution works When you run `npm run build`, the build pipeline calls `wkg wit fetch` as a setup step. This resolves the WIT package references (like `wasi:keyvalue`) and downloads their definitions into the `wit/deps/` directory automatically. You don't need to manually download any WIT files. The bundler (`rolldown`) is already configured with `external: /wasi:.*/` in `rolldown.config.mjs`, which tells it to leave all `wasi:*` imports as external -- they'll be resolved at component instantiation time, not at bundle time. This covers any new `wasi:` interface you add. ## Step 2: Import and use the interface in TypeScript With the WIT world updated, you can now import functions from the new interfaces in your TypeScript code. The import paths match the WIT interface names exactly. ### Importing WIT interfaces Add these imports at the top of `src/routes/items.ts`: ```typescript import { open } from 'wasi:keyvalue/store@0.2.0-draft'; import { increment } from 'wasi:keyvalue/atomics@0.2.0-draft'; ``` The `jco guest-types` command generates `.d.ts` files for your WIT interfaces into `generated/types/`, and `tsconfig.json` references them through its `types` field. TypeScript resolves the bare `wasi:` specifier imports against these generated ambient module declarations, so you get full type checking and IDE autocompletion. At runtime, `rolldown` leaves these imports external (via `external: /wasi:.*/`) and `jco componentize` wires them up during Wasm component creation. ### Key pattern: import path = WIT interface name The import path always matches the fully-qualified WIT interface name: ``` wasi:keyvalue/store@0.2.0-draft --> import { open } from 'wasi:keyvalue/store@0.2.0-draft' wasi:keyvalue/atomics@0.2.0-draft --> import { increment } from 'wasi:keyvalue/atomics@0.2.0-draft' ``` To discover which functions are available on an interface, look at the generated type file (e.g. `generated/types/interfaces/wasi-keyvalue-store.d.ts`) after running `npm run build` once, or read the WIT definition in `wit/deps/`. ### Serialization: values are `Uint8Array` The `wasi:keyvalue/store` interface uses `list` (mapped to `Uint8Array` in JS) for values. To store structured data, you need to serialize and deserialize: ```typescript const encoder = new TextEncoder(); const decoder = new TextDecoder(); function serializeItem(item: Item): Uint8Array { return encoder.encode(JSON.stringify(item)); } function deserializeItem(bytes: Uint8Array): Item { return JSON.parse(decoder.decode(bytes)); } ``` ### Opening a bucket All key-value operations happen on a **bucket**. Open one with: ```typescript function getBucket() { return open('default'); } ``` The `'default'` bucket is automatically provisioned by the wasmCloud runtime. Call `getBucket()` inside each route handler rather than at module scope -- the bucket handle should be obtained per-request. ### Key namespacing Use a prefix to separate different kinds of data in the same bucket: ```typescript const ITEM_PREFIX = 'item:'; // Item keys: "item:1", "item:2", ... // Metadata keys: "next_id" ``` This prevents collisions between item data and metadata (like the ID counter). ### Synchronous API All bucket operations (`get`, `set`, `delete`, `exists`, `listKeys`) and `increment` are **synchronous** in the JCO bindings. The WebAssembly component model uses blocking calls, so these don't return Promises. Your existing synchronous Hono route handlers work without modification. --- ## Step 3: Route-by-route changes Here is exactly what changed in each CRUD route handler, showing the before (in-memory `Map`) and after (`wasi:keyvalue` bucket). ### Removed code The entire mock data store and its initialization were removed: ```typescript // REMOVED: Simulated database const items = new Map(); let nextId = 1; // REMOVED: Sample data initialization items.set('1', { id: '1', name: 'Sample Item', description: 'This is a sample item', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); nextId = 2; ``` With persistent storage, the store starts empty and users create items via the API. Data survives component restarts. ### GET `/` -- List all items **Before:** ```typescript itemsRouter.get('/', (c) => { const itemList = Array.from(items.values()); // ... filtering and pagination ... }); ``` **After:** ```typescript itemsRouter.get('/', (c) => { const bucket = getBucket(); const keys = bucket.listKeys(undefined) as { keys: string[] }; const itemKeys = keys.keys.filter((k: string) => k.startsWith(ITEM_PREFIX)); const itemList: Item[] = []; for (const key of itemKeys) { const bytes = bucket.get(key); if (bytes) { itemList.push(deserializeItem(bytes)); } } // ... filtering and pagination unchanged ... }); ``` `listKeys(undefined)` returns all keys in the bucket. We filter for the `item:` prefix, then fetch and deserialize each item. ### GET `/:id` -- Get single item **Before:** ```typescript const item = items.get(id); if (!item) { /* 404 */ } return c.json(item); ``` **After:** ```typescript const bucket = getBucket(); const bytes = bucket.get(`${ITEM_PREFIX}${id}`); if (!bytes) { /* 404 */ } return c.json(deserializeItem(bytes)); ``` `bucket.get()` returns `Uint8Array | undefined`. If the key doesn't exist, it returns `undefined`. ### POST `/` -- Create new item **Before:** ```typescript const id = String(nextId++); // ... build newItem ... items.set(id, newItem); ``` **After:** ```typescript const bucket = getBucket(); const id = String(increment(bucket, 'next_id', 1n)); // ... build newItem ... bucket.set(`${ITEM_PREFIX}${id}`, serializeItem(newItem)); ``` `increment(bucket, 'next_id', 1n)` atomically increments the `next_id` key by 1 and returns the new value (as a `bigint`). On first call the key is created starting at 0, so the first returned ID is `1`. This is persistent and atomic -- safe even if multiple instances of the component are running. ### PUT `/:id` -- Update item **Before:** ```typescript const existing = items.get(id); if (!existing) { /* 404 */ } // ... build updated ... items.set(id, updated); ``` **After:** ```typescript const bucket = getBucket(); const bytes = bucket.get(`${ITEM_PREFIX}${id}`); if (!bytes) { /* 404 */ } const existing = deserializeItem(bytes); // ... build updated ... bucket.set(`${ITEM_PREFIX}${id}`, serializeItem(updated)); ``` ### DELETE `/:id` -- Delete item **Before:** ```typescript if (!items.has(id)) { /* 404 */ } items.delete(id); ``` **After:** ```typescript const bucket = getBucket(); if (!bucket.exists(`${ITEM_PREFIX}${id}`)) { /* 404 */ } bucket.delete(`${ITEM_PREFIX}${id}`); ``` `bucket.exists()` returns a boolean. `bucket.delete()` removes the key. ## Build and verify ### Build ```bash npm run build ``` This runs the full pipeline: 1. `wash wit fetch` -- downloads `wasi:keyvalue` WIT definitions into `wit/deps/` 2. `jco types` -- generates TypeScript type definitions in `generated/types/` 3. `rolldown` -- bundles TypeScript into `dist/component.js` (leaving `wasi:` imports external) 4. `jco componentize` -- compiles `dist/component.js` into `dist/http_handler_hono.wasm` ### Run ```bash npm run dev ``` This starts `wash dev`, which automatically provisions a NATS-KV keyvalue provider to satisfy the `wasi:keyvalue` imports. ### Test ```bash # Create an item curl -X POST http://localhost:8000/api/items \ -H "Content-Type: application/json" \ -d '{"name":"Test Item","description":"Testing keyvalue"}' # List items curl http://localhost:8000/api/items # Get by ID curl http://localhost:8000/api/items/1 # Update curl -X PUT http://localhost:8000/api/items/1 \ -H "Content-Type: application/json" \ -d '{"name":"Updated Item"}' # Delete curl -X DELETE http://localhost:8000/api/items/1 ``` To verify persistence, restart the component and confirm previously created items are still returned by `GET /api/items`. ## Summary: checklist for adding any WIT interface 1. **Add `import` lines to `wit/world.wit`** for the interfaces you need. 2. **Run `npm run build` once** so `wash wit fetch` downloads the WIT definitions and `jco types` generates TypeScript types you can reference. 3. **Import functions in TypeScript** using the exact WIT interface name as the import path. TypeScript resolves the import against the ambient module declarations in `generated/types/`. 4. **Use the imported functions** in your route handlers. Remember that WASM component model calls are synchronous. 5. **Handle serialization** -- most WIT interfaces use `Uint8Array` for binary data; use `TextEncoder`/`TextDecoder` and `JSON.stringify`/`JSON.parse` for structured data. 6. **No bundler changes needed** -- `rolldown.config.mjs` already externalizes all `wasi:*` imports. 7. **No new npm dependencies needed** -- WIT interfaces are provided by the runtime, not by npm packages. 8. **Update documentation** to reflect the new capability. --- ## API reference: `wasi:keyvalue` operations used | Operation | Signature | Description | |-----------|-----------|-------------| | `open(name)` | `(string) => Bucket` | Open a named bucket | | `bucket.get(key)` | `(string) => Uint8Array \| undefined` | Get a value by key | | `bucket.set(key, value)` | `(string, Uint8Array) => void` | Set a key to a value | | `bucket.delete(key)` | `(string) => void` | Delete a key | | `bucket.exists(key)` | `(string) => boolean` | Check if a key exists | | `bucket.listKeys(cursor)` | `(bigint \| undefined) => { keys: string[] }` | List all keys in the bucket | | `increment(bucket, key, delta)` | `(Bucket, string, bigint) => bigint` | Atomically increment a numeric key | ## Further reading - [TypeScript Language Guide](../index.mdx) — toolchain overview, HTTP patterns, framework integration, and library compatibility - [Blob Storage](./blob-storage.mdx) — Add streaming blob storage for larger objects - [Language Support overview](../../index.mdx) — summary of all supported languages and toolchains --- ## Messaging(Typescript) You can add messaging capabilities to a component with the [`wasmcloud:messaging`](https://github.com/wasmCloud/wasmCloud/tree/main/wit/messaging.wit) interface. This guide walks through the wasmCloud [`http-api-with-distributed-workloads`](https://github.com/wasmCloud/typescript/tree/main/templates/http-api-with-distributed-workloads) template, which demonstrates how to dispatch work from an HTTP API to a background component via a message broker. You can use this guide as a model for implementing `wasmcloud:messaging` in your own components. ## Overview Messaging typically spans **at least two components**. A component can import `consumer` to publish events without a handler in the same deployment; for example, the component might publish audit events that an external NATS subscriber consumes. A handler can also receive from multiple senders. The example template used on this page demonstrates a two-component request-reply pattern that may serve as a starting point. The template consists of: - **`http-api`** — An HTTP component that accepts a `POST /task` request and uses `wasmcloud:messaging/consumer` to dispatch a request message and await its reply. - **`task-worker`** — A headless component that exports `wasmcloud:messaging/handler` to receive messages, process them, and publish a reply. ![Messaging flow](../../../images/messaging-flow.svg) :::info[NATS in production] `wasmcloud:messaging` uses NATS as its production transport, built into the wasmCloud runtime. During development with `wash dev`, messaging is handled in-process; no NATS server is required. In production, the runtime connects to NATS automatically when the host starts with a NATS URL. ::: Adding `wasmcloud:messaging` in this component project touches four files: two WIT worlds and two bundler configs: * `http-api/wit/world.wit` — Declares the messaging consumer import in the HTTP component * `task-worker/wit/world.wit` — Declares the consumer import and handler export * `http-api/rolldown.config.mjs` — Extends the externals list to cover `wasmcloud:` imports * `task-worker/rolldown.config.mjs` — Same change as above Plus the two TypeScript implementation files: * `http-api/src/component.ts` — Uses `request()` to dispatch tasks and receive replies * `task-worker/src/component.ts` — Exports a `handler` object with `handleMessage()` :::tip[Complete example] A full working example is available as a template: ```bash wash new https://github.com/wasmCloud/typescript.git \ --name http-api-with-distributed-workloads \ --subfolder templates/http-api-with-distributed-workloads ``` ::: ## Step 1: Declare the WIT worlds Every capability a component uses or provides must be declared in its WIT world. This section covers: - The `http-api` WIT world — imports `consumer` only - The `task-worker` WIT world — imports `consumer`, exports `handler` - What each interface provides - How `wasmcloud:messaging` WIT dependencies are resolved ### HTTP API — import the consumer The `http-api` component only sends messages; it never receives them. In `http-api/wit/world.wit`, declare a single import: ```wit {4} package wasmcloud:templates@0.1.0; world typescript-http-api-with-distributed-workloads-api { import wasmcloud:messaging/consumer@0.2.0; export wasi:http/incoming-handler@0.2.6; } ``` ### Task worker — import consumer, export handler The `task-worker` component receives messages **and** sends replies. Receiving requires exporting `handler`; replying requires importing `consumer`. In `task-worker/wit/world.wit`: ```wit {4-5} package wasmcloud:templates@0.1.0; world typescript-http-api-with-distributed-workloads-worker { import wasmcloud:messaging/consumer@0.2.0; export wasmcloud:messaging/handler@0.2.0; } ``` ### What these interfaces provide | Interface | Direction | Purpose | |---|---|---| | [`wasmcloud:messaging/consumer`](https://github.com/wasmCloud/wasmCloud/blob/main/wit/messaging.wit) | import | Send messages (`request`, `publish`) | | [`wasmcloud:messaging/handler`](https://github.com/wasmCloud/wasmCloud/blob/main/wit/messaging.wit) | export | Receive messages (`handleMessage`) | The underlying message type used by both interfaces: ```wit record broker-message { subject: string, body: list, // Uint8Array in TypeScript reply-to: option, } ``` ### How dependency resolution works When you run `npm run build`, the build pipeline calls `wash wit fetch` as a setup step. `wasmcloud:messaging` is hosted at the `wasmcloud.com` package registry — distinct from the `wasi.dev` registry used for standard WASI interfaces. Both registries are resolved automatically; no extra configuration is required. ## Step 2: Update `rolldown.config.mjs` Unlike `wasi:` imports, `wasmcloud:` imports are not covered by the default external pattern in `rolldown.config.mjs`. Both components need this pattern extended. **Before** (default in single-component templates): ```diff - external: /wasi:.*/, ``` **After** — `http-api/rolldown.config.mjs` (required for any component using `wasmcloud:` interfaces): ```javascript {5} import { defineConfig } from "rolldown"; export default defineConfig({ input: "src/component.ts", external: [/wasi:.*/, /wasmcloud:.*/], output: { file: "dist/component.js", format: "esm", }, }); ``` Apply the same change to `task-worker/rolldown.config.mjs`. Without this, rolldown will attempt to bundle the `wasmcloud:messaging/consumer` module and fail with a "could not resolve" error. ## Step 3: Use `request()` in the HTTP API With the WIT world updated, you can import messaging functions in TypeScript using the exact WIT interface name as the import path. ### Import In `http-api/src/component.ts`: ```typescript import { request } from 'wasmcloud:messaging/consumer@0.2.0'; ``` The `jco guest-types` command (run by `npm run setup:wit`) generates `.d.ts` files in `generated/types/` that declare `wasmcloud:messaging/consumer@0.2.0` as an ambient module. Because `tsconfig.json` references those files via its `types` field, TypeScript resolves the bare specifier import with full type checking and IDE autocompletion. At runtime, `rolldown` marks the import as external and `jco componentize` wires it to the real interface when building the Wasm component. The generated signatures are available in `generated/types/interfaces/wasmcloud-messaging-consumer.d.ts` for reference: ```typescript declare module 'wasmcloud:messaging/consumer@0.2.0' { export function request(subject: string, body: Uint8Array, timeoutMs: number): BrokerMessage; export function publish(msg: BrokerMessage): void; export type BrokerMessage = import('wasmcloud:messaging/types@0.2.0').BrokerMessage; } ``` ### Calling `request()` `request()` implements the request-reply pattern: it publishes `body` to `subject`, blocks until a reply arrives (up to `timeoutMs` milliseconds), and returns the reply message. It is **synchronous**, with no `await` required. On error (timeout, no subscriber, or delivery failure) `request()` throws rather than returning. Always wrap it in `try/catch`. In `http-api/src/component.ts`: ```typescript const encoder = new TextEncoder(); const decoder = new TextDecoder(); const REQUEST_TIMEOUT_MS = 5000; app.post('/task', async (c) => { const body = await c.req.json<{ worker?: string; payload: string }>(); const subject = `tasks.${body.worker ?? 'default'}`; const msgBody = encoder.encode(body.payload); try { const reply = request(subject, msgBody, REQUEST_TIMEOUT_MS); return c.text(decoder.decode(reply.body)); } catch (err) { const message = err instanceof Error ? err.message : String(err); return c.text(`Messaging error: ${message}`, 502); } }); ``` :::warning[Timeout behavior] If no component is subscribed to `subject` within `timeoutMs` milliseconds, `request()` throws with a timeout error. During `wash dev`, the wasmCloud runtime routes calls in-process, so timeouts only occur if the handler throws or takes too long. In production, a missing NATS subscription or an unreachable server both surface as timeout errors. ::: ### How import paths map to WIT interfaces The import path is always the fully-qualified WIT interface name: ``` wasmcloud:messaging/consumer@0.2.0 → import { request, publish } from 'wasmcloud:messaging/consumer@0.2.0' ``` ### Subject naming Subjects are arbitrary dot-separated strings following NATS subject conventions. The template uses `tasks.{worker}` so that multiple worker types can each subscribe to their own sub-subject: ``` tasks.task-worker → handled by the leet-speak task-worker component tasks.summarizer → would be handled by a hypothetical summarizer component ``` In development with `wash dev`, all messages are routed in-process regardless of subject; the subject only becomes meaningful in production where NATS subscription patterns control routing. ## Step 4: Implement `handleMessage()` in the task worker The task-worker exports `wasmcloud:messaging/handler@0.2.0`. Exporting a WIT interface is different from importing one: instead of calling a function, you provide an object that the runtime calls into. ### The handler export `componentize-js` maps `export wasmcloud:messaging/handler@0.2.0` in the WIT world to a named JavaScript export. The export name is the segment of the interface path between `/` and `@`: ``` wasmcloud:messaging/handler@0.2.0 ^^^^^^^ → export name: handler ``` That export must be an object with a `handleMessage` method. You cannot rename it. In `task-worker/src/component.ts`: ```typescript export const handler = { handleMessage(msg: BrokerMessage): void { // process msg and optionally publish a reply }, }; ``` ### Replying to a message The incoming message's `replyTo` field holds the subject the original caller is listening on. Publish your response there using `publish()`. `BrokerMessage` is exported by the `wasmcloud:messaging/types@0.2.0` interface — import the type directly rather than redeclaring it. In `task-worker/src/component.ts`: ```typescript import { publish } from 'wasmcloud:messaging/consumer@0.2.0'; import type { BrokerMessage } from 'wasmcloud:messaging/types@0.2.0'; const encoder = new TextEncoder(); const decoder = new TextDecoder(); export const handler = { handleMessage(msg: BrokerMessage): void { if (!msg.replyTo) { throw new Error('missing reply_to — cannot send response'); } const payload = decoder.decode(msg.body); const result = processPayload(payload); publish({ subject: msg.replyTo, body: encoder.encode(result), }); }, }; ``` `publish()` is fire-and-forget: it delivers the message and returns. Like `request()`, it throws on delivery error. To signal a handler failure, **throw** from `handleMessage`. The thrown error propagates back to the caller: the wasmCloud runtime logs it, and if the sender called `request()`, the throw surfaces there as a caught error. ### Fire-and-forget handlers If the handler doesn't need to reply (one-way notification pattern), omit the `publish()` call: ```typescript export const handler = { handleMessage(msg: BrokerMessage): void { const event = JSON.parse(decoder.decode(msg.body)); processEvent(event); // side effects only, no reply }, }; ``` ## Step 5: Configure multi-component development A multi-component project has a root `package.json` workspace and a root `.wash/config.yaml`. The `component_path` in the config points to the primary component (the HTTP API); additional components are declared under `dev: components:`. In `.wash/config.yaml`: ```yaml new: command: npm install build: command: npm run build component_path: http-api/dist/http_api.wasm dev: components: - name: task-worker file: task-worker/dist/task_worker.wasm ``` The `dev: components:` list tells `wash dev` to deploy the task-worker alongside the main component. `wash dev` automatically links both components through the wasmCloud runtime; no NATS server is required needed during development. The root `package.json` coordinates the build across both sub-packages: ```json { "scripts": { "build": "npm run build --workspace=http-api && npm run build --workspace=task-worker", "dev": "nodemon" }, "workspaces": ["http-api", "task-worker"] } ``` :::note `npm run dev` runs `nodemon`, which watches source files and re-runs `npm run build` on changes. It handles the build side of the development loop only. Use `wash dev` to start the full environment: build, deploy, and serve. ::: ### Dependencies The task-worker has no runtime `dependencies`, only `devDependencies`. It doesn't need `hono` or `@bytecodealliance/jco-std` because it doesn't handle HTTP. All messaging plumbing comes from the WIT bindings at componentization time. In `task-worker/package.json`: ```json { "devDependencies": { "@bytecodealliance/jco": "^1.15.4", "rimraf": "^6.1.2", "rolldown": "^1.0.0-beta.47", "typescript": "^5.9.3" } } ``` ## Build and verify ### Build ```bash npm install npm run build ``` This runs the full pipeline for both components: 1. `wash wit fetch` — downloads `wasmcloud:messaging` WIT definitions into `wit/deps/` 2. `jco guest-types` — generates TypeScript type definitions in `generated/types/` 3. `rolldown` — bundles TypeScript into `dist/component.js` (leaving `wasi:` and `wasmcloud:` imports external) 4. `jco componentize` — compiles `dist/component.js` into a `.wasm` component ### Run ```bash wash dev ``` `wash dev` builds both components, starts an HTTP server on port 8000, and routes messaging calls between them in-process. ### Test Open http://localhost:8000 to use the web UI, or test with curl: ```bash # Send a task — the task-worker converts the payload to leet speak curl -s -X POST http://localhost:8000/task \ -H 'Content-Type: application/json' \ -d '{"worker": "task-worker", "payload": "Hello World"}' # => H3110 W0r1d # Missing payload returns 400 curl -s -X POST http://localhost:8000/task \ -H 'Content-Type: application/json' \ -d '{}' # => Missing required field: payload ``` ## Production deployment Both components run on the same wasmCloud host. Start the host with a NATS URL; the runtime's built-in messaging plugin connects to NATS automatically. Deploy both components together with a `WorkloadDeployment` manifest: ```yaml apiVersion: runtime.wasmcloud.dev/v1alpha1 kind: WorkloadDeployment metadata: name: http-api-with-distributed-workloads spec: replicas: 1 template: spec: hostInterfaces: - namespace: wasi package: http interfaces: - incoming-handler config: host: your-domain.example.com # HTTP Host header used for routing - namespace: wasmcloud package: messaging interfaces: - consumer - handler config: subscriptions: "tasks.>" # NATS subjects the task-worker subscribes to components: - name: http-api image: /http_api:latest - name: task-worker image: /task_worker:latest ``` Key points about the manifest: - **`hostInterfaces`** declares which built-in host capabilities the workload needs. No separate HTTP server or NATS component is listed — both are provided by the runtime. - **`interfaces: [consumer, handler]`** declares both the sending (`consumer`) and receiving (`handler`) sides of the messaging interface. The runtime automatically wires subscriptions to the component that exports `handler` (task-worker), not to http-api which only imports `consumer`. - **`subscriptions`** is a comma-separated list of NATS subject patterns. `tasks.>` matches any subject starting with `tasks.`, covering any number of worker types. - **HTTP `host` config** is the Host header the runtime uses to route incoming HTTP requests to this workload. The actual listen address is set at host startup (`--http-addr`). :::info[Subject-based routing in production] Because NATS subscriptions are configured per-workload at deploy time, subject naming becomes a contract between sender and handler. The `tasks.{worker}` convention in this template means each worker type can be independently scaled and replaced without modifying the HTTP API; just change the subscription on the new workload's manifest. ::: For Kubernetes deployment, see the [runtime-operator documentation](https://github.com/wasmCloud/wasmCloud/tree/main/runtime-operator). ## Summary: checklist for adding `wasmcloud:messaging` **For a component that sends messages (consumer):** 1. **Add `import wasmcloud:messaging/consumer@0.2.0` to `wit/world.wit`**. 2. **Add `wasmcloud:` to rolldown externals**: `external: [/wasi:.*/, /wasmcloud:.*/]`. 3. **Run `npm run setup:wit` once** to download WIT definitions and generate type stubs (`jco guest-types`). 4. **Import** `import { request, publish } from 'wasmcloud:messaging/consumer@0.2.0'` — TypeScript resolves the bare specifier against the ambient module declarations in `generated/types/`. 5. **Use `request(subject, body, timeoutMs)`** for request-reply; wrap in try/catch since it throws on error or timeout. 6. **Use `publish(msg)`** for fire-and-forget delivery with no reply expected. **For a component that handles messages (handler):** 1. **Add both `import wasmcloud:messaging/consumer@0.2.0` and `export wasmcloud:messaging/handler@0.2.0` to `wit/world.wit`**. The import is needed for `publish()` (replies); the export declares the handler. 2. **Add `wasmcloud:` to rolldown externals** (same as above). 3. **Export a `handler` object with a `handleMessage` method**: ```typescript export const handler = { handleMessage(msg: BrokerMessage): void { ... } }; ``` 4. **Use `msg.replyTo`** to find the reply subject for request-reply patterns; call `publish({ subject: msg.replyTo, body })` to respond. 5. **Throw to signal errors** — the thrown value is returned as the WIT `result` error variant. 6. **No runtime npm dependencies needed** — the handler component only needs dev tooling. **For multi-component development:** 1. **Declare `dev: components:` in root `.wash/config.yaml`** with the path to each additional component's `.wasm` file. 2. **`wash dev` routes messages in-process** — no NATS server required during development. 3. **In production**, use a `WorkloadDeployment` manifest with a `wasmcloud:messaging` hostInterface entry. Include `handler` in the interfaces list and set `subscriptions` in the `config` block. --- ## API reference: `wasmcloud:messaging` functions used | Function | Signature | Description | |---|---|---| | `request(subject, body, timeoutMs)` | `(string, Uint8Array, number) => BrokerMessage` | Publish to `subject` and block until a reply arrives (or timeout elapses). Throws on error. | | `publish(msg)` | `(BrokerMessage) => void` | Fire-and-forget publish. Throws on delivery error. | | `handler.handleMessage(msg)` | `(BrokerMessage) => void` | Called by the runtime for each delivered message. Throw to signal failure. | **`BrokerMessage` fields:** | Field | Type | Description | |---|---|---| | `subject` | `string` | The message subject (NATS topic) | | `body` | `Uint8Array` | Raw message payload | | `replyTo` | `string \| undefined` | Reply subject set automatically by `request()` | ## Further reading - [TypeScript Language Guide](../index.mdx) — toolchain overview, HTTP patterns, framework integration, and library compatibility - [Key-Value Storage](./key-value-storage.mdx) — Persistent state for a single component - [Blob Storage](./blob-storage.mdx) — Object storage for larger payloads - [Language Support overview](../../index.mdx) — summary of all supported languages and toolchains --- ## Network Access and Socket Isolation wasmCloud enforces a well-defined socket policy at the host level, giving you predictable security boundaries without requiring components to implement their own restrictions. This page explains what's allowed and denied by default, how the isolation model works, and when to use each networking pattern. ## Host policy: what's allowed and what's not The wasmCloud host applies socket policy to all workloads. The policy is implemented in the host runtime and applies regardless of what a component's code attempts to do. ### Allowed by default - **Outbound TCP connections** — components and services can connect to non-loopback addresses. This enables components to make outbound HTTP requests, connect to databases, or use any TCP-based protocol. - **TCP bind on loopback for services** — services can bind and listen on `127.0.0.1` (or the unspecified address `0.0.0.0`). This is how a service becomes the "localhost" for its workload. ### Denied by default - **DNS / name resolution** — `ip-name-lookup` is disabled by default. Components and services must use IP addresses directly rather than hostnames. If your workload requires DNS resolution, it must be explicitly enabled. - **TCP bind for regular components** — only services can bind TCP ports and act as listeners. A regular component that attempts to bind a TCP port will be denied by the host. :::info[Policy is enforced at the host level] These restrictions are enforced by the wasmCloud runtime, not by the component itself. A component does not need to implement its own socket restrictions—the host ensures policy is applied regardless of what the component code attempts. ::: ## The isolation model ### In-process loopback When a component connects to `127.0.0.1`, it does **not** reach the OS loopback interface. Instead, it connects to the service in its own workload via an in-process virtual network within the wasmCloud runtime. This means: - The connection never leaves the wasmCloud process - Components in one workload cannot reach services in another workload via loopback - The service is genuinely isolated to its own workload boundary ### Port isolation across workloads Multiple workloads on the same wasmCloud host can each have services listening on the same port (for example, port 8080) without conflict. Each workload has its own isolated loopback network, so port numbers are scoped to the workload, not the host. ### External access Because services bind to the in-process loopback network, they are not directly accessible from outside the wasmCloud process. To expose a service's functionality externally, pair it with a component that accepts external requests (for example, via `wasi:http/incoming-handler`) and proxies to the service over loopback. ## Choosing a networking pattern ### Service model (recommended for production) The [service model](../../overview/workloads/services.mdx) is the idiomatic approach for TCP communication between components and stateful processes in wasmCloud. A service runs continuously for the lifetime of the workload, binds TCP ports on the in-process loopback, and acts as the "localhost" for companion components. **Use the service model when:** - You need connection pooling, caching, or other stateful, long-running behavior - You're building with TCP-based protocols (database drivers, custom protocols) - You need to bridge between the WIT component model and existing TCP-based software - You're targeting production workloads on wasmCloud **Getting started:** The [`service-tcp` template](https://github.com/wasmCloud/wasmCloud/tree/main/templates/service-tcp) is a `wash new`-compatible Rust template for a two-component TCP service: ```shell wash new https://github.com/wasmCloud/wasmCloud.git --name my-service --subfolder templates/service-tcp ``` ### wasi-virt (for testing and cross-runtime portability) The [`wasi-virt`](https://github.com/bytecodealliance/wasi-virt) CLI tool virtualizes WASI interfaces at the component level, embedding stub implementations directly into a component binary. This is useful for: - **Unit testing** — run a component in isolation without a full runtime, with sockets virtualized to stubs that return controlled responses - **Cross-runtime portability** — produce a component that runs on runtimes that don't support `wasi:sockets` by embedding a stub implementation **`wasi-virt` is not the recommended approach for socket control on wasmCloud.** On wasmCloud, host policy is the sandboxing mechanism—you don't need component-level socket restrictions for security. Virtualizing sockets in your production component adds complexity without adding protection that the host doesn't already provide. The appropriate use of `wasi-virt` on wasmCloud is for **testing and portability**, not for production socket management. ### Host policy enforcement Because the host enforces socket restrictions unconditionally, the security model for sockets on wasmCloud is: 1. **Write your component or service** using `wasi:sockets` as needed—connect outbound, or (for services) bind and listen 2. **Trust the host** to enforce policy — regular components cannot bind, DNS is off by default 3. **Enable DNS explicitly** if your workload genuinely needs name resolution You don't need to implement your own socket access control in component code. The isolation comes from the host and the in-process network model, not from restrictions embedded in the component binary. ## Practical example: `service-tcp` template The [`service-tcp` template](https://github.com/wasmCloud/wasmCloud/tree/main/templates/service-tcp) is a two-component Rust workspace that demonstrates the full service model pattern: - **`service-leet`** is a TCP service that listens on port 7777 and transforms text to leet speak - **`http-api`** is a component that accepts HTTP requests and proxies them to `service-leet` over TCP The service entry point uses the `#[wstd::main]` macro, which satisfies the `wasi:cli/run` export requirement automatically. It binds on `0.0.0.0:7777` and accepts incoming TCP connections: ```rust use wstd::io::{AsyncRead, AsyncWrite}; use wstd::iter::AsyncIterator; use wstd::net::TcpListener; #[wstd::main] async fn main() -> anyhow::Result<()> { let listener = TcpListener::bind("0.0.0.0:7777").await?; let mut incoming = listener.incoming(); while let Some(stream) = incoming.next().await { let stream = stream?; wstd::runtime::spawn(async move { // process connection... }) .detach(); } Ok(()) } ``` The companion component connects to `127.0.0.1:7777` to reach the service. Even though the service binds on `0.0.0.0`, the in-process loopback model means this connection stays inside the wasmCloud runtime — it reaches the service in the same workload, not the OS network stack: ```rust let client = wstd::net::TcpStream::connect("127.0.0.1:7777").await?; ``` This is the core pattern: a component uses a plain TCP connect to `127.0.0.1` to reach its companion service, with the runtime enforcing workload isolation transparently. ## Keep reading - Learn more about the service model in the [Services overview](../../overview/workloads/services.mdx) and the [Creating services guide](./create-services.mdx) - Learn more about [interfaces](../../overview/interfaces.mdx) and how `wasi:sockets` fits into the WASI P2 interface set - Browse [wasmCloud examples](https://github.com/wasmCloud/wasmCloud/tree/main/examples) on GitHub --- ## Useful WebAssembly Tools Beyond the wasmCloud toolchain, the WebAssembly community maintains many open source projects that are invaluable for Wasm users. This is a non-comprehensive list of tools we've found particularly useful. ## WIT Syntax Highlighting Syntax highlighting can make WIT definitions easier to read and bring a more legible WIT experience to your IDE. Here are a few options for syntax highlighting in popular editors: ### VS Code WIT Syntax Highlighting The [VSCode extension](https://github.com/bytecodealliance/vscode-wit/) maintained by the Bytecode Alliance can be installed either via the [extensions marketplace](https://marketplace.visualstudio.com/items?itemName=BytecodeAlliance.wit-idl) or manually. ![WIT syntax highlighting](../images/wit-syntax.png) #### VS Code Extension Manual installation For a manual install, you will need [`npm`](https://www.npmjs.com/). Run: ```shell git clone https://github.com/bytecodealliance/vscode-wit.git && cd vscode-wit ``` ```shell npm ci && npm run install-plugin ``` ### Neovim WIT Syntax Highlighting The [tree-sitter-wit](https://github.com/liamwh/tree-sitter-wit/) Tree-sitter parser maintained by [liamwh](https://github.com/liamwh) can be installed with [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter). Installation instructions can be found on either of the two linked repositories. ## WebAssembly artifact manipulation with `wasm-tools` Sometimes you might want to observe or manipulate a component independently of wasmCloud—perhaps to view the component's WIT interface, compose Wasm components into one, or even for more niche jobs like converting a WebAssembly module to a component. The [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools) CLI tool is a utility belt with a long list of options. You will need [Rust and `cargo`](https://doc.rust-lang.org/cargo/getting-started/installation.html). Install `wasm-tools` by running: ```shell cargo install wasm-tools ``` ## A standalone WebAssembly runtime: Wasmtime [Wasmtime](https://github.com/bytecodealliance/wasmtime) is the WebAssembly runtime used by the wasmCloud host. If you'd like to test a component against standalone Wasmtime or explore runtime features, you can also install Wasmtime locally and use it to run components. To install on Linux or macOs run: ```shell curl https://wasmtime.dev/install.sh -sSf | bash ``` ## Virtualize components with WASI Virt [WASI Virt](https://github.com/bytecodealliance/wasi-virt) allows you to encapsulate a virtualized component within another component. This is particularly useful when you want to use potentially sensitive WASI APIs such as Sockets and Filesystem but don't want to give them access to core operating system interfaces. WASI Virt requires the nightly release channel for [Rust](https://www.rust-lang.org/tools/install): ```bash rustup toolchain install nightly ``` Install the `wasi-virt` command line tool with `cargo`: ```bash cargo +nightly install --git https://github.com/bytecodealliance/wasi-virt ``` Learn more about using WASI Virt with wasmCloud on the [Virtualize](/docs/v1/developer/components/virtualize) developer page. ## Combine components with WebAssembly Compositions (WAC) [WAC](https://github.com/bytecodealliance/wac) is a powerful CLI tool for declaratively composing components, enabling you to [link components at build](/docs/v1/concepts/linking-components/linking-at-build). WAC uses a superset of WIT called WAC to describe component relationships. You can learn how to use the WAC language in the [WAC language guide](https://github.com/bytecodealliance/wac/blob/main/LANGUAGE.md). WAC requires [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html). Once you've installed Cargo: ```shell cargo install wac-cli ``` ## Claude Code skills for WebAssembly development [Claude Code](https://docs.anthropic.com/en/docs/claude-code) users can install skills that give Claude specialized knowledge about WebAssembly component development and the `wash` CLI. Skills are reusable expertise files that Claude loads on demand. ### WebAssembly Component Development The [`webassembly-component-development`](https://github.com/cosmonic-labs/skills/tree/main/webassembly-component-development) skill provides guidance on WASI fundamentals, component composition patterns, language interoperability, runtime compatibility, and troubleshooting. Install it with: ```shell claude skill install cosmonic-labs/skills/webassembly-component-development ``` ### wash CLI The [`wash`](https://github.com/cosmonic-labs/skills/tree/main/wash) skill provides expertise on the wasmCloud Shell CLI — building, running, and managing WebAssembly components and wasmCloud applications. Install it with: ```shell claude skill install cosmonic-labs/skills/wash ``` --- ## FAQ(Wash) # Wasm Shell Frequently Asked Questions (FAQ) ## General Questions **How can I run components built with Wasm Shell?** You can use `wash` to create and publish WebAssembly components for any runtime or runtime environment that supports the [WebAssembly Component Model](https://component-model.bytecodealliance.org/), including wasmCloud, Spin, jco, wasmtime, and others. ## Differences from `wash` 1.x and below **What are the differences between __Wasm Shell__ (`wash` v2.x and higher) and __wasmCloud Shell__ (`wash` v1.x and below)?** **wasmCloud Shell**, the previous iteration of `wash`, was designed as a utility for building WebAssembly components *and* managing deployments for wasmCloud 1.x and below. **Wasm Shell** (`wash` v2.x and higher) is a more tightly focused utility for WebAssembly component development, designed to be usable across the Wasm ecosystem. This means that many commands and features that were available in `wash` v0.x are not included in Wasm Shell. The table below outlines major differences in versions: | FEATURE | Wasm Shell (v2.x and higher) | wasmCloud Shell (v1.x and below) | Notes | | ------------------------------------------------ | ---------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------- | | Create a Wasm component project | Yes, with `wash new ` | Yes, with `wash new component` | | | Start a development server | Yes, with `wash dev` | Yes, with `wash dev` | | | Build a Wasm binary | Yes, with `wash build` | Yes, with `wash build` | | | Push a component to registry as an OCI artifact | Yes, with `wash oci push` | Yes, with `wash push` | Wasm Shell uses the `oras` library—learn more in the Registries docs | | Pull a component OCI artifact from a registry | Yes, with `wash oci pull` | Yes, with `wash pull` | Wasm Shell uses the `oras` library—learn more in the Registries docs | | Self-update `wash` | Yes, with `wash update` | No | | | Inspect a component's WIT | Yes, with `wash inspect` | Yes, with `wash inspect` | | | Create a wasmCloud capability provider | No | Yes, with `wash new provider` | The capability provider model is being overhauled—learn more in the Roadmap | | Run a wasmCloud host | Yes, with `wash host` | Yes, with `wash up` | | | Manage wasmCloud deployments | No | Yes, with commands such as `wash app deploy` | In wasmCloud v2, deployments are managed via the Workload API using Kubernetes or another orchestration system of your choice | | Washboard UI | No | Yes, with `wash ui` | | ## Troubleshooting ### Building with TinyGo **How do I set up a TinyGo project for `wash build`?** TinyGo projects use a `Makefile` to wrap the build steps and a `.wash/config.yaml` file to tell `wash` which commands to run. Your `.wash/config.yaml` should specify the build command: ```yaml dev: command: make build build: command: make bindgen build ``` Your `Makefile` invokes TinyGo directly with the `-wit-package` and `-wit-world` flags: ```makefile .PHONY: build build: tinygo build -target wasip2 -wit-package ./wit -wit-world hello -o my-component.wasm ./ .PHONY: bindgen bindgen: go generate ./... ``` For full instructions, see the [Go (TinyGo) Language Guide](./developer-guide/language-support/go/index.mdx). #### Common issues with TinyGo builds There are a number of common issues with TinyGo builds that can be difficult to interpret based on error messages: ##### Imports Not Generated Properly ```shell error: failed to encode a component from module Caused by: 0: failed to decode world from module 1: module was not valid 2: failed to resolve import `wasi:http/outgoing-handler@0.2.0::handle` 3: module requires an import interface named `wasi:http/outgoing-handler@0.2.0` `wasm-tools component new` failed: exit status 1 ``` **What happened:** You don't have your imports generated properly. **What to do:** Re-generate imports, attempt to build again, and debug imports if unsuccessful. ##### WIT Files Not Available ```shell error: failed to read path for WIT [./wit] Caused by: 0: No such file or directory (os error 2) `wasm-tools component embed` failed: exit status 1 ``` **What happened:** Your WIT files aren't available at the specified path. **What to do:** Ensure that your WIT files are available at the specified path. ##### Importing Unsupported Interface ```shell failed to pre-instantiate component Caused by: 0: component imports instance `wasi:http/types@0.2.0`, but a matching implementation was not found in the linker 1: instance export `fields` has the wrong type 2: resource implementation is missing ``` **What happened:** You are importing an interface that `wash` doesn't know about. **What to do:** If you believe `wash` should support the interface, [file an issue on GitHub](https://github.com/wasmCloud/wasmCloud/issues/new). ##### Missing WIT Interface Definition ```shell ERROR tinygo build failed stderr=error: failed to encode a component from module Caused by: 0: failed to decode world from module 1: module was not valid 2: failed to resolve import `wasi:logging/logging@0.1.0-draft::log` 3: module requires an import interface named `wasi:logging/logging@0.1.0-draft` `wasm-tools component new` failed: exit status 1 TinyGo build failed: error: failed to encode a component from module Caused by: 0: failed to decode world from module 1: module was not valid 2: failed to resolve import `wasi:logging/logging@0.1.0-draft::log` 3: module requires an import interface named `wasi:logging/logging@0.1.0-draft` `wasm-tools component new` failed: exit status 1 ``` **What happened:** `wash` could not find the WIT definition. **What to do:** Verify that your WIT definition is available at the expected path. ##### World Not Found - Defaults to CLI ```shell ➜ wash inspect ./build/output.wasm package root:component; world root { import wasi:cli/environment@0.2.0; import wasi:io/error@0.2.0; import wasi:io/streams@0.2.0; import wasi:cli/stdin@0.2.0; import wasi:cli/stdout@0.2.0; import wasi:cli/stderr@0.2.0; import wasi:clocks/monotonic-clock@0.2.0; import wasi:clocks/wall-clock@0.2.0; import wasi:filesystem/types@0.2.0; import wasi:filesystem/preopens@0.2.0; import wasi:random/random@0.2.0; export wasi:cli/run@0.2.0; } ``` **What happened:** If TinyGo can't find the specified world for some reason, it assumes that you are targeting `wasi:cli/run` instead of your intended target world. **What to do:** Verify that you have correctly specified your target world in the configuration file and that your WIT definition is in the expected path. --- ## Introduction(Wash) import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Wasm Shell (wash) CLI ### **Wasm Shell** (`wash`) is the comprehensive command-line tool for developing, building, and publishing WebAssembly components. The CLI provides an intuitive developer experience for the modern Wasm ecosystem, from project scaffolding to building and pushing components to OCI registries. The Wasm Shell CLI is [available on GitHub](https://github.com/wasmcloud/wasmCloud) as part of the open source [wasmCloud](https://github.com/wasmCloud) project, hosted by the [Cloud Native Computing Foundation (CNCF)](https://www.cncf.io/). See the [Installation](../installation.mdx) page to install `wash`. ## What does `wash` do? :::note[] This quick command tour requires the [Rust toolchain](https://www.rust-lang.org/tools/install) and the `wasm32-wasip2` target for Rust: `rustup target add wasm32-wasip2` ::: Create a new component and navigate to the project directory: ```shell wash new https://github.com/wasmCloud/wasmCloud.git --subfolder templates/http-hello-world --name hello ``` ```shell cd hello ``` Build a component project into a WebAssembly binary: ```shell wash build ``` Start a component development loop: ```shell wash dev ``` Update `wash`: ```shell wash update ``` ## Next steps * Explore the `wash` CLI's [Command Reference](./commands.mdx). * Read the [Developer Guide](./developer-guide/index.mdx) to create a WebAssembly component. --- ## Registries # Wasm Shell and OCI Registries Wasm Shell enables component developers to push WebAssembly components to Open Container Initiative (OCI) registries as standard [**OCI artifacts**](https://oras.land/docs/concepts/artifact/), and to pull component artifacts from registries. OCI artifacts include a wide range of content types such as WebAssembly components, Software Bill of Materials (SBOMs), and Helm charts as well as container images. ### Authenticating to OCI registries `wash` supports loading Docker credentials. There are multiple [ways to authenticate with Docker credentials](https://docs.docker.com/reference/cli/docker/login/), including the `docker login` command with the docker CLI: ```shell docker login -u -p ``` ### How to push a WebAssembly component to an OCI registry Push the component to your registry: ```shell wash oci push ghcr.io/cosmonic-labs/components/hello:0.2.0 ./dist/http-hello-world.wasm ``` * The target registry address (including artifact name and tag) are specified for the first option with `wash oci push`. * The second option defines the target path for the component binary to push. ### How to pull a WebAssembly component from an OCI registry Pull the component from your registry: ```shell wash oci pull ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 ``` ### Further reading * `wash` uses the `oras` client library (Rust) for OCI functionality. See the [`oras` documentation](https://oras.land/docs/) for more information. * Learn more about the OCI standard at [https://opencontainers.org/](https://opencontainers.org/).