Most flag tools answer 'is this on?'. Paywall answers 'is this on for this customer, on this plan, after this payment?', and delivers the answer as a token your backend verifies without a network call.
One field in the dashboard, one type. The type decides how the flag compiles into config, the entitlement token and the per-user snapshot.
The classic flag. Granted by a plan or comped for one customer, it compiles to true in the token.
Seats, exports per month, projects. Each plan sets its own number; an empty limit means unlimited.
A tier name, a model id, a region. Whatever string your app should read for this customer.
A flag's value comes from the customer's plan. A per-customer override wins over the plan and survives refunds, so a comp you granted by hand is never revoked by a payment event.
{
"iss": "paywall",
"sub": "user_8f2a",
"plan": "pro",
"status": "active",
"features": {
"priority_support": true,
"exports": 500, // from the plan
"model": "large", // per-customer override
"beta_editor": true // comped by hand
},
"exp": 1757165400
}Each plan is a grant matrix over the app's flags. Publishing compiles every plan into one config blob at the edge.
Grant, comp or restrict a flag for one customer from the dashboard or the admin API. The snapshot is rewritten immediately.
identify returns a 15-minute HS256 token signed with the app's secret. Your backend verifies it locally, so gating costs nothing per request.
Per-user snapshots live in KV next to the config. A fresh read endpoint exists for the seconds right after a payment lands.
The React SDK keeps the customer token fresh and exposes each flag with its value. The server SDK verifies the entitlement token with the secret it already holds.
const exports = useFeature("exports");
const model = useFeature("model");
if (!exports.enabled) return <Upsell plan="pro" />;
return <Exporter quota={exports.value} model={model.value} />;const claims = await hub.verifyToken(req.headers.authorization);
// signed by Paywall, verified here — no network call
const quota = claims.features.exports;
if (quota === undefined) return forbidden();
if (quota !== true && used >= quota) return limitReached();Every flag page lists the plans and customers that grant it, so you can see who gets what before you publish.
The dashboard compares the draft catalogue with the published blob and shows exactly what a publish would change.
A number flag rejects a non-numeric limit at write time, in the UI and the admin API alike, so agents can't publish garbage.
Define the flags, group them into plans, add prices, publish. Or hand the whole job to your coding agent with one prompt.