Skip to content

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.

Always-visible inline placements.

An always-visible upgrade CTA in a nav slot (button).

Example.tsx
import { Slot } from "@revturbine/sdk";
export function Example() {
return (
<Slot
id="nav_bar_right"
surfaceTemplateIds={["button"]}
/>
);
}

An inline pricing/plans card (in_page).

Example.tsx
import { Slot } from "@revturbine/sdk";
export function Example() {
return (
<Slot
id="pricing_main_content"
surfaceTemplateIds={["in_page_card"]}
/>
);
}

A usage meter with progress bar and counter (in_page).

Example.tsx
import { Slot } from "@revturbine/sdk";
export function Example() {
return (
<Slot
id="sidebar_usage_widget"
surfaceTemplateIds={["usage_counter"]}
/>
);
}

An annual-plan nudge banner (banner).

Example.tsx
import { Slot } from "@revturbine/sdk";
export function Example() {
return (
<Slot
id="dashboard_top_banner"
surfaceTemplateIds={["banner_placement"]}
/>
);
}

Entitlement gates that render an upsell when access is denied.

A blocking modal gate on a paid feature (modal).

Example.tsx
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>
);
}

An inline gate on a branding feature (in_page).

Example.tsx
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>
);
}

A card gate on a brand-kit feature (in_page).

Example.tsx
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>
);
}

Ephemeral, moment-driven messages.

A “running low” usage banner (banner).

Example.tsx
import { Slot } from "@revturbine/sdk";
export function Example() {
return (
<Slot
id="global_banner"
surfaceTemplateIds={["banner_placement","in_page_card"]}
/>
);
}

A limit-reached modal (modal).

Example.tsx
import { Slot } from "@revturbine/sdk";
export function Example() {
return (
<Slot
id="global_modal"
surfaceTemplateIds={["modal_overlay"]}
/>
);
}

A trial-welcome toast (toast).

Example.tsx
import { Slot } from "@revturbine/sdk";
export function Example() {
return (
<Slot
id="global_toast"
surfaceTemplateIds={["toast_message"]}
/>
);
}

The imperative SDK, no slot components.

Driving a placement imperatively.

Example.tsx
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>
);
}

An entitlement check without React.

Example.tsx
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>
);
}

A one-shot session evaluation.

Example.tsx
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>
);
}

Each scenario runs the SDK in an isolated Sandpack environment:

  1. revturbine.playbook.json provides plans, entitlements, placements, surface templates, and targeting rules.
  2. RevTurbineProvider initializes the SDK in local_only mode — no network calls.
  3. 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.
  4. Entitlement rules bind each entitlement to plans by handle — data_export, branding, and brand_kit are 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.
  5. 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.