Skip to main content

Background

A web app on Cowboy runs as a CIP-10 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). 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 §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

--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. The proxy table the template writes:
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: 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:
Any JOSE library works the same way. With jose:
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.
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:
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

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:

Worked example: Elmo

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.