Feature gating is how you turn your free users into paying customers. By restricting access to premium features based on plan, you create natural upgrade moments that drive revenue — without frustrating your users.
Feature types
BillingOS supports three types of features:
| Type | Example | How it works |
|---|
| Boolean | Custom branding | On/off — user either has access or doesn’t |
| Usage quota | 1,000 API calls/month | Counted — resets each billing period |
| Numeric limit | 5 team members | Fixed cap — doesn’t reset |
Client-side gating
Using the FeatureGate component
The simplest approach — wrap content with <FeatureGate>:
import { FeatureGate } from "@billingos/sdk";
<FeatureGate feature="custom_branding">
<BrandingEditor />
</FeatureGate>
Content is only rendered if the user has access. Add a fallback for users who don’t:
import { FeatureGate, UpgradePrompt } from "@billingos/sdk";
<FeatureGate
feature="custom_branding"
fallback={<UpgradePrompt feature="custom_branding" />}
>
<BrandingEditor />
</FeatureGate>
Using the useFeature hook
For more control, check access programmatically:
import { useFeature } from "@billingos/sdk";
function ExportButton() {
const { data } = useFeature("export_pdf");
return (
<button
disabled={!data?.has_access}
onClick={handleExport}
>
Export PDF
{data?.limit && <span>({data.usage}/{data.limit})</span>}
</button>
);
}
Using the useFeatureGate hook
Combines access checking with automatic callbacks:
import { useFeatureGate } from "@billingos/sdk";
function ApiCallButton() {
const { hasAccess, remaining } = useFeatureGate("api_calls", {
onAccessDenied: () => toast.error("Upgrade to access this feature"),
onQuotaExceeded: (usage, limit) =>
toast.warning(`${usage}/${limit} calls used — upgrade for more`),
});
return (
<button disabled={!hasAccess}>
Make API Call ({remaining} left)
</button>
);
}
Server-side gating
For API routes and server actions, use the Node SDK:
import { BillingOS } from "@billingos/node";
const billing = new BillingOS({ secretKey: process.env.BILLINGOS_SECRET_KEY! });
export async function POST(request: Request) {
const userId = getAuthenticatedUser(request);
const entitlement = await billing.checkEntitlement(userId, "api_calls");
if (!entitlement.has_access) {
return Response.json({ error: "Upgrade required" }, { status: 403 });
}
// Process the request...
// Track the usage
await billing.trackUsage({
customerId: userId,
featureKey: "api_calls",
quantity: 1,
});
return Response.json({ result: "success" });
}
Always validate entitlements on the server for security-critical features. Client-side gates improve UX but can be bypassed.