Next.js bindings for the Redeyed Sentinel CAPTCHA: a "use client"
component for rendering the widget plus a server-side verifySentinel() helper
for Route Handlers / Server Actions / API routes. Free to use — you just
need a free site key + secret key from Redeyed Lab → Sentinel → Sites.
npm i @redeyed_/sentinel-nextjsShips TypeScript/TSX source under
src/; Next.js transpiles it for you. The client component and the server helper are split via the packageexportsmap so your secret key never bundles into client code.
Both keys come from Redeyed Lab → Sentinel → Sites. The Secret Key is shown once when you create the site — copy it then and keep it server-side.
Set your environment variables:
# .env.local
NEXT_PUBLIC_SENTINEL_SITE_KEY=pk_your_site_key # public — safe in the browser
SENTINEL_SECRET_KEY=sk_your_secret_key # SECRET — server only"use client";
import { useState } from "react";
import { SentinelCaptcha } from "@redeyed_/sentinel-nextjs";
import { signup } from "./actions";
export function SignupForm() {
const [token, setToken] = useState<string | null>(null);
return (
<form action={signup}>
{/* keep the token in a hidden field so the Server Action receives it */}
<input type="hidden" name="sentinel-token" value={token ?? ""} />
{/* ...your fields... */}
<SentinelCaptcha
siteKey={process.env.NEXT_PUBLIC_SENTINEL_SITE_KEY!}
onVerify={setToken}
onError={(err) => console.error(err)}
/>
<button type="submit" disabled={!token}>
Sign up
</button>
</form>
);
}// app/actions.ts
"use server";
import { verifySentinel } from "@redeyed_/sentinel-nextjs/server";
export async function signup(formData: FormData) {
const token = String(formData.get("sentinel-token") ?? "");
const passed = await verifySentinel(token, {
siteKey: process.env.NEXT_PUBLIC_SENTINEL_SITE_KEY!,
secretKey: process.env.SENTINEL_SECRET_KEY!, // secret — server only
});
if (!passed) {
throw new Error("CAPTCHA verification failed");
}
// ...create the account
}// app/api/signup/route.ts
import { NextResponse } from "next/server";
import { verifySentinel } from "@redeyed_/sentinel-nextjs/server";
export async function POST(req: Request) {
const { token } = await req.json();
const passed = await verifySentinel(token, {
siteKey: process.env.NEXT_PUBLIC_SENTINEL_SITE_KEY!,
secretKey: process.env.SENTINEL_SECRET_KEY!,
// Optional: forward the caller IP for extra signal.
remoteip: req.headers.get("x-forwarded-for") ?? undefined,
});
if (!passed) {
return NextResponse.json({ error: "captcha_failed" }, { status: 400 });
}
return NextResponse.json({ ok: true });
}| Prop | Type | Required | Description |
|---|---|---|---|
siteKey |
string |
yes | Public site key. If missing, renders nothing + console.warn. |
onVerify |
(token: string) => void |
no | Called with the token once solved. |
onError |
(error: Error) => void |
no | Called if the Sentinel script fails to load. |
widget |
string |
no | Widget variant (data-widget). |
theme |
string |
no | Theme name (data-theme). |
scheme |
string |
no | Color scheme (data-scheme): default, ocean, forest, sunset, graphite, royalty, ruby, hacker, monochrome, midnight, aurora. |
width |
string |
no | Widget width, e.g. full / 100% / 340px (data-width). |
difficulty |
string | number |
no | Challenge strength: easy/medium/hard/max or 1-6 (data-difficulty). |
baseUrl |
string |
no | Asset/script base URL. Defaults to https://redeyed.com. |
className |
string |
no | Extra class on the container. |
POSTs to {baseUrl}/sentinel/siteverify, reCAPTCHA/Turnstile-style, with a JSON
body of { secret, response, remoteip? }. Returns true only when the response
JSON has success === true; any network/HTTP/parse failure returns false
(fail-closed). If no secretKey is configured it fails open (returns
true) so unconfigured dev/preview environments aren't hard-blocked. Import
from @redeyed_/sentinel-nextjs/server only — never from client code.
- The client widget solves the challenge and hands you a token.
- You POST that token to
{baseUrl}/sentinel/siteverifyasresponse, along with your secret key assecret(and optionally the caller'sremoteip). - Sentinel replies with
{ success, outcome, score }; the request passes whensuccess === true.
The public site key stays in the browser (widget); the secret key never leaves your server. Both are issued in Redeyed Lab → Sentinel → Sites.
- Add
widthprop (data-width) andmidnight/auroraschemes.
- Server verify migrated to the reCAPTCHA/Turnstile-style secret-key flow.
verifySentinel()now POSTs{ secret, response, remoteip? }to{baseUrl}/sentinel/siteverify(wasX-Api-Key+{ site_key, token }to/api/v1/verify) and passes on top-levelsuccess === true. - Renamed the option
apiKey→secretKeyand the env varSENTINEL_API_KEY→SENTINEL_SECRET_KEY; added an optionalremoteip. - Now fails open when no secret key is configured.
MIT © 2026 Redeyed Corporation