Replace Hard-Coded Plan Checks
You already have a check like if (user.plan === 'pro') somewhere. It works — until the requirements arrive. Here’s that one check growing up, and where it lands.
The check that grows
Section titled “The check that grows”1. The naive check
Section titled “1. The naive check”function ExportButton({ user }) { if (user.plan !== 'pro') return <UpgradePrompt />; return <button onClick={exportData}>Export</button>;}Fine on day one.
2. …plus a beta list and a comp window
Section titled “2. …plus a beta list and a comp window”Sales comped a few accounts; beta users get everything:
const BETA_ACCOUNTS = ['acct_123', 'acct_456'];
function ExportButton({ user }) { const entitled = user.plan === 'pro' || BETA_ACCOUNTS.includes(user.accountId) || (user.compedUntil && user.compedUntil > Date.now()); if (!entitled) return <UpgradePrompt />; return <button onClick={exportData}>Export</button>;}3. …plus segmented upgrade copy
Section titled “3. …plus segmented upgrade copy”Marketing wants different messaging by plan cadence and region:
function ExportButton({ user }) { const entitled = user.plan === 'pro' || BETA_ACCOUNTS.includes(user.accountId) || (user.compedUntil && user.compedUntil > Date.now()); if (!entitled) { const message = user.billing === 'annual' ? 'Unlock exports on your annual plan' : user.region === 'EU' ? 'Passez au plan Pro pour exporter' : 'Unlock exports — upgrade to Pro'; return <UpgradePrompt message={message} />; } return <button onClick={exportData}>Export</button>;}4. …plus a usage limit and a grandfathered plan
Section titled “4. …plus a usage limit and a grandfathered plan”Free users get 3 exports a month; the retired starter_2022 plan keeps unlimited:
function ExportButton({ user }) { const isLegacy = user.plan === 'starter_2022'; const entitled = user.plan === 'pro' || isLegacy || BETA_ACCOUNTS.includes(user.accountId) || (user.compedUntil && user.compedUntil > Date.now()); const overLimit = !entitled && user.exportsThisMonth >= 3; // ...and now the copy branch has to know about isLegacy and overLimit too.}By step 4 it’s a knot of business exceptions living in your component — and changing the beta list, the copy, or the limit is a code change, a deploy, and an eng ticket.
The after
Section titled “The after”import { Gate } from '@revturbine/sdk';
function ExportButton() { return ( <Gate id="export" can="data_export"> <button onClick={exportData}>Export</button> </Gate> );}The beta list, the comp window, the segmented copy, the usage limit, and the grandfathered plan all move into the Playbook — entitlement rules, segments, and the placement copy. Your component asks one question: can this user export?
Still your job: the server check and the CTA
Section titled “Still your job: the server check and the CTA”The after is not a magic one-liner — your app still owns two things:
// 1. The money-safe decision, on your server, before the paid work runs.async function handleExport(req, res) { if (!(await userIsEntitled(req.userId, 'data_export'))) { return res.status(402).json({ error: 'upgrade_required' }); } await runExport(req);}// 2. The upgrade navigation, wired to the placement the <Gate> renders when denied.<Gate id="export" can="data_export" onCtaClick={(path) => router.push(path.href ?? '/pricing')}> <button onClick={exportData}>Export</button></Gate>See Client vs Server Enforcement for why the server check stays.
The real difference
Section titled “The real difference”Related
Section titled “Related”- Recommended API Path —
can()vsgate()vs<Gate> - Entitlements — feature / usage-limit / credits / seats rules
- Run an Experiment with Segments — the segmented-copy branch, done in config