Asymmetric, and that is the whole design
Registration is the kit’s account of the same act, scopes is what a key may do, how your data reaches your browser is the path it takes, and revoking us is the property all of it exists to make true.
The dashboard signs each call with an Ed25519 private key. You register the matching public key.
Nothing secret of yours reaches us, and nothing we hold is worth stealing from you.
Why not a shared secret
Our breach would become your breach.
A per-customer shared secret means a compromise on our side yields keys replayable against every customer’s production Worker until each of them rotates.
Under asymmetric, a full compromise of our infrastructure leaks private keys that are useless the moment you revoke the corresponding public one — and revoking is a row you delete.
Why not OAuth, or Cloudflare Access
OAuth exists so a third party can act on behalf of an end user. There is no user delegation on this leg — it is our server talking to your Worker — and turning every adopter’s Worker into an OAuth provider is a large surface for a property nobody needs.
Access service tokens and mTLS both move the authorization decision out of your Worker into Cloudflare’s edge configuration. That needs Zero Trust, and it breaks the one sentence this design exists to keep true: your Worker is the authority.
One keypair per connection
Per customer, per project, per environment.
A leak exposes one connection rather than a fleet, and rotation and revocation are naturally scoped.
We generate it, and the private half never leaves our infrastructure. Your CLI generating one and sending it to us would put key material on the wire for no benefit.
Every call is bounded
| Single-scope | The one scope this call needs — never the whole grant |
| Single-use | Spent on arrival, in a database insert that cannot race |
| Body-bound | Carrying the SHA-256 of the body, so a payload cannot be swapped |
| Sixty seconds | And a token claiming longer is rejected outright |
| Environment-bound | A staging credential cannot reach production |
Eleven checks, every one default-denying, and one error code for all of them — because distinguishing unknown key from bad signature from replayed token would tell an attacker exactly how far they got.
Rotation cannot lock you out
Append, prove, then expire. The order is the safety property.
The new key is registered signed with the current one, so trust flows forward from trust. Then a real call proves the new key works. Only then is the old one expired — and that call must itself be signed with the successor, because naming a live key is not proof you can sign with it.
Your Worker refuses an expiry that would leave no live key at all.
No session, no user row
A dashboard user is not a user of your app.
Minting a user row for one would put people who never signed up into your user table, your counts, and your data-subject exports.
Exactly two things outlive a call: the spent token id, and the audit event.