oakdata-node is the server-side companion to oakdata-js. Use the browser SDK for pageviews, autocapture, and replay; use oakdata-node for trusted server-side eventsthe 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:
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=<my secret key, starts with oak_sec_> 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: '<snake_case_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 install oakdata-nodeRequires 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.
# Server-only - never prefix this with NEXT_PUBLIC_
OAK_SECRET_KEY=oak_sec_xxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_OAK_HOST=https://oakdata.coKeep 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.
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.
'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()
}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
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.
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 $pageviewper inbound page request, with the visitor's user-agent and IP forwarded automatically so OakData can classify and verify the crawler.
// 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()
}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:
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 for how the signals combine, and bots & data quality 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.
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 timerClient options
| Option | 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.