proven on the hosted testnet instance · 2026-09-13 · QA-14
Bring your own issuer
What you will have at the end
Section titled “What you will have at the end”Your users authenticating to Permafrost with tokens your own issuer signed. A tenant per user, created on first use and keyed by that user’s Sui address. Nothing shown once, nothing to store, nothing to mail: no API key is ever minted for a user on this lane.
Everything downstream is identical to the key lane. The edge injects the same headers with the same values, so the storage layer cannot tell the two lanes apart, and every command in the Quickstart works unchanged with a token in the bearer slot.
Prerequisites
Section titled “Prerequisites”- An issuer you control that signs compact JWTs and publishes a JWKS over HTTPS. If you already sign anything for your own users, you have one.
- Ed25519 signing keys. See the token rules below — this is the only algorithm accepted.
- A Sui address per user. The token’s subject is that address, so your system has to know it at signing time.
- One email to
hello@permafrost.livewith the JSON below. We enable it; you cannot enable it yourself.
What you send us
Section titled “What you send us”One object per issuer:
{ "issuer": "https://issuer.example/", "jwks_url": "https://issuer.example/.well-known/jwks.json", "audience": "permafrost-edge"}audience is ours and is the same for everyone: permafrost-edge. The other two
are yours. Send the exact strings your tokens will carry — iss is matched
against issuer literally.
The rules a token has to meet
Section titled “The rules a token has to meet”All of them, on every request:
algisEdDSA(Ed25519). Every other algorithm is rejected before a key is loaded — includingnone, and including every HMAC algorithm.kidis required and must name a key in your JWKS withkty: OKPandcrv: Ed25519. Akidwe have not seen forces exactly one refresh of your JWKS, never a loop.issis the issuer you registered, andaudis your audience — either as a string, or as an array containing it.expis present and has not passed, andnbf, if you send it, has. Both are checked with up to a minute of clock skew allowed, so an issuer whose clock has drifted will start failing.subis the user’s Sui address in canonical form:0xfollowed by 64 lowercase hex characters. It must arrive canonical. The subject is signed by you, so nothing folds case on this path — an upper-case address in a signed token is refused, not repaired.
What happens on first use
Section titled “What happens on first use”The subject is the tenant. The first request carrying a valid token for an address provisions that tenant with the same defaults a redeemed invite gets, and every later request reuses the same row. Two first requests racing each other converge on one tenant, because the address is the primary key.
No key is issued at any point in that. There is nothing to show once, nothing to rotate, and nothing for a user to lose. Revoking is one call on our side against the tenant, and it denies both lanes at once.
Every failure looks the same
Section titled “Every failure looks the same”A refused token is answered 401, and that 401 is identical whatever went
wrong: expired, not yet valid, wrong audience, unknown kid, wrong algorithm,
subject that is not a canonical address, an issuer we have never heard of. No
claim, no issuer name and no reason is echoed back. A failure to fetch your JWKS
denies as well, rather than answering a server error.
This is deliberate. The response cannot be used to map what we have configured — and it has a practical consequence: you cannot debug a token against our answer. Verify it against your own JWKS first, then ask us.
What we do on our side
Section titled “What we do on our side”We add your issuer to this instance’s roster and restart the edge. Your keys are fetched before the port opens, and refreshed in the background before the cache goes stale, so a user request never waits on a fetch of your JWKS.
Until we have done that, a token from your issuer is refused exactly like an
unknown key — the same identical 401. There is no state you can query to find
out whether we have enabled you yet, so the sequence is: send the JSON, wait for
us to confirm, then try.
One case stays open after that. A key you rotate without announcing it, in the middle of a cache window, is discovered by a request that then has to wait for the fetch — and that one request is denied while the fetch completes and fills the cache. The next request succeeds. Tell us before you rotate and it does not happen at all.
The cost of this lane, stated plainly
Section titled “The cost of this lane, stated plainly”A token-lane tenant has no self-service view. /portal and /me
authenticate by pf_sk_, and this lane has no key to present, so neither answers
for your users — no tier page, no usage page, no key list, nothing to rotate. See
Keys, rotation, and the portal for what your
users are not getting.
Their usage is visible to us on the operator surface instead, and we will tell you on request. Teaching those two routes this lane is not built.
Put the token where the quickstart puts the key — it is the same bearer slot:
export AUTH="Authorization: Bearer <the token your issuer signed>"Then run the Quickstart from the top. Nothing else in it changes.
How you know it worked
Section titled “How you know it worked”The bucket call at the top of the quickstart answers 200 under a token your
issuer signed. Then sign a second, fresh token for the same subject and list that
bucket with it: the object you stored under the first token is there. Same
subject, same tenant.
Before we have enabled you, the same call answers the identical 401 an unknown
key gets. That is the signal to ask us rather than to keep trying.
Permafrost runs on Sui testnet and Walrus testnet. Everything here describes a shipped testnet instance, not a production service.