Integrations · Updated 2026-10-20

Block fake signups in Next.js

Screen signups in a Next.js App Router server action or route handler, with the visitor's real IP and a fail-open timeout.

Run the check on the server, in the same server action or route handler that creates the account. Your API key never reaches the browser.

1. Add your key

# .env.local
VEQEN_API_KEY=vq_live_...

2. A small helper

// lib/veqen.ts
import "server-only";

export type Verdict = "admit" | "review" | "refuse";

export async function screenSignup(input: { email: string; ip?: string | null; phone?: string }) {
  try {
    const res = await fetch("https://veqen-app.com/v1/signup/check", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.VEQEN_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(input),
      cache: "no-store",
      signal: AbortSignal.timeout(4000),
    });
    if (!res.ok) return { verdict: "review" as Verdict, result: null };
    const result = await res.json();
    return { verdict: result.risk.verdict as Verdict, result };
  } catch {
    return { verdict: "review" as Verdict, result: null }; // fail open
  }
}

3. Use it in a server action

// app/signup/actions.ts
"use server";
import { headers } from "next/headers";
import { screenSignup } from "@/lib/veqen";

export async function signUp(formData: FormData) {
  const email = String(formData.get("email") ?? "");
  const ip = (await headers()).get("x-forwarded-for")?.split(",")[0]?.trim() ?? null;

  const { verdict, result } = await screenSignup({ email, ip });
  if (result?.email?.did_you_mean) {
    return { error: `Did you mean ${result.email.did_you_mean}?` };
  }
  if (verdict === "refuse") {
    return { error: "Please use a permanent email address." };
  }
  const user = await createUser({
    email,
    canonicalEmail: result?.email?.canonical ?? email.toLowerCase(),
    requiresVerification: verdict === "review",
  });
  return { ok: true, userId: user.id };
}

On Vercel, x-forwarded-for is set by the platform, so its first address is the visitor. Behind your own proxy, make sure the proxy sets it.

Route handler version

// app/api/signup/route.ts
import { NextResponse } from "next/server";
import { screenSignup } from "@/lib/veqen";

export async function POST(request: Request) {
  const { email, password } = await request.json();
  const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? null;
  const { verdict } = await screenSignup({ email, ip });
  if (verdict === "refuse") {
    return NextResponse.json({ error: "Please use a permanent email address." }, { status: 400 });
  }
  // ...create the user
  return NextResponse.json({ ok: true });
}

Using an auth library?

If NextAuth/Auth.js, Clerk, Supabase or Firebase creates the user for you, see their pages: Clerk, Supabase, Firebase.