The @Product class
Declare a product as a decorated TypeScript class with @farthershore/product.
Declare a product as a decorated TypeScript class with @farthershore/product.
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) 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 { /* … */ }
| Option | Type | Meaning |
|---|---|---|
origin | string (required) | The business-logic origin FartherShore calls for customer-facing actions. Must be a valid URL. |
name | string | Product handle. Defaults to the class name. |
displayName | string | Human-facing product name. |
description | string | Short product description. |
billOn4xx | boolean | When true, 4xx responses still count toward request metering. Defaults to false. |
sandboxOrigin | string | Separate origin for preview/sandbox environments. |
visibility | "public" | "private" | Listing visibility. |
logoUrl | string | Wordmark URL. |
primaryColor | string | Brand color (validated as a color). |
authHeader | string | API-key header the gateway reads. Defaults to x-api-key. |
upstreamAuth | { type: "none" | "static_bearer"; token? } | How the gateway authenticates to your origin. |
envBranchPrefix | string | null | Branch prefix that drives preview environments. |
operatorPolicies | object | Operator automation (zero-traffic cleanup, change-approval risk gates). |
customerContext | object | Identity 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.
Everything else about the product is a member or stackable class decorator. Each links to its own page:
@Requests, @Meter, @Resource: the billable
and enforceable dimensions.@Feature: gateway routes keyed "METHOD /path",
their costs, the meters they report, and bound actions.@Capability, @Entitlement,
capabilityGrant: access bundles granted on plans.@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.
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.
# 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.