Farther ShoreDocs
Go to Farther Shore
What is FartherShore
Install the CLI
Quickstart
Core concepts
The @Product class
@Product optionsWhat goes on the classDeterminismBuild it locally
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
Change a price
Prepaid credits
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Define your product/The @Product class

The @Product class

Declare a product as a decorated TypeScript class with @farthershore/product.

PreviousCore conceptsNextMeters & resources

A FartherShore product is a TypeScript program. You author one decorated @Product class in product/product.config.ts, export default it, and FartherShore compiles that class to backend-owned Manifest IR, validates it, and applies it through Core. There is no YAML — the class is the product contract.

import { Product, 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,
})
export default class CronCloud {
  @Requests()
  requests!: unknown;

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

  @Plan("starter", {
    name: "Starter",
    price: { amount: 2900, currency: "usd", interval: "month" },
    limits: { requests: { rate: 600, interval: "minute", enforcement: "enforce" } },
  })
  starter!: unknown;
}

Member decorators (@Requests, @Meter, @Feature, @Plan, …) run first and enqueue their declarations; the @Product class decorator runs last and finalizes them into one product. Each member is a placeholder field — someName!: unknown — that only exists to carry the decorator. Cross-references between members are by string key (a route's reports: "compute" names the @Meter("compute") key, a plan's grant names a capability key, and so on). Decorators never hand handles to one another.

@Product options

@Product(options) takes one options object. origin is the only required field.

@Product({
  name: "croncloud",            // product handle; defaults to the class name
  origin: "https://api.example.com", // REQUIRED business-logic origin FS calls upstream
  displayName: "CronCloud",     // human-facing name in dashboards/portals
  description: "Managed cron jobs",
  billOn4xx: false,             // count 4xx responses toward request metering? (default false)
})
export default class CronCloud { /* … */ }
OptionTypeMeaning
originstring (required)The business-logic origin FartherShore calls for customer-facing actions. Must be a valid URL.
namestringProduct handle. Defaults to the class name.
displayNamestringHuman-facing product name.
descriptionstringShort product description.
billOn4xxbooleanWhen true, 4xx responses still count toward request metering. Defaults to false.
sandboxOriginstringSeparate origin for preview/sandbox environments.
visibility"public" | "private"Listing visibility.
logoUrlstringWordmark URL.
primaryColorstringBrand color (validated as a color).
authHeaderstringAPI-key header the gateway reads. Defaults to x-api-key.
upstreamAuth{ type: "none" | "static_bearer"; token? }How the gateway authenticates to your origin.
envBranchPrefixstring | nullBranch prefix that drives preview environments.
operatorPoliciesobjectOperator automation (zero-traffic cleanup, change-approval risk gates).
customerContextobjectIdentity requirement, context tokens, portal auth strategy.

origin is enforced at decoration time. Passing the legacy baseUrl key, or omitting origin, throws a ManifestBuilderError before anything compiles:

// @ts-expect-error origin is required
@Product({ name: "broken" })            // throws: options.origin is required
class _Broken {}

name defaults to the class name, but the canonical example passes it explicitly (name: "croncloud") so the product handle is decoupled from the TypeScript identifier. The handle is the stable slug used everywhere downstream.

What goes on the class

Everything else about the product is a member or stackable class decorator. Each links to its own page:

  • Meters & resources — @Requests, @Meter, @Resource: the billable and enforceable dimensions.
  • Features & routes — @Feature: gateway routes keyed "METHOD /path", their costs, the meters they report, and bound actions.
  • Capabilities & entitlements — @Capability, @Entitlement, capabilityGrant: access bundles granted on plans.
  • Plans & pricing — @Plan: price, limits, grants, overage, trials, spend caps.

@Frontend({ nav, pages }) and @Raw({ productPatch, … }) are stackable class decorators (write them above the class, alongside @Product); @Surface, @Workflow, @Policy, and @Backend are members like @Meter. Every decorator is exported from @farthershore/product — see the Product SDK reference for the full export list.

Determinism

product/product.config.ts and everything it imports must be deterministic: no dates, randomness, network calls, or process state. The build runs twice and rejects the push if the two generated IR hashes differ. Sorted collections (plans, meters, capabilities) make output stable; route order stays as you declared it because the gateway matcher is first-match-wins.

Build it locally

# Execute product/product.config.ts → deterministic Manifest IR, written to manifest-ir.json
farthershore-manifest-build --entry product/product.config.ts --out manifest-ir.json

The default entry is product/product.config.ts, so a bare invocation works inside a generated repo. This is the same binary the GitHub bot and build-runner use, so a clean local build means a clean push. To apply changes to a live product, commit product/** and push — the bot validates and applies. See the Manifest IR for the envelope shape and the determinism gate.