> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cowboy.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# Ship a Web App

> Put an existing web app on Cowboy behind cby-hire-proxy, with Sign in with Cowboy instead of your own accounts

## Background

A web app on Cowboy runs as a [CIP-10](https://docs.cowboy.inc/cips/cip-10-runner-containers.md) persistent workload: a long-running container on a Runner, reached through the gateway at its owner actor's address. People pay for it by hiring its Store listing ([CIP-33](https://docs.cowboy.inc/cips/cip-33-actor-hiring-and-distribution.md)). Every workload image is built on a Cowboy base (`cby-runner-node`, `cby-runner-python` or `cby-runner-rust`), and every base ships `cby-hire-proxy`.

`cby-hire-proxy` is the workload's front door. For each request it checks the caller's Cowboy account signature ([CIP-15](https://docs.cowboy.inc/cips/cip-15-public-asset-hosting.md) §8.15), checks that the account holds an active, paid-up hire of your listing, and only then forwards the request to your app on loopback. An account without a hire gets a 403 and never reaches your code.

## Goal

Your app should not need its own sign-up, passwords or API-key issuing to know who is calling. With **Sign in with Cowboy** the proxy hands your app a short-lived signed identity token on every request, and your app trusts it the way it would trust an SSO login. This page takes you from nothing to a deployed, signed-in web app, then shows how Elmo, a real Store listing, uses the same pieces.

**What you'll learn:**

* How `cowboy workload init --template web-app` lays out a project
* What the identity token carries and how to verify it, by hand or with `jose`, passport or next-auth
* When to use a per-hire credential instead, and how the provisioning hook creates it
* How to deploy, list and test the app

## Prerequisites

* `cowboy` with `cowboy workload init` (`cowboy workload init --help`)
* Node.js 24 for the template's tests
* An environment whose network registers a workload resource class (`small` on canyon) and the `cby-runner-node` base
* The account that will publish your Store listing

## 1. Scaffold

```bash theme={null}
mkdir notes && cd notes
cowboy workload init notes --template web-app --creator 0xYourPublisherAccount
```

`--creator` is the account that signs your listing's `declare`; the proxy serves hirers of any listing that account publishes for the owner actor. Once the listing exists you can pin it instead with `--declaration-id 0x<64 hex>`. `--login` picks how the app learns who is calling: `sign-in` (the default), `credential`, or `both`. Add `--dry-run` to see every file first.

| Path | What it is |
| - | - |
| `cowboy.toml` | `[workloads.notes]` with its route (`/*`) and its `proxy` table. |
| `actors/notes_owner/` | The owner actor. It owns the workload and the route; it has no behavior of its own. |
| `workloads/notes/server.mjs` | The app. It listens on `127.0.0.1:$PORT`, which the proxy sets to 3000. |
| `workloads/notes/cowboy-login.mjs` | Verifies the identity token with `node:crypto`; no dependencies. |
| `workloads/notes/cowboy-login.test.mjs` | `npm test`. |
| `workloads/notes/Dockerfile` | The base's own Node pin, for running the app locally the same way. |

The proxy table the template writes:

```toml theme={null}
[workloads.notes.proxy]
upstream = "http://127.0.0.1:3000"

[workloads.notes.proxy.login]
ttl_s = 60

[workloads.notes.proxy.hire]
creator = "0xYourPublisherAccount"

[[workloads.notes.proxy.health.checks]]
kind = "http"
url = "http://127.0.0.1:3000/_cowboy/health"
```

`cowboy deploy` adds the rest (RPC URL, chain id, network, owner actor and workload id) from the environment you deploy to.

## 2. Who is calling

With `[workloads.<name>.proxy.login]` every request that passed the signature and hire checks reaches your app with an `x-cowboy-id-token` header. It is an ES256 JWT shaped like an OIDC ID token:

| Claim | Value |
| - | - |
| `sub` | The caller's Cowboy account, `0x` + 40 lowercase hex. Use it as the user id. |
| `hire_id` | The hire the request came in on, as a decimal string. |
| `listing` | That hire's listing (declaration id), `0x` + 64 hex. |
| `iss` | `urn:cowboy:<network>:<chain_id>:workload:<workload id>:hire-proxy` |
| `aud` | `login.audience` when set, else `urn:cowboy:<network>:<chain_id>:workload:<workload id>` |
| `iat`, `nbf`, `exp` | Issued now; expires after `login.ttl_s` seconds (default 60, at most 300). |
| `jti` | A random id, unique per token. |
| `actor`, `workload`, `network`, `chain_id` | This deployment. |

The proxy starts your app with what it needs to check the token: `COWBOY_JWKS_URL` (the proxy's JWKS on loopback, `http://127.0.0.1:$PORT/_cowboy/.well-known/jwks.json`), `COWBOY_ID_TOKEN_ISSUER`, `COWBOY_ID_TOKEN_AUDIENCE` and `COWBOY_ID_TOKEN_HEADER`. Check the signature, `iss`, `aud` and `exp`, then treat `sub` as signed in.

The template's helper does exactly that:

```js theme={null}
import { cowboyLogin } from "./cowboy-login.mjs";
const login = cowboyLogin(); // reads the four COWBOY_ variables
const claims = await login.user(req); // null without a token; throws if it does not verify
```

Any JOSE library works the same way. With [`jose`](https://github.com/panva/jose):

```js theme={null}
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(new URL(process.env.COWBOY_JWKS_URL));
const { payload } = await jwtVerify(req.headers["x-cowboy-id-token"], jwks, {
	issuer: process.env.COWBOY_ID_TOKEN_ISSUER,
	audience: process.env.COWBOY_ID_TOKEN_AUDIENCE,
	algorithms: ["ES256"],
});
```

With passport, use `passport-jwt` with `ExtractJwt.fromHeader("x-cowboy-id-token")`, `jwks-rsa`'s `passportJwtSecret({ jwksUri: process.env.COWBOY_JWKS_URL })`, and the same `issuer`, `audience` and `algorithms`. With next-auth, add a Credentials provider whose `authorize` runs the `jwtVerify` check on the request header and returns `{ id: payload.sub }`. The proxy publishes an OIDC discovery document at `/_cowboy/.well-known/openid-configuration` naming the issuer and JWKS, but it has no authorization or token endpoint: the token arrives on every request, so there is no redirect login to configure.

> [!NOTE]
>
> **Keep sessions short.** The proxy re-checks the hire on each request, caching a found hire for 15 seconds, so a lapsed or revoked hire stops at the proxy within seconds. A session cookie your app keeps beyond the token's `exp` outlives that check. Re-verify the token on each request, or expire your session with it.

### Keys and rotation

The signing key is generated inside the proxy process when the workload starts and is never written to disk, logged or sent anywhere, so it cannot leave the workload and no other workload can mint tokens your app accepts. A restart makes a new key and drops the old one, so a token minted just before a restart stops verifying once your app refetches the JWKS; the next request carries a fresh one. Every `login.rotate_s` seconds (default 3600) the proxy signs with a new key and keeps the previous one in the JWKS for one more period, longer than any token it signed lives. Libraries that cache the JWKS refetch it when they see an unknown `kid`; the template's helper does the same.

## 3. A per-hire credential instead

Some apps already have their own tenants and API keys and should keep them. Use `--login credential` (or `both` for the token as well). The proxy then sends each hire's own key as `Authorization: Bearer <key>`, creating it the first time the hire calls:

```toml theme={null}
[workloads.notes.proxy.credential]
file = "/tmp/cowboy-hires/{hire_id}/credential.json"
json_key = "api_key"
header = "authorization"
prefix = "Bearer "

[workloads.notes.proxy.hook]
url = "http://127.0.0.1:3000/_cowboy/provision"
```

On a hire's first request the proxy POSTs the hire's chain row (`hire_id`, `hirer`, `declaration_id`, its term and status) to `hook.url`, once, with other requests for that hire waiting. Your endpoint creates whatever the hire needs (an organization, a user, an API key) and answers `{"api_key": "..."}`; the proxy writes that answer to `credential.file` and injects it from then on. A failed or malformed answer writes nothing and the next request tries again, so the endpoint must be safe to call twice for one hire.

`hook.url` must be a loopback URL under `/_cowboy/`. The proxy never forwards any `/_cowboy/` path from outside, signed or not, so only the proxy can reach your provisioning endpoint. Put `credential.file` on a mounted rw volume to keep keys across restarts.

## 4. Build and deploy

```bash theme={null}
(cd workloads/notes && npm test)
cowboy env use <env>
cowboy deploy notes      # first run: the owner actor and its route
cowboy workload build notes
cowboy deploy notes      # the workload
```

The proxy config inside the image names the owner actor's address and the workload id derived from it, so the image can only be built once the owner actor exists: the first deploy creates it and stops, the second builds and deploys the workload. `cowboy deploy` prints the gateway URL; `cowboy workload status notes` shows the running attempt. `GET /_cowboy/health` through the gateway is 200 once the app answers its own health path.

## 5. List it

Publish a Store listing for the owner actor from the account you named in `--creator`. From then on, every account that hires the listing can use the app, and its first request is its sign-in.

To test without the Store UI, hire the listing from a second account and send a signed request. Any request without a hire, or with a stale or forged signature, stops at the proxy:

| Request | Answer |
| - | - |
| Signed by an account with an active hire | 200 from your app, with `x-cowboy-id-token` |
| Signed by an account with no hire | 403 `no active hire of this listing for the signing account` |
| Unsigned, outside `[[proxy.public]]` | 400 (no route context) or 401 (no account signature) |
| A signed request sent a second time | 401 `account signature replay` |
| Any path under `/_cowboy/` other than health and the JWKS | 404 |

## Worked example: Elmo

[Elmo](https://github.com/cowboyinc/ext-elmo), an AI search visibility tracker, is a Store listing built this way. It is a full Next.js app with Postgres and a worker, so it shows the parts a real app adds:

* **One workload, many processes.** `run` starts `supervisor.mjs`, which starts Postgres, runs migrations, then the worker and web server on `127.0.0.1:3000`. The proxy fronts the web server.
* **A private volume.** `[[workloads.elmo.mounts]]` mounts the `ext-elmo-data` volume rw at `/app/offchain/data`, which holds each hire's config and credential and Postgres snapshots.
* **A per-hire credential.** Elmo already scopes everything to an organization and an API key, so it uses `[proxy.credential]` with `header = "authorization"`. Its hook, `provision-hire.mjs`, reads the hire's `config.json`, creates the organization, brand and key in Elmo, and writes `data/hires/<hire_id>/credential.json`. A new app would serve that logic at `/_cowboy/provision` and set `hook.url`; Elmo runs it as `hook.command`, which the proxy also supports.
* **A public page.** `[[workloads.elmo.proxy.public]] path_prefix = "/manage"` serves the management page without a signature; the page's own API calls are signed.
* **Listing by creator.** `hire.creator` names the publisher account, so the image is built before the listing exists.

An Elmo built today could also turn on `[proxy.login]` and read `sub` from the token, for example to show which account a brand belongs to, while keeping its keys.

## Security notes

* The proxy trusts the RPC it is configured with to report account policies and hires; point it at an RPC you operate.
* The token is only ever set by the proxy: any `x-cowboy-id-token` or credential header a caller sends is removed first, on public paths too.
* Lifetimes are short by construction: tokens live at most 300 seconds, and the signature on each request is valid for at most 300 seconds with a single-use nonce.
* The token says who is calling; it is not proof of the request it came with. It names no method, path or body, and nothing marks it used. The proxy already enforced the per-request signature and its single-use nonce, so trust the token as it arrives from the proxy and do not accept it from anywhere else as authorization for an operation.
* The audience names this workload. If you set `login.audience`, give each workload its own value. If your app forwards the token to another service, that service must check `aud` and refuse tokens meant for someone else.
* The proxy refuses any path it would not forward exactly as written (backslashes, dot segments, encoded slashes), so a path can't be spelled to reach `/_cowboy/` after normalization.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.