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.
oakdata-node is the server-side companion to oakdata-js. Use the browser SDK for pageviews, autocapture, and session replay; use oakdata-node to send trusted server-side events the browser can't see or shouldn't be trusted to report — signups, payments, subscription changes, and background jobs. Both send to the same OakData project, so client and server events resolve to one timeline per user.
1. Install the package
npm install oakdata-nodeRequires Node 18+ (for the built-in global `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. Unlike the public key used in the browser, a secret key isn't origin-restricted — it must stay on your server and out of client bundles. It's shown once on creation, so 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 expose 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. Because Next.js server functions are short-lived, set flushAt: 1 and flushInterval: 0 so events send immediately rather than 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 is flushed 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 events 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)
})Methods
Identity matches the browser SDK: pass the same distinctId you use with oak.identify(userId) on the client so server and client events stitch to one user. See identity resolution.
oak.capture({ distinctId, event, properties?, groups?, timestamp? })
oak.identify({ distinctId, properties? }) // properties = user traits
oak.alias({ distinctId, alias }) // link a new id
oak.groupIdentify({ groupType, groupKey, properties? })
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. |
Capture never throws
Analytics can't take down your app: capture errors are swallowed, and transient transport failures retry with backoff and are dropped if still failing (visible with debug: true). You don't need to wrap calls in try/catch.