Leave Localhost logoLeave LocalhostDocs
Recipes

Add a New Paid Feature

Gate a feature behind a billing capability so only workspaces on the right plan can use it, enforced server-side and reflected in the UI.

A paid feature is gated by a billing capability (what the workspace bought), usually combined with a role check. This recipe ties together capabilities, permissions, and the billing catalog.

1. Define the capability

Add the key to permissions/capabilities.ts and its validator in permissions/validators.ts:

export const capabilityKeys = [/* ...existing */, "feature.exports"] as const;

2. Attach it to the paid plans

In billing/plans.config.ts, add the capability to every plan that should include it:

pro_monthly: { /* ... */ capabilityKeys: [/* ... */, "feature.exports"] },

Workspaces with an active grant for those plans now resolve the capability. See Entitlements and Grants.

3. Require it on the action

Add an app permission rule that requires the capability (and a role statement), then enforce it server-side:

// permissions/appPermissions.ts
"report.export": {
  betterAuth: { billing: ["read"] },
  capabilities: ["feature.exports"],
},
await requireAppPermission(ctx, { permission: "report.export" });

A workspace on a plan without the capability is denied — even if the member's role would otherwise allow the action.

4. Gate and upsell in the UI

const canExport = useAppPermission("report.export");
// show the feature, or an upgrade prompt linking to /settings/billing

Hiding the button is convenience; the backend capability check is the boundary.

5. Track usage (optional)

Emit the paid_feature_used analytics event at the feature seam so you can measure adoption — see Analytics (PostHog).

On this page