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.
When to Use Server-Side
Section titled “When to Use Server-Side”| Use case | Client SDK | Server SDK |
|---|---|---|
| Render placements in React | ✅ | — |
| Gate API endpoints | — | ✅ |
| Enforce usage limits before processing | — | ✅ |
| SSR placement decisions | ✅ (local mode) | ✅ |
| Track server-side events | — | ✅ |
Headless Server Re-check
Section titled “Headless Server Re-check”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, 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.
Instance lifecycle
Section titled “Instance lifecycle”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
LocalOnlyinstance — the model the Python and Rust SDKs use. Load the Playbook once per process (revturbine download --liveon deploy, or a timer), then constructinitRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, user })per request, as in Bundled Local Runtime below. Note that aLocalOnlyinstance 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.
Server-Owned CTA Actions
Section titled “Server-Owned CTA Actions”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!, 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.
Bundled Local Runtime
Section titled “Bundled Local Runtime”The local runtime evaluates entitlements and placements entirely from a Playbook JSON snapshot. No API server connection required.
Installation
Section titled “Installation”pnpm add @revturbine/sdkEntitlement Checks
Section titled “Entitlement Checks”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 entitlementconst result = await sdk.can('data_export');
if (!result.allowed) { // deny access — user needs to upgrade console.log('Upgrade required:', result.status);}Placement Decisions (SSR)
Section titled “Placement Decisions (SSR)”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 placementconst banner = await sdk.getPlacement({ slotId: 'dashboard_banner' });
if (banner) { // Render banner.content into your template}SSR with React (Local Mode)
Section titled “SSR with React (Local Mode)”For frameworks like Next.js, resolve placements with the local runtime and hydrate client-side:
'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> );}Client Sessions
Section titled “Client Sessions”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.
Server SDK languages
Section titled “Server SDK languages”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.
| Language | Package | Guide |
|---|---|---|
| Node.js / edge | @revturbine/sdk/headless | This page; Headless API |
| Python | revturbine (PyPI) | Python SDK |
| Rust | revturbine (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():
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.
Next Steps
Section titled “Next Steps”- User Context — canonical fields and cross-language shapes
- Entitlements Guide — entitlement types and checking patterns
- Runtime Modes — choosing the right runtime mode
- Headless API — imperative SDK usage without React