Skip to content

Client vs Server Enforcement

Every team evaluating RevTurbine asks the same question: “If entitlements are checked in the browser, what stops a user from bypassing them?”

The browser can be tampered with, so a client check is a UX hint, not the authority. Your backend re-runs the check at the moment it matters, immediately before a billable action. The re-check is the same SDK evaluating the same Playbook, on your server. It calls our servers only for additive data. Verify wherever being wrong costs you money; pure display gating doesn’t need it.

The rest of this page is the division of responsibility, and which tool to use where.

  • RevTurbine is authoritative for monetization policy. Which plans exist, what each plan entitles, when a placement shows — that lives in your Playbook and the control plane, and a PM changes it with no code deploy.
  • Your app owns enforcement. RevTurbine tells you the policy answer; your app is what actually allows or blocks the action.
  • No grant, no access. An entitlement check that can’t justify a grant denies rather than guessing, so a misconfiguration never leaks a paid feature (the reason codes say why).
  • Money-safe decisions belong on your server. A browser check can be tampered with. For anything that costs money or invites abuse, re-run the check server-side before you grant access.

The SDK gives you three ways to act on an entitlement. They sound alike; they are not the same.

ToolKindRunsUse it for
<Gate>React componentClient (UI)Show/hide a subtree and render an upsell when denied
rt.gate(action, fn)Imperative verbClientRun fn only if entitled; get the entitlement back to open a paywall
rt.can(handle) · useCan · checkEntitlementCheckClient or serverAsk “is this allowed?” and get the full result — the one you re-run server-side

<Gate> and gate() are convenience wrappers around a can()/checkEntitlement() call. All three run in the browser and deny whenever they can’t affirm a grant. The check itself — checkEntitlement / can — is also what you run on your server (via the server SDK or your own logic) when the decision has to be trusted.

ActionClient check enough?
Show or hide a button, badge, or menu item✅ Yes
Render an upgrade prompt or paywall✅ Yes
Branch UI copy or messaging✅ Yes
Start a paid export, consume credits, provision a seat❌ Re-check on your server
Anything a user could profit from by faking❌ Re-check on your server

The rule: the client check drives the UI; your server makes the money-safe decision. They use the same entitlement — you’re just not trusting the browser for the part that matters.

Use the client check for a fast, friendly UX:

React
import { Gate } from '@revturbine/sdk';
// Client: instant show/hide + upsell when denied; no grant means no access.
<Gate id="editor_export" can="data_export">
<ExportButton />
</Gate>

Then verify again on your server before doing the paid work — with the same SDK, evaluating the same Playbook in-process:

TypeScript
import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless';
// Server: the trusted decision. Nothing on this path calls RevTurbine —
// the headless SDK evaluates the Playbook it holds.
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('data_export');
if (!result.allowed) return res.status(402).json({ error: 'upgrade_required' });
await runExport(req);
}

Two conventions worth keeping: answer a denied entitlement with 402 (upgrade to proceed) and reserve 403 for permission denials, so clients can tell the two apart; and on a request hot path, don’t pay the Playbook fetch per request — see Instance lifecycle.

The client <Gate> keeps the UI honest and fast; the server check keeps the revenue safe. Both evaluate the same Playbook locally, so neither depends on our servers at decision time.