Next.js blurs the line between client and server, which is exactly where device fingerprinting has to be careful. The rich signals that identify a device can only be gathered in the browser, but the decision you make from them must happen somewhere the browser cannot tamper with. Get that boundary right and fingerprinting slots cleanly into the App Router; get it wrong and you have a verdict an attacker can forge in the dev tools.
This guide walks through a correct Next.js integration: collecting signals in a client component, verifying server-side in a route handler or server action, and using edge middleware for cheap pre-filtering. It assumes the App Router, though the principles apply to the Pages Router too.
The client and server split that matters
The fundamental rule of any device fingerprinting integration is that the client identifies and the server decides. In Next.js this maps naturally onto the component model.
- Client components run in the browser and can access the APIs fingerprinting depends on, such as canvas, WebGL, and audio. This is where you load the agent and obtain a request ID.
- Server components and route handlers run on your server and never touch browser APIs. This is where you verify the result and act on it.
The mistake to avoid is returning a verdict to the browser and trusting it. Anything the client receives, the client can rewrite. The client should only ever get an opaque request ID, which your server exchanges for the real, sealed result. Our React integration guide covers the component patterns in more depth, and the Node server-side verification article covers the backend exchange that Next.js route handlers wrap.
Collecting signals in a client component
Collection belongs in a 'use client' component because it needs browser APIs. Keep it small and let it hand the request ID up to your server.
'use client';
import { useEffect, useState } from 'react';
import { Prynt } from '@prynt/js';
export function useVisitor() {
const [requestId, setRequestId] = useState<string | null>(null);
useEffect(() => {
const agent = Prynt.load({ endpoint: '/_prynt' }); // first-party proxy
agent.then((p) => p.get()).then((r) => setRequestId(r.requestId));
}, []);
return requestId;
}
Serving the agent through a first-party path such as /_prynt matters. Third-party scripts get stripped by ad-blockers and privacy extensions, which silently degrades your coverage. First-party agent serving explains why routing the agent through your own domain preserves signal quality, and Next.js rewrites make the proxy a few lines of config.
Verifying in a route handler
The request ID is meaningless until your server exchanges it for the actual device intelligence. In the App Router this is a route handler under app/api.
// app/api/verify/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verify } from '@prynt/server';
export async function POST(req: NextRequest) {
const { requestId } = await req.json();
const result = await verify(requestId, {
apiKey: process.env.PRYNT_SECRET!, // server-only secret
});
const risky =
result.confidence > 0.9 &&
(result.signals.bot || result.signals.residentialProxy);
return NextResponse.json({
visitorId: result.visitorId,
action: risky ? 'step_up' : 'allow',
});
}
Two details make this correct. First, the secret key lives in a server-only environment variable and never reaches the bundle. Second, the browser gets back an action, not the raw signals, so it cannot learn exactly what tripped the decision or replay it. Wire this into a signup fraud or login protection flow and the verdict becomes something you can trust.
Edge middleware for cheap pre-filtering
Next.js middleware runs at the edge before the page renders, on every matched request. It cannot do full fingerprinting because there is no browser yet, but it is the perfect place for coarse, header-and-IP checks that reject the obvious junk before it consumes any application work.
// middleware.ts
import { NextResponse, NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
const ip = req.headers.get('x-forwarded-for') ?? '';
const ua = req.headers.get('user-agent') ?? '';
if (isKnownBadAsn(ip) || isObviousBot(ua)) {
return new NextResponse('Forbidden', { status: 403 });
}
return NextResponse.next();
}
export const config = { matcher: ['/login', '/signup', '/api/:path*'] };
This mirrors the two-tier pattern from server-side versus client-side bot detection: filter cheaply at the edge, inspect deeply on the client, decide on the server. Middleware handles the first tier, the client component the second, and the route handler the third.
Putting the flow together
The end-to-end path in an App Router application:
- Middleware rejects obvious bot traffic at the edge.
- A client component loads the agent through a first-party rewrite and obtains a request ID.
- The client posts the request ID to a route handler or server action.
- The handler verifies it server-side with the secret key and returns an action.
- Your protected flow, such as signup or checkout, honors that action.
| Layer | Runs where | Job |
|---|---|---|
| Middleware | Edge | Reject obvious bots |
| Client component | Browser | Collect signals, get request ID |
| Route handler | Server | Verify, decide, return action |
Frequently asked questions
Where should I verify a fingerprint in Next.js?
Always verify server-side in a route handler or server action, never in the browser. The client obtains a request ID, and your server exchanges it for the sealed result so an attacker cannot forge the verdict.
Can I fingerprint in Next.js middleware?
Middleware runs before the page and is ideal for coarse edge checks using request headers and IP, but full device fingerprinting needs the browser, so the rich signals are collected client-side and verified in a route handler.
Does fingerprinting work with the App Router and server components?
Yes. Collect signals in a client component, send the request ID to a server action or route handler, and verify there. Server components cannot access browser APIs, so collection stays on the client boundary.
A clean Next.js integration respects the client-server boundary at every step: browser collects, edge pre-filters, server decides. Keep the secret key server-side, serve the agent first-party, and return actions rather than raw signals. Browse the full SDKs for framework-specific packages, or try the signals live in the playground before you wire them in.
Run it yourself
Prynt is open-source, self-hostable device intelligence — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics you own end to end.