Bot-proof your Clerk sign-up.
Bots never see the form.
Clerk handles your authentication beautifully — but anything that renders a public sign-up form attracts automated registrations. This guide gates Clerk's <SignUp/> behind invisible behavioral verification: real visitors flow through after a few seconds of natural interaction; bots hit a wall they can't script around. Every snippet below was live-tested on Next.js 16 + @clerk/nextjs 7 + usehuma 1.2.1.
01Install
npm install @clerk/nextjs usehumaYou need usehuma@1.2.1+ — earlier versions verified before signals were collected. Grab keys from your Clerk dashboard and a free useHUMA key at humaverify.com/signup.
# .env.local — use YOUR keys from dashboard.clerk.com
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_••••••••
CLERK_SECRET_KEY=sk_test_••••••••
NEXT_PUBLIC_HUMA_KEY=huma_live_••••••••02Clerk middleware (Next.js 16: proxy.ts)
Next.js 16 renamed middleware.ts to proxy.ts — same idea, earlier in the pipeline. Create it at the project root:
// proxy.ts (Next.js 16 — replaces middleware.ts)
import { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware();
export const config = {
matcher: [
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
"/(api|trpc)(.*)",
],
};03Wrap your app with ClerkProvider
// app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}04Gate the sign-up page
// app/page.tsx — verify before showing the signup link
"use client";
import { useEffect, useState } from "react";
export default function Home() {
const [human, setHuman] = useState<boolean | null>(null);
useEffect(() => {
const s = document.createElement("script");
s.src = "https://humaverify.com/huma.js";
s.onload = () => setTimeout(async () => {
// Your own route holds the API key. Never ship it to the browser.
const r = await fetch("/api/huma-check", {
method: "POST",
body: JSON.stringify({
userId: "clerk_signup_visitor",
sessionData: (window as any).Huma.debug().features,
}),
}).then((r) => r.json()).catch(() => ({ human: true }));
setHuman(r.human);
}, 3000);
document.head.appendChild(s);
}, []);
if (human === null) return <p>Verifying you're human…</p>;
if (!human) return <p>Something looks automated. Email us if this is wrong.</p>;
return <SignUp />;
}Why gate instead of CAPTCHA?
A CAPTCHA punishes every legitimate user to inconvenience bots that increasingly solve puzzles anyway. The gate inverts the burden: humans do nothing, and automation has to imitate an entire session of human micro-behavior — across whatever framework it drives the browser with.