Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions docs/content/sips/026-packaging-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
title = "SIP 026 - Packaging Components"
template = "main"
date = "2026-08-25T00:00:00Z"
---

Summary: This SIP introduces a standalone component manifest (`component.toml`) and the CLI support to build and distribute an individual Spin component on its own, independent of any application. Reusable components — HTTP middleware being the prime example — can then be built, versioned, published to a registry, and pulled back down.

Owner(s): Michelle Dhanani <mdhanani@akamai.com>

Created: August 25, 2026

## Background

A Spin application is, today, always a *runnable* unit: the manifest is required
to declare one or more triggers, and the tooling (`spin up`, `spin build`,
`spin registry push`) is oriented around that assumption. This works well for
services, but it does not describe a component that is meant to be *consumed by
other applications* rather than run on its own (i.e. dependencies)

The motivating case is HTTP middleware. A component such as a GitHub OAuth gate
is not an application — it has no trigger and does nothing on its own — yet it is
highly reusable across applications. [SIP 020 (Component
Dependencies)](../sips/020-component-dependencies.md) already lets an application
consume such a component from a registry, and [SIP 024 (`spin deps` CLI
DX)](../sips/024-spin-deps-cli-dx.md) makes wiring one up interactive. The gap is
on the *author* side: there is no way to describe a single component, build it,
and publish it so that others can depend on it.

Tools such as wkg can already push and pull a bare component wasm to a registry. But a component alone advertises only its WIT world — the imports and exports it declares — not the operational configuration a consuming application must grant it: variables and secrets, allowed outbound hosts, key-value stores, SQLite databases, AI models, or mounted files. Spin needs a manifest that travels with the component so tooling can determine programmatically what a consumer must provide.

An earlier draft proposed a manifest dedicated to packaging *middleware*. But
nothing about building and distributing a component is specific to middleware: a
plain library component, a trigger-less utility, and a piece of HTTP middleware
all share the same needs. This SIP therefore supersedes that middleware-only
proposal with a general **component manifest** that applies to any component.

## Proposal

Introduce a standalone component manifest, `component.toml`, that describes a
single component: its identity, how to build it, and the host capabilities and
configuration it requires. Extend `spin build` to build from a component manifest and
embed required configuration in component binary (see [Alternatives considered](#alternatives-considered)), and extend `spin registry` to
publish and fetch components as registry packages.

### The component manifest (`component.toml`)

```toml
component_manifest_version = 1

[component]
name = "github-oauth"
source = "target/wasm32-wasip2/release/github_oauth.wasm"
version = "0.1.0"
description = "HTTP middleware that gates requests behind GitHub OAuth"
authors = ["Michelle Dhanani <mdhanani@akamai.com>"]
repository = "https://github.com/michellen/github-oauth-middleware"
license = "Apache-2.0"

[build]
command = "cargo build --target wasm32-wasip2 --release"

[requires]
variables = [
"github_client_id",
{ name = "github_client_secret", secret = true },
]
allowed_outbound_hosts = ["https://github.com", "https://api.github.com"]
key_value_stores = ["default"]
sql_variables = ["default"]

@fibonacci1729 fibonacci1729 Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sql_variables = ["default"]
sqlite_databases = ["default"]

ai_models = ["llama2-chat"]
environment_variables = ["staging", { name = "region", default = "us" }]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
environment_variables = ["staging", { name = "region", default = "us" }]
environment = ["staging", { name = "region", default = "us" }]

files = ["assets/**/*", { source = "local/path", destination = "/mounted/path" }]
```

The manifest is intentionally close to a single `[component.<id>]` entry in
`spin.toml`, but reorganised so a component can stand on its own.

#### `component_manifest_version`

`component_manifest_version = 1` identifies the file as a component manifest and
distinguishes it from an application manifest (`spin_manifest_version`). The
value is a fixed `1`; future revisions will bump it.

#### `[component]` — identity and artifact

| Field | Required | Description |
| --- | --- | --- |
| `name` | yes | The component's identifier. |
| `source` | yes | Path to the built Wasm artifact. Required because it is the file that is published and packaged. |
| `version` | for publishing | Semver version. Used as the version when publishing. |
| `description`, `authors`, `repository`, `license` | no | Human-readable metadata. |

`version` is only required when the component is published; a component that is only built locally may omit them.

`source` lives under `[component]` (not `[build]`) because it is the component's
artifact — the thing that is packaged — and exists whether or not the component
is built locally (for example, a pre-built component that is only being
republished).

#### `[build]` — how to build (optional)

Mirrors the `[component.<id>.build]` table in `spin.toml`:

| Field | Required | Description |
| --- | --- | --- |
| `command` | yes (if `[build]` present) | The build command, or an array of commands run in sequence. |
| `workdir` | no | Working directory for the build, relative to the manifest. |

`[build]` is optional: a pre-built component may be described and published with
only a `source`. A `watch` field (globs for `spin watch`) is intentionally *not*
included yet — `spin watch` does not operate on component manifests, so the field
would have no effect. It will be added together with `spin watch` support for
component manifests (see [Future work](#future-work)).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct to say a standalone component cannot have dependencies?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now at least. Not forever.


#### `[requires]` — host capabilities (optional)

Declares the capabilities the component expects the host application to grant it.
These mirror the capability fields of a `spin.toml` component:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I maybe wouldn't say these mirror the capability fields in spin.toml -- it's likely, at least some of these, will diverge from the schema associated with the name used in spin.toml (e.g. environment is divergent).


| Field | Description |
| --- | --- |
| `variables` | Configuration variables the component consumes. Each entry is a bare name, or `{ name, default, secret }`. |
| `key_value_stores` | Key-value store labels the component accesses. |
| `sql_variables` | SQLite database labels the component accesses. |

@fibonacci1729 fibonacci1729 Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `sql_variables` | SQLite database labels the component accesses. |
| `sqlite_databases` | SQLite database labels the component accesses. |

This comment was marked as resolved.

| `ai_models` | AI models the component accesses. |
| `allowed_outbound_hosts` | Outbound network destinations the component is allowed to reach. |
| `environments_variables` | Environment variables the component needs. Each entry is a bare name, or `{ name, default }`. |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `environments_variables` | Environment variables the component needs. Each entry is a bare name, or `{ name, default }`. |
| `environment` | Environment variables the component needs. Each entry is a bare name, or `{ name, default }`. |

| `files` | Files the component may read: a glob, or `{ source, destination }` mount. |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably not allow glob patterns here; globs are relative to the consuming app's directory and the dependency component has no idea what's there. What the component actually knows is the guest path it expects to read so i'd consider restricting this to { destination }.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an interesting thought @fibonacci1729. I like the idea of being able to say "I expect to find whatever files you want me to use at /foo, but it's up to you where you source them."

There is another use case, but I am not sure about generality: static assets. Here the component / dep / middleware doesn't want to put it on the app to supply files, because it already has specific ones it wants to use. E.g. the HTML, JavaScript and CSS for the "authorisation failed" page, or a CSV file of internal data. However, while this (I think) makes sense for a local component (a TOML file), we can't currently publish it as a standalone Wasm binary.


`[requires]` is descriptive: it documents what an application must provide when it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this emphasis. The component cannot grant itself the permissions because we can't trust it, but we need to know what it wants from us.

(no action, just wanted to applaud)

adopts the component. It is consumed at application-assembly time (see [Future
work](#future-work)) rather than at build time.

### Building a component

`spin build` recognizes a component manifest, runs its `[build].command`, and embeds

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recognises it by the manifest version declaration?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll need to enable push to embed metadata since build is optional.

the component manifest (omitted the `[build]` section) as JSON in a custom section of the built binary:

```console
$ spin build -f component.toml
Building component github-oauth with `cargo build --target wasm32-wasip2 --release`
Finished building all Spin components
```

When invoked without `-f`, `spin build` searches for a manifest, preferring an
application manifest (`spin.toml`) and falling back to a component manifest
(`component.toml`). If the component manifest has no `[build]` section, `spin
build` reports that there is nothing to build (the component is treated as
pre-built).

Only `spin build` operates on component manifests. `spin up`, `spin deploy`, and
similar commands continue to require an application manifest, because a lone

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEDANTRY CORNER: spin up -f bare.wasm does work. (but your point is still right of course)

component has no trigger and cannot be run on its own.

### Publishing and fetching components

Reusable components are distributed as
[wasm-pkg](https://github.com/bytecodealliance/wasm-pkg-tools) component packages
— the same package format Spin already resolves when a component declares a
registry dependency (SIP 020). This makes a published component immediately
consumable as a dependency by other applications.

#### `spin registry push`

```console
$ spin registry push -f component.toml --registry ghcr.io/michellen/spin-components
Pushed component spin-components:github-oauth@0.1.0
```

- The published **package reference** is `namespace:name` where namespace is derived from
registry or is overriden by the `--package-namespace` flag, name is `[component].name` and
the version is `[component].version`.
- The component's built `source` must exist; otherwise Spin reports an error and
suggests building first (`spin registry push --build`).
- `--build` performs a default `spin build` (component-aware) before publishing.
- `spin registry push` detects a component manifest and takes the component path;
an application manifest continues to be pushed as a Spin application OCI
artifact (with its registry reference argument), unchanged.

#### `spin registry pull`

```console
$ spin registry pull ghcr.io/michellen/spin-components/github-oauth:0.1.0 --output github-oauth.wasm
Pulled component to github-oauth.wasm
```

- The version portion is a semver requirement; when omitted, the latest
non-yanked release is pulled.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting a bit confused across OCI and wkg at this point, sorry. The draft uses OCI terminology and references in some places, and wkg in others (which is what is mentioned in the preamble).

- `--output` selects where the component Wasm is written; it defaults to
`<name>.wasm` in the current directory.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spin registry pull with applications puts them in the Spin cache I believe.


## Relationship to other SIPs

- **[SIP 020 — Component Dependencies](../sips/020-component-dependencies.md):**
this SIP is the producer side of that consumer feature. A component published
here can be referenced in another application's `[component.dependencies]` and `[trigger.dependencies]`.
- **[SIP 024 — `spin deps` CLI DX](../sips/024-spin-deps-cli-dx.md):** components
published here are exactly what `spin deps add` resolves and wires up,
including HTTP middleware.
- **[SIP 008 — OCI registries](../sips/008-using-oci-registries.md):**
application distribution continues to use OCI application artifacts.
Component distribution uses wasm-pkg component packages so components are
resolvable as dependencies.

## Future work

- **Assembling an application from `[requires]`.** Tooling like `spin deps` could
read `[requires]` and scaffold or validate the host application's grants when a
component is adopted.
- **`component.toml` discovery for more commands.** Only `spin build` and `spin
registry` recognise component manifests today; `spin watch` and others could
follow if there is demand.
- **Enabling composition** for standalone components. Components should be able to
consume dependencies the same as a component in a Spin application manifest.

## Alternatives considered

- **Publish a component as a single-component Spin application.** A component
could be wrapped in a synthetic `spin.toml` and pushed as an application OCI
artifact. This reuses the application pipeline but produces an artifact that is
semantically an *application* (with no trigger) and is **not** resolvable as a
component dependency, defeating the primary purpose. Publishing a wasm-pkg
component package instead makes the result directly consumable by spin, wkg and
potentially other tools.
- **A middleware-specific manifest.** The original draft targeted middleware
only. Since building and distributing a component is not middleware-specific,
a general component manifest serves middleware and all other reusable
components with one mechanism.
- **Using OCI annotations** to relay the `[requires]` information was considered rather
than embedding component manifest in the component binary but that would lock component packages to OCI registries and we may want the option to distribute via github releases or other distribution platforms.
9 changes: 8 additions & 1 deletion docs/content/sips/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,11 @@ Here is a list of accepted SIPs:
- [Support for Inbound WebSockets](../sips/016-inbound-websockets.md)
- [Application-Internal Service Chaining](../sips/017-service-chaining.md)
- [Adding OTel tracing to Spin](../sips/018-adding-otel-tracing-to-spin.md)
- [Spin Governance](../sips/019-governance.md)
- [Spin Governance](../sips/019-governance.md)
- [Component Dependencies](../sips/020-component-dependencies.md)
- [Spin Factors](../sips/021-spin-factors.md)
- [Build Profiles](../sips/022-build-profiles.md)
- [Fine-grained Capability Inheritance for Component Dependencies](../sips/023-fine-grained-capability-inheritance.md)
- [spin deps CLI DX](../sips/024-spin-deps-cli-dx.md)
- [Target Environment Validation](../sips/025-validate-target-environment.md)
- [Packaging Components](../sips/026-packaging-components.md)
Loading