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
price — integer centslimits — rate limits and resource capsgrants and capabilitiesmeter — per-unit overage pricingtrials, spend caps, and behaviorChanging a published planWhat it compiles to
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
Change a price
Prepaid credits
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Define your product/Plans & pricing

Plans & pricing

@Plan — price, limits, grants, overage pricing, trials, and spend caps, all as structured value types.

PreviousCapabilities & entitlementsNextThe Manifest IR

A @Plan is a sellable tier: its recurring price, the rate limits and resource caps it enforces, the capabilities it grants, optional per-unit overage pricing, trials, and spend caps. Plan values are structured value types — price is integer cents, limits are { rate, interval }, overage is { micros } — that the SDK normalizes into the IR. Money is integer-only and taken verbatim: no ×100.

import { Product, Requests, Plan, capabilityGrant } from "@farthershore/product";

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

  @Plan("starter", {
    name: "Starter",
    price: { amount: 2900, currency: "usd", interval: "month" },   // $29.00 / 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" },  // $199.00 / month
    grants: [capabilityGrant("managed-cron", { limits: { cron_jobs: 100 } })],
    limits: { requests: { rate: 6000, interval: "minute", enforcement: "enforce" } },
  })
  pro!: unknown;
}

Plans are sorted by key in the emitted IR (pro before starter), so declaration order does not affect the IR hash.

price — integer cents

price is a Price value: either a paid recurring fee or { free: true }.

price: { amount: 2900, currency: "usd", interval: "month" }   // $29.00/mo — amount is CENTS
price: { amount: 19900, currency: "usd", interval: "year" }   // $199.00/yr
price: { free: true }                                          // free plan
  • amount is integer cents, taken verbatim into recurring_fee_cents. 2900 is $29.00, not $2,900. A non-integer or negative amount throws.
  • currency must be "usd".
  • interval is "month" or "year".
  • Omit price entirely for a zero-fee plan, or pass { free: true } to mark it free.

amount is cents, but meter overage micros is micro-dollars — two different money units that must not be crossed. $29.00 is amount: 2900; a $0.002/unit overage is micros: 2000. See value-types.ts for the exact conventions.

limits — rate limits and resource caps

limits is a per-dimension record. Each entry is either a rate limit ({ rate, interval }) or a resource count cap ({ count }). The SDK splits them: rate limits become the IR limits[]; counts become capability_limits.

limits: {
  requests: { rate: 600, interval: "minute", enforcement: "enforce" },  // → limits[]
  cron_jobs: { count: 10 },                                             // → capability_limits
}

A rate limit (RateLimit):

FieldTypeMeaning
ratenumber (positive integer)Capacity per window.
interval"second" | "minute" | "hour" | "day" | "week" | "month"The window. (No year — yearly is a price interval, not a window.)
enforcement"enforce" | "track" (optional)enforce blocks over-limit; track only records. Omitted ⇒ no enforcement key in the IR.

A count cap is { count: number }. The caps field is a count-only alias for the same capability_limits destination, accepting { count: 10 } or a bare 10:

caps: { cron_jobs: 10 }   // equivalent to limits: { cron_jobs: { count: 10 } }

Every plan must carry at least one rate-limit rule. A limitless plan fails the build with PLAN_RATE_LIMIT_REQUIRED — the hint shows the minimal shape: limits: { requests: { rate: 600, interval: "minute" } }. This is checked at farthershore-manifest-build time, identical to the publish gate.

grants and capabilities

Grant capabilities (optionally with per-capability resource limits) via grants and capabilityGrant, or grant limit-less capabilities via capabilities. Credit grants also ride grants.

grants: [capabilityGrant("managed-cron", { limits: { cron_jobs: 10 } })],
capabilities: ["premium_tools"],   // limit-less capability grants

grants also accepts credit grants in the IR GrantJson shape ({ kind: "credit" | "top_up", … }) for prepaid credit and purchasable packs.

meter — per-unit overage pricing

meter prices metered usage above the included allotment. Each entry is a MeterOverage: micros is the flat per-unit rate in micro-dollars, and includedUnits is an optional free allotment. Entries become the plan's meters[] in declaration order.

meter: {
  tokens_used: { micros: 2000, includedUnits: 100000 },  // $0.002/token after 100k free
}
FieldTypeMeaning
microsnumber (non-negative integer)Per-unit price in micro-dollars (2000 = $0.002). Taken verbatim into price_per_unit_micros.
includedUnitsnumber (positive integer, optional)Free allotment before overage applies.

If a meter entry needs platform-native keys the structured MeterOverage does not model, use the raw meters passthrough (the platform 5-knob meters[] shape) instead — but meter and meters are mutually exclusive on one plan.

trials, spend caps, and behavior

OptionTypeMeaning
trialDaysnumberFree-trial length in days → trial_days.
maxMonthlySpendCentsnumberHard spend cap (cents) → max_monthly_spend_cents.
minMonthlySpendCentsnumberMinimum monthly spend (cents) → min_monthly_spend_cents.
overageBehavior"block" | "allow_and_bill"What happens past included usage.
featureGatesRecord<string, boolean>Per-plan feature-gate flags.
detailsstring[]Marketing bullet points.
selfServeEnabledbooleanWhether subscribers can self-select this plan.
legacybooleanMarks a grandfathered plan.
archive{ at?, transitionTo?, strategy? }Archive schedule and migration target.
rawRecord<string, unknown>Escape hatch merged onto the emitted plan spec last (A/B variants, future fields).
@Plan("pro", {
  name: "Pro",
  price: { amount: 19900, currency: "usd", interval: "month" },
  limits: { requests: { rate: 6000, interval: "minute", enforcement: "enforce" } },
  meter: { tokens_used: { micros: 1500, includedUnits: 1_000_000 } },
  trialDays: 14,
  maxMonthlySpendCents: 500_00,   // $500.00 hard cap
  overageBehavior: "allow_and_bill",
})
pro!: unknown;

Changing a published plan

A @Product class is a snapshot. Repricing or changing a plan and pushing does not move existing subscribers — they are grandfathered by default. Actively migrating subscribers to a new plan version is an operate action, not a declaration:

# Move every croncloud "pro" subscriber from version 1 to the latest at their next renewal.
# Policies: grandfather | next_renewal | immediate | by_date | opt_in
farthershore plan migrate croncloud pro --from 1 --to head --policy next_renewal

So: edit the @Plan, push to apply the new contract; run plan migrate only when you want existing subscribers moved. See migrations for the full operate flow (also available over MCP).

What it compiles to

The starter plan above folds into one PlanSpecJson:

{
  "key": "starter",
  "name": "Starter",
  "recurring_fee_cents": 2900,
  "billing_interval": "month",
  "limits": [{ "dimension": "requests", "window": { "type": "named", "name": "minute" }, "capacity": 600, "enforcement": "enforce" }],
  "capabilities": ["managed-cron"],
  "capability_limits": { "cron_jobs": 10 }
}

See the Manifest IR for the full envelope and the determinism gate.