Read this as a table of two columns
The strategy column is verification strategies, the scopes are dashboard scopes, refusals carry an error code, and what mounts them is the Worker contract.
A path, and the strategy that guards it. There is no implicit authentication anywhere in the kit, so every row below names one of five:
| Strategy | Who gets in |
|---|---|
bearer | A short-lived access token in Authorization: Bearer |
session | A cookie session, CSRF-guarded |
signed-webhook | The sender’s own proof, over the exact bytes received |
control-plane | A scoped machine credential the customer issued. Default-denied |
public | Nobody is checked. A deliberate choice, written down |
bearer and session are one guard in practice — requireAuth() accepts either, and the rows below say user where that guard runs. What separates them is the client, not the route.
Every base path is yours to move
The prefixes below are defaults. Each is a config key, and moving one moves every route under it.
| Capability | Default prefix |
|---|---|
| Auth | /auth |
| Payments | /payments |
| Storage | /storage |
| Media | /media |
/email | |
| Vector | /vector |
| Secrets | /secrets |
| Support | /support |
| Testers | /testers |
| Audit | /audit |
| Ledger | /ledger |
| Leaderboard | /leaderboard |
| Matchmaking | /matchmaking |
| Multiplayer | /multiplayer |
| Rating | /rating |
| The control plane | /control-plane |
Auth
| Route | Strategy |
|---|---|
POST /auth/token/rotate | public + CSRF — the refresh credential is the proof |
GET /auth/devices | user |
POST /auth/devices/revoke | user + CSRF |
GET /auth/admin/users | control-plane |
GET /auth/admin/users/:userId | control-plane |
GET /auth/admin/devices | control-plane |
POST /auth/admin/sessions/revoke | control-plane |
POST /auth/admin/users/:userId/sessions/revoke | control-plane |
POST /auth/admin/users/:userId/devices/revoke | control-plane |
ALL /auth/* | Better Auth’s own surface |
That last row is most of what a sign-in flow actually calls. Sign-in, sign-out, the OAuth callback, verification, /token, /jwks and session listing are Better Auth’s endpoints, mounted under the same prefix and answering in Better Auth’s own response shape — deliberately, because createAuthClient is a documented client surface and re-homing the body would strand every adopter reading error.code.
The admin routes are registered before the catch-all, or they would be dead: /auth/admin/users would reach Better Auth, which knows no such endpoint, and the 404 would come from the wrong layer entirely.
When Turnstile is composed, the magic-link and OTP send routes wear the humanity check automatically. You do not wire that.
Payments
| Route | Strategy |
|---|---|
POST /payments/purchases | user |
GET /payments/entitlements | user |
POST /payments/restore | user |
POST /payments/checkout | user |
POST /payments/portal | user |
GET /payments/pricing | user |
POST /payments/webhooks/apple | signed-webhook |
POST /payments/webhooks/google | signed-webhook |
POST /payments/webhooks/stripe | signed-webhook |
POST /payments/webhooks/lemon-squeezy | signed-webhook |
POST /payments/webhooks/paddle | signed-webhook |
GET /payments/admin/catalog | control-plane |
GET /payments/admin/purchases | control-plane |
GET /payments/admin/subscriptions | control-plane |
GET /payments/admin/entitlements | control-plane |
GET /payments/admin/entitlements/:subjectType/:subjectId | control-plane |
GET /payments/admin/reconcile-runs | control-plane |
GET /payments/admin/discounts | control-plane |
POST /payments/admin/discounts | control-plane |
POST /payments/entitlements/grant | control-plane |
POST /payments/entitlements/revoke | control-plane |
A grant is a control-plane call, not an admin user’s click. Comping somebody a year of Pro leaves a row signed by an issued credential with a scope on it, which is a different kind of record from a session that happened to belong to staff.
Storage
| Route | Strategy |
|---|---|
POST /storage | user — start an upload |
GET /storage | user — list |
POST /storage/:id/complete | user |
POST /storage/:id/abort | user |
GET /storage/:id/parts | user |
POST /storage/:id/copy | user |
POST /storage/:id/shares | user |
DELETE /storage/:id/shares/:token | user |
GET /storage/:id/url | user — presign |
PATCH /storage/:id | user |
DELETE /storage/:id | user |
HEAD /storage/:id | user |
GET /storage/:id | public, and the handler authorizes |
GET /storage/share/:token | public — the token is the credential |
GET /storage/:id carries no requireAuth, on purpose: a public object has to be readable without a session, so authorization moves into the handler where it can see whether this object is public. The HEAD beside it does require a session — a metadata probe is not a download.
Media
| Route | Strategy |
|---|---|
POST /media | user |
GET /media | user |
POST /media/:id/finalize | user |
POST /media/duplicates | user |
GET /media/:id | user |
DELETE /media/:id | user |
| Route | Strategy |
|---|---|
GET /email/jobs | control-plane · email:jobs:read |
GET /email/jobs/:id | control-plane · email:jobs:read |
POST /email/jobs/:id/retry | control-plane · email:jobs:retry |
GET /email/suppressions | control-plane · email:suppressions:read |
POST /email/suppressions | control-plane · email:suppressions:write |
POST /email/suppressions/remove | control-plane · email:suppressions:delete |
GET /_pithy/email/c/:token | public — a click |
GET /_pithy/email/o/:token | public — an open |
GET, POST /_pithy/email/u/:token | public — unsubscribe |
There is no end-user surface in the management file at all. A recipient interacts with email by receiving it; the three routes a recipient calls are the public callbacks, and each is gated by the signature on the token in its path.
Removing a suppression is a POST with a body, not DELETE /suppressions/:address. An address in a path is an address in every access log, proxy, trace and referrer between the client and the Worker.
Vector
| Route | Strategy |
|---|---|
POST /vector/:index/documents | user |
POST /vector/:index/query | user |
GET /vector/:index/documents/:id | user |
DELETE /vector/:index/documents/:id | user |
Secrets
| Route | Strategy |
|---|---|
GET /secrets/admin/status | control-plane · secrets:status:read |
GET /secrets/admin/status/:name/rotations | control-plane |
POST /secrets/admin/status/:name/rotate | control-plane |
Status, and never value. Nothing on this surface returns a secret; it returns whether one exists, when it was last rotated, and what happened on each rotation.
Audit
| Route | Strategy |
|---|---|
GET /audit/events | control-plane |
GET /audit/events/:eventId | control-plane |
Reads only. The trail is append-only, and this surface has no write route to omit — there is nothing to leave out, because there is nothing there.
Support
| Route | Strategy |
|---|---|
POST /support/feedback | user — open a thread |
GET /support/feedback | user — my threads |
GET /support/feedback/:id | user |
GET /support/threads | control-plane |
GET /support/threads/:id | control-plane |
GET /support/replies | control-plane |
POST /support/threads/:id/archive | control-plane |
POST /support/threads/:id/reply | control-plane |
POST /support/threads/:id/reclassify | control-plane |
POST /support/threads/:id/flags | control-plane |
Testers
| Route | Strategy |
|---|---|
GET /testers/confirm/:token | public — the invitation link |
GET /testers/opt-in/:token | public |
GET, POST /testers/opt-out/:token | public |
GET /testers/status | user |
GET /testers/cohorts | control-plane |
POST /testers/invite | control-plane |
POST /testers/resend | control-plane |
POST /testers/remove | control-plane |
POST /testers/nudge | control-plane |
Opting out answers both verbs. A one-click unsubscribe header sends a POST; a person clicking the same link in a mail client sends a GET. Refusing either would refuse somebody trying to leave.
Ledger
| Route | Strategy |
|---|---|
GET /ledger/:currency | user — my balance |
GET /ledger/:currency/transactions | user |
POST /ledger/:currency/credit | user + an admin scope |
POST /ledger/:currency/debit | user + an admin scope |
GET /ledger/admin/accounts | control-plane |
GET /ledger/admin/accounts/:userId | control-plane |
GET /ledger/admin/accounts/:userId/:currency/transactions | control-plane |
The two write routes stack a second guard on the first. A session alone reaches the balance; moving a balance also requires the configured admin scope on the caller. A user who could credit themselves is not a ledger.
Games
Leaderboard
| Route | Strategy |
|---|---|
GET /leaderboard | user |
GET /leaderboard/:board/top | user |
GET /leaderboard/:board/me | user |
GET /leaderboard/:board/around | user |
POST /leaderboard/:board/segment | user |
PUT /leaderboard/:board/me/visibility | user |
POST /leaderboard/:board | user + the submit scope, when server-authoritative |
PUT /leaderboard/:board/entries/:userId/hidden | user + admin scope |
DELETE /leaderboard/:board/entries/:userId | user + admin scope |
Rating
| Route | Strategy |
|---|---|
POST /rating/games/:game/outcomes | user + the record scope, when server-authoritative |
GET /rating/games/:game/me | user |
GET /rating/games/:game/players/:userId | user |
Multiplayer
| Route | Strategy |
|---|---|
POST /multiplayer/games/:game | user |
POST /multiplayer/sessions/:id/join | user |
POST /multiplayer/sessions/:id/action | user |
POST /multiplayer/sessions/:id/leave | user |
POST /multiplayer/sessions/:id/close | user |
GET /multiplayer/sessions/:id | user |
GET /multiplayer/sessions/:id/result | user |
GET /multiplayer/sessions/:id/socket | user — the WebSocket upgrade |
Matchmaking
| Route | Strategy |
|---|---|
POST /matchmaking/games/:game/rooms | user |
POST /matchmaking/rooms/:code/join | user |
POST /matchmaking/games/:game/invites | user |
GET /matchmaking/invites | user |
POST /matchmaking/invites/:id/accept | user |
POST /matchmaking/invites/:id/decline | user |
GET /matchmaking/friends | user |
POST /matchmaking/friends/:userId/request | user |
POST /matchmaking/friends/:userId/accept | user |
POST /matchmaking/friends/:userId/decline | user |
DELETE /matchmaking/friends/:userId | user |
POST /matchmaking/games/:game/queue | user |
GET /matchmaking/games/:game/queue | user |
DELETE /matchmaking/games/:game/queue | user |
GET /matchmaking/presence | user |
serverAuthoritative is the switch worth understanding before you ship a game. With it on, a score or an outcome is only accepted from a caller carrying the configured scope — which means from your own server, not from a client somebody can open in a debugger.
The control plane, and the kit’s own paths
| Route | Strategy |
|---|---|
GET /control-plane/ping | control-plane — any verified caller |
GET /control-plane/manifest | control-plane · manifest read |
GET /control-plane/keys | control-plane · keys rotate |
POST /control-plane/keys | control-plane · keys rotate |
POST /control-plane/keys/:keyId/expire | control-plane · keys rotate |
GET /health | public — status and the deployed version |
POST /__pithy/workflows/:binding | the kit’s own durable-job dispatch |
GET /__pithy/dev-login | local development only |
/health is public because a health check that needs a credential is a health check nothing runs.
Two things this list implies
Your asset allowlist is derived from this table. run_worker_first has to name the paths the Worker owns, and every path above is one — which is why the CLI computes the list rather than asking you to keep one by hand.
A capability you have not composed mounts nothing. These are not reserved paths waiting to be claimed. Remove the capability and its prefix is free.