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.
The model, stated once
Section titled “The model, stated once”- 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.
Three tools, and where each runs
Section titled “Three tools, and where each runs”The SDK gives you three ways to act on an entitlement. They sound alike; they are not the same.
| Tool | Kind | Runs | Use it for |
|---|---|---|---|
<Gate> | React component | Client (UI) | Show/hide a subtree and render an upsell when denied |
rt.gate(action, fn) | Imperative verb | Client | Run fn only if entitled; get the entitlement back to open a paywall |
rt.can(handle) · useCan · checkEntitlement | Check | Client or server | Ask “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.
When do I need the server?
Section titled “When do I need the server?”| Action | Client 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.
In practice
Section titled “In practice”Use the client check for a fast, friendly UX:
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:
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.
Related
Section titled “Related”- Recommended API Path — when to reach for
can()vsgate()vs<Gate> - Server-Side Integration — running entitlement checks on your backend
- Entitlements — the entitlement model behind every check
- Error Handling — placement degradation and what a check returns when it can’t justify a grant