Features & gating
A Feature is a short key attached to a plan. Your app gates functionality by checking for the feature, not the plan name — so you can rename, split, or merge plans without touching code.
Copy this quickstart guide as a prompt for LLMs to implement KolayLogin in your application.
Add a feature to a plan
- Open the plan from
/app/[appId]/billing. - In the Features panel, enter a key (
pro_export), a human name (Pro Export), and an optional description. - Click
Add feature. Saved to the plan immediately.
snake_case (lowercase letters, digits, underscores). That keeps them safe to pass around in JSON, query strings, and code comparisons.Gate on the client
React components <Protect> and <Show> accept a feature or plan prop:
import { Protect, Show, useAuth } from '@kolaylogin/react';
function App() {
const { has } = useAuth();
return (
<>
{/* Hard gate — fallback shows if the feature is missing */}
<Protect feature="pro_export" fallback={<UpgradeCta />}>
<BulkExportButton />
</Protect>
{/* Toggle UI entirely based on plan key */}
<Show when={{ plan: 'gold' }} fallback={<BronzePlansNotice />}>
<GoldOnlyDashboard />
</Show>
{/* Imperative check */}
{has({ feature: 'advanced_analytics' }) && <AnalyticsLink />}
</>
);
}Loading state
has() returns undefined until billing data has loaded on the client. <Protect> and <Show>render nothing during that window so you don't flash the fallback before the check completes.
Gate on the server
Use the Backend SDK in server components, route handlers, or server actions:
import { KolayLoginBackendClient } from '@kolaylogin/backend';
const kl = new KolayLoginBackendClient({ // baseUrl defaults to https://api.kolaylogin.com });
export async function POST(req: Request) {
const { has, userId } = await kl.auth({ headers: { cookie: req.headers.get('cookie') ?? '' } });
if (!userId) return new Response('unauthorized', { status: 401 });
if (!has({ feature: 'pro_export' })) {
return new Response('upgrade_required', { status: 402 });
}
// ... run the protected action
return Response.json({ ok: true });
}Checking plan keys
If you truly need to branch on the plan itself (e.g. pricing copy), use has({ plan: 'pro' }). We recommend gating by feature everywhere else — it survives plan rebranding without a deploy.
has({ plan: 'pro' })— matches the plan's stablekey.has({ feature: 'pro_export' })— matches a feature key attached to any plan the user is on.has({ role: 'admin' })— matches the session'sorgRoleclaim (unchanged, existing behavior).
API
GET /v1/billing/me— caller's active plan + feature list. Used byuseAuth().has().GET /v1/dashboard/billing/plans/:planId/featuresPOST /v1/dashboard/billing/plans/:planId/features— body{ key, name, description? }.DELETE /v1/dashboard/billing/plans/:planId/features/:featureKey