> ## Documentation Index
> Fetch the complete documentation index at: https://captcha.ribaunt.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js

> Create Ribaunt CAPTCHA challenge and verify handlers for Next.js App Router and Pages Router, with environment setup and replay protection tips.

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.

<Warning>
  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.
</Warning>

## Environment setup

Add your secret to `.env.local`. Next.js loads this file automatically and keeps its values server-side only:

```env theme={null}
# .env.local — server-side only
RIBAUNT_SECRET="your_very_strong_random_secret_string"
```

## App Router (Next.js 13+)

<Tip>
  Place these files at `app/api/captcha/challenge/route.ts` and `app/api/captcha/verify/route.ts`.
</Tip>

Create the two route handler files shown below. Each file exports a named HTTP-method function (`GET` or `POST`) — the App Router convention.

<Tabs>
  <Tab title="challenge/route.ts">
    ```ts theme={null}
    import { NextResponse } from 'next/server';
    import { createChallenge } from 'ribaunt';

    export async function GET() {
      try {
        const challenges = createChallenge({
          difficulty: 5,
          amount: 4,
          ttlSeconds: 60,
        });
        return NextResponse.json({ challenges });
      } catch (error) {
        return NextResponse.json({ error: 'Failed to generate challenge' }, { status: 500 });
      }
    }
    ```

    `createChallenge({ difficulty: 5, amount: 4, ttlSeconds: 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.
  </Tab>

  <Tab title="verify/route.ts">
    ```ts theme={null}
    import { NextResponse } from 'next/server';
    import { verifySolution } from 'ribaunt';

    export async function POST(req: Request) {
      const { tokens, solutions } = await req.json();

      if (!tokens || !solutions) {
        return NextResponse.json(
          { success: false, error: 'Missing tokens or solutions' },
          { status: 400 }
        );
      }

      const result = await verifySolution(tokens, solutions, {
        onWarning: (warning) => console.log('captcha warning', warning.reason),
      });

      if (result.valid) {
        return NextResponse.json({ success: true });
      }
      return NextResponse.json(
        { success: false, error: 'Invalid CAPTCHA solution' },
        { status: 400 }
      );
    }
    ```

    `verifySolution` validates the JWT signature, checks that each hash meets the required difficulty, rejects expired tokens, and (by default) blocks token replay within the current process. It returns a structured result object with `valid`, `reason`, and `message`.
  </Tab>
</Tabs>

## 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`.

<Tabs>
  <Tab title="pages/api/captcha/challenge.ts">
    ```ts theme={null}
    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({
          difficulty: 5,
          amount: 4,
          ttlSeconds: 60,
        });
        res.status(200).json({ challenges });
      } catch (error) {
        res.status(500).json({ error: 'Failed to generate challenge' });
      }
    }
    ```
  </Tab>

  <Tab title="pages/api/captcha/verify.ts">
    ```ts theme={null}
    import type { NextApiRequest, NextApiResponse } from 'next';
    import { verifySolution } from 'ribaunt';

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

      const { tokens, solutions } = req.body;

      if (!tokens || !solutions) {
        return res.status(400).json({ success: false, error: 'Missing tokens or solutions' });
      }

      const result = await verifySolution(tokens, solutions, {
        onWarning: (warning) => {
          console.log('captcha warning', warning.reason, warning.message);
        },
      });

      if (result.valid) {
        return res.status(200).json({ success: true });
      }
      return res.status(400).json({ success: false, error: 'Invalid or expired CAPTCHA solution' });
    }
    ```
  </Tab>
</Tabs>

## Serverless and edge deployments

<Note>
  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):

  ```ts theme={null}
  const result = 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.
</Note>
