SDK Reference

Server-Side Feature Flags

The Databuddy Node SDK includes a server-side feature flags manager optimized for server environments with request deduplication, batching, and stale-while-revalidate caching.

Installation

bash
bun add @databuddy/sdk

Quick Start

tsx
import { createServerFlagsManager } from "@databuddy/sdk/node";

const flags = createServerFlagsManager({
clientId: process.env.DATABUDDY_CLIENT_ID!,
user: {
  userId: "user-123",
  organizationId: "org-456"
}
});

// Wait for initialization. Server managers only preload flags here when
// autoFetch is true; getFlag() below fetches this flag on demand.
await flags.waitForInit();

// Check a flag
const result = await flags.getFlag("new-feature");
if (result.enabled) {
// Show new feature
}

Creating a Manager

createServerFlagsManager(config)

Creates a new server-side flags manager instance:

tsx
import { createServerFlagsManager } from "@databuddy/sdk/node";

const flags = createServerFlagsManager({
clientId: process.env.DATABUDDY_CLIENT_ID!,
apiUrl: "https://api.databuddy.cc",
user: {
  userId: "user-123",
  organizationId: "org-456",
  properties: {
    role: "admin",
    workspace_type: "team"
  }
},
environment: "production",
cacheTtl: 60_000,      // 1 minute cache
staleTime: 30_000,     // Revalidate after 30s
maxCacheSize: 5000,    // Cap in-memory cache entries
debug: false
});

Configuration

OptionTypeDefaultDescription
clientIdstringRequiredYour client ID
apiUrlstringhttps://api.databuddy.ccAPI endpoint
userUserContext-User context for targeting
environmentstring-Environment name
cacheTtlnumber60000Cache TTL in ms
staleTimenumber30000Revalidate after (ms)
maxCacheSizenumber5000Maximum in-memory cache entries
autoFetchbooleanfalseFetch all flags on init
debugbooleanfalseEnable debug logging
disabledbooleanfalseDisable flag evaluation

User Context

OptionTypeDescription
userIdstringUser identifier for targeting
emailstringEmail for targeting
organizationIdstringOrganization for group rollouts
teamIdstringTeam for group rollouts
propertiesobjectCustom properties for targeting

Fetching Flags

getFlag(key, user?)

Fetch a single flag with caching and deduplication:

tsx
const result = await flags.getFlag("my-feature");

console.log({
enabled: result.enabled,    // boolean
value: result.value,        // boolean | string | number
variant: result.variant,    // string (for A/B tests)
reason: result.reason       // evaluation reason
});

Override user context for a specific check:

tsx
const result = await flags.getFlag("advanced-feature", {
userId: "different-user",
properties: { role: "admin" }
});

Per-call user context is sent to the flags API for that evaluation and overrides the manager's default user. Cache entries are isolated by flag key, environment, and the full targeting context (userId, email, organizationId, teamId, and properties).

fetchAllFlags(user?)

Pre-fetch all flags for a user before using synchronous reads:

tsx
// Fetch all flags upfront
await flags.fetchAllFlags();

// Now synchronous checks are fast
const state = flags.isEnabled("feature-1");
const value = flags.getValue("max-items", 10);

isEnabled(key)

Synchronous read from cache (call after fetchAllFlags or getFlag). Returns FlagState:

tsx
const state = flags.isEnabled("my-feature");

if (state.status === "ready" && state.on) {
// Feature is enabled
}
tsx
interface FlagState {
on: boolean;
status: "ready" | "loading" | "error" | "pending";
loading: boolean;
value?: boolean | string | number;
variant?: string;
}

getValue(key, defaultValue)

Get a typed value from cache:

tsx
const maxItems = flags.getValue("max-items", 10);
const theme = flags.getValue<"light" | "dark">("theme", "light");

Usage Patterns

Next.js API Routes

app/api/data/route.tstsx
import { createServerFlagsManager } from "@databuddy/sdk/node";
import { NextResponse } from "next/server";

const flags = createServerFlagsManager({
clientId: process.env.DATABUDDY_CLIENT_ID!
});

export async function GET(request: Request) {
const userId = request.headers.get("x-user-id");

const result = await flags.getFlag("new-api-version", {
  userId: userId ?? undefined
});

if (result.enabled) {
  return NextResponse.json({ version: "v2", data: await getNewData() });
}

return NextResponse.json({ version: "v1", data: await getLegacyData() });
}

Next.js Server Components

app/dashboard/page.tsxtsx
import { createServerFlagsManager } from "@databuddy/sdk/node";
import { auth } from "@/lib/auth";

export default async function DashboardPage() {
const session = await auth();

const flags = createServerFlagsManager({
  clientId: process.env.DATABUDDY_CLIENT_ID!,
  user: session?.user ? {
    userId: session.user.id,
    organizationId: session.user.organizationId,
    properties: {
      role: session.user.role,
      workspace_type: session.user.workspaceType
    }
  } : undefined
});

const newDashboard = await flags.getFlag("new-dashboard");

if (newDashboard.enabled) {
  return <NewDashboard />;
}

return <LegacyDashboard />;
}

Express Middleware

tsx
import express from "express";
import { createServerFlagsManager } from "@databuddy/sdk/node";

const app = express();

// Create a shared manager
const flags = createServerFlagsManager({
clientId: process.env.DATABUDDY_CLIENT_ID!,
autoFetch: true
});

// Wait for init
await flags.waitForInit();

// Middleware to attach flags to request
app.use(async (req, res, next) => {
const userId = req.headers["x-user-id"] as string;

req.flags = {
  isEnabled: async (key: string) => {
    const result = await flags.getFlag(key, { userId });
    return result.enabled;
  }
};

next();
});

app.get("/api/feature", async (req, res) => {
const enabled = await req.flags.isEnabled("my-feature");
res.json({ enabled });
});

Serverless Functions

tsx
import { createServerFlagsManager } from "@databuddy/sdk/node";

// Create manager outside handler for reuse
const flags = createServerFlagsManager({
clientId: process.env.DATABUDDY_CLIENT_ID!
});

export async function handler(event: any) {
const userId = event.headers["x-user-id"];

const result = await flags.getFlag("feature", { userId });

return {
  statusCode: 200,
  body: JSON.stringify({
    enabled: result.enabled,
    variant: result.variant
  })
};
}

Measure Server-Side Outcomes

Server-side flag evaluation does not emit browser exposure events. When a server action, API route, webhook, or job is the source of truth, track the durable outcome on the server and include the flag key and variant. If the outcome belongs to a browser session, send anonId and sessionId from the client using getTrackingIds() and map anonId to anonymousId.

client.tsxtsx
import { getTrackingIds } from "@databuddy/sdk";

async function startExport(format: string) {
const { anonId, sessionId } = getTrackingIds();

await fetch("/api/export", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ anonId, sessionId, format })
});
}
app/api/export/route.tstsx
import { Databuddy, createServerFlagsManager } from "@databuddy/sdk/node";

const events = new Databuddy({
apiKey: process.env.DATABUDDY_API_KEY!,
websiteId: process.env.DATABUDDY_WEBSITE_ID!,
source: "server"
});

const flags = createServerFlagsManager({
clientId: process.env.DATABUDDY_CLIENT_ID!
});

export async function POST(request: Request) {
const { anonId, sessionId, format } = await request.json();
const userId = request.headers.get("x-user-id") ?? undefined;

const exportFlow = await flags.getFlag("export-flow", { userId });
const variant = exportFlow.variant ?? "control";
const result = await runExport({ format, variant });

await events.track({
  name: "report_exported",
  anonymousId: anonId,
  sessionId,
  properties: {
    flag_key: "export-flow",
    variant,
    status: result.ok ? "success" : "failed",
    format: result.format
  }
});

await events.flush();
return Response.json({ ok: result.ok });
}

Caching Behavior

The server manager uses stale-while-revalidate caching:

  1. Fresh cache: Returns immediately
  2. Stale cache: Returns immediately, revalidates in background
  3. No cache: Fetches from API
tsx
const flags = createServerFlagsManager({
clientId: process.env.DATABUDDY_CLIENT_ID!,
cacheTtl: 60_000,      // Cache valid for 1 minute
staleTime: 30_000,     // Revalidate after 30 seconds
maxCacheSize: 5000     // Evict old entries above this size
});

// First call: fetches from API
const result1 = await flags.getFlag("my-feature");

// Within 30s: returns cached value (fresh)
const result2 = await flags.getFlag("my-feature");

// After 30s but within 60s: returns cached, revalidates in background
const result3 = await flags.getFlag("my-feature");

// After 60s: fetches from API
const result4 = await flags.getFlag("my-feature");

Expired entries are pruned during cache writes and reads. Long-lived shared managers should keep the default maxCacheSize or set a limit that matches their traffic shape, especially when passing per-request users.

Request Batching

Multiple concurrent flag requests with the same API URL and targeting context are batched automatically:

tsx
// These 3 concurrent requests become 1 API call
const [flag1, flag2, flag3] = await Promise.all([
flags.getFlag("feature-1"),
flags.getFlag("feature-2"),
flags.getFlag("feature-3")
]);

Requests for different users, organizations, teams, properties, or environments are intentionally sent through separate batches so evaluations cannot mix targeting contexts.

Request Deduplication

Identical concurrent requests are deduplicated:

tsx
// Only 1 API call is made
const [result1, result2] = await Promise.all([
flags.getFlag("same-feature"),
flags.getFlag("same-feature")
]);

Updating User Context

tsx
// Update user for subsequent calls
flags.updateUser({
userId: "new-user",
organizationId: "org-456",
properties: { role: "admin" }
});

// Refresh flags for new user
await flags.refresh();

Manager Lifecycle

waitForInit()

Wait for the manager to initialize:

tsx
const flags = createServerFlagsManager({
clientId: process.env.DATABUDDY_CLIENT_ID!,
autoFetch: true
});

await flags.waitForInit();
// Now all flags are cached

isReady()

Check if the manager is ready:

tsx
if (flags.isReady()) {
// Safe to use synchronous reads
const state = flags.isEnabled("my-feature");
}

destroy()

Clean up resources:

tsx
flags.destroy();

Singleton Pattern

For most applications, create a single manager instance:

lib/flags.tstsx
import { createServerFlagsManager } from "@databuddy/sdk/node";

let flagsManager: ReturnType<typeof createServerFlagsManager> | null = null;

export function getFlags() {
if (!flagsManager) {
  flagsManager = createServerFlagsManager({
    clientId: process.env.DATABUDDY_CLIENT_ID!,
    autoFetch: true,
    maxCacheSize: 5000
  });
}
return flagsManager;
}
In your routestsx
import { getFlags } from "@/lib/flags";

const flags = getFlags();
const result = await flags.getFlag("my-feature", { userId });

TypeScript Types

tsx
import {
createServerFlagsManager,
ServerFlagsManager,
type FlagsConfig,
type FlagResult,
type FlagState,
type UserContext
} from "@databuddy/sdk/node";

How is this guide?