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
The canonical exampleDecoratorsRoute entries@Plan optionsValue typesMigration is not a decorator
@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/Reference/@farthershore/product

@farthershore/product

Decorators and value types for authoring a product as a TypeScript class.

PreviousCLI referenceNext@farthershore/backend

@farthershore/product is the product-as-code SDK. You author a product as one decorated @Product class in product/product.config.ts and export default it; Farther Shore compiles it to deterministic Manifest IR, validates it, and applies it through Core. There is no YAML — the class is the source of truth. Pin it in lockstep with @farthershore/backend and @farthershore/farthershore-js.

pnpm add @farthershore/product

Member decorators (@Meter, @Feature, @Plan, …) declare resources at class-definition time; the @Product class decorator drains and finalizes them into one product. Cross-references are by string key — a route's reports: "tokens_used" names a @Meter key, a plan grant names a @Capability key. The build is deterministic: no dates, randomness, network, or process state.

The canonical example

CronCloud — a metered cron-jobs product — exercises most of the surface:

import {
  Product, Frontend, Requests, Meter, Resource,
  Capability, Feature, Plan, capabilityGrant,
} from "@farthershore/product";

@Product({
  name: "croncloud",
  origin: "https://api.example.com",
  displayName: "CronCloud",
  description: "Managed cron jobs",
  billOn4xx: false,
})
@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", props: { feature: "cron-jobs" } },
      ],
    },
  ],
})
export default class CronCloud {
  @Requests()
  requests!: unknown;

  @Meter("compute", { display: "Compute", unit: "ms" })
  compute!: unknown;

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

  @Capability("managed-cron", {
    title: "Managed Cron Jobs",
    includesFeatures: ["cron-jobs"],
  })
  managedCron!: unknown;

  @Feature("cron-jobs", {
    description: "Cron job CRUD",
    plans: ["starter", "pro"],
    actions: [
      { id: "cron-job.create", kind: "mutation", title: "Create cron job",
        resource: { resource: "cron_jobs", effect: "create" } },
      { id: "cron-job.delete", kind: "mutation", title: "Delete cron job",
        subject: { type: "cron_job", from: "path_param", name: "id" },
        resource: { resource: "cron_jobs", effect: "delete" }, audit: "full" },
    ],
    routes: {
      "GET /v1/cron-jobs": {},
      "POST /v1/cron-jobs": { action: "cron-job.create" },
      "DELETE /v1/cron-jobs/{id}": { action: "cron-job.delete" },
    },
  })
  cronJobsFeature!: unknown;

  @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;
}

Decorators

@Product and @Frontend / @Raw decorate the class (the latter two are stackable); the rest decorate members.

DecoratorPurpose
@Product(options)The class decorator. options.origin (the business-logic origin Farther Shore calls) is required; options.name defaults to the class name.
@Surface(type, options?)Declare a customer-facing surface: frontend | api | docs | widget | webhook | worker | agent | dashboard.
@Requests(options?)Declare the platform-managed successful-request meter; applies requests = 1 to every metered route. No backend code needed.
@Meter(key, options?)Declare a billable/enforceable dimension. { display, unit, estimate, routeDefault, aggregation }. A routeDefault is a reusable fixed cost on every metered route.
@Resource(name, options?)Declare a counted resource for resource-count constraints ({ display, countSource }). Capped per-plan via caps / capability limits.
@Capability(key, options?)Declare a capability bundle ({ title, includesFeatures }). Grant it on a plan via capabilityGrant(key, { limits }).
@Feature(key, options?)Declare gateway routes (keyed "METHOD /path"), costs/reports/estimates, actions, and the plans that grant the feature.
@Plan(key, options?)Declare pricing, limits, grants, trial, caps. See below.
@Entitlement(key, options?)Group capabilities, featureGates, limits, and meters into reusable access metadata.
@Workflow(key, options?)Declare a non-HTTP workflow ({ kind, trigger, capabilities, meters, estimates }) — e.g. an agent task or scheduled job.
@Backend(id, options?)Declare a first-class backend; bind routes via a route's backend. See @farthershore/backend.
@Policy(name, options)Declare a policy layer in code.
@Frontend(options)Declare the frontend nav + pages manifest (class decorator, stackable).
@Raw(options)Escape hatch to merge platform-schema JSON the typed SDK lacks sugar for: , , , , .

capabilityGrant(key, { limits }) builds a capability grant (with optional per-capability limits) for a plan's grants[] — the only grant value that isn't a plain string.

Route entries

Each @Feature route value (keyed "METHOD /path", insertion order preserved because the gateway is first-match-wins) accepts:

FieldMeaning
costFixed gateway-known costs, keyed by meter key ({ api_credits: 10 }).
report / reportsDynamic meter key(s) the upstream reports with @farthershore/backend.
estimatesPer-route pre-request estimates for admission checks, keyed by meter.
actionBind a declared action to this route by id.
backendBind this route to a declared backend by id.
onStatusCodesOverride the successful-response range ("200-299,304" or [200, 201]).
unmeteredtrue clears all inherited + explicit route metering.
inheritDefaultMetersfalse disables inherited defaults (e.g. @Requests) only.

A meter cannot be both a fixed cost and a dynamic report — that throws at class-definition time. So does a route key that isn't "METHOD /path", an integer-like key, a NaN estimate, or an estimate for an undeclared/unbound meter.

@Meter("api_credits", { unit: "credit", routeDefault: 1 })
apiCredits!: unknown;

@Feature("exports", {
  routes: {
    "POST /v1/bulk-export": { cost: { api_credits: 10 } }, // defaults → 11
    "GET /healthz": { unmetered: true },
    "GET /status": { inheritDefaultMeters: false },
  },
})
exports!: unknown;

@Plan options

OptionType / meaning
nameDisplay name (defaults to the member name).
price{ amount, currency: "usd", interval: "month" | "year" } (cents, verbatim) or { free: true }. Omit for a $0 plan.
limitsPer-dimension RateLimit or Count, declaration order kept. Rate limits → limits[]; counts → capability limits.
capsResource caps → capability limits (alias for count-only limits): { cron_jobs: 10 }.
meterPer-dimension MeterOverage → plan meters[]. Mutually exclusive with meters.
metersRaw platform-native meters[] passthrough.
grantsCredit grants and/or capabilityGrant(...) entries.
trialDays, maxMonthlySpendCents, minMonthlySpendCentsTrial length; optional hard/soft monthly spend caps.
capabilities, featureGatesCapability keys granted without limits; per-key feature gates.
overageBehavior, selfServeEnabled, legacy, archive, raw"block" | "allow_and_bill"; self-serve flag; legacy flag; archive policy; per-plan escape hatch.

Publishing requires every plan to carry at least one rate-limit rule (run at farthershore build). The minimal shape: limits: { requests: { rate: 600, interval: "minute" } }.

Value types

The author-facing structured types the decorators consume. Money is integer-only and taken verbatim (no ×100).

TypeShapeNotes
Price{ amount, currency: "usd", interval: "month" | "year" } | { free: true }amount is integer cents.
RateLimit{ rate, interval, enforcement? }interval: second|minute|hour|day|week|month; enforcement: "enforce" | "track".
Count{ count }A counted-resource cap; lands in capability limits.
MeterOverage{ micros, includedUnits? }micros = per-unit rate in micro-dollars (not microcents).
RouteCostRecord<string, number>Fixed gateway-known costs, keyed by meter key.

Migration is not a decorator

A @Product class is a snapshot of product state. Moving live subscribers onto a new plan version is a transition — an OPERATE verb, not a declaration. Reprice or change a plan and publish; existing subscribers are grandfathered by default. To actively migrate them, run farthershore plan migrate <product> <plan> --from <v> --to head --policy <policy> (see the CLI reference).

productPatch
plan
routeLayer
policyLayer
capabilityLayer