# Fermyon Spin

A Spin component can read secrets without holding any. You declare the
**seekrit secrets component** as a dependency in `spin.toml`; Spin composes it
with your component at load time, and your code calls an interface:

```rust
use seekrit::secrets::store;

let api_key = store::get("STRIPE_API_KEY")?.expect("STRIPE_API_KEY is in scope");
```

The seekrit component holds the service token, makes one call to the API, and
decrypts inside its own linear memory. Your component never sees the token, and
no plaintext exists anywhere in the Spin host.

> **Note:** This page is the Spin-specific setup. The component itself, its interface, and the other runtimes it works on are covered in [WebAssembly components](/docs/guides/wasm).

## Setup

Mint a read-only token bound to one environment, and export it the way Spin
reads application variables:

```bash
seekrit token create --name spin-checkout --app checkout --env production
export SPIN_VARIABLE_SEEKRIT_TOKEN="skt_..."
```

Add the dependency to `spin.toml`:

{/* x-release-please-start-version */}

```toml
[component.app]
source = "target/wasm32-wasip2/release/app.wasm"
allowed_outbound_hosts = ["https://api.seekrit.dev"]
dependencies_inherit_configuration = true

[component.app.variables]
seekrit_token = "{{ seekrit_token }}"

[component.app.dependencies]
"seekrit:secrets/store" = { version = "0.3.0", package = "seekritdev:secrets-spin", registry = "ghcr.io" }
```

{/* x-release-please-end */}

Declare the variable at the top of the manifest so Spin refuses to start
without it, rather than failing on the first request:

```toml
[variables]
seekrit_token = { required = true }
```

Then `spin build && spin up`. A full handler is in
[A complete example](#a-complete-example) below.

This setup is verified against Spin 4.1.0. The dependency mechanism and Spin's
`wasi:config` version have both been in place since Spin 3.0.

## Four things that must be right

Each of these is a start-up or first-request failure whose message does not
point at seekrit.

### The package is `secrets-spin`

seekrit publishes the same component several times, because hosts disagree
about how to spell one interface version.

| Reference | Imports | For |
| --- | --- | --- |
| `seekritdev:secrets-spin` | `wasi:config/store@0.2.0-draft-2024-09-27` | Spin |
| `seekritdev:secrets` | `wasi:config/store@0.2.0-draft` | wasmCloud 1.x |
| `seekritdev:secrets` (`-rc1` tags) | `wasi:config/store@0.2.0-rc.1` | wasmCloud `wash-runtime` |

The interface is identical in all of them — same functions, same types. Only
the package version differs, and wasmtime gives pre-release versions no
semver-compatible matching, so those three are unrelated names to the linker.
Using the wrong one stops `spin up` before it serves anything:

```
Error: component imports instance `wasi:config/store@0.2.0-draft`,
but a matching implementation was not found in the linker

Caused by:
   0: instance export `get` has the wrong type
   1: function implementation is missing
```

### `dependencies_inherit_configuration` must be on

Spin gives a dependency none of the parent component's permissions by default.
Without this flag the seekrit component cannot make its outbound call and
cannot read the variable holding its token:

```toml
[component.app]
dependencies_inherit_configuration = true
```

The flag is all-or-nothing across a component's dependencies, so set it only
when you trust all of them. Without it the app starts normally and the first
request comes back:

```
not configured: no service token: set SEEKRIT_TOKEN
(or the host's `seekrit_token`) to an skt_ token
```

### The outbound host must be allowed

Spin's outbound HTTP is default-deny. Without `https://api.seekrit.dev` in
`allowed_outbound_hosts`, the component returns `unavailable` and names the
allowlist in the message.

### The token goes in `variables`, not `environment`

The seekrit component reads `wasi:config` first and the environment second.
On Spin, use `wasi:config` — which is what `[component.app.variables]` feeds.

`component.app.environment` looks like an alternative and is not: Spin's
manifest expressions are only supported in a short list of fields, and
`environment` is not one of them. The only way to set a token there is to write
it literally into a file you commit.

> **Warning:** Never put a `skt_` token in `spin.toml`, and never compile one into a component. Spin applications are pushed to OCI registries; a token in either place is a published credential.

## Where the token comes from

One credential reaches Spin, and everything else is derived from it. In
development that is an environment variable:

```bash
export SPIN_VARIABLE_SEEKRIT_TOKEN="skt_..."
```

In a deployment it is whatever your platform already uses to hand a process one
value — a Kubernetes secret, a CI secret, your host's credential store. If your
platform has a Spin **variables provider**, declaring it in
`runtime-config.toml` points `seekrit_token` at it and changes nothing in the
component or the manifest.

Whichever you pick, it carries one short-lived, read-only token scoped to one
environment — not the secrets themselves. That is the point of the setup: the
thing your platform has to protect is a credential you can revoke, and the
values behind it are never in its custody at all.

## Rotation and caching

The seekrit component memoizes its decrypted set per **component instance**.
Spin instantiates a component per request, so in a Spin app the cache holds for
one request and nothing survives it: a busy app makes one resolve per request,
and a rotated secret is picked up on the next one.

Failures are never cached. An API that was briefly unreachable is retried on
the next request rather than remembered.

## A complete example

Add a `wit/world.wit` naming what your component imports:

```wit
package myapp:handler;

world app {
  import seekrit:secrets/store@0.1.0;
}
```

and generate bindings from it:

```rust
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_service;

spin_sdk::wit_bindgen::generate!({
    path: "wit",
    world: "app",
    runtime_path: "::spin_sdk::wit_bindgen::rt",
    generate_all,
});

use seekrit::secrets::store;

#[http_service]
async fn handle(_req: Request) -> anyhow::Result<impl IntoResponse> {
    let key = match store::get("STRIPE_API_KEY") {
        Ok(Some(v)) => v,
        // `ok(none)` means the token resolved and this name is not in scope —
        // a configuration answer, not an outage.
        Ok(None) => return Ok(text(500, "STRIPE_API_KEY is not in scope")),
        Err(e) => return Ok(text(503, &format!("{e:?}"))),
    };

    charge(&key)?;
    Ok(text(200, "charged"))
}

fn text(status: u16, body: &str) -> impl IntoResponse {
    Response::builder()
        .status(status)
        .header("content-type", "text/plain")
        .body(body.to_string())
}
```

> **Note:** `spin_sdk::dependencies!()` is the shorter way to get these bindings, and it does not work for this component. It generates from the `root` world of the `spin-dependencies.wit` that `spin build` writes, and Spin selects that world's interface by bare name. This component imports `wasi:config/store` and exports `seekrit:secrets/store` — both called `store` — so the match lands on the wrong one and you get `cannot find module or crate seekrit`. Generating from your own `wit/` sidesteps it. Composition is unaffected: Spin composes against the real component imports, not that file.

`store::Error` is a WIT variant, not a Rust `std::error::Error`, so match on it
rather than reaching for `?`. The four cases map to the four setup problems
above: `not-configured` (no token reached the component), `denied` (the API
said no), `unavailable` (could not reach the API — usually the allowlist), and
`decrypt` (the response arrived and would not decrypt). None of them carries a
value, a token, or ciphertext, so all four are safe to log.

## The zero-effort alternative

If you do not need decryption inside the sandbox, `seekrit run` works on Spin
today with no component at all, because Spin reads `SPIN_VARIABLE_*` from its
own environment:

```bash
seekrit run --app checkout --env production -- spin up
```

Every secret granted to the token becomes a `SPIN_VARIABLE_*` value that Spin's
environment provider picks up. The difference: the plaintext exists in the Spin
host process, which is the thing the component approach avoids. Use this for
local development and the component for anything running untrusted code.

## Next steps

- [WebAssembly components](/docs/guides/wasm) — the interface, the other
  runtimes, and how the component is published
- [Service tokens](/docs/guides/service-tokens) — scoping and rotating the
  credential Spin passes in
- [Secret references](/docs/guides/references) — `${...}` expansion, which the
  component performs before your code sees a value
