Next.js
Identify the companies visiting your Next.js site by forwarding visitor IPs to Evident in real time. This guide describes the recommended implementation — Edge middleware that fires a non-blocking fetch to /v1/ingest on every page request — so identified accounts appear on the Accounts dashboard within seconds.
Prerequisites
- A Next.js project (App Router or Pages Router both work — middleware is router-agnostic).
- A Vercel deployment. The guide assumes this; adapt the IP extraction and
waitUntildetails for other Edge runtimes as needed. - An Evident account with an API key generated via Settings → API Keys. Store the raw key immediately — it is shown once at creation and cannot be retrieved later.
Architecture
[ Visitor Browser ]
│
▼ HTTPS request
[ Next.js Edge Middleware ] ──── waitUntil ──► fetch POST /v1/ingest
│ │
▼ ▼
[ Page response to visitor ] [ Evident Public API ]
│
▼
[ Enrichment → Accounts page ]Why middleware (rather than a client-side <script> or a per-page server component):
- Server-side IP capture — the visitor’s IP is in HTTP headers reaching the Edge; the browser doesn’t know its own public IP reliably.
- Single source of truth for events — runs on every matched route automatically; no per-page wiring.
- API key stays server-side — never exposed to the browser.
waitUntilkeeps the response fast — the fetch to Evident runs after the response is returned to the visitor.
Why real-time (rather than a cron / log-replay approach):
- The Accounts dashboard updates within seconds of a visit — essential for demos and any workflow where identifying the visitor triggers something else (chat widget, personalization, sales alert).
- Evident deduplicates visits per-IP per-day server-side, so a cron + batch approach does not save on your quota vs. real-time. Both bill one IP per unique-visitor-per-day.
- No additional infrastructure — no scheduler, no buffer, no access-log pipeline.
You can still pair real-time ingestion with a one-time backfill if you want a non-empty Accounts page on day one. See Optional: one-time backfill.
Configuration
Add these to your Next.js project’s environment (Vercel dashboard, .env.local for local dev, .env.production for prod-mode builds):
# Base URL of the Evident API.
EVIDENT_INGEST_URL=https://api.useevident.com
# Server-side API key. NEVER prefix with NEXT_PUBLIC_ — that exposes it
# to the browser. Generate from Evident dashboard → Settings → API Keys.
EVIDENT_API_KEY=ipid_<64-hex-chars>Important: do not use NEXT_PUBLIC_* for the API key. NEXT_PUBLIC_* is inlined into the client bundle and visible to every visitor. Treat the API key like a database password.
Implementation
Place at the project root (same directory as next.config.js).
// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
// Match every navigation request except static assets, Next.js internals,
// API routes, and the favicon. Tune to match what counts as a "visit"
// for your site.
export const config = {
matcher: [
'/((?!api/|_next/|favicon\\.ico|robots\\.txt|sitemap\\.xml|.*\\.(?:png|jpg|jpeg|gif|svg|webp|ico|css|js|woff2?)).*)',
],
};
// Extract the visitor's public IP from the request headers in the order
// most platforms set them. Vercel sets `x-forwarded-for` and `request.ip`;
// Cloudflare additionally sets `cf-connecting-ip` (more reliable when
// CF sits in front of Vercel).
function extractVisitorIp(req: NextRequest): string | null {
const cfIp = req.headers.get('cf-connecting-ip');
if (cfIp) return cfIp.trim();
const xff = req.headers.get('x-forwarded-for');
if (xff) return xff.split(',')[0]!.trim();
return req.ip ?? null;
}
// Single-event POST. Sync `/v1/ingest` is the right endpoint for
// per-request fires — it persists immediately so the Accounts page
// updates with no batch delay. Cap is 100 events per call; we send 1.
async function sendIngest(event: Record<string, unknown>) {
const baseUrl = process.env.EVIDENT_INGEST_URL;
const apiKey = process.env.EVIDENT_API_KEY;
if (!baseUrl || !apiKey) return; // No-op until configured.
try {
await fetch(`${baseUrl}/v1/ingest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
},
body: JSON.stringify({ events: [event] }),
// Don't let a slow/down Evident hang the response to the visitor.
signal: AbortSignal.timeout(2_000),
});
} catch {
// Fire-and-forget. We never want ingest errors to surface to the
// visitor or appear in their experience.
}
}
export function middleware(req: NextRequest) {
const ip = extractVisitorIp(req);
if (ip) {
const event = {
ip,
event_id: crypto.randomUUID(),
event_type: 'page_view',
timestamp: new Date().toISOString(),
// Optional fields. Evident preserves these for replay / audit
// when raw archival is enabled on your workspace.
properties: {
path: req.nextUrl.pathname,
referer: req.headers.get('referer') ?? null,
user_agent: req.headers.get('user-agent') ?? null,
},
};
// waitUntil ensures the fetch is allowed to complete after the
// response is returned to the visitor, without blocking it. Falls
// back to a bare promise if waitUntil isn't available (e.g. local
// `next dev` outside the Edge runtime).
const promise = sendIngest(event);
// @ts-expect-error: waitUntil is available on the Edge runtime context.
if (typeof req.waitUntil === 'function') req.waitUntil(promise);
}
return NextResponse.next();
}That’s the entire implementation — one file, no new packages.
Verification
Local
Run next dev and hit a page. The middleware will execute against localhost, where extractVisitorIp returns ::1 or 127.0.0.1 — both are private IPs that Evident marks as invalid_ip and does not persist. That’s expected; local dev verifies the wiring, not the enrichment.
To exercise enrichment without deploying, temporarily hard-code a routable test IP:
// TEMPORARY for local testing — remove before commit.
const ip = '203.0.113.10'; // TEST-NET-3, unfiltered by classification.
You can also confirm your API key + base URL are correct with a curl:
curl -X POST $EVIDENT_INGEST_URL/v1/ingest \
-H "X-API-Key: $EVIDENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"events":[{"ip":"203.0.113.10","event_type":"page_view"}]}'The response shape is documented in the REST API reference.
After deploy
- Visit a public page on your deployed site from a corporate network (your company’s office or VPN — not from a CDN / cloud IP, which Evident’s pipeline filters as infrastructure noise).
- Open the Evident dashboard → Accounts. Your company should appear within 5–10 seconds.
Optional: one-time backfill
If you want the Accounts dashboard pre-populated with the last 30 days of visitor history — for a stakeholder demo or an internal review — run a one-shot backfill from your access logs.
# 1. Export the last 30 days of access logs from Vercel / Cloudflare / etc.
# Format: one JSON event per line (NDJSON), each line shaped like:
# {"ip":"203.0.113.42","timestamp":"2026-06-15T12:00:00Z","event_type":"page_view","properties":{"path":"/"}}
# 2. POST the NDJSON to the async batch endpoint.
curl -X POST $EVIDENT_INGEST_URL/v1/ingest/batch \
-H "X-API-Key: $EVIDENT_API_KEY" \
-H "Content-Type: application/x-ndjson" \
--data-binary @access-logs-30d.ndjson
# 3. The response is `202 Accepted` with a job_id. Poll status:
curl $EVIDENT_INGEST_URL/v1/ingest/batch/$JOB_ID \
-H "X-API-Key: $EVIDENT_API_KEY"Batch limits are 1M events / 100 MB per call — chunk larger backfills across multiple requests.
Caveats and gotchas
Infrastructure noise is filtered automatically
Cloud-provider / CDN / bot IPs (AWS scanners, GoogleBot, Cloudflare, etc.) are classified and excluded from the Accounts page. This is a feature — without it the Accounts page would be unreadable — but it has two implications:
- Your raw traffic count is higher than the Accounts page count. Don’t be alarmed.
- A demo from a personal AWS instance or a VPN will not surface a company. Use a corporate office IP or a laptop on a corp network when demoing.
The results[] array from /v1/ingest still returns filtered events with status: "filtered", so you can gate UX inline (e.g. drop a chat-widget prompt for suspected bots) without a follow-up lookup.
Per-day per-IP deduplication
Evident deduplicates billable IPs by (tenant, IP, day). A visitor who hits your site 500 times today counts as one billable IP. Sync vs. batch ingestion is identical for cost — both go through the same dedup.
Idempotency window
Evident deduplicates by (tenant, event_id) for 7 days. The crypto.randomUUID() in the middleware ensures every visit is a distinct event; if you set a deterministic event_id derived from session + page, be aware that re-fires within 7 days are no-ops server-side.
IP extraction by platform
- Vercel:
x-forwarded-foris populated and the first entry is the visitor IP. - Cloudflare in front of Vercel: prefer
cf-connecting-ip;x-forwarded-forwill list Cloudflare’s edge, then the visitor. - Self-hosted behind nginx: configure nginx to set
x-real-ipandx-forwarded-for; the helper above prefersx-forwarded-for.
Always test with a real visit on the target deploy before declaring done — the IP that lands in the Evident dashboard should match your public IP (check at https://ifconfig.me).
Edge runtime cost on Vercel
Vercel paid plans charge per middleware invocation. For a typical marketing site (a few thousand visits/day) this is rounding error, but a high-traffic site (>1M visits/month) may want to:
- Move to a sampled approach (only fire on
n%of requests). - Move ingestion into a server component on key landing pages, instead of running middleware on every navigation.
Privacy and consent
Visitor IPs are personal data under GDPR. Confirm with whoever owns privacy compliance that:
- Your site’s privacy policy discloses that visitor IPs are sent to Evident for IP-to-company enrichment.
- Any consent banner the site shows is honored before middleware fires. You can wrap the
sendIngestcall in a consent check by reading a cookie set by the consent banner.
Rollout plan
- Local: hardcode
203.0.113.10, runnext dev, confirm the test IP appears via a curl (see Verification). Remove the hardcode. - Preview / staging: deploy with
EVIDENT_INGEST_URLandEVIDENT_API_KEYset to staging values. Visit from your own network and confirm your company appears on Accounts. - Production: rotate to a production API key, set production env vars, deploy. Monitor for the first 24 hours via your normal site error monitoring — the middleware’s
try/catchswallows errors, so ingest failures will silently lose events unless you add explicit visibility.
Monitoring and troubleshooting
The middleware fails closed (silent) by design. For explicit visibility:
- Add a counter: log to your platform’s logging surface (Vercel Logs / Datadog / etc.) on each fire. The volume will match your traffic.
- Add a failure log: log the error inside the
catchblock. Do not surface to the visitor. - Watch the Evident dashboard: a sudden drop in Accounts-page traffic often means ingestion is failing.
Common failures:
| Symptom | Likely cause | Fix |
|---|---|---|
| No events appear on Accounts | env vars missing, API key revoked, network blocked from Edge | Check env config; regenerate key; test curl from a known-good environment |
401 from /v1/ingest |
API key invalid or revoked | Generate a new key, update env, redeploy |
413 from /v1/ingest |
Sending more than 100 events per call | Should not happen with this middleware (1 event per call); if you batch, switch to /v1/ingest/batch |
429 from /v1/ingest |
Rate limit exceeded | Check your plan’s RPS on the Quotas page and back off if traffic is bursty |
Every event shows invalid_ip |
IP extraction is returning private / local addresses | Confirm cf-connecting-ip or x-forwarded-for is set on your deploy |
References
- REST API reference — full endpoint contract, limits, and error codes.
- Next.js middleware docs — Next.js-side reference.
- Evident dashboard → Settings → API Keys — credential management.
- Evident dashboard → Integrations → Public API — discovery entry point.