Skip to content

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.

function ExportButton({ user }) {
if (user.plan !== 'pro') return <UpgradePrompt />;
return <button onClick={exportData}>Export</button>;
}

Fine on day one.

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>;
}

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.

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.