Skip to main content
This guide covers the most common issues you may encounter when integrating Ribaunt and how to resolve them. For each issue you’ll find the likely cause and the exact fix to apply.
Cause: RIBAUNT_SECRET is missing from your environment, or it is not loaded into the process before the first call to createChallenge or verifySolution.Fix: Ensure the variable is defined before your server handles its first request. If you use a .env file, load it at the top of your entry point with a tool like dotenv:
import 'dotenv/config'; // Must come before any ribaunt imports
import { createChallenge } from 'ribaunt';
For hosted runtimes (Vercel, Railway, Fly.io, etc.), set RIBAUNT_SECRET through your platform’s environment variable dashboard rather than committing it to source control. Rotate the secret immediately if it is ever accidentally exposed.
Cause: The browser solver requires a secure context. Plain HTTP on any host other than localhost does not qualify. This commonly affects devices on a local network accessed via an IP address such as http://192.168.x.x.Fix:
  • Production: always serve your app over HTTPS.
  • Development: use http://localhost rather than your machine’s LAN IP. If you need other devices on the network to reach your dev server, terminate TLS with a tool like mkcert or a local reverse proxy.
Cause: The challenge token’s TTL elapsed before the browser submitted its solution. This can happen on pages that take a long time to load, forms where users spend several minutes before submitting, or if the server and client clocks are significantly skewed.Fix: Increase ttlSeconds in your createChallenge call to give users more time. The default is 30 seconds; consider 120–300 for complex pages or forms:
// Allow up to 5 minutes to complete the form
const tokens = createChallenge(5, 4, 300);
Cause: The same challenge tokens were submitted more than once. This is intentional — Ribaunt’s default local replay store marks tokens as consumed on first use and rejects any subsequent submission.Fix: If this is happening in normal user flows, it most likely means your frontend is re-submitting the same tokens (e.g. on form retry). Issue fresh tokens for each attempt by calling your challenge endpoint again before re-submitting.If you are seeing this in serverless or multi-instance deployments, the issue is that each function invocation has its own in-memory store. Tokens consumed in one instance are invisible to others. Switch to replayPrevention: 'remote' with a distributed store:
const isValid = await verifySolution(tokens, solutions, {
  replayPrevention: 'remote',
  replayStore: {
    consume: async (jti, expiresAt) => {
      // Use Redis/Valkey SET NX EX for atomic consume semantics
      const result = await redisClient.set(jti, '1', { NX: true, EXAT: expiresAt });
      return result === 'OK';
    },
  },
});
Cause: The auto-verify attribute is absent or the widget is in a disabled state, which prevents auto-verify from firing even when set.Fix: Add auto-verify="true" to the HTML element, or autoVerify={true} in the React wrapper:
<ribaunt-widget
  challenge-endpoint="/api/challenge"
  verify-endpoint="/api/verify"
  auto-verify="true"
></ribaunt-widget>
Also check that disabled is either absent or explicitly set to "false". While the widget is disabled, auto-verify will not trigger.
Cause: The widget script is not loading correctly. Common causes include an incorrect path to the built file, loading the script without type="module", or a bundler that is not resolving the package’s browser export correctly.Fix: Load the widget script as a module and verify the path points to dist/widget-browser.js:
<script type="module" src="/node_modules/ribaunt/dist/widget-browser.js"></script>
If you are bundling the widget yourself, make sure your bundler resolves the browser export condition from the Ribaunt package. Check your framework’s documentation for how to configure export conditions if the widget is tree-shaken or not found.
Cause: The default local replay store lives in process memory. In serverless environments each function invocation is a fresh process, so tokens consumed in a previous invocation are unknown to the next one. This causes the in-memory store to treat every submission as its first — but if two concurrent invocations race, both may consume the same token, or neither may hold state long enough to protect against replay.Fix: Switch to replayPrevention: 'remote' with a Redis or Valkey adapter that uses atomic SET NX EX semantics:
const isValid = await verifySolution(tokens, solutions, {
  replayPrevention: 'remote',
  replayStore: {
    consume: async (jti, expiresAt) => {
      const result = await redis.set(`ribaunt:${jti}`, '1', {
        NX: true,
        EXAT: expiresAt,
      });
      return result === 'OK';
    },
  },
});
Cause: The <ribaunt-widget> web component registers itself using browser-only APIs such as customElements and window. These are not available during server-side rendering.Fix: Add 'use client' at the top of any component file that imports or renders RibauntWidget:
'use client';

import { RibauntWidget } from 'ribaunt/widget-react';
Do not wrap the component with next/dynamic — the Ribaunt React wrapper already handles dynamic importing of the browser bundle internally. Adding a second layer of dynamic import can interfere with this mechanism and cause double-loading or hydration mismatches.

Enabling debug logging

By default, Ribaunt logs verification warnings to the console when NODE_ENV is set to development. In other environments, warnings are silent unless you opt in. Use the debug option to enable console output explicitly, or use onWarning for structured logging that works in any environment.
const valid = await verifySolution(tokens, solutions, {
  debug: true, // Logs warnings to console in development
  onWarning: (w) => console.log(w.reason, w.message),
});
In NODE_ENV=development, debug is enabled by default — you don’t need to pass it explicitly during local development. In production, rely on onWarning instead of debug: true to avoid flooding application logs with low-signal noise.
Use the onWarning callback in production to capture structured warning reasons (invalid-token, expired-token, replay-detected, etc.) for monitoring without flooding logs. Route warning events to your observability platform (Datadog, Sentry, etc.) to get visibility into unusual patterns without enabling verbose console output.