Engineering · TypeScript · 2026
revocable-jwt
Stateless JWT auth you can actually revoke, plus the OTP and rate limiting a phone-first login needs. Zero dependencies, WebCrypto only, so it runs on Deno, Node, Bun and Cloudflare Workers.
The problem
A JWT is a signed claim that carries its own authority. Any process holding the secret can verify one without asking a database, and that is the whole difficulty: there is no way to take one back. Until it expires, whoever holds it is that user.
The usual answers each give something up. Short lifetimes shrink the window without closing it. A blocklist of revoked tokens costs a lookup per request on a set that grows forever. A session table brings back the state you were trying to avoid.
The mechanism
Put a version number in the token, keep the current version on the user’s row, and compare them at verification. Changing a password, losing a device or downgrading a role becomes one UPDATE: every token the user holds now carries a number that is no longer current. The token itself is never touched.
The cost is one indexed read per verification, on a row most endpoints load anyway. In exchange, revocation is constant-time, with nothing to prune and nothing that grows.
Two granularities
A version revokes a whole account. It cannot sign out one device and leave the others alone. That is what the refresh tokens are for: they live in a table, one row per session, so a single session can be closed on its own. Revoking everything does both at once, so nothing can be exchanged for a fresh pair in the gap.
The race that breaks rotation
Two requests arrive with the same refresh token: a retry, a flaky network, two tabs. If the code reads the row, checks it, then marks it spent, both requests pass and one single-use token becomes two live sessions. Consuming the token has to be one statement, and its row count has to be the answer:
UPDATE auth_refresh_tokens
SET revoked_at = $3, replaced_by_hash = $2
WHERE id = $1 AND revoked_at IS NULL
RETURNING id;
Zero rows back means someone else already spent it. There is a test for exactly this case.
Why not an off-the-shelf provider
The audience it was designed for does not have a working email habit. An email or OAuth wall is not a login screen to them, it is the end of the signup. So the identifier is a phone number, the proof is a code by SMS, and verification runs on edge functions with no session store. Small enough to read in an afternoon, which is the only reason writing it was defensible.
What it does not do
It does not revoke a whole token family when a spent refresh token is reused, it signs with HS256 only, it has no key-rotation helper, and nobody has audited it. It is a reference implementation, and the README says so before anything else.