Skip to content

Server-Side Integration

The headless SDK runs on Node.js and edge runtimes. In Server mode it fetches the launched Playbook and evaluates each entitlement and placement in-process; in LocalOnly mode you bundle the Playbook yourself and make no decisioning network calls.

Use caseClient SDKServer SDK
Render placements in React
Gate API endpoints
Enforce usage limits before processing
SSR placement decisions✅ (local mode)
Track server-side events

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:

import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless';
async function handleExport(req) {
const rt = await initRevTurbine({
tenantId: process.env.REVTURBINE_TENANT_ID!,
apiKey: process.env.REVTURBINE_API_KEY!,
endpoint: process.env.REVTURBINE_ENDPOINT!,
mode: 'snippet',
runtimeMode: RuntimeMode.Server,
user: {
id: req.user.id,
plan_handle: req.user.planHandle,
usage: req.user.usage,
},
});
const result = await rt.can('data_export');
if (!result.allowed) {
return new Response('Upgrade required', { status: 403 });
}
await rt.track('data_export_started');
await rt.flushEvents();
return runExport(req);
}

Server mode refreshes the launched Playbook from the control plane, then evaluates locally. A fetch failure denies access rather than granting it.

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:

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:

import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless';
export async function POST(request: Request) {
const user = await requireUser(request);
const rt = await initRevTurbine({
tenantId: process.env.REVTURBINE_TENANT_ID!,
apiKey: process.env.REVTURBINE_API_KEY!,
endpoint: process.env.REVTURBINE_ENDPOINT!,
mode: 'snippet',
runtimeMode: RuntimeMode.Server,
user: { id: user.id, plan_handle: user.planHandle },
});
const access = await rt.can('trial_extension');
if (!access.allowed) return new Response('Not allowed', { status: 403 });
const trial = await extendTrial(user.id);
return Response.json({
plan_handle: user.planHandle,
trial: { in_trial: true, days_remaining: trial.daysRemaining },
});
}

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

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:

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, mints short-lived, browser-safe client sessions from a secret server key. It is available today, but it is not the decision evaluator; use the headless session above for checks.

A Python server SDK is also available, with byte-identical decisioning parity to the Node SDK:

LanguagePackageStatus
Node.js / edge@revturbine/sdk/headless✅ Available
Pythonrevturbine (PyPI)✅ Available

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

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

See the Python SDK guide for setup and usage.