Interactive Playground
Every scenario below is a live SDK demo running entirely in local_only mode — a bundled revturbine.playbook.json with plans, entitlements, placements, and targeting, and no decision-time network calls. The code is on the page; press Open in Sandpack on any scenario to edit it alongside a live preview, and switch demo users to see the engine respond.
Fixed Slots
Section titled “Fixed Slots”Always-visible inline placements.
F-1: Upgrade Button
Section titled “F-1: Upgrade Button”An always-visible upgrade CTA in a nav slot (button).
import { Slot } from "@revturbine/sdk";
export function Example() { return ( <Slot id="nav_bar_right" surfaceTemplateIds={["button"]} /> );}F-2: Plans & Pricing Card
Section titled “F-2: Plans & Pricing Card”An inline pricing/plans card (in_page).
import { Slot } from "@revturbine/sdk";
export function Example() { return ( <Slot id="pricing_main_content" surfaceTemplateIds={["in_page_card"]} /> );}F-3: Quota Meter
Section titled “F-3: Quota Meter”A usage meter with progress bar and counter (in_page).
import { Slot } from "@revturbine/sdk";
export function Example() { return ( <Slot id="sidebar_usage_widget" surfaceTemplateIds={["usage_counter"]} /> );}F-4: Annual Nudge Banner
Section titled “F-4: Annual Nudge Banner”An annual-plan nudge banner (banner).
import { Slot } from "@revturbine/sdk";
export function Example() { return ( <Slot id="dashboard_top_banner" surfaceTemplateIds={["banner_placement"]} /> );}Access Gates
Section titled “Access Gates”Entitlement gates that render an upsell when access is denied.
G-1: Data Export Gate
Section titled “G-1: Data Export Gate”A blocking modal gate on a paid feature (modal).
import { Gate } from "@revturbine/sdk";
export function Example() { return ( <Gate id="editor_export_action" surfaceTemplateIds={["modal_overlay"]} check={{ entitlement: "data_export" }} > <div style={{ padding: 16, background: "#e8f5e9", borderRadius: 8, border: "1px solid #a5d6a7" }}> ✅ Access granted — premium content visible </div> </Gate> );}G-2: Branding Gate
Section titled “G-2: Branding Gate”An inline gate on a branding feature (in_page).
import { Gate } from "@revturbine/sdk";
export function Example() { return ( <Gate id="editor_branding_toggle" surfaceTemplateIds={["inline_gate_message"]} check={{ entitlement: "branding" }} > <div style={{ padding: 16, background: "#e8f5e9", borderRadius: 8, border: "1px solid #a5d6a7" }}> ✅ Access granted — premium content visible </div> </Gate> );}G-3: Brand Kit Gate
Section titled “G-3: Brand Kit Gate”A card gate on a brand-kit feature (in_page).
import { Gate } from "@revturbine/sdk";
export function Example() { return ( <Gate id="settings_brand_section" surfaceTemplateIds={["in_page_card"]} check={{ entitlement: "brand_kit" }} > <div style={{ padding: 16, background: "#e8f5e9", borderRadius: 8, border: "1px solid #a5d6a7" }}> ✅ Access granted — premium content visible </div> </Gate> );}Global Slots
Section titled “Global Slots”Ephemeral, moment-driven messages.
M-1: Usage Warning Banner
Section titled “M-1: Usage Warning Banner”A “running low” usage banner (banner).
import { Slot } from "@revturbine/sdk";
export function Example() { return ( <Slot id="global_banner" surfaceTemplateIds={["banner_placement","in_page_card"]} /> );}M-2: Usage Exhausted Modal
Section titled “M-2: Usage Exhausted Modal”A limit-reached modal (modal).
import { Slot } from "@revturbine/sdk";
export function Example() { return ( <Slot id="global_modal" surfaceTemplateIds={["modal_overlay"]} /> );}M-3: Trial Welcome Toast
Section titled “M-3: Trial Welcome Toast”A trial-welcome toast (toast).
import { Slot } from "@revturbine/sdk";
export function Example() { return ( <Slot id="global_toast" surfaceTemplateIds={["toast_message"]} /> );}Headless API
Section titled “Headless API”The imperative SDK, no slot components.
H-1: PlacementController
Section titled “H-1: PlacementController”Driving a placement imperatively.
import React, { useEffect, useState, useRef } from "react";import { initRevTurbine, PlacementController, RuntimeMode } from "@revturbine/sdk/headless";import playbook from "./playbook.json";import { demoUsers } from "./demoUsers";import { selectedUserId } from "./demoUser";
const activeUser = demoUsers[selectedUserId] ?? demoUsers.user_alice;
export function Example() { const [state, setState] = useState({ isLoading: true, error: "", visible: false, content: null, placementId: "" }); const ctrlRef = useRef(null);
useEffect(() => { (async () => { const session = await initRevTurbine({ // local_only runs against the bundled playbook — no network for // decisions. previewMode keeps this docs demo out of adoption // telemetry; omit it in your own app. runtimeMode: RuntimeMode.LocalOnly, previewMode: true, localRuntime: { playbook }, user: activeUser.context, uiPathResolvers: { navigate_to_plans: async (ctx) => { console.log("[uiPath] navigate_to_plans", ctx); }, open_checkout_modal: async (ctx) => { console.log("[uiPath] open_checkout_modal", ctx); }, custom_url: async (ctx) => { console.log("[uiPath] custom_url", ctx); }, }, });
const ctrl = session.placement({ surfaceSlot: { id: "dashboard_top_banner", name: "dashboard_top_banner", surfaceTemplateIds: ["banner_placement"], }, }); ctrlRef.current = ctrl;
// The controller notifies on every state change; read its snapshot. ctrl.onChange(() => setState(ctrl.state)); await ctrl.load(); })();
return () => { ctrlRef.current?.dispose?.(); }; }, []);
if (state.isLoading) return <p>Loading…</p>; if (state.error) return <pre style={{ color: "red" }}>{state.error}</pre>; if (!state.visible) return <p>No placement matched.</p>;
return ( <div style={{ padding: 16, fontFamily: "system-ui, sans-serif" }}> <h3>Headless: PlacementController</h3> <pre style={{ background: "#f5f5f5", padding: 12, borderRadius: 8, fontSize: 12, overflow: "auto" }}> {JSON.stringify(state, null, 2)} </pre> </div> );}H-2: EntitlementGate
Section titled “H-2: EntitlementGate”An entitlement check without React.
import React, { useEffect, useState } from "react";import { initRevTurbine, RuntimeMode } from "@revturbine/sdk/headless";import playbook from "./playbook.json";import { demoUsers } from "./demoUsers";import { selectedUserId } from "./demoUser";
const activeUser = demoUsers[selectedUserId] ?? demoUsers.user_alice;
export function Example() { const [result, setResult] = useState(null); const [error, setError] = useState(null);
useEffect(() => { (async () => { const session = await initRevTurbine({ // local_only runs against the bundled playbook — no network for // decisions. previewMode keeps this docs demo out of adoption // telemetry; omit it in your own app. runtimeMode: RuntimeMode.LocalOnly, previewMode: true, localRuntime: { playbook }, user: activeUser.context, uiPathResolvers: {}, });
// The check: bound to this session, evaluated against the local playbook. const gate = session.entitlement({ handle: "brand_kit" }); const entitlement = await gate.check(); gate.dispose();
// On denial, resolve the upgrade placement configured for this gate — // the same lookup <Gate> makes before rendering it. let upsell = null; if (gate.denied) { const ctrl = session.placement({ surfaceSlot: { id: "settings_brand_section", name: "settings_brand_section", surfaceTemplateIds: ["in_page_card"], metadata: { surface_slot_category: "gated", entitlement_handle: "brand_kit" }, }, }); const decision = await ctrl.load(); ctrl.dispose(); upsell = decision?.visible ? decision.content : null; } setResult({ entitlement, upsell }); })().catch((e) => setError(e.message)); }, []);
return ( <div style={{ padding: 16, fontFamily: "system-ui, sans-serif" }}> <h3>Gate: EntitlementGate</h3> <p>Entitlement: <code>brand_kit</code></p> {error && <pre style={{ color: "red" }}>{error}</pre>} {result && <pre style={{ background: "#f5f5f5", padding: 12, borderRadius: 8, fontSize: 12, overflow: "auto" }}>{JSON.stringify(result, null, 2)}</pre>} {!result && !error && <p>Checking…</p>} </div> );}H-3: SdkSession One-Shot
Section titled “H-3: SdkSession One-Shot”A one-shot session evaluation.
import React, { useEffect, useState } from "react";import { initRevTurbine, RuntimeMode } from "@revturbine/sdk/headless";import playbook from "./playbook.json";import { demoUsers } from "./demoUsers";import { selectedUserId } from "./demoUser";
const activeUser = demoUsers[selectedUserId] ?? demoUsers.user_alice;
export function Example() { const [result, setResult] = useState(null); const [error, setError] = useState(null);
useEffect(() => { (async () => { const session = await initRevTurbine({ // local_only runs against the bundled playbook — no network for // decisions. previewMode keeps this docs demo out of adoption // telemetry; omit it in your own app. runtimeMode: RuntimeMode.LocalOnly, previewMode: true, localRuntime: { playbook }, user: activeUser.context, uiPathResolvers: {}, });
// initRevTurbine already returns the SdkSession. One-shot: build a // controller, resolve one decision, and dispose it straight away. const ctrl = session.placement({ surfaceSlot: { id: "dashboard_top_banner", name: "dashboard_top_banner", surfaceTemplateIds: ["banner_placement"], }, }); try { setResult(await ctrl.load()); } finally { ctrl.dispose(); } })().catch((e) => setError(e.message)); }, []);
return ( <div style={{ padding: 16, fontFamily: "system-ui, sans-serif" }}> <h3>Session: SdkSession One-Shot</h3> {error && <pre style={{ color: "red" }}>{error}</pre>} {result && <pre style={{ background: "#f5f5f5", padding: 12, borderRadius: 8, fontSize: 12, overflow: "auto" }}>{JSON.stringify(result, null, 2)}</pre>} {!result && !error && <p>Resolving…</p>} </div> );}How it works
Section titled “How it works”Each scenario runs the SDK in an isolated Sandpack environment:
revturbine.playbook.jsonprovides plans, entitlements, placements, surface templates, and targeting rules.RevTurbineProviderinitializes the SDK inlocal_onlymode — no network calls.- Demo users have different plans, usage, and roles: Alice and Bob are on Professional, Carol and Dan on Starter, and Eve and Frank on Enterprise.
- Entitlement rules bind each entitlement to plans by handle —
data_export,branding, andbrand_kitare granted on Professional and Enterprise and disabled on Starter. So every Access Gate shows its content for Alice, Bob, Eve, and Frank, and its configured upgrade placement for Carol and Dan. - Targeting rules evaluate against the user context to decide which placements to show.
Switch demo users in a scenario’s code to see how the SDK renders different placements for different segments.