The challenge-response flow
Every Ribaunt verification follows a three-step cycle.1
Server issues challenge tokens
Your server calls
createChallenge(), which produces an array of signed JWT tokens. Each token encodes a random challenge string, the difficulty level (how many leading zeros the winning hash must start with), an expiry timestamp, and a unique token ID (jti) for replay prevention. The tokens are signed with your RIBAUNT_SECRET, so they cannot be forged or tampered with by the client.2
Browser solves the challenges
The browser receives the tokens and runs the solver. For each token it decodes the challenge string and difficulty, then iterates through candidate nonces —
0, 1, 2, … — computing SHA-256(challenge + nonce) for each one using the Web Crypto API. The moment it finds a nonce whose hash starts with the required number of leading zeros, it records that nonce and hash as the solution and moves on to the next token.3
Server verifies the solutions
The browser submits the original tokens alongside the solutions it found. Your server calls
verifySolution(), which:- Verifies the JWT signature with
RIBAUNT_SECRET - Checks that the token has not expired
- Recomputes
SHA-256(challenge + nonce)and confirms it starts with the required leading zeros - Checks the
jtiagainst the replay store to ensure the token has never been used before
verifySolution() return { valid: true }.Challenge tokens
Each challenge token is a standard signed JWT. When decoded, its payload contains:
The tokens are signed with
RIBAUNT_SECRET, an environment variable you set on your server. Your server is the only party that can produce or validate tokens — the browser only ever sees the signed, opaque JWT.
Never expose
RIBAUNT_SECRET to the browser. In Next.js, do not prefix it with NEXT_PUBLIC_.Difficulty
Thedifficulty parameter controls how many leading zeros the winning hash must have. Because each additional zero cuts the probability of a random hash qualifying by a factor of 16, every increment roughly doubles the expected number of hash attempts — and therefore the solve time.
Stateless design
Ribaunt does not require a database to issue or verify challenges. All the information the server needs to verify a solution — the challenge string, the required difficulty, and the expiry — is encoded directly in the signed JWT token. Verification reduces to checking the JWT signature and recomputing the hash; no round-trips to a data store are necessary for the core proof check. Replay protection is the one area that requires state. Without it, a single valid solution could be resubmitted repeatedly within the token’s TTL window. Ribaunt handles this with a lightweight replay store:- In the default
localmode, an in-processMaptracks used token IDs within the current Node.js process. - In
remotemode, you supply a distributed store (such as Redis or Valkey) so that multiple processes or serverless function instances share a consistent view of which tokens have already been consumed.
For more detail on configuring replay protection, including a full Redis example, see Replay Protection.
Secure context requirement
The browser solver uses the Web Crypto API (crypto.subtle), which browsers only expose in secure contexts. This means client-side solving works on:
https://origins (production)http://localhost(local development)
http://192.168.x.x may not expose crypto.subtle, especially on mobile browsers. Always serve your application over HTTPS in production.