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 usage — feature_usage events from API routes, background jobs and webhooks.
  • Revenue & subscriptions — purchases and refunds arrive from your connected payment provider; subscription MRR events are fired from your billing webhooks.
  • 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 // environment: "production", // defaults to NODE_ENV 7});

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" });

Revenue & subscriptions

Purchases and refunds are not SDK calls. Connect your payment provider in Website settings → Payments and Flowgrid receives its webhook directly: money is recorded where it is confirmed, with its real amount and currency, and nothing in your code can drop or double-count it. See Revenue attribution for the setup.

What the SDK does own is the 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.For payments, Flowgrid does this itself when the provider's webhook arrives: nothing to write. For subscription events you send from your own webhook, 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// customer.subscription.created 5const { visitorId, sessionId } = attributionFromMetadata(event.data.object); 6 7await fg.trackSubscription( 8 { event: "start", subscriptionId, userId, plan, mrr }, 9 { visitorId, sessionId }, // ← the subscription joins the real visitor's journey 10);

What this buys you

With the real visitorId + sessionId on the payment, revenue ties back to the exact session that produced it — the campaign, referrer and landing page — instead of appearing as unattributed server traffic.

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.