Skip to content

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 waitUntil details 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.
  • waitUntil keeps 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

  1. 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).
  2. 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-for is populated and the first entry is the visitor IP.
  • Cloudflare in front of Vercel: prefer cf-connecting-ip; x-forwarded-for will list Cloudflare’s edge, then the visitor.
  • Self-hosted behind nginx: configure nginx to set x-real-ip and x-forwarded-for; the helper above prefers x-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 sendIngest call in a consent check by reading a cookie set by the consent banner.

Rollout plan

  1. Local: hardcode 203.0.113.10, run next dev, confirm the test IP appears via a curl (see Verification). Remove the hardcode.
  2. Preview / staging: deploy with EVIDENT_INGEST_URL and EVIDENT_API_KEY set to staging values. Visit from your own network and confirm your company appears on Accounts.
  3. 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/catch swallows 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 catch block. 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.