@farthershore/product
v0.5.0
Published
Farther Shore product-as-code SDK — declare your software product in TypeScript
Maintainers
Readme
@farthershore/product
Product-as-Code SDK for Farther Shore. Builder repos use this package from
product/product.config.ts to declare product contracts in TypeScript. Builders
author and export a Product; Farther Shore compiles that program to
backend-owned IR, validates it, and applies it through Core. The Product SDK
describes the sellable software product: its business logic origin,
surfaces, plans, capabilities, meters, and lifecycle. Every product is created
with a GitHub repo that contains the editable frontend/ starter and the
Product SDK entrypoint; connecting GitHub is a product-creation precondition.
Install
pnpm add @farthershore/productBuilder repos generated by Farther Shore already pin this dependency in
product/package.json.
Repo Layout
product/
package.json
product.config.ts
meters.ts
plans/
free.ts
pro.ts
routes/
api.ts
frontend/
...product/ is authored Product SDK code and may be split into any imported files
the builder wants. frontend/ is the generated editable starter app and is built
by the frontend pipeline only. Runtime state, accepted specs, compilation
records, deployment runs, and compiled IR are not checked into the builder repo;
Farther Shore stores them in Core.
Example
import { fs } from "@farthershore/product";
import { configureMeters } from "./meters.js";
import { configureCronRoutes } from "./routes/cron.js";
import { configurePlans } from "./plans/index.js";
const product = fs.product("croncloud", {
origin: "https://app.example.com",
displayName: "CronCloud",
description: "Managed cron jobs",
});
product.surface("frontend");
product.surface("api");
product.use(configureMeters, configureCronRoutes, configurePlans);
export default product;Modules are plain synchronous functions:
import { fs, type ProductModule } from "@farthershore/product";
export const configureMeters: ProductModule = (product) => {
product.requests();
const tokens = product.meter("tokens_used", {
unit: "token",
estimate: 500,
});
product.feature("runs").route("POST /v1/runs", {
reports: tokens,
});
};
export const configurePlans: ProductModule = (product) => {
product.plan("starter", {
name: "Starter",
price: fs.price.monthly(29),
limits: [
{
dimension: "requests",
window: { type: "named", name: "minute" },
capacity: 600,
enforcement: "enforce",
},
],
});
};Metering
product.requests() declares the platform-managed request meter and applies
requests = 1 to every metered route. Builders do not need backend code for
plain request counting.
const product = fs.product("croncloud", {
origin: "https://app.croncloud.com",
});
product.requests();
const tokens = product.meter("tokens_used", {
unit: "token",
estimate: 500,
});
product.feature("runs").route("POST /v1/runs", {
reports: tokens,
});Route metering is explicit:
- omitted route metering inherits product defaults such as
requests = 1 reportsdeclares dynamic meters the upstream may report with@farthershore/meteringcostsdeclares gateway-known fixed usage values for a routeestimatesoverrides reusable meter estimates for admission checksunmetered: trueclears all inherited and explicit route meteringinheritDefaultMeters: falsedisables inherited defaults only
const credits = product.meter("api_credits", { unit: "credit" });
product.defaultMeters(credits.fixed(1));
product.feature("exports").route("POST /v1/bulk-export", {
costs: credits.fixed(10),
});
product.feature("health").route("GET /healthz", {
unmetered: true,
});Lifecycle apply paths
Generated product repos use GitHub as the required automation and frontend workspace:
- Loads
product/product.config.ts. - Requires the default export to be the
Productreturned byfs.product(...). - Executes imported modules and compiles the product into deterministic Manifest IR.
- Validates the result against the deployed platform contract.
- Publishes the accepted release through Core so edge artifacts propagate.
The same IR can be built locally with farthershore build; accepted lifecycle
state is validated and applied by the GitHub bot after product/** changes are
committed and pushed. The repo remains the required frontend customization
workspace because frontend/ is where the starter UI and all custom React code
live.
The bundled farthershore-manifest-build binary is shared by the bot,
build-runner, and CLI. It emits the deterministic Manifest IR envelope that Core
accepts; Core, not the user repo, remains the lifecycle authority.
Public API
fs.product(name, options)— create the product builder.options.originis required and is the business logic origin Farther Shore calls for customer-facing actions.product.use(...modules)— compose Product SDK modules from any files underproduct/.product.surface(...)— declare the product surfaces customers interact with (frontend,api,docs,widget,webhook,worker, oragent).product.entitlement(...)— group capabilities, feature gates, limits, and meters into reusable access metadata.product.offering.plan(...)— declare a subscription plan through the generalized offering namespace.product.plan(...)remains the direct plan helper.product.meter(...)— declare billable or enforceable dimensions.product.requests()— declare and inherit the platform-managed successful request meter.product.defaultMeters(...)— apply reusable fixed costs to metered routes.product.resource(...)— declare counted resources for resource-count constraints.product.capability(...)— declare capability bundles and plan grants.product.feature(...)/product.api.route(...)— declare gateway routes, static costs, dynamic reports, estimates, and action metadata.product.policy(...)— declare policy files in code.product.plan(...)— declare plan pricing, limits, grants, and lifecycle behavior.product.lifecycle.*— declare migrations.product.raw.*— escape hatches for platform-schema JSON when the typed SDK does not yet have sugar.
Determinism
product/product.config.ts and everything it imports must be deterministic: no
dates, randomness, network calls, or process state. Sorted collections produce
stable output, while route order remains semantic because the gateway matcher is
first-match-wins.
The GitHub bot runs the build twice and rejects the push if the two generated hashes differ.
Release
@farthershore/product publishes to public npm through the GitHub workflow
.github/workflows/publish-product-sdk.yml.
Release sequence:
- Bump
packages/product/package.jsonversion. - Run the package checks:
pnpm --filter @farthershore/product run lint pnpm --filter @farthershore/product run test pnpm --filter @farthershore/product run build pnpm --filter @farthershore/product run pack:check - Commit, push, and merge through the normal PR flow.
- Tag from
mainwith the exact package version:git tag product-v<version> git push origin product-v<version>
The publish workflow verifies that the tag equals
product-v<packages/product/package.json version>, builds/tests the package and
its dependencies, runs pack:check, then publishes with NPM_PUBLISH_TOKEN.
