Server to server

Three shapes, and they are not interchangeable

Choosing a client credential is the decision above this one. The three below are: your own trusted server minting a token, a management client you built yourself, and a store calling you — which is webhooks and replay.

You areUse
Your own trusted server, acting for a userA bearer token with the right scope
A management client, administering the projectThe control-plane seam
A store, delivering an eventA signed webhook

Your own trusted server

This is the one that submits scores and records rating outcomes.

Both are server-authoritative by default, and each needs a scope you mint for your server’s token and never for a player’s.

POST /leaderboard/weekly-distance     + the submit scope
POST /rating/games/duel/outcomes      + the record scope

The body carries the result and nothing else — the player comes from the authenticated session, the timestamp from the server’s clock, the pool from config.

The same shape covers a ledger movement over HTTP: credit and debit routes take a session plus the configured admin scope, because a user who could credit themselves is not a ledger.

In-process beats HTTP when you own both ends

If the caller is your own Worker, call the capability directly.

const ledger = openLedger(env.DB);
await ledger.credit("alice", "chips", 1000, "signup-bonus:alice");

The ledger is a server-authoritative primitive, and its in-process API is the primary interface — the HTTP routes exist for the cases where the caller is somewhere else.

A management client

Not a session. Not a bearer token.

A signed, single-scope, single-use, body-bound, sixty-second token, verified against a public key you registered and can revoke.

Your Worker is the authority — you hold the public key, you decide the scopes, and revoking is a row you delete.

Build your own with the contract module: it carries the six calls, the six response shapes and the hosted origin, reaches for no timer, no fetch and nothing from node, and compiles in a Worker as readily as in a build script.

Discover, do not configure. Compose your calls from the manifest, which names the resolved paths — a client that hardcoded /payments would 404 against exactly the adopters who customized anything.

An inbound webhook

The sender’s own proof over the exact bytes received, with the timestamp inside the signed payload.

Any route can declare the strategy — the payment rails are callers of the same guard rather than a second copy of it.

Minting a token for CI

pithy token

No minted secret value is printed, logged, or put in an audit event by any command in the kit — which is a property worth not undermining with an environment variable in a CI log.

What never works

A user id in a request body. Every route binds to the authenticated caller, and no route accepts an identity the caller asserts.

ESC