This is presentation, not security. Never describe it as a security boundary.
The package never verifies a licence, never reads a key, and never calls anything. It shows
what you tell it to show. Anyone can open devtools and change the props — so the gate stops
honest users from wandering into a paid feature, and that is all it is for.
Real enforcement belongs on your server, where documents are saved and
rendered. See the last section — it is the important one.
Declaring the plan
You validate the subscription on your own server and pass the result down. The package trusts
it completely, because it has no way not to.
The feature strings are yours. The package never interprets them — it only compares the ones a
block asks for against the ones you granted.
Gating a block
registry.extend('lineItems', { requiresFeature: 'premium-blocks' })
// …or on a block of your own
const premiumChart = { type: 'chart', requiresFeature: 'premium-blocks', /* … */ }
A gated block whose feature is not granted:
stays visible in the palette
with a lock badge
is not draggable
it cannot be added
calls onLicenseRequired on click
so you can show your own upgrade screen
Visible-but-locked beats hidden, and it is deliberate.
A user who cannot see a feature cannot want it. A lock badge is how someone discovers there is
a reason to upgrade — hiding it means they never learn the paid tier exists.
No built-in block is gated out of the box.
Everything in the palette works with no license prop at all. Gating is entirely
your decision.
Gating AI
AI is usually the first thing worth charging for, because it costs you money per use.
Without the feature, the AI button shows a lock and activating it calls
onLicenseRequired instead of opening the dialog. The licence is re-checked when
the request is actually submitted, so flipping the prop mid-session cannot leave a dialog open
that should not be.
Do not rely on this to control your provider bill.
The check that protects your spend is the one on your AI endpoint. This one only keeps
the button quiet.
The package ships no pricing page, no checkout and no upgrade dialog.
It tells you which feature was wanted and from which block; the offer, the wording and the
payment are yours. That is the only way it can fit your pricing rather than dictating it.
onLicenseRequired is also a good analytics signal: it tells you which locked
feature people actually reach for, which is better evidence for pricing than a survey.
Where real enforcement lives
Put the check at every point where a paid capability costs you something or produces something
of value:
Endpoint
Enforce
POST /api/ai/design
Reject with 402 unless the account's plan includes it. This is the one that protects your provider bill.
POST /api/documents
Reject a document containing gated block types.
POST /api/render-pdf
Refuse to render what the plan does not cover.
POST /api/uploads
Enforce storage quotas here, not in the UI.
// Express, but the shape is the same anywhere
app.post('/api/ai/design', requireAuth, async (req, res) => {
if (!req.user.plan.features.includes('ai-design')) {
return res.status(402).json({ error: 'Upgrade required' })
}
res.json(await callProvider(req.body))
})
Belt and braces, and they do different jobs.
The UI gate makes the product legible — people can see what they would get. The server gate is
what actually holds. Ship both; describe only the second one as enforcement.