Decisions, not just rendering
Caps, cooldowns, eligibility, and priority across competing placements are decided for you. One <Slot> returns the winner. No client-side orchestration, no homegrown segment logic.
RevTurbine is monetization for product-led SaaS in one SDK: plans, entitlements, usage limits, trials, and the upgrade prompts that convert — decided per user, inside your app, from a Playbook you keep. We add no latency and our availability never affects your product: the SDK evaluates your Playbook locally, so decisions never depend on our servers.
This page is the whole happy path. It runs in local mode first — no account, no key — and ends with hosted mode, where the same Playbook changes without a deploy.
Paste this into Claude Code, Cursor, or whichever agent you use. The scaffold installs the RevTurbine agent skills, so the agent knows the rest.
Run `npm create revturbine@latest` in this project. Then follow therevturbine-start-here skill: wrap the app in <RevTurbineProvider> in localmode with the generated revturbine.playbook.json, and replace ONEnon-critical hard-coded plan check with <Gate>. Do not touch server-sideenforcement. Show me the diff before you finish.Starting a new app instead? Scaffold it with its framework first (for example create-next-app), then paste this:
Run `npm create revturbine@latest` in this new app. Then follow therevturbine-start-here skill and build monetization in from the start: putevery plan, limit and upgrade prompt in revturbine.playbook.json, never inapp code, and run `npx revturbine validate ./revturbine.playbook.json` aftereach change. Gate features with <Gate> or can(), and re-check anything paidon the server. Show me the Playbook and the diff before you finish.The fuller prompts for both cases — and the CLI loop an agent uses to keep changing monetization afterwards — are on Add RevTurbine with a Coding Agent.
npm create revturbine@latestThat installs @revturbine/sdk, pins @revturbine/cli to the project, drops a local-mode revturbine.playbook.json with a free and a pro plan, one gated feature and one upgrade prompt, and installs the agent skills. npx revturbine create and npx @revturbine/cli init run the same command. Already have an account? revturbine download --live --save ./revturbine.playbook.json pulls your real Playbook instead.
Local mode, no account: the Playbook you just generated is the whole configuration.
import { RevTurbineProvider } from '@revturbine/sdk';import playbook from './revturbine.playbook.json';import { useMemo } from 'react';
export default function App() { const options = useMemo(() => ({ localRuntime: { playbook }, user: { id: 'user_123', plan_handle: 'starter' }, }), []);
return ( <RevTurbineProvider options={options}> <YourApp /> </RevTurbineProvider> );}plan_handle is the plan’s handle from your Playbook — the value plan-scoped rules match on. In hosted mode it arrives from Stripe on its own (step 7); here you supply it.
<Gate> renders its children when the user is entitled. When they aren’t, it renders the upgrade prompt your Playbook configures for that gate — copy, offer and CTA authored in config, not in your app.
import { Gate } from '@revturbine/sdk';
export function BatchExport() { return ( <Gate id="batch_export_gate" can="batch_export"> <BatchExportButton /> </Gate> );}Start the dev server. With the starter Playbook’s free plan, the gate shows the upgrade prompt; change plan_handle to 'pro' and the button appears.
For anything that isn’t a React subtree — an event handler, a keyboard shortcut, a server route — ask the SDK directly.
import { useRevTurbine } from '@revturbine/sdk';
function ExportMenu() { const { sdk: rt } = useRevTurbine();
async function onExport() { // Ask: the answer, and nothing else. const access = await rt.can('batch_export');
// Ask and act: runs exportPdf only when entitled; hands back the // entitlement so you can open the upgrade prompt otherwise. const gated = await rt.gate('export_pdf', () => exportPdf()); if (!gated.ran) openUpgrade(gated.entitlement); }
return <button onClick={onExport}>Export</button>;}can() returns { status, allowed, reason } — plus limit / used / remaining when a usage or credit rule matched. See Entitlements.
A <Slot> is a spot where RevTurbine may show something — a banner, a modal, a toast, a button. Which placement fills it, for which user, with what copy and how often, is decided from the Playbook: targeting, caps, cooldowns and priority between competing prompts are the engine’s job, not yours.
import { Slot } from '@revturbine/sdk';
export function Dashboard() { return ( <div> <h1>Dashboard</h1> {/* An upgrade banner for the users the Playbook targets — or nothing */} <Slot id="dashboard_top_banner" surfaceTemplateIds={["banner_placement"]} /> </div> );}When a user clicks a CTA inside a placement, the SDK calls a resolver you provide — navigate_to_plans, open_checkout_modal, and so on — so navigation and checkout stay in your code. The starter Playbook uses navigate_to_plans; add one line to options:
uiPathResolvers: { navigate_to_plans: () => { window.location.href = '/pricing'; } },Every action type and what it carries: Placements → UI paths.
Usage limits and credits need to know the balance; the Playbook decides what to do about it.
await rt.update({ usage: { ai_credits_used: 42 } }); // absolute balance, not a deltaawait rt.track('ai_generation_completed', { credits: 3 });Balances drive quota meters, low-credit warnings and limit prompts. Events feed targeting and analytics.
The browser check drives the UI; your server makes the money-safe decision before paid work runs — with the same SDK, evaluating the same Playbook in-process.
import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless';
async function handleExport(req, res) { 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: req.userId, plan_handle: req.user.planHandle }, }); const result = await rt.can('batch_export'); if (!result.allowed) return res.status(402).json({ error: 'upgrade_required' }); await runExport(req);}Why the re-check, and which actions need it: Client vs Server Enforcement.
Everything above works forever in local mode, free. Hosted mode is where you stop deploying to change monetization.
Create an account and connect the CLI. revturbine.com/app, then revturbine login (a browser device flow — no keys to paste).
Connect Stripe in Settings → Stripe. From then on a purchase updates what the user can do with no app code: plan and payment state arrive through a client session. See Billing & Stripe.
Switch the provider to hosted mode. Mint a publishable ingest key under Settings → API tokens, then:
const options = useMemo(() => ({ tenantId: 'tenant_abc', publicKey: 'rtk_…', // public key — safe in the browser user: { id: currentUser.id }, // plan_handle now comes from Stripe clientSession: () => fetch('/api/revturbine-session', { method: 'POST' }).then((r) => r.json()).then((j) => j.client_token),}), [currentUser.id]);Change something and reload. Open the dashboard, edit the banner’s copy or the free plan’s export limit, launch the release. The SDK picks it up on its next refresh — no deploy. Or do it from the terminal: revturbine download --live, edit, revturbine validate, revturbine diff --live, revturbine launch.
That loop — see what converted, change the Playbook, launch — is the product. Your agent can drive it too: Add RevTurbine with a Coding Agent.
Decisions, not just rendering
Caps, cooldowns, eligibility, and priority across competing placements are decided for you. One <Slot> returns the winner. No client-side orchestration, no homegrown segment logic.
Entitlements and prompts in one engine, tied to billing
A <Slot> surfaces usage warnings, trial nudges and lifecycle prompts tied to billing state; rt.can() confirms or denies access. Gate a feature and show the upgrade prompt that converts it, from one Playbook.
Changed by product, not by a deploy
Drop a slot once. Copy, targeting, limits, offers and prices change in the dashboard or through the CLI; resolvers keep navigation in your code. Every launch is a versioned release you can roll back.
The same answer everywhere
@revturbine/sdk/headless imports no React; Python and Rust evaluate the same Playbook. A cross-language gate diffs their decisions byte-for-byte, so a check answers identically wherever you make it.
| Task | Guide |
|---|---|
| Initialize React and inspect readiness | React Integration |
| Run checks without React | TypeScript / Node, Python, Rust |
| Choose Playbook delivery and optional wiring | Runtime Modes, Configuration |
| Match your app’s colors | Theming |
| Route blocked actions to a paywall | Paywall Host Pattern |
| Explain a missing placement or denied check | Troubleshooting, Debugging Decisions |
| Check release changes before upgrading | SDK changelog |
Coding agents can fetch individual pages from this index. llms-small.txt is an abridged documentation bundle, including the language guides; llms-full.txt contains the complete documentation.
can() vs gate() vs <Gate>, and the vocabulary