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
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
How a capability gates a routeAdd a premium gateReflect the gate in the UIBuild and verifyVerify it worksRelated
Change a price
Prepaid credits
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Cookbook/Gate a feature

Gate a feature

Restrict a route to subscribers who hold a capability, and reflect the gate in the UI.

PreviousAdd a metered capabilityNextChange a price

You want a feature that only certain plans can reach — enforced at the gateway and surfaced honestly in the product UI. A capability is the unit that does this: you declare a @Capability, bind a @Feature's routes to it, grant it on the plans that should have it, and place a gated component on the frontend page.

CronCloud already gates its cron-job routes behind the managed-cron capability. This recipe walks that wiring end to end and shows how to add a second, premium gate.

How a capability gates a route

A capability lists the features it unlocks via includesFeatures. A plan grants the capability with capabilityGrant(...) in its grants. The gateway allows a subscriber to reach a feature's routes only if their plan grants a capability that includes that feature.

// product/product.config.ts
import {
  Product,
  Requests,
  Resource,
  Capability,
  Feature,
  Plan,
  capabilityGrant,
} from "@farthershore/product";

@Product({
  name: "croncloud",
  origin: "https://api.example.com",
  displayName: "CronCloud",
})
export default class CronCloud {
  @Requests()
  requests!: unknown;

  @Resource("cron_jobs", { display: "Cron jobs", countSource: "action_inferred" })
  cronJobs!: unknown;

  // 1. Declare the capability and the features it unlocks.
  @Capability("managed-cron", {
    title: "Managed Cron Jobs",
    includesFeatures: ["cron-jobs"],
  })
  managedCron!: unknown;

  // 2. The feature's routes are gated by any capability that includes it.
  @Feature("cron-jobs", {
    description: "Cron job CRUD",
    plans: ["starter", "pro"],
    routes: {
      "GET /v1/cron-jobs": {},
      "POST /v1/cron-jobs": {},
      "DELETE /v1/cron-jobs/{id}": {},
    },
  })
  cronJobsFeature!: unknown;

  // 3. Grant the capability on the plans that should reach the feature.
  //    The optional `limits` cap a counted resource (here: cron jobs).
  @Plan("starter", {
    name: "Starter",
    price: { amount: 2900, currency: "usd", interval: "month" },
    grants: [capabilityGrant("managed-cron", { limits: { cron_jobs: 10 } })],
    limits: { requests: { rate: 600, interval: "minute", enforcement: "enforce" } },
  })
  starter!: unknown;

  @Plan("pro", {
    name: "Pro",
    price: { amount: 19900, currency: "usd", interval: "month" },
    grants: [capabilityGrant("managed-cron", { limits: { cron_jobs: 100 } })],
    limits: { requests: { rate: 6000, interval: "minute", enforcement: "enforce" } },
  })
  pro!: unknown;
}

A plan that does not grant managed-cron cannot call any cron-jobs route — the gateway denies it before the request reaches your origin.

The SDK validates references at build time: a @Capability whose includesFeatures names a feature you didn't declare fails the build, as does a plan that grants a capability you didn't declare. Misspellings surface locally, not in production.

Add a premium gate

To gate a subset behind a higher tier, declare a second capability, attach it to a second feature, and grant it only on Pro.

  @Capability("priority-runs", { title: "Priority Execution" })
  priorityRuns!: unknown;

  @Feature("priority", {
    description: "Jump-the-queue execution",
    routes: { "POST /v1/cron-jobs/{id}/run-now": {} },
  })
  priorityFeature!: unknown;

Keep the two capabilities separate (don't fold priority into managed-cron's includesFeatures), and grant the new one only on Pro:

  // In @Plan("pro"):
  grants: [
    capabilityGrant("managed-cron", { limits: { cron_jobs: 100 } }),
    capabilityGrant("priority-runs"),
  ],

Now Starter keeps cron CRUD but cannot call run-now; Pro can do both.

Reflect the gate in the UI

The product's @Frontend manifest declares pages and the components on them. A page can require a capability, and a feature_panel component renders a gated surface with a gateMode of hide, disable, or upsell. CronCloud upsells the cron page to subscribers who lack managed-cron:

import { Frontend } from "@farthershore/product";

@Frontend({
  nav: [{ label: "Cron", path: "/cron", capability: "managed-cron" }],
  pages: [
    {
      path: "/cron",
      title: "Cron jobs",
      requiresAuth: true,
      capability: "managed-cron",
      components: [
        { component: "usage_card", props: { meter: "requests" } },
        {
          component: "feature_panel",
          capability: "managed-cron",
          gateMode: "upsell", // hide | disable | upsell
          props: { feature: "cron-jobs" },
        },
      ],
    },
  ],
})
  • hide — the panel is absent for ungranted subscribers.
  • disable — the panel renders read-only / non-interactive.
  • upsell — the panel shows an upgrade prompt pointing at a plan that grants it.

The nav item's capability hides the link the same way. The generated frontend/ app reads this manifest, so the UI gate and the gateway gate come from one source.

Build and verify

farthershore build --format json

Verify the gate end to end with a test persona that you assign to each plan:

# A Starter persona reaches cron CRUD but not run-now;
# a persona on a plan WITHOUT the grant is denied at the gateway.
farthershore persona bootstrap croncloud --env preview --plan starter --format json

Verify it works

  • A subscriber on a plan that grants managed-cron can call the cron-jobs routes.
  • A subscriber without the grant is denied those routes at the gateway.
  • The cron page upsells (rather than 404s) for ungranted subscribers.
  • priority-runs, granted only on Pro, leaves Starter unable to call run-now.

Related

  • Add a metered capability — meter and bill the routes you just gated.
  • Change a price — reprice the plan that unlocks the capability.
  • Operate via an agent — mint a persona per plan to test each gate.