Skip to content

Start

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 the
revturbine-start-here skill: wrap the app in <RevTurbineProvider> in local
mode with the generated revturbine.playbook.json, and replace ONE
non-critical hard-coded plan check with <Gate>. Do not touch server-side
enforcement. 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 the
revturbine-start-here skill and build monetization in from the start: put
every plan, limit and upgrade prompt in revturbine.playbook.json, never in
app code, and run `npx revturbine validate ./revturbine.playbook.json` after
each change. Gate features with <Gate> or can(), and re-check anything paid
on 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.

Local mode, no account: the Playbook you just generated is the whole configuration.

src/App.tsx
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.

src/components/BatchExport.tsx
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.

React
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.

src/components/Dashboard.tsx
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:

React
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.

TypeScript
await rt.update({ usage: { ai_credits_used: 42 } }); // absolute balance, not a delta
await 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.

TypeScript
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.

8. Go hosted: connect Stripe, change it without a deploy

Section titled “8. Go hosted: connect Stripe, change it without a deploy”

Everything above works forever in local mode, free. Hosted mode is where you stop deploying to change monetization.

  1. Create an account and connect the CLI. revturbine.com/app, then revturbine login (a browser device flow — no keys to paste).

  2. 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.

  3. Switch the provider to hosted mode. Mint a publishable ingest key under Settings → API tokens, then:

    React
    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]);
  4. 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.

TaskGuide
Initialize React and inspect readinessReact Integration
Run checks without ReactTypeScript / Node, Python, Rust
Choose Playbook delivery and optional wiringRuntime Modes, Configuration
Match your app’s colorsTheming
Route blocked actions to a paywallPaywall Host Pattern
Explain a missing placement or denied checkTroubleshooting, Debugging Decisions
Check release changes before upgradingSDK 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.