# Quickstart: Server-side (Node) > Capture trusted server-side events from Node.js, Next.js server actions, route handlers, and edge functions with the oakdata-node SDK - and catch AI crawlers with the request middleware. Source: https://oakdata.co/docs/quickstart/server --- `oakdata-node` is the server-side companion to [`oakdata-js`](https://oakdata.co/docs/quickstart/nextjs). Use the browser SDK for pageviews, autocapture, and replay; use `oakdata-node` for **trusted server-side events** the browser can't see or shouldn't be trusted to report - signups, payments, plan changes, background jobs. Both send to the same project, so client and server events resolve to one timeline per user. In a hurry? Paste this prompt into Claude Code, Cursor, or any agent working in your repo, and give it your secret key when asked: **Prompt for a coding agent:** ``` Set up server-side OakData analytics in this Node.js project using the oakdata-node SDK. 1. Install the package: run "npm install oakdata-node". 2. Add OAK_SECRET_KEY= to the server-only env file. Ask me for the value. Never expose it to the client - no NEXT_PUBLIC_ or VITE_ prefix. 3. Create a small client factory (e.g. lib/oak.ts) that returns new OakClient(process.env.OAK_SECRET_KEY, { host: 'https://oakdata.co', flushAt: 1, flushInterval: 0 }) - that config is for serverless/short-lived functions. If this is a long-running server (Express, worker), instead create one shared OakClient with default options and call oak.shutdown() on SIGTERM. 4. In my key business-logic paths (signup completed, checkout/payment completed, plan changed), call oak.capture({ distinctId: userId, event: '', properties: { ... } }). In short-lived functions, await oak.shutdown() before returning so events flush. 5. Use the same user id the browser SDK passes to oak.identify() so client and server events stitch to one user. Don't add any other analytics providers. Show me the changes before applying them. ``` Prefer to wire it up yourself? The steps below are exactly what that prompt does. ## 1. Install the package **npm** ```bash npm install oakdata-node ``` **pnpm** ```bash pnpm add oakdata-node ``` **yarn** ```bash yarn add oakdata-node ``` **bun** ```bash bun add oakdata-node ``` Requires Node 18+ (for the built-in `fetch` and `crypto.randomUUID`). ## 2. Create a secret key The server SDK authenticates with a **secret** key (`oak_sec_…`), created under **Project → Settings → API keys**. It's shown once on creation - store it immediately. **.env** ```bash # Server-only - never prefix this with NEXT_PUBLIC_ OAK_SECRET_KEY=oak_sec_xxxxxxxxxxxxxxxxxxxxxxxx NEXT_PUBLIC_OAK_HOST=https://oakdata.co ``` > **Keep secret keys server-side** A secret key can write events for your whole project with no domain check. Never put it in browser code or a `NEXT_PUBLIC_` variable. If one leaks, revoke it on the API keys page and create a new one. ## 3. Create a client Make a small factory you can import anywhere. Next.js server functions are short-lived, so set `flushAt: 1` and `flushInterval: 0` - events send immediately instead of waiting in a batch that may never flush before the function returns. **lib/oak.ts** ```ts import { OakClient } from 'oakdata-node' export function oakClient() { return new OakClient(process.env.OAK_SECRET_KEY!, { host: process.env.NEXT_PUBLIC_OAK_HOST, flushAt: 1, flushInterval: 0, }) } ``` ## 4. Capture from the App Router Call `capture` from a server action or route handler, then `await oak.shutdown()` so the event flushes before the function returns. **app/actions.ts** ```ts 'use server' import { oakClient } from '@/lib/oak' export async function upgradePlan(userId: string) { // ...perform the upgrade... const oak = oakClient() oak.capture({ distinctId: userId, event: 'plan_upgraded', properties: { plan: 'pro', mrr: 49 }, }) await oak.shutdown() } ``` **app/api/checkout/route.ts** ```ts import { oakClient } from '@/lib/oak' export async function POST(req: Request) { const { userId, amount } = await req.json() const oak = oakClient() oak.capture({ distinctId: userId, event: 'checkout_completed', properties: { amount } }) await oak.shutdown() return Response.json({ ok: true }) } ``` ## 5. Capture from the Pages Router **pages/api/track.ts** ```ts import type { NextApiRequest, NextApiResponse } from 'next' import { OakClient } from 'oakdata-node' export default async function handler(req: NextApiRequest, res: NextApiResponse) { const oak = new OakClient(process.env.OAK_SECRET_KEY!, { host: process.env.NEXT_PUBLIC_OAK_HOST, flushAt: 1, flushInterval: 0, }) oak.capture({ distinctId: req.body.userId, event: 'newsletter_signup' }) await oak.shutdown() res.status(200).json({ ok: true }) } ``` ## 6. Long-running servers In a persistent process (Express, a worker, a queue consumer), create the client **once** and let it batch in the background. Flush on graceful shutdown so nothing is lost. **server.ts** ```ts import { OakClient } from 'oakdata-node' // Batches: flushes at 20 events or every 10s by default. const oak = new OakClient(process.env.OAK_SECRET_KEY!) process.on('SIGTERM', async () => { await oak.shutdown() process.exit(0) }) ``` ## 7. Catch AI crawlers with the middleware AI crawlers - GPTBot, ClaudeBot, PerplexityBot - **never run JavaScript**, so the browser SDK can't see them. Capturing requests on the server is the only way to surface (and verify) that traffic. The middleware does it in one line: a `$pageview` per inbound page request, with the visitor's user-agent and IP forwarded automatically so OakData can classify and verify the crawler. **Next.js middleware** ```ts // middleware.ts import { NextResponse, type NextRequest } from 'next/server' import { oakClient } from '@/lib/oak' export function middleware(request: NextRequest) { oakClient().trackRequest(request) return NextResponse.next() } ``` **Express / Connect** ```ts import { OakClient } from 'oakdata-node' const oak = new OakClient(process.env.OAK_SECRET_KEY!) app.use(oak.expressMiddleware()) ``` Static assets (`.js`, `.css`, images, `/_next/…`) and non-`GET`/`HEAD` requests are skipped by default, and anonymous requests collapse into one visitor per IP + user-agent - so a crawler shows up as a single row. Capturing your own events instead? Forward the two fields by hand to get the same verification: **manual** ```ts oak.capture({ distinctId, event: '$pageview', userAgent: req.headers['user-agent'], // classify bot traffic ip: req.headers['x-forwarded-for'], // verify GPTBot / Googlebot claims }) ``` See [bot handling](https://oakdata.co/docs/concepts/bots) for how the signals combine, and [bots & data quality](https://oakdata.co/docs/using/bots) for what shows up in the dashboard. ## Methods Identity matches the browser SDK: pass the same `distinctId` you use with `oak.identify(userId)` on the client, and server and client events stitch to one user. See [identity resolution](https://oakdata.co/docs/concepts/identity). **API** ```ts oak.capture({ distinctId, event, properties?, groups?, timestamp?, userAgent?, ip? }) oak.identify({ distinctId, properties? }) // properties = user traits oak.alias({ distinctId, alias }) // link a new id oak.groupIdentify({ groupType, groupKey, properties?, distinctId? }) oak.trackRequest(request, options?) // Next.js middleware / Web Request oak.expressMiddleware(options?) // Express / Connect: app.use(...) await oak.flush() // send queued events now await oak.shutdown() // flush + stop the timer ``` ## Client options | Name | Type | Description | | --- | --- | --- | | `host` | string | OakData host events are sent to. Posts to `${host}/api/oak/ingest`. Defaults to `https://oakdata.co`. | | `flushAt` | number | Flush once this many events are queued. Default `20`. Use `1` in serverless functions to send immediately. | | `flushInterval` | number | Flush automatically every N milliseconds. Default `10000`. Set `0` to disable the timer. | | `requestTimeout` | number | Per-request network timeout in ms. Default `10000`. | | `maxRetries` | number | Retry attempts for a failed batch (network, `429`, or `5xx`) with exponential backoff. Default `3`. | | `disabled` | boolean | When `true`, every call is a no-op - handy for tests or gating analytics by environment. Default `false`. | | `debug` | boolean | Log queueing, flushes, and errors to the console. Default `false`. | | `ingestPath` | string | Override the ingest path (rarely needed - useful behind a reverse proxy). Default `/api/oak/ingest`. | | `fetch` | typeof fetch | Custom `fetch` implementation. Defaults to the global `fetch`. | > **Capture never throws** Analytics can't take down your app: capture errors are swallowed, and transient transport failures retry with backoff (visible with `debug: true`). No `try/catch` needed.