Browse docs

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

  1. Open the plan from /app/[appId]/billing.
  2. In the Features panel, enter a key (pro_export), a human name (Pro Export), and an optional description.
  3. Click Add feature. Saved to the plan immediately.
Key format
Feature keys are 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 stable key.
  • has({ feature: 'pro_export' }) — matches a feature key attached to any plan the user is on.
  • has({ role: 'admin' }) — matches the session's orgRole claim (unchanged, existing behavior).

API

  • GET /v1/billing/me— caller's active plan + feature list. Used by useAuth().has().
  • GET /v1/dashboard/billing/plans/:planId/features
  • POST /v1/dashboard/billing/plans/:planId/features — body { key, name, description? }.
  • DELETE /v1/dashboard/billing/plans/:planId/features/:featureKey