Calling from a browser

The problem

A browser cannot hold a long-lived signing key. Shipping one to a page is shipping it to every extension, every debugger, and every copy of that page’s storage.

The answer

The management client’s server mints a short-lived, single-scope, user-bound token and hands it to the browser, which then calls your Worker directly.

Response bodies never touch their origin on that path, and their private key never leaves their server.

Your Worker cannot tell the difference, deliberately

A browser call and a machine call are the same 60-second, single-scope, body-bound token, verified by the same code.

There is no second token class to get wrong, and no route whose auth model depends on which client made the call.

What does transit their infrastructure

The calls a management client makes from its own server, which for the hosted dashboard are two: the ping that proves a new connection answers, and the key rotation that appends a successor. Both have to be server-side, because only a server holds a private key — and neither carries a pane’s data.

For anything that does reach their origin, the commitment is a zero-logging policy on your data: response bodies fetched from your Worker are not logged, cached, or persisted.

What is stored is theirs — users, subscriptions, connection metadata, and their own private keys. Never your users, your purchases, your audit events, or your support mail.

Eleven things a call must satisfy

Every step default-denies.

  1. Read the key id from the token header and the audience from its claims. Untrusted at this point — a parsed token is a well-formed shape, never an authentic one.
  2. Load the connection the audience names. None registered: controlplane/not_connected.
  3. The key must exist on that connection, not be revoked, and be inside its validity window.
  4. Verify the signature via WebCrypto. The algorithm is pinned to the literal EdDSA at parse — the algorithm-confusion defense.
  5. The audience must equal the loaded connection’s id, and its environment must equal this Worker’s. A staging credential cannot reach production.
  6. The issuer must equal the one that connection was registered against.
  7. Expiry and issued-at are checked with a bounded clock skew, and a token claiming a lifetime longer than the configured maximum is rejected outright.
  8. The token id is claimed in the replay set, as a single-use insert.
  9. The SHA-256 digest of the raw body is recomputed and compared. A signature over claims that did not bind the body would let an attacker swap the payload.
  10. The scope check.
  11. An audit event is emitted — allowed or denied — and only then does the handler run.

The replay guard is a guarantee, not a best effort

The claim is a single-use insert where the token id is the primary key — insert, on conflict do nothing, returning. The insert either wins the key or conflicts, so there is no window between deciding and recording, and of any number of concurrent presentations SQLite admits exactly one, wherever those requests landed.

It is the same move auth makes on the other side, consuming a refresh token with a conditional delete.

The key is the token id alone, not the pair of token and connection. A composite would let a token captured from one connection be spent again against another — which is the property the guard exists to deny. The connection id is recorded beside it for the incident, never for the decision.

The cost of closing it is one database write, and here that is close to free. A control-plane hot path is an administrator clicking something — low volume, high privilege — which is a different calculation from a per-request user path.

Rows carry an expiry and are pruned after a successful claim, so the table cannot only grow. Pruning deliberately does not run on a refused claim — otherwise replaying one token in a loop would drive an unbounded delete per attempt.

The KV backend is still selectable behind the same interface. It remains best-effort, with the race above as its stated price, and is a reasonable trade only where every management operation is idempotent. Choosing it also brings back the KV binding; the default needs no KV namespace at all.

Nothing is left behind

A control-plane call creates nothing in Better Auth. That is precisely why this is its own strategy rather than a flavor of bearer.

A dashboard user is not a user of your app. Minting a user or session row for one would put people who never signed up into your user table — inflating your counts, appearing in your data-subject exports, and creating a credential that might reach routes it was never scoped for.

The verified caller lands on c.var.controlPlane, never on c.var.auth. If it landed on auth, every requireAuth() in every capability would pass for a management client — a scope escalation across the whole tree from one convenient assignment. A test asserts the isolation, because it is the kind of property that regresses silently.

Exactly two things outlive a call: the token id in the replay set, and the audit event. Both tables are in the reference.

ESC