Skip to main content
This guide shows you how to add the two Ribaunt CAPTCHA API routes to a Next.js application — one to issue challenges and one to verify solutions. Both the App Router (app/ directory, Next.js 13+) and the Pages Router (pages/api/, Next.js 12 and below) are covered below.
Never prefix RIBAUNT_SECRET with NEXT_PUBLIC_. Variables prefixed that way are bundled into the client at build time and exposed to every browser. Keep this value server-only.

Environment setup

Add your secret to .env.local. Next.js loads this file automatically and keeps its values server-side only:
# .env.local — server-side only
RIBAUNT_SECRET="your_very_strong_random_secret_string"

App Router (Next.js 13+)

Place these files at app/api/captcha/challenge/route.ts and app/api/captcha/verify/route.ts.
Create the two route handler files shown below. Each file exports a named HTTP-method function (GET or POST) — the App Router convention.
import { NextResponse } from 'next/server';
import { createChallenge } from 'ribaunt';

export async function GET() {
  try {
    const challenges = createChallenge(5, 4, 60);
    return NextResponse.json({ challenges });
  } catch (error) {
    return NextResponse.json({ error: 'Failed to generate challenge' }, { status: 500 });
  }
}
createChallenge(5, 4, 60) generates 4 signed challenge tokens at difficulty 5, each valid for 60 seconds. The function returns an array of JWT strings; wrapping it in { challenges } matches the response contract the Ribaunt widget expects.

Pages Router (Next.js 12 and below)

If you are using the pages/api/ directory, create the two handler files below. Each exports a default async function that receives NextApiRequest and NextApiResponse.
import type { NextApiRequest, NextApiResponse } from 'next';
import { createChallenge } from 'ribaunt';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'GET') {
    return res.status(405).json({ error: 'Method Not Allowed' });
  }

  try {
    const challenges = createChallenge(5, 4, 60);
    res.status(200).json({ challenges });
  } catch (error) {
    res.status(500).json({ error: 'Failed to generate challenge' });
  }
}

Serverless and edge deployments

The default replayPrevention: 'local' mode stores used token IDs in an in-process Map. When your application runs as serverless functions or across multiple instances, each cold start begins with an empty store, so a token solved against one instance can be replayed against another.To prevent cross-instance replays, pass replayPrevention: 'remote' together with a replayStore adapter backed by an atomic distributed store (for example Redis with a SET NX operation):
const isValid = await verifySolution(tokens, solutions, {
  replayPrevention: 'remote',
  replayStore: myRedisReplayStore,
});
The replayStore must implement a consume(jti: string, expiresAt: number): Promise<boolean> method that atomically returns true the first time a given jti is seen and false on any subsequent call.