Skip to content

Server-Side Integration

The headless SDK runs on Node.js and edge runtimes. On a server it evaluates the Playbook snapshot you load, in-process, one user per instance, with no decisioning network call. Your server key authenticates hosted Playbook loading and event ingest. The separate RevTurbineServer helper mints client sessions for the browser; it does not evaluate entitlements.

Use caseClient SDKServer SDK
Render placements in React✅—
Gate API endpoints—✅
Check a usage allowance before processing—✅ (your application owns atomic consumption)
SSR placement decisions✅ (local mode)✅
Report events to RevTurbine✅✅ TypeScript connected mode; see Events
Mint client sessions—✅ RevTurbineServer

Use the ./headless entry so a server or edge function does not load React. Always repeat money-, data-, or compute-sensitive checks at this boundary.

Load the Playbook once per process, and build one instance per request. The SDK holds a single user context per instance, so a shared instance would let two concurrent requests overwrite each other’s user. A request-scoped instance built from an already-loaded Playbook makes no network call to decide:

src/server/revturbine.ts
import {
initRevTurbine,
RuntimeMode,
type RevTurbineUserContext,
} from '@revturbine/sdk/headless';
import playbook from './revturbine.playbook.json';
/** One instance per request. The Playbook is loaded once, when this module is. */
export function revturbineFor(user: RevTurbineUserContext) {
return initRevTurbine({
runtimeMode: RuntimeMode.LocalOnly,
localRuntime: { playbook },
user,
});
}
src/server/export-route.ts
import type { RevTurbineUserContext } from '@revturbine/sdk/headless';
import { revturbineFor } from './revturbine';
import { getSignedInUser, runExport } from './app'; // your own code
export async function POST(request: Request): Promise<Response> {
const user = await getSignedInUser(request);
if (!user) return new Response('Unauthorized', { status: 401 });
const context: RevTurbineUserContext = {
id: user.id,
plan_handle: user.planHandle, // from your own billing record: the server decides, not the browser
usage: {
api_calls: {
entitlement_handle: 'api_calls',
unit: 'calls',
amount: user.apiCallCount,
limit: user.apiCallLimit,
},
},
};
const rt = await revturbineFor(context);
try {
const result = await rt.can('data_export');
if (!result.allowed) return new Response('Upgrade required', { status: 403 });
return await runExport(request);
} finally {
rt.sdk.dispose();
}
}

The Playbook file comes from your launched Playbook: revturbine download --live --save ./revturbine.playbook.json, run in your build or deploy step. A launch reaches a running server when it next loads that file, so restart or redeploy after a launch.

Initial user.usage entries use the rich UserUsageEntry shape shown above. After initialization, call rt.update({ usage: { api_calls: total } }) with absolute numeric balances, not deltas.

TypeScript’s RuntimeMode.Server supports a backend apiKey for Playbook loading and event ingest. Browser integrations use publicKey; browser and worker contexts never send the server secret. A backend public key without an allowed browser origin is not a substitute for a server key.

Evaluation still runs locally over UserContext and Playbook. The application owns requests and any shared snapshot; never mutate a shared user-bound SDK instance to serve different users. The LocalOnly pattern above evaluates the snapshot supplied by the application. For an explicitly connected instance, a supplied localRuntime.playbook is a startup seed that hosted loading refreshes.

A failed load is visible through getPlaybookLoadFailure(). Without a readable Playbook, entitlement checks fail closed with config_unavailable.

A LocalOnly instance makes no hosted decision or event-ingest call. Its track() calls do not reach RevTurbine ingest.

A TypeScript hosted backend instance authenticates track() events with its server key. Call await rt.flushEvents() before ending a short-lived operation; backend instances have no browser page-unload lifecycle. flushEvents() is best-effort transport work, not a durable delivery receipt; call rt.sdk.dispose() in finally after using the session. Events belong to that instance’s user context. This transport does not turn the SDK into a request handler or share identity between users.

Python and Rust evaluate a Playbook supplied by the application and send no events.

An authored CTA such as extend_trial starts in the browser but must mutate state on your server. Register a serverActions handler with the client SDK; it calls your route and returns fresh server-authoritative context:

TypeScript
const options = {
// ...your normal RevTurbineProvider options
serverActions: {
extend_trial: async ({ placement, params }) => {
const response = await fetch('/api/trial/extend', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
decisionId: placement.decision_id,
extensionDays: params.extension_days,
}),
});
if (!response.ok) return { success: false };
return { success: true, userContext: await response.json() };
},
},
};

The route repeats the entitlement check before it changes anything:

TypeScript
import { revturbineFor } from './revturbine';
import { extendTrial, requireUser } from './app'; // your own code
export async function POST(request: Request): Promise<Response> {
const user = await requireUser(request);
const rt = await revturbineFor({ id: user.id, plan_handle: user.planHandle });
try {
const access = await rt.can('trial_extension');
if (!access.allowed) return new Response('Upgrade required', { status: 403 });
const trial = await extendTrial(user.id);
return Response.json({
plan_handle: user.planHandle,
trial: { in_trial: true, days_remaining: trial.daysRemaining },
});
} finally {
rt.sdk.dispose();
}
}

The SDK contains rejection and { success: false }, leaves held context unchanged, and records the result on the canonical placement_interaction event. There is no client-side extendTrial() mutation.

The local runtime evaluates entitlements and placements entirely from a Playbook JSON snapshot. No API server connection required.

Terminal window
pnpm add @revturbine/sdk
TypeScript
import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless';
import playbook from './revturbine.playbook.json';
const sdk = await initRevTurbine({
runtimeMode: RuntimeMode.LocalOnly,
localRuntime: { playbook },
user: {
id: 'user_123',
plan_handle: 'starter',
entitlements: { data_export: false },
},
});
// Check an entitlement
const result = await sdk.can('data_export');
if (!result.allowed) {
// deny access — user needs to upgrade
console.log('Upgrade required:', result.status);
}

Resolve placements server-side for SSR or email templates:

TypeScript
import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless';
import playbook from './revturbine.playbook.json';
const sdk = await initRevTurbine({
runtimeMode: RuntimeMode.LocalOnly,
localRuntime: { playbook },
user: {
id: req.user.id,
plan_handle: req.user.planHandle,
},
});
// Report current usage (balances live under `usage`; a bare handle is not
// a usage report)
sdk.update({ usage: { api_calls: req.user.apiCallCount } });
// Resolve a specific slot's placement
const banner = await sdk.getPlacement({ slotId: 'dashboard_banner' });
if (banner) {
// Render banner.content into your template
}

For frameworks like Next.js, resolve placements with the local runtime and hydrate client-side:

sdk.hydrate(payload) applies user context, entitlements, trial status, and usage. Legacy payload placement decisions are not reused: they lack a binding to the producer’s complete evaluation context and Playbook. The browser evaluates placements locally from its current inputs, without a decision HTTP request.

components/Dashboard.tsx
'use client';
import { RevTurbineProvider, Slot, RuntimeMode } from '@revturbine/sdk';
import playbook from './revturbine.playbook.json';
import { useMemo } from 'react';
export default function Dashboard({ user }) {
const options = useMemo(() => ({
runtimeMode: RuntimeMode.LocalOnly,
localRuntime: { playbook },
user: {
id: user.id,
plan_handle: user.planHandle,
entitlements: user.entitlements,
},
}), [user]);
return (
<RevTurbineProvider options={options}>
<Slot
id="dashboard_banner"
surfaceTemplateIds={["banner_placement"]}
/>
{/* rest of your dashboard */}
</RevTurbineProvider>
);
}

RevTurbineServer, exported from @revturbine/sdk/headless, holds your server key and mints short-lived, browser-safe client sessions for one user at a time. This is the hosted-mode job for your server: it is how plan, trial and payment state from Stripe reaches the browser SDK. It is not a decision evaluator; use the headless session above for checks.

app/api/revturbine-session/route.ts
import { RevTurbineServer } from '@revturbine/sdk/headless';
import { getSignedInUser } from '@/server/auth'; // your own auth: the signed-in user, or null
const revturbine = new RevTurbineServer({
tenantId: process.env.REVTURBINE_TENANT_ID!,
apiKey: process.env.REVTURBINE_API_KEY!, // the server key, never NEXT_PUBLIC_
endpoint: process.env.REVTURBINE_ENDPOINT ?? 'https://revturbine.com/app',
});
export async function POST(): Promise<Response> {
const user = await getSignedInUser();
if (!user) return new Response('Unauthorized', { status: 401 });
const { client_token } = await revturbine.clientSessions.create({ subject: user.id });
return Response.json({ client_token });
}

The browser half, an Express version, the failure behavior, and how the SDK refreshes the token are on Client sessions.

TypeScript has both browser/React and headless entry points. Python and Rust are headless libraries for your backend; all three evaluate the same Playbook with parity-checked decisions.

LanguagePackageGuide
Node.js / edge@revturbine/sdk/headlessThis page; Headless API
Pythonrevturbine (PyPI)Python SDK
Rustrevturbine (crates.io)Rust SDK

TypeScript supports local mode everywhere and hosted Playbook delivery in both browsers and trusted backends. A backend uses its server apiKey for delivery and evaluates locally in a request-scoped session; the separate RevTurbineServer helper mints browser client sessions. Python and Rust evaluate a supplied Playbook locally; the application owns refreshing that snapshot. Their guides describe installation, input shapes and the supported surfaces.

Python exposes the same re-check question as can():

Python
result = sdk.can("data_export")
if not result["allowed"]:
raise PermissionError(result["reason"])

See the Python SDK guide and Rust SDK guide for setup and usage.

Follow Cloud and edge entitlement checks for Vercel, Firebase, AWS, Google Cloud and Azure. It keeps authentication and request handling in your application, caches only immutable Playbooks, and constructs a fresh SDK session for each operation. The Vercel walkthrough also exercises the browser-compatible bundle in an Edge VM.