The challenge-response flow
Every Ribaunt verification follows a three-step cycle.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.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.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 true.Challenge tokens
Each challenge token is a standard signed JWT. When decoded, its payload contains:| Field | Description |
|---|---|
challenge | A random base64 string generated fresh for each token |
difficulty | The number of leading zeros the SHA-256 hash must start with |
expires | A Unix timestamp (seconds) after which the token is rejected |
jti | A UUID that uniquely identifies this token, used for replay prevention |
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.
| Setting | Approximate solve time | Recommended use |
|---|---|---|
createChallenge(4, 4, 30) | Milliseconds | Fast / background checks |
createChallenge(5, 4, 60) | ~1 second | Moderate / form submission |
createChallenge(5, 8, 120) | ~2 seconds | High / sensitive actions |
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.