Getting Started

Server-side tracking

Track from your backend with flowgrid-sdk/server — a Node/edge-safe client scoped to identity, feature usage, ecommerce & revenue and API / server error tracking. Same wire contract as the browser SDK, so server and browser events resolve to one identity in your dashboard.

Overview

Some of the most valuable events never happen in the browser: the OAuth callback that actually creates the account, the API route a feature runs through, the webhook that fails at 3am. FlowGridServer covers exactly that surface:

  • Identity — identify users, update traits, alias, reset on logout, and the auth-level events (signup / login / active-user) fired from server auth flows.
  • Feature usagefeature_usage events from API routes, background jobs and webhooks.
  • Ecommerce & revenue — purchases, refunds, order lifecycle and subscription MRR events, fired from payment webhooks where the money is actually confirmed.
  • Errors — server exceptions as error_events with route / method / status context.

Runs anywhere fetch runs

No window, no document, no storage, no Node built-ins — only global fetch. Works in Node ≥ 18, Next.js API routes and middleware, Vercel serverless & edge, Cloudflare Workers, Bun and Deno.

Init — once per process

Initialise in a dedicated lib file and import the instance everywhere server-side.init() is idempotent — calling it twice returns the same instance.

lib/flowgrid.server.ts
1import { FlowGridServer } from "flowgrid-sdk/server"; 2 3export const fg = FlowGridServer.init({ 4 webId: process.env.NEXT_PUBLIC_FLOWGRID_WEB_ID!, // same site id as the browser SDK 5 apiKey: process.env.FLOWGRID_API_KEY!, // PRIVATE key — server env only 6 // endpoint: "https://core.flow-grid.xyz", // default 7 // environment: "production", // defaults to NODE_ENV 8});

Key hygiene

The server client requires your private API key. Keep it in a server-only env var (no NEXT_PUBLIC_ prefix) — never ship it to the browser.

Identity & auth events

trackAuth() is the one-call path for server auth flows — it identifies the user (writing the identity store, so they appear as a Known visitor), fires the signup/login event, and marks them active for DAU/WAU/MAU:

app/api/auth/callback/route.ts
1import { fg } from "@/lib/flowgrid.server"; 2 3// In your OAuth callback / session endpoint, once the user is known: 4await fg.trackAuth({ 5 event: "login", // or "signup" 6 userId: user.id, 7 email: user.email, // email + name make the identity resolvable 8 name: user.name, 9 method: "google", 10});

The individual operations are all available too — same semantics as the browser SDK:

identity.ts
1await fg.identifyUser(user.id, { email: user.email, plan: "pro" }); 2await fg.updateTraits(user.id, { plan: "enterprise" }); // after an upgrade 3await fg.aliasUser("old_id", user.id); // ID migrations 4await fg.pingActiveUser(user.id, { email: user.email }); // DAU/WAU/MAU signal 5await fg.signup(user.id, "google", "pricing"); // funnel "Signed up" stage 6await fg.login(user.id, "google"); // returning-user signal 7await fg.logout(user.id); // reset identity on sign-out

Feature usage

Features that run through an API — exports, imports, integrations, AI actions — are tracked where they actually execute. Events land in the same Feature Usage dashboards as browser events (see the Feature Usage guide).

app/api/export/route.ts
1import { fg } from "@/lib/flowgrid.server"; 2 3// Direct: 4await fg.trackFeature({ 5 featureId: "csv_export", 6 featureName: "CSV Export", 7 userId: user.id, 8 properties: { rows: 12_000 }, 9}); 10 11// Or bind once and use semantic verbs (server twin of fg.defineFeature): 12const csvExport = fg.defineFeature("csv_export", "CSV Export", { category: "reports" }); 13await csvExport.used({ userId: user.id, rows: 12_000 }); 14await csvExport.completed({ userId: user.id }); 15await csvExport.abandoned({ userId: user.id, reason: "timeout" });

Ecommerce & revenue

Browser purchase events are structurally lossy — ad blockers, tabs closed on the checkout redirect, interrupted payments. Your payment webhookis where money is actually confirmed, so it's the authoritative place to record revenue. Server events use the same ecommercecontract as the browser SDK and land in the same revenue & monetization dashboards.

app/api/webhooks/stripe/route.ts
1import { fg } from "@/lib/flowgrid.server"; 2 3// checkout.session.completed — the purchase, recorded where it's confirmed 4await fg.trackPurchase({ 5 order: { 6 orderId: session.id, 7 items, // CartItem[] — same shape as the browser SDK 8 subtotal, discountTotal: 0, shippingTotal: 0, taxTotal, 9 total: session.amount_total / 100, 10 currency: "USD", 11 payment: { method: "credit_card", provider: "stripe" }, 12 }, 13 userId: session.client_reference_id, 14}); 15 16// charge.refunded — keeps net revenue accurate 17await fg.trackRefund({ 18 orderId, 19 refundId: refund.id, 20 amount: refund.amount / 100, 21 currency: "USD", 22 reason: "customer_request", 23 isFullRefund: true, 24 userId, 25});

Order lifecycle events from fulfilment systems:

fulfilment.ts
1await fg.trackOrderStatus(orderId, "processing", "pending", { userId }); 2await fg.trackOrderShipped(orderId, { method: "express", cost: 5, trackingNumber }); 3await fg.trackOrderDelivered(orderId);

Subscription / MRR lifecycle — one method, discriminated on event. These power the MRR and churn dashboards, and billing state lives on your server, so emit them from your billing webhooks:

app/api/webhooks/stripe/route.ts
1// customer.subscription.created 2await fg.trackSubscription({ 3 event: "start", // "change" | "cancel" | "renew" | "trial_converted" | "trial_expired" 4 subscriptionId: sub.id, 5 userId, 6 plan: "professional", 7 mrr: { amount: 9900, currency: "USD" }, // smallest currency unit (cents) 8 billingCycle: "monthly", 9 trialDays: 14, 10}); 11 12// customer.subscription.deleted 13await fg.trackSubscription({ 14 event: "cancel", 15 subscriptionId: sub.id, 16 userId, 17 reason: sub.cancellation_details?.reason ?? "unknown", 18 lostMrr: { amount: 9900, currency: "USD" }, 19});

Revenue attribution — Stripe & Lemon Squeezy

A payment webhook arrives with no cookies — on its own, FlowGrid can only attach a purchase to a synthetic server visitor. To attribute revenue to the real visitor (their session, landing page, UTM campaign), the visitor and session ids must round-trip through the payment provider: embed them in the checkout's metadata when you create it, read them back in the webhook. Both FlowGrid trackers mirror the ids to cookies (visitor_id / session_id, npm SDK: fg_session_id) precisely so your checkout route can read them.

Leg 1 — embed the ids at checkout creation:

app/api/checkout/route.ts
1import { cookies } from "next/headers"; 2import { checkoutAttributionFromCookies } from "flowgrid-sdk/server"; 3 4// Stripe — goes into the checkout session's metadata 5const session = await stripe.checkout.sessions.create({ 6 line_items, mode: "payment", success_url, cancel_url, 7 metadata: { ...checkoutAttributionFromCookies(await cookies()) }, 8 // adds: flowgrid_visitor_id + flowgrid_session_id 9}); 10 11// Lemon Squeezy — goes into checkoutData.custom 12const checkout = await createCheckout(storeId, variantId, { 13 checkoutData: { 14 custom: { ...checkoutAttributionFromCookies(await cookies()) }, 15 }, 16});

Creating the checkout from the browser instead? The browser SDK exposes the same keys: fg.checkoutAttribution() — POST them to your checkout endpoint alongside the price id.

Leg 2 — read them back in the webhook and attach them to the revenue event. attributionFromMetadata() unwraps Stripe objects (.metadata) and Lemon Squeezy payloads (.meta.custom_data) automatically:

app/api/webhooks/stripe/route.ts
1import { attributionFromMetadata } from "flowgrid-sdk/server"; 2import { fg } from "@/lib/flowgrid.server"; 3 4// checkout.session.completed 5const { visitorId, sessionId } = attributionFromMetadata(event.data.object); 6 7await fg.trackPurchase( 8 { order, userId: session.client_reference_id }, 9 { visitorId, sessionId }, // ← revenue joins the real visitor's journey 10); 11 12// Works for subscriptions too: 13await fg.trackSubscription( 14 { event: "start", subscriptionId, userId, plan, mrr }, 15 { visitorId, sessionId }, 16);

What this buys you

With the real visitorId + sessionId on the purchase, revenue ties back to the exact session that produced it — the campaign, referrer and landing page — instead of appearing as unattributed server traffic. You can additionally pass attribution: { source, medium, campaign } to trackPurchase if you store campaign data yourself.

API & server error tracking

Thrown values of any shape become error_events. Pass route / method / statusCode so the dashboard can break failures down per endpoint:

app/api/teams/route.ts
1import { fg } from "@/lib/flowgrid.server"; 2 3try { 4 // … 5} catch (err) { 6 await fg.trackError(err, { 7 route: "/api/teams/edit/[teamId]", 8 method: "PATCH", 9 statusCode: 500, 10 userId: user.id, 11 }); 12 throw err; 13}

Or wrap the whole handler — the error is tracked and rethrown unchanged, so your framework's error handling behaves exactly as before:

wrapped.ts
1export const PATCH = fg.withErrorTracking( 2 async (req: NextRequest) => { /* … */ }, 3 { route: "/api/teams/edit/[teamId]", method: "PATCH" }, 4); 5 6// Opt-in process-level capture (Node only) — reports uncaughtException / 7// unhandledRejection as fatal, observes only, never swallows: 8const detach = fg.captureUncaught();

FlowGrid never reports itself

Internal notes (messages starting with [FlowGrid]) are filtered out before sending, so FlowGrid's own delivery hiccups never pollute your error analytics.

Delivery guarantees

  • Never throws. Every method resolves { ok, status?, reason? } — a tracking failure can never break your API route.
  • Retries. Network failures and 5xx responses retry with jittered exponential backoff (2 retries, 10s timeout by default — tunable in init).
  • Server context. Every event is stamped with _server: true, the runtime, and _environment (from NODE_ENV), so you can segment server vs. browser traffic.
  • Fire-and-forget friendly. Don't want to await? void fg.trackFeature(…) is safe — failures are debug-noted, never raised.