Skip to content

Tutorial: Gate a Premium Feature

In this tutorial, you’ll gate a premium feature (video export) behind an entitlement check and show an upgrade modal when access is denied. Takes about 10 minutes.

A video export button that:

  • Checks the data_export entitlement before allowing access
  • Shows an upgrade modal when the user doesn’t have the entitlement
  • Works instantly in local mode, no API calls needed
  • A React 18+ application with @revturbine/sdk installed
  • A RevTurbineProvider set up (see Quickstart)
  1. Add entitlement and gate placement to your config

    Add to your exported_config.json:

    exported_config.json
    {
    "version": "v1",
    "plans": [
    { "id": "plan_free", "unique_handle": "free", "name": "Free", "tier_position": 0, "sort_order": 0 },
    { "id": "plan_pro", "unique_handle": "professional", "name": "Professional", "tier_position": 1, "sort_order": 1 }
    ],
    "entitlements": [
    { "id": "ent_data_export", "unique_handle": "data_export", "name": "Data Export", "type": "feature" }
    ],
    "entitlement_rules": [
    {
    "id": "er_data_export_pro",
    "entitlement_id": "ent_data_export",
    "targets": [{ "kind": "plan", "id": "plan_pro" }],
    "segment_ids": [],
    "type_fields": { "kind": "feature", "enabled": true }
    }
    ],
    "segments": [],
    "content_ui_paths": [],
    "surface_templates": [
    { "id": "modal_overlay", "surface_type": "modal", "fields": [] }
    ],
    "placements": [
    {
    "id": "pl_export_gate",
    "name": "Export gate",
    "category": "gated",
    "trigger": { "type": "entitlement_gate", "entitlement_handle": "data_export" },
    "payloads": [
    {
    "id": "pl_export_gate_p0",
    "target": { "plan_ids": ["plan_free"], "segment_chips": [] },
    "surfaces": [
    {
    "template_id": "modal_overlay",
    "fields": {
    "modal_type": "Blocking",
    "header": "Unlock Data Export",
    "body": "Video export is available on the Professional plan. Upgrade to export your projects in MP4, WebM, and GIF formats."
    },
    "ctas": [
    { "label": "View Plans", "path": "open_checkout", "config": { "purchase": "professional" } },
    { "label": "Maybe later", "path": "dismiss" }
    ]
    }
    ],
    "caps": {},
    "status": "active"
    }
    ],
    "order": 0
    }
    ]
    }
  2. Create the gated component

    src/components/ExportPanel.tsx
    import { AccessGateSurfaceSlot } from '@revturbine/sdk';
    function ExportControls() {
    return (
    <div style={{ padding: 16, background: '#e8f5e9', borderRadius: 8 }}>
    <h3>Export Settings</h3>
    <select>
    <option>MP4 (1080p)</option>
    <option>WebM (720p)</option>
    <option>GIF (480p)</option>
    </select>
    <button style={{ marginTop: 8 }}>Start Export</button>
    </div>
    );
    }
    export function ExportPanel() {
    return (
    <AccessGateSurfaceSlot
    id="editor_export_action"
    surfaceTemplateIds={['modal_overlay']}
    entitlementHandle="data_export"
    >
    {/* This only renders when the entitlement is granted */}
    <ExportControls />
    </AccessGateSurfaceSlot>
    );
    }
  3. Use it in your page

    src/pages/Editor.tsx
    import { ExportPanel } from '../components/ExportPanel';
    export default function Editor() {
    return (
    <div>
    <h1>Video Editor</h1>
    <div>{/* ... editor UI ... */}</div>
    <h2>Export</h2>
    <ExportPanel />
    </div>
    );
    }
  4. Test with different users

    // Free user → sees upgrade modal
    <RevTurbineProvider
    options={{
    // ...
    user: { id: 'user_1', context: { plan_handle: 'free' } },
    }}
    >
    // Professional user → sees export controls
    <RevTurbineProvider
    options={{
    // ...
    user: { id: 'user_2', context: { plan_handle: 'professional' } },
    }}
    >

The AccessGateSurfaceSlot component:

  1. Checks the data_export entitlement against the user’s plan
  2. If granted → renders the children (ExportControls)
  3. If denied → resolves the gate placement and renders the upgrade modal

The gate check happens instantly in local_only mode using the ExportedConfig.

For more control, use the useEntitlement hook directly:

import { useEntitlement, usePlacement } from '@revturbine/sdk';
function ExportPanel() {
const { allowed, denied } = useEntitlement({
handle: 'data_export',
});
const { visible, content, dismiss, ctaClick } = usePlacement({
surfaceSlot: {
id: 'editor_export_action',
surfaceTemplateIds: ['modal_overlay'],
},
autoLoad: denied, // Only load placement if denied
});
if (allowed) return <ExportControls />;
if (visible) {
return (
<div className="modal-overlay">
<h2>{content?.header}</h2>
<p>{content?.body}</p>
<button onClick={() => ctaClick()}>{content?.cta_label}</button>
<button onClick={() => dismiss()}>Maybe Later</button>
</div>
);
}
return <button disabled>Export (Upgrade Required)</button>;
}

Interactive Playground → Data Export Gate