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-node

Requires 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.

.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 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.

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 is flushed 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 events 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)
})

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.

API
ts
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 timer

Client options

OptionTypeDescription
hoststringOakData host events are sent to. Posts to ${host}/api/oak/ingest. Defaults to https://oakdata.co.
flushAtnumberFlush once this many events are queued. Default 20. Use 1 in serverless functions to send immediately.
flushIntervalnumberFlush automatically every N milliseconds. Default 10000. Set 0 to disable the timer.
requestTimeoutnumberPer-request network timeout in ms. Default 10000.
maxRetriesnumberRetry attempts for a failed batch (network, 429, or 5xx) with exponential backoff. Default 3.
disabledbooleanWhen true, every call is a no-op — handy for tests or gating analytics by environment. Default false.
debugbooleanLog queueing, flushes, and errors to the console. Default false.
ingestPathstringOverride 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.