Farther ShoreDocs
Go to Farther Shore
What is FartherShore
Install the CLI
Quickstart
Core concepts
The @Product class
Meters & resources
Features & routes
Capabilities & entitlements
Plans & pricing
The Manifest IR
Bring your own backend
Transport modes
Metering & verification
Runtime tokens
Frontend SDK
Root & data components
Auth & sessions
Entitlement gates
Hooks (custom gating)Composing with upsellNext
Connect Stripe
Subscriptions & usage
Plan changes & grandfathering
Billing strategies
Apply & deploy
Environments
Migrations
Docs versions & archive
Operate with an agent
Operation classes
MCP server
End-to-end via CLI/MCP
CLI reference
@farthershore/product
@farthershore/backend
@farthershore/farthershore-js
Environment variables
Response & deny codes
Add a metered capability
Gate a feature
Change a price
Prepaid credits
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Build the frontend/Entitlement gates

Entitlement gates

Gate UI by capability and feature with FeatureGate and RequireCapability.

PreviousAuth & sessionsNextConnect Stripe

Entitlement gates reveal a subtree only when the current subscriber's pinned plan grants it. <FeatureGate> checks a feature flag; <RequireCapability> checks a numeric or boolean capability. Both are self-managed — they read the entitlement snapshot through the SDK hooks, so you pass nothing beyond the key under <FartherShoreRoot>.

These mirror the platform's contract: a @Capability in your @Product class declares what a plan can do, and @Plan grants it. CronCloud declares @Capability("managed-cron", { includesFeatures: ["cron-jobs"] }) and both plans grant it (Starter caps cron_jobs: 10, Pro cron_jobs: 100). See Capabilities and Plans.

import { FeatureGate, RequireCapability } from "@farthershore/farthershore-js/components";

<FeatureGate>

Reveal children only when the pinned plan enables a feature gate. While the snapshot resolves it renders nothing (no flash of forbidden content, but aria-busy so assistive tech announces the pending state). On denial it renders your renderUpsell (an upsell CTA) or renderDisabled (a plain disabled affordance), else nothing.

import { FeatureGate, UpgradePrompt } from "@farthershore/farthershore-js/components";
import { FeatureNotGrantedError } from "@farthershore/farthershore-js";

<FeatureGate
  feature="cron-jobs"
  renderUpsell={() => (
    <UpgradePrompt error={new FeatureNotGrantedError("cron-jobs")} />
  )}
>
  <CronJobsEditor />
</FeatureGate>;
interface FeatureGateProps {
  feature: string;                  // the feature-gate key on the pinned plan
  children?: ReactNode;             // revealed when granted
  renderUpsell?: () => ReactNode;   // denied + you want an upsell CTA (wins)
  renderDisabled?: () => ReactNode; // denied, plain disabled affordance
}

Resolving → nothing; denied → renderUpsell ?? renderDisabled ?? nothing.

<RequireCapability>

Reveal children only when the pinned plan satisfies a capability. For a numeric capability the plan's limit must be >= min (default 1 — "the plan allows at least one"). For a boolean capability min is ignored and the toggle must be true.

import { RequireCapability } from "@farthershore/farthershore-js/components";

// CronCloud: show the bulk-create UI only when the plan allows ≥ 5 cron jobs.
<RequireCapability capability="managed-cron" min={5} renderDisabled={() => <UpsellRow />}>
  <BulkCreateCronJobs />
</RequireCapability>;
interface RequireCapabilityProps {
  capability: string;               // the capability key on the pinned plan
  min?: number;                     // numeric: limit must be >= min (default 1)
  children?: ReactNode;
  renderUpsell?: () => ReactNode;   // denied + you want an upsell CTA (wins)
  renderDisabled?: () => ReactNode; // denied, plain disabled affordance
}

The fail rules (important)

  • Absent on a KNOWN plan = unlimited → allowed. A plan that doesn't declare a capability doesn't cap it, mirroring Core.
  • Signed-out / no subscription = fail CLOSED → denied. An anonymous viewer has no plan, so a gated subtree must not reveal. This is symmetric with <FeatureGate>.

That distinction — "absent on a known plan" vs "no plan at all" — is why the gates read hasSubscriber, not just the raw value.

Hooks (custom gating)

When a component isn't a clean wrap, read the entitlement state directly. All derive from the same cached /me snapshot — no parallel fetches.

import {
  useEntitlements,
  useFeatureGate,
  useCapability,
  useCapabilityUsage,
} from "@farthershore/farthershore-js/react";

useFeatureGate(key)

const { enabled, loading } = useFeatureGate("cron-jobs");
// Gate on `loading` first (enabled is false while loading) to avoid a flash.
if (loading) return null;
return enabled ? <CronJobsEditor /> : <Upsell />;

useCapability(key)

const { value, hasSubscriber, loading } = useCapability("managed-cron");
// value: the raw limit (numeric cap | boolean toggle | null when absent/loading)
// hasSubscriber: false for signed-out / no-subscription (empty snapshot)

value is the raw limit; combine with hasSubscriber to apply the absent-vs-no-plan rule yourself.

useEntitlements()

The full snapshot — { hasSubscriber, featureGates, capabilityLimits } — when you need to branch on several gates at once.

const { data, loading } = useEntitlements();
const canCron = data?.featureGates["cron-jobs"] === true;
const jobCap = data?.capabilityLimits["managed-cron"]; // number | boolean | undefined

useCapabilityUsage() — the "N of M" surface

The limit lives in the snapshot, but current usage is a live count, so this reads the dedicated /me/capability-usage route. It returns { limit, current } per capability for the pinned plan — render "7 of 10 cron jobs used". Signed-out / no subscription → an empty map.

const { data } = useCapabilityUsage();
const cron = data?.["managed-cron"]; // { limit: 10, current: 7 } | undefined

For the zero-prop rendered surface, mount <CapabilityUsageCard/> (from /components) — it wraps this hook and renders a "used / limit" row per capability for the pinned plan.

Gating the UI is a courtesy, not enforcement. The Gateway is the source of truth: an over-limit or under-entitled request is denied server-side (a LimitExceededError / FeatureNotGrantedError) even if your UI didn't gate it. Always handle those errors too — see gateway enforcement and limits & credits.

Composing with upsell

renderUpsell pairs naturally with <UpgradePrompt> plus the matching error. The same <UpgradePrompt> also handles a LimitExceededError thrown by a mutation at the cap, so a denied click and a gated section share one upsell affordance.

import { FeatureGate, UpgradePrompt } from "@farthershore/farthershore-js/components";
import { FeatureNotGrantedError } from "@farthershore/farthershore-js";

<FeatureGate
  feature="cron-jobs"
  renderUpsell={() => <UpgradePrompt error={new FeatureNotGrantedError("cron-jobs")} />}
>
  <CronJobsEditor />
</FeatureGate>;

Next

  • Capabilities and Plans — declare and grant the gates these read.
  • Auth & sessions — a signed-out viewer fails capability gates closed.
  • Root & data components — gates are self-fed under <FartherShoreRoot>.