Enable Bot Protection
Wire Vercel BotID into auth and billing Server Actions via the lib/botid.ts opt-in scaffold. Add a Vercel WAF rule at the edge.
What you are building
Two layers of bot protection on top of the rate limiting that already ships in your template. The first is application-layer client classification on high-value Server Actions (login, signup, password reset, Stripe checkout), wired through the lib/botid.ts opt-in scaffold and Vercel BotID. The second is edge-level blocking on the public marketing surface (/blog/*), wired through a Vercel WAF custom rule that blocks documented AI scraper user-agents.
Why both: rate limiting catches abuse from identified sources (one IP hammering login). BotID catches unidentified automation (distributed scrapers, Playwright, headless browsers) before your Server Action runs. The WAF rule blocks polite AI training crawlers at Vercel's edge before any function invocation is billed.
The full reasoning is in the bot protection and DDoS mitigation pillar. This recipe is the operator-facing checklist.
Layer 1: Application-layer BotID on Server Actions
The template ships lib/botid.ts as an opt-in wrapper. By default it short-circuits to { isBot: false } so your actions behave identically. Flip one env var and install one package to turn it on.
Step 1: Install the BotID package
npm i botid
The dependency is not in package.json by default. Customers who never enable BotID never pay the bundle weight.
Step 2: Set the enablement flag
In .env.local and in your Vercel environment variables:
NEXT_PUBLIC_BOTID_ENABLED=true
Production scope only on Vercel. The value is not a secret, but Production-only is the right discipline (Preview deploys do not need BotID, and BotID Deep Analysis costs $1 per 1,000 calls on Pro and Enterprise plans).
Step 3: Configure protected routes in instrumentation-client.ts
Create or extend instrumentation-client.ts at the project root, listing the routes your forms submit to. For the template's default auth + billing surfaces:
// instrumentation-client.ts
import { initBotId } from 'botid/client/core'
initBotId({
protect: [
{ path: '/login', method: 'POST' },
{ path: '/signup', method: 'POST' },
{ path: '/reset-password', method: 'POST' },
{ path: '/pricing', method: 'POST' },
],
})
Next.js Server Actions submit as POSTs against the route the form lives on, so the path matches the page route, not an /api/* endpoint.
Step 4: (Optional) Enable Deep Analysis
In the Vercel dashboard, go to your project, Firewall, Rules, and toggle Vercel BotID Deep Analysis on. Required for catching sophisticated bots (Playwright, Puppeteer, ML-evading automation); skip if your threat model is only low-sophistication scripts.
Pricing: $1 per 1,000 checkBotId() calls. The template wires checkBotProtection() into four Server Actions, so the cost ceiling is roughly $1 per 1,000 attempted logins + signups + password resets + checkouts combined. Low-traffic apps will not move the needle; high-traffic public signup pages may.
How the wrapper works
The wrapper handles enabled/disabled, missing package, and unexpected errors as one return type:
// lib/botid.ts
export async function checkBotProtection(): Promise<{ isBot: boolean }> {
if (process.env.NEXT_PUBLIC_BOTID_ENABLED !== 'true') {
return { isBot: false }
}
try {
const { checkBotId } = await import('botid/server')
const result = await checkBotId()
return { isBot: result.isBot }
} catch {
return { isBot: false }
}
}
Three properties matter here. First, the dynamic import means botid is not pulled into the bundle when disabled, so the dependency is genuinely optional. Second, every failure path returns { isBot: false }, so if BotID itself errors out, your users still get through (the rate limiter is the downstream defense). Third, calling code is unchanged: every Server Action calls await checkBotProtection() regardless of whether BotID is on.
How it is wired into Server Actions
The template adds one block at the top of each protected action, above the existing rate limiter:
export async function login(data: LoginInput) {
const { isBot } = await checkBotProtection()
if (isBot) {
return { error: 'Request blocked.' }
}
// ... existing rate limiter + Supabase auth call
}
Same pattern in signup, resetPassword, and createCheckoutSession. The order matters: BotID runs before the rate limiter because rejecting a bot is cheaper than parsing its request and computing its rate-limit key.
Layer 2: Vercel WAF rule for AI scrapers (Pro plan)
The application-layer check still costs a Function Invocation per rejected bot, because BotID runs inside your function. To stop the cost entirely on public routes that should never be in AI training data, block at Vercel's edge.
In the Vercel dashboard:
- Navigate to your project, Firewall, Rules, + Add Rule.
- Name it
Block AI training crawlers on public surface. - Conditions (combined with AND):
Request Pathmatches^/blog/.*(extend with marketing routes as needed)Request User Agentcontains one ofthe comma-separated list:
Bytespider, GPTBot, ClaudeBot, CCBot, Amazonbot, Meta-ExternalAgent, PetalBot, GoogleOther, TikTokSpider, DuckAssistBot, anthropic-ai, OAI-SearchBot, PerplexityBot, Google-Extended, Google-CloudVertexBot, Applebot-Extended
- Action: Deny. Returns 403 at Vercel's edge; no Function Invocation, no Active CPU, no Provisioned Memory billed.
The rule applies BEFORE the Function ever runs, so the cost-attribution math works out cleanly: a blocked AI bot adds zero to your bill. The tradeoff is that this is Pro plan only, and you have to decide which crawlers to allow (some operators want ChatGPT to cite their blog and remove GPTBot from the list; others want a complete block).
What if I am on Vercel Hobby?
Two options. The simpler one is to lean on app/robots.ts for the well-behaved crawlers (Common Crawl, GPTBot, ClaudeBot respect robots.txt). The honest one is that if a scraper is hammering your invoice, the right answer is one of: upgrade to Pro for the WAF, put Cloudflare in front (Vercel officially recommends against this, with reasons documented in the pillar post), or migrate the public surface off Vercel for the read-heavy routes.
The application-layer BotID scaffold still works on Hobby, though Deep Analysis does not. Basic BotID catches non-browser HTTP clients and tools like curl without the browser-side challenge, which covers the lowest-sophistication attacks for free.
Common mistakes
- Enabling BotID without configuring
instrumentation-client.ts. The server-sidecheckBotId()fails if the client never registered the route as protected. Result:isBotreturns true for all traffic, including real users, and your login form locks everyone out. Always pair env var enablement with the client setup. - Listing API paths instead of page paths in
protect. Server Actions submit to the route the form lives on (/login, not/api/auth/login). Use the page route. - Expecting BotID to eliminate Function Invocation costs. BotID itself runs inside a function. The cost it saves is "Stripe API call avoided" or "Supabase write avoided", not "function never ran." For cost elimination on public routes, use the Vercel WAF rule above.
- Adding
'unsafe-inline'to your CSP to fix BotID errors. BotID's client-side challenge is loaded by the framework wrapper Vercel ships, not by inline scripts. If you see CSP errors, check that the protected route paths ininstrumentation-client.tsmatch the form's submission target.
What to read next
- The full architectural reasoning: bot protection and DDoS mitigation for indie SaaS on Vercel
- The downstream defense: how to rate limit Next.js Server Actions before they get abused
- The broader hardening surface: the Next.js security hardening checklist