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.

Every decision RevTurbine makes combines two things, and the SDK combines them locally:

Plain-English definitions, how initialization and context changes drive a decision, and a worked example are on Runtime and Playbook. The code for every runtime is on Initialize the SDK.

Use these focused guides when moving beyond the first gate:

These pages are also included in the generated llms-full.txt for coding agents.

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';
import playbook from './revturbine.playbook.json';
export async function POST(request: Request): Promise<Response> {
const user = await getSignedInUser(request); // your own auth
const rt = await initRevTurbine({
runtimeMode: RuntimeMode.LocalOnly,
localRuntime: { playbook }, // loaded once per process; `revturbine download --live` on deploy
user: { id: user.id, plan_handle: user.planHandle },
});
const result = await rt.can('batch_export');
if (!result.allowed) return Response.json({ error: 'upgrade_required' }, { status: 402 });
return runExport(request);
}

One instance per request, built from a Playbook you loaded once, so the check makes no network call. Why the re-check, and which actions need it: Client vs Server Enforcement. Why a server does not use hosted mode, and what it can report: Server-Side Integration.

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. Put revturbine_account_id and revturbine_user_id in the metadata of every Stripe customer you create so each payer links to an account from day one: Link Stripe customers to accounts. See Billing & Stripe.

  3. Switch the provider to hosted mode. Mint an ingest key under Settings → API Token Management → Ingest Keys and add your app’s origins to its allowed origins. Then add the client-session route to your server (it mints a short-lived token with your server key, so the plan Stripe holds reaches the SDK) and pass both to the provider:

    React
    import { RevTurbineProvider } from '@revturbine/sdk';
    import { useMemo, type ReactNode } from 'react';
    import { fetchClientSession } from './revturbine-client-session';
    export function RevTurbine({ userId, children }: { userId: string; children: ReactNode }) {
    const options = useMemo(() => ({
    publicKey: process.env.NEXT_PUBLIC_REVTURBINE_PUBLIC_KEY!, // public key, safe in the browser
    user: { id: userId }, // plan_handle now comes from Stripe
    clientSession: fetchClientSession, // calls YOUR route; see Client sessions
    }), [userId]);
    return <RevTurbineProvider options={options}>{children}</RevTurbineProvider>;
    }

    The route and the fetchClientSession function are on Client sessions; every initialization variant is on Initialize the SDK.

  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.

Use the cloud integration tutorial for runnable Vercel, Firebase, AWS, Google Cloud and Azure adapters. It covers trusted user context, Playbook freshness, local entitlement checks, and server enforcement.