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.
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 | — | ✅ |
| 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 |
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.
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:
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, });}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.
Hosted delivery on a backend
Section titled “Hosted delivery on a backend”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.
What a server reports
Section titled “What a server reports”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.
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 { 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.
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:
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.
'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, 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.
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.
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 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():
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
Runnable cloud examples
Section titled “Runnable cloud examples”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.