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:

TypeScript
import {
initRevTurbine,
RuntimeMode,
type RevTurbineUserContext,
} from '@revturbine/sdk/headless';
async function handleExport(req) {
const user: RevTurbineUserContext = {
id: req.user.id,
plan_handle: req.user.planHandle,
usage: {
api_calls: {
entitlement_handle: 'api_calls',
unit: 'calls',
amount: req.user.apiCallCount,
limit: req.user.apiCallLimit,
},
},
};
const rt = await initRevTurbine({
tenantId: process.env.REVTURBINE_TENANT_ID!,
apiKey: process.env.REVTURBINE_API_KEY!,
endpoint: process.env.REVTURBINE_ENDPOINT!,
runtimeMode: RuntimeMode.Server,
user,
});
const result = await rt.can('data_export');
if (!result.allowed) {
return new Response('Upgrade required', { status: 402 });
}
await rt.track('data_export_started');
await rt.flushEvents();
return runExport(req);
}

Server mode fetches the launched Playbook from the control plane, then evaluates locally. Before the first Playbook has arrived, a check denies rather than grants; once it has, a failed refresh keeps the last-known-good Playbook.

Each initRevTurbine call in Server mode fetches the launched Playbook and then caches it for that instance, re-checking for a new release at most once a minute. A request-scoped instance therefore pays that fetch on every request. Two patterns avoid it:

  • One long-lived instance per process where a single user context per instance fits — workers, cron jobs, a per-tenant service.
  • A shared Playbook snapshot with a request-scoped LocalOnly instance — the model the Python and Rust SDKs use. Load the Playbook once per process (revturbine download --live on deploy, or a timer), then construct initRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, user }) per request, as in Bundled Local Runtime below. Note that a LocalOnly instance does not send events to RevTurbine ingest.

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.

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 { 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!,
runtimeMode: RuntimeMode.Server,
user: { id: user.id, plan_handle: user.planHandle },
});
const access = await rt.can('trial_extension');
if (!access.allowed) return new Response('Upgrade required', { status: 402 });
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
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:

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.

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 and hosted modes. 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.