# Farther Shore Docs
Source commit: 10d3029de26218a283d8957422d8cc074c3055e6
---
# Find every platform capability
Canonical URL: https://docs.farthershore.com/get-started/capability-map
Farther Shore supplies the identity, entitlement, commerce, hosting and operating layer around your application. Your application owns its product behavior and data. Start with the customer's task, choose the platform capabilities it needs, and follow the owning surface below.
## Choose the owning surface
| Task | Read first | Execution surface |
| ------------------------------------------------ | ------------------------------------------ | ------------------------------------- |
| Define routes, grants, plans, pricing and bounds | [Business program](/define/business-class) | TypeScript in business/ |
| Verify callers and report measurements | [Backend metering](/backend/metering) | Backend SDK |
| Build a custom customer application | [Frontend overview](/frontend/overview) | Frontend SDK |
| Understand a bill, balance or subscription pin | [Commerce guide](/commerce/overview) | Billing reference and CLI reads |
| Explain a denied request | [Gateway guide](/gateway/overview) | Denial evidence and signed context |
| Publish, recover, manage customers or secrets | [Operations guide](/operations/overview) | CLI and Git |
| Work with governed agents and automations | [Agent guide](/agents/navigation) | Explicit agent tools and handoffs |
| Find an exact command or tool schema | [CLI and MCP guide](/cli/overview) | Generated command and tool references |
## Read with an agent
Fetch the [machine index](/llms.txt), then the collection relevant to your task. The [complete corpus](/llms-full.txt) contains all visible page source. The [capability catalog](/capabilities.json) records package versions, exact public exports, commands, tools, permissions and handoff reasons. The corpus includes MDX: preserve code fences and read callout content rather than treating the file as executable code.
Each reference is tied to the workspace packages used to generate it. Compare that version with your application's package pin and the installed CLI. A locally installed CLI can lag the docs. Use the pinned package contract and current command help; do not combine examples from incompatible majors.
## Learn the connected system
Read [ownership](/agents/operation-classes), [identity](/define/tenancy), [economics versus entitlements](/concepts/entitlements-vs-economics), and [release pins](/concepts/cohorts-and-versions) before composing a product. These explain why an authorized member can still hit a plan bound, why a frontend permission gate cannot protect backend data, and why publishing a new price does not silently reprice every subscriber.
For each change, record the business and environment, choose its owner, perform the documented workflow, then verify the resulting state. An accepted write, completed apply, healthy backend and settled bill are different observations.
---
# How Farther Shore works
Canonical URL: https://docs.farthershore.com/get-started/overview
Farther Shore puts the commercial and access-control layer around your software:
customer identity, plans, checkout, API credentials, gateway authorization,
limits, usage collection, hosted customer UI, and business operations.
You work through two surfaces with a clear ownership boundary.
## Repository-owned contract
The managed repository answers **what the business is**. Its `business/`
TypeScript program declares routes, plans, prices, meters, counted resources,
limits, policies, call surfaces, backend identities, and managed RBAC.
Change that state by editing the program, building it, and pushing Git:
```text
business/ source → deterministic Manifest IR → Git check → accepted contract
```
The platform never edits the business program back into the repository. A
successful push is the handoff from authored intent to the control plane.
## Platform-owned operations
The CLI answers **what is happening now**. Use it for state that is not source
code: selecting an organization, inspecting apply checks, binding deployed
backends, managing environments and runtime variables, publishing, observing
usage, and operating customers.
```bash
farthershore operations list --format json
```
That command is the current machine-readable catalog. Each entry says whether
the operation belongs in the repository or has an executable CLI command.
## The creation handoff
There is one business-creation path:
```bash
CREATE_ATTEMPT=$(node -e 'console.log(crypto.randomUUID())')
farthershore business create quillby \
--idempotency-key "$CREATE_ATTEMPT"
```
Human output is exactly the managed repository URL. The command does not report
success until that repository exists. The repository starts with tooling and
instructions, not a sample plan, route, meter, or frontend; you author the real
business from its requirements.
For shell automation:
```bash
REPO_URL=$(farthershore business create quillby \
--idempotency-key "$CREATE_ATTEMPT")
git clone "$REPO_URL"
```
## The normal lifecycle
1. Run `farthershore login` and select an organization if necessary.
2. Create the business and clone the returned repository.
3. Author one deterministic `business/` program.
4. Build locally, commit, and push.
5. Inspect the repository checks and the Apply Timeline.
6. Bind runtime infrastructure and test in a preview environment.
7. Publish only after reviewing the business and commercial-release diff.
8. Observe and operate the live business through the CLI.
Continue with the [Quickstart](/get-started/quickstart), then use
[Core concepts](/get-started/concepts) as the ownership reference.
---
# Choose a product shape
Canonical URL: https://docs.farthershore.com/get-started/product-shapes
Farther Shore supports three common shapes. They share the same plan,
subscription, identity, and enforcement model, so a business can grow from one
shape into another without changing its customer boundary.
## Hosted app
Choose a hosted app when customers primarily use a web interface. Build the
application in `frontend/` with `@farthershore/farthershore-js`. The SDK provides
customer sessions and managed account, plan, checkout, usage, and team UI.
You only need your own backend when the app performs domain work that cannot be
done in the browser or through a declared frontend integration.
Read [Frontend SDK](/frontend/overview).
## API business
Choose an API business when customers call HTTP endpoints with customer
credentials. Declare every exposed method with `fs.route()`, group the route
refs if useful, grant them from plans, and bind each logical `fs.backend()` to a
deployed origin per environment.
Farther Shore authenticates and authorizes before proxying to the backend. The
backend verifies the signed runtime context and reports any usage the gateway
cannot know from the request alone.
Read [Routes](/define/routes) and [Bring your own backend](/backend/overview).
## Hybrid product
Choose a hybrid when the hosted app and public API are two surfaces over the
same business. Route `surfaces` control where an operation may be called;
omitting them permits all supported authenticated surfaces.
```ts
const reports = fs.route("/v1/reports", {
get: { surfaces: [fs.surfaces.ui, fs.surfaces.api] },
});
```
Do not duplicate plans or customer organizations for the two surfaces. A single
subscription can grant the UI and API routes it purchased.
## Decide from customer behavior
- Start hosted when the customer expects a product UI.
- Start API-first when the customer integrates from code.
- Use hybrid when both surfaces sell the same underlying capability.
- Add a backend only for server-side domain work.
- Use a frontend integration only for a tightly constrained third-party call;
it is not a general backend replacement.
---
# Install the CLI
Canonical URL: https://docs.farthershore.com/get-started/install
The CLI is the automation surface for platform-owned operations. It also builds
and validates the repository-owned business program locally.
## Install
Farther Shore requires Node.js 22 or newer.
```bash
npm install -g @farthershore/cli@0.33.5
farthershore --version
```
## Sign in
```bash
farthershore login
```
The CLI opens the complete authorization request in your browser without
printing a code. A human approves the request. This is a user-bound login: the session acts as your current
Farther Shore user across all current and future organization and business
memberships. Normal login offers no permission or scope choices.
On a remote machine, print the manual verification URL and short-lived code
without trying to open a local browser:
```bash
farthershore login --headless
```
The code expires, so complete the browser step while the command is polling.
The CLI validates the credential before saving it and never needs a secret in a
command-line argument.
## Organizations
One signed-in user can belong to multiple organizations. See them and select a
default context without signing in again:
```bash
farthershore auth organization list
farthershore auth organization use acme
farthershore auth whoami
```
Override the selected organization for one command with the global
`--organization ` option.
Authorization uses live role evaluation rather than a copied permission list.
Membership and role changes take effect on the next authenticated request.
## Restricted automation override
When a human intentionally issues an organization-scoped automation credential,
pipe it over stdin instead of placing it in process arguments:
```bash
printf '%s' "$FARTHERSHORE_MAKER_TOKEN" | farthershore login --token-stdin
```
For a single invocation, `FARTHERSHORE_TOKEN` is an ephemeral environment
override and is not persisted automatically. Use this path only when automation
must be narrower than the signed-in user's live authority; the regular login
flow remains the default.
## Agent output
Use JSON for scripts and agents:
```bash
farthershore business list --format json --no-input
```
Successful commands return an operation-keyed data envelope. Failures include a
stable error code and remediation hint. Use `farthershore --help` or
`farthershore operations list --format json` instead of guessing command names.
## Sign out
```bash
farthershore logout
```
`farthershore logout` revokes the current CLI session and deletes the local
credential file.
---
# Quickstart
Canonical URL: https://docs.farthershore.com/get-started/quickstart
This path creates one managed business repository, defines a minimal contract,
and verifies the platform accepted it.
## 0. Confirm you can deploy
Farther Shore is the gateway, billing, and entitlement plane in front of an HTTP
service that you run. It does not host that service, and a production publish
fails until every declared backend has a real origin bound to it. Before you
start, confirm all three:
1. **Somewhere to run it** — a host that serves a long-lived HTTP process on a
public HTTPS URL: Railway, Render, Fly.io, Cloud Run, AWS, or your own
infrastructure.
2. **Somewhere to put secrets** — the ability to set environment variables on
that host, because the service reads `FS_RUNTIME_TOKEN` from its environment
and that value must never be committed.
3. **Somewhere to read logs** — bootstrap and signature-verification failures
appear only in the service's own logs.
If any is missing, resolve it first. For one service, the host's own CLI is
enough; for a preview environment and a production environment that must stay in
step, see [Infrastructure with OpenTofu](/backend/infrastructure-opentofu).
## 1. Sign in
```bash
farthershore login
farthershore auth whoami
```
If your user belongs to several organizations, select the intended one:
```bash
farthershore auth organization list
farthershore auth organization use acme
```
## 2. Create and clone the managed repository
```bash
CREATE_ATTEMPT=$(node -e 'console.log(crypto.randomUUID())')
REPO_URL=$(farthershore business create quillby \
--idempotency-key "$CREATE_ATTEMPT")
git clone "$REPO_URL"
cd "$(basename "$REPO_URL" .git)"
```
To create in an organization other than the saved default, note that
`--organization` is a **global** option and must precede the subcommand:
`farthershore --organization acme business create quillby …`. Placed after
`create`, it is rejected as an unknown option.
Human-mode stdout is only the repository URL. For structured recovery metadata,
add `--format json` to the same keyed command and read `.data.repoUrl`.
The repository contains `business/package.json`, TypeScript tooling, and agent
instructions. It intentionally contains no predefined business shape.
## 3. Author `business/business.ts`
Create the program from the actual product requirements. This complete example
declares one metered route and one free plan:
```ts
import * as fs from "@farthershore/business";
fs.backend("api");
const requests = fs.requests();
const health = fs.route("/v1/health", {
get: { costs: [requests.fixed(1)] },
});
fs.plan("starter", {
kind: fs.plan.kind.free,
grants: [health],
limits: [requests.perMinute(60)],
});
export default fs.business({
customerContext: {
// Required when a signed-in browser will mint fsc_ gateway credentials.
contextTokens: { enabled: true },
// Preview environments can use disposable personas; production auth is
// configured by the platform connection.
customerAuth: { strategy: "test-personas" },
},
});
```
`fs.requests()` does not attach itself to every route in SDK 2.0. The explicit
`costs` entry is what makes the plan limit meaningful.
`customerContext.contextTokens` is what makes authenticated browser-to-gateway
requests possible. Do not omit it when the customer application calls protected
backend routes through the Farther Shore gateway.
## 4. Build locally
```bash
cd business
npm install
npm run build
cd ..
farthershore validate
```
Fix every compiler diagnostic. A build must be deterministic and every ref,
grant, metering attachment, and limit must resolve.
## 5. Push and inspect apply
```bash
git add business
git commit -m "Define business"
git push
```
A direct push reports two checks on the commit: `farthershore/build` for the
Manifest IR build, then `farthershore/apply` for the compile, accept, and publish
phases. `farthershore/validate` is the pull-request check and does not appear on
a plain push, so do not wait for it.
Inspect the repository check for that commit, then confirm the accepted state:
```bash
farthershore apply-timeline list quillby
farthershore business contract quillby
farthershore business routes quillby
```
Do not treat a local build or pushed commit as accepted until the apply check
succeeds.
## 6. Prove the first-customer path
A successful contract apply, backend deployment, frontend build, or
authenticated HTTP call is necessary but not sufficient. Before release, prove
one new customer can complete this entire public path in the same environment:
1. Open the environment's returned portal hostname and sign in.
2. Select the intended organization; do not assume the personal workspace is
the subscribed organization.
3. Start onboarding from the published offer. For a free plan, let Core select
the current free compiled plan instead of copying an old plan id.
4. Read `/me` again and require `subscriber.status: "ACTIVE"` plus a non-null
`subscriber.compiledPlanId`. A success message or plan catalog entry does
not prove enrollment.
5. Mint the subscriber gateway credential through the SDK and call a declared
route. Require a response marker produced by the backend, not merely a 2xx to
4xx status that could have come from the gateway.
6. Read the record back through the application UI.
For preview environments, public resolution must return that environment's
runtime hostname. Browser SDK traffic must never fall back to the production
gateway. See [Test in a preview](/cookbook/preview-environment).
`FartherShoreRoot` from `@farthershore/farthershore-js/components` owns the organization,
onboarding, entitlement-refetch, legal, and payment gates. Keep product content
inside the root so an incomplete subscriber cannot enter the application.
## 7. Continue the real build
- Scaffold the backend service: `farthershore create api --node` from the
repository root, then [Scaffold a backend](/backend/scaffold).
- Add route policies and backend refs: [Routes](/define/routes).
- Bind the deployed service: [Bring your own backend](/backend/overview).
- Provision more than one environment: [Infrastructure with
OpenTofu](/backend/infrastructure-opentofu).
- Design economics and bounds: [Plans & pricing](/define/plans).
- Build customer UI: [Frontend SDK](/frontend/overview).
- Test in a branch environment: [Environments](/operate/environments).
- Review plan impact before release: [Plan changes](/monetize/plan-changes).
Publishing production is a separate, explicit action after runtime bindings,
payments, and release checks are ready. It fails with `BACKEND_TARGET_REQUIRED`
unless every backend declared in `business/` has a concrete production origin,
and with `DEFAULT_BACKEND_REQUIRED` when several backends exist without one
marked default.
## 8. Operate through the CLI
Use CLI commands for state that has no code representation:
```bash
farthershore usage summary quillby --format json
farthershore frontend status quillby --format json
farthershore notifications preferences quillby --format json
```
Use `farthershore operations list --format json` to discover available
operations and their exact command shapes.
## Verify
- `farthershore business create ` returned one managed repository URL.
- The repository began without a predefined business shape.
- `farthershore build` and `farthershore validate` succeeded locally.
- The pushed commit has successful build/apply checks.
- `business status` identifies the accepted business state.
- A newly signed-in customer has an active compiled plan and receives a
backend-produced response through the environment gateway.
## Recover
- Create timed out: rerun the exact command with the persisted create key, then
use `business show` to read current state.
- Build failed: correct `business/` and rerun build and validate.
- Push check failed: inspect its annotations, fix the repo, and push again.
- Onboarding looked successful but `/me` has no compiled plan: treat the
customer as not enrolled, repeat onboarding only after reading current state,
and do not render the application yet.
- Preview UI calls the production gateway: inspect public business resolution;
its `runtimeHostname` must equal the selected preview environment hostname.
- Platform operation failed: branch on the stable error code and follow its
hint; do not move platform-owned state into the repo or contract state into a
CLI write.
## Agent prompt
```text
Generate and record one private attempt key, then create Farther Shore business
quillby with `farthershore business create quillby --idempotency-key
`. Clone the returned repository URL and read its AGENTS.md.
Gather the requirements, then author the complete business/ program from
scratch using the functional @farthershore/business SDK. Run build and
validate, commit and push, inspect the Farther Shore checks and Apply Timeline,
and report exact results. Use the CLI only for platform state that has no code
representation.
```
Continue with [Core concepts](/get-started/concepts) or the recipe closest to
your product shape.
---
# Core concepts
Canonical URL: https://docs.farthershore.com/get-started/concepts
## Business program
The `business/` folder is the contract source of truth. The compiler imports
every supported source module in canonical order and accepts exactly one
default-exported `fs.business()` result. The conventional starter is
`business/business.ts`, but discovery is filename-agnostic.
Declarations return immutable branded refs. Plans grant route, group, and
frontend-integration refs rather than joining unrelated strings.
## Deterministic build
`farthershore build` executes the program twice and rejects differing output.
Do not make contract declarations depend on time, randomness, network calls,
filesystem state, or environment variables. The resulting Manifest IR is the
wire contract; Core never executes arbitrary builder code.
## Accepted contract and apply
A local build proves that the program compiles. A pushed commit still has to
pass repository validation and apply checks. The accepted contract is the last
successful applied result, not whatever happens to be in an unverified branch.
Use the Apply Timeline to correlate commits, semantic diffs, phases, and
failures:
```bash
farthershore apply-timeline list
farthershore business contract
```
## Customer boundary
Every subscriber is an organization, including a solo customer. Plans,
subscriptions, billing, and business-level limits attach to that organization.
A verified member principal identifies a person inside it; a verified service
principal identifies an organization-owned machine credential.
The platform enforces plan access at the edge. Your backend uses the verified
principal for row-level ownership and collaboration rules.
## Entitlements, economics, and bounds
A plan combines three independent decisions:
- route and integration grants decide access;
- flat and metered prices decide economics;
- meter and resource limits decide bounds.
A granted route may be free. A priced meter may have no hard cap. A hard cap
may be unpriced. Read [Entitlements vs economics](/concepts/entitlements-vs-economics).
## Environments and release
Preview environments let a branch apply without changing production. Concrete
backend origins, runtime variables, and hosted frontend releases are scoped to
an environment. The business program names logical contract objects; the
platform binds their environment-specific operational values.
Publishing creates immutable business and plan release state. Existing
subscriber cohorts are evaluated against the new candidate independently; use
`farthershore commercial-release diff ` before activation.
## Repository vs CLI
When a write is represented in `business/`, edit and push the program. When it
has no code representation, use the CLI. `farthershore operations list` is the
authoritative classifier for the current installed version.
---
# Build a hybrid product
Canonical URL: https://docs.farthershore.com/cookbook/hybrid-product
## Outcome
Customers get a hosted app and an API under one subscription. Use this when the UI and API share plans, route grants, and usage.
## Prerequisites
- A Farther Shore business repo and authenticated CLI
- A public HTTPS backend
## Define the product
Add the API route and both surfaces to the single default-exported `fs.business()` result in `business/`:
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const api = fs.backend("api", {
transport: { mode: "direct" },
default: true,
});
const listJobs = fs.route("/v1/jobs", {
get: { backend: api, costs: [requests.fixed(1)] },
});
const createJob = fs.route("/v1/jobs/create", {
post: { backend: api, costs: [requests.fixed(1)] },
});
const deleteJob = fs.route("/v1/jobs/{id}", {
delete: { backend: api, costs: [requests.fixed(1)] },
});
const managedJobs = fs.group("managed-jobs", [listJobs, createJob, deleteJob]);
fs.plan("starter", {
kind: fs.plan.kind.free,
grants: [managedJobs],
// A free plan carries no economics — bound it structurally, otherwise it is
// an uncapped invitation to spend your money.
limits: [requests.perMinute(60)],
});
export default fs.business();
```
Build, then push to a preview branch:
```bash
farthershore build --format json
git push -u origin HEAD:env/hybrid-preview
farthershore backend create acme \
--env hybrid-preview \
--name "Acme API (preview)" \
--slug api \
--transport direct \
--origin-url https://preview-api.example.com \
--default \
--idempotency-key \
--format json
```
The managed repo starts without sample frontend code. Add the custom
`frontend/` Vite application, install `@farthershore/farthershore-js`, and push
it on the same preview branch before using `farthershore frontend status`.
## Verify
Open the preview portal, subscribe with a test persona on `starter`, load the
app, and call `POST /v1/jobs/create`. Then run:
```bash
farthershore usage summary acme --format json
```
## Common failures
- `MANAGED_BY_CODE`: edit `business/`; do not retry a contract write through the API.
- API request denied: confirm the subscriber has `starter` and the route matches exactly.
- App works but API fails: check the backend status and origin separately.
## Recover
Revert the business commit and push the preview branch again. Production is unchanged until explicitly published.
## Next steps
See [frontend setup](/frontend/overview), [backend setup](/backend/overview), and [access-aware UI](/frontend/access-aware-ui).
## Agent prompt
> In this Farther Shore repo, add a frontend plus metered API using the existing functional business program. Read `AGENTS.md`, preserve existing plans, run `farthershore build --format json`, and report the preview test commands. Do not publish production.
---
# A pay-as-you-go API
Canonical URL: https://docs.farthershore.com/cookbook/usage-based-api
## Outcome
Sell **CronCloud**, an API billed per request. Farther Shore authenticates API
keys, applies plan limits, meters admitted traffic, and forwards it to your
origin.
Use this recipe when customers call your service from their own code. For a
hosted web app, use [Subscription SaaS](/cookbook/saas-subscription-app).
## Prerequisites
- A reachable HTTPS origin
- Authenticated CLI and a managed business repository
- An API-surface business
## Define the API
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const calls = fs.measure("calls");
const apiUsage = fs.meter("api_usage", { measures: [calls] });
const apiPricing = fs.pricing("api_usage", {
meter: apiUsage,
catalog: [fs.rate.per(1000, fs.money.usd(5))],
});
const api = fs.backend("api", {
transport: { mode: "direct" },
default: true,
});
const listJobs = fs.route("/v1/cron-jobs", {
get: { backend: api, costs: [requests.fixed(1)], reports: [apiUsage] },
});
fs.meterRoutes("cron-jobs-usage", listJobs, { reports: [apiUsage] });
fs.plan("payg", {
kind: fs.plan.kind.usage,
usagePricing: apiPricing.current(),
grants: [listJobs],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
`payg` is a `usage` plan: postpaid, every reported call rated at $5 per
thousand ($0.005 each, exactly) and settled on the invoice. The origin reports
one unit per served call:
```ts
await req.fartherShore.report({ meter: "api_usage", values: { calls: 1 } });
```
Discovery is folder-based. Keep exactly one default-exported `fs.business()`
result across `business/`.
## Validate and launch
```bash
farthershore build --format json
farthershore validate --format json
git add business/ && git commit -m "define CronCloud API" && git push
farthershore backend create croncloud \
--name "CronCloud API" \
--slug api \
--transport direct \
--origin-url https://api.example.com \
--default \
--idempotency-key \
--format json
farthershore business publish croncloud --dry-run --format json
# After explicit approval of the first draft activation:
farthershore business publish croncloud --format json --idempotency-key
farthershore business status croncloud --format json
```
Poll until `ACTIVE` and `live: true`. In a preview environment, mint a one-time
test credential and copy the returned `fsk_test_*` value:
```bash
farthershore backend create croncloud \
--env preview \
--name "CronCloud API (preview)" \
--slug api \
--transport direct \
--origin-url https://preview-api.example.com \
--default \
--idempotency-key \
--format json
farthershore persona bootstrap croncloud --env preview --plan payg --format json --idempotency-key
```
Personas are limited to test-strategy environments. See
[API keys and test personas](/operate/environments) for preview setup.
## Verify
```bash
GATEWAY_HOST="https://preview.example.com" # replace with environment hostname
FSK_TEST_KEY="fsk_test_..." # replace with bootstrap output
curl -i "$GATEWAY_HOST/v1/cron-jobs" \
-H "x-api-key: $FSK_TEST_KEY"
farthershore usage summary croncloud --format json
```
Confirm the request reaches the origin, `api_usage` increases, the bill
preview's rated total grows by exactly $0.005 per call, and an invalid key is
rejected before origin forwarding.
## Common failures and recovery
| Symptom | Fix |
| ------------------------------- | ------------------------------------------------------------------------------------ |
| Gateway returns an origin error | Verify HTTPS reachability, route path, and backend status. |
| Usage stays at zero | Confirm the route matched the declared operation and the origin reports `api_usage`. |
| `MANAGED_BY_CODE` | Edit `business/`, build, and push instead of mutating the contract through the API. |
| Valid caller gets 429 | Inspect response limit metadata and the plan's rate ceiling. |
Correct routes, pricing, and limits in `business/`, then publish forward. A
catalog reprice reaches every `current()`-bound subscriber from the release's
activation forward; usage already admitted is rated at the old rate.
## Next steps
- [Connect a backend](/backend/overview)
- [Gate API routes by plan](/cookbook/grant-routes)
- [Deploy on Railway](/backend/deploy-railway)
- [Diagnose a denied request](/cookbook/diagnose-denied-request)
## Agent prompt
```text
Define CronCloud as an API business at the supplied HTTPS origin. Add a
`calls` measure on an `api_usage` meter, a pricing catalog at $5 per thousand,
three cron-job routes bound with meterRoutes, a kind-usage plan, and an
enforced rate limit.
Build, validate, push, and verify in preview. Show the production publish dry
run and ask for approval before publishing. Create a safe test identity, call
the gateway, and verify forwarding, rejection of an invalid key, and usage.
```
---
# A subscription SaaS app
Canonical URL: https://docs.farthershore.com/cookbook/saas-subscription-app
## Outcome
Build **Quillby**, a hosted app with a $19/month Pro plan. Farther Shore serves
the account UI and runs checkout. You do not need an
origin API for this managed-component path.
Use this recipe when customers consume your product through a signed-in web UI.
For an API, use [Pay-as-you-go API](/cookbook/usage-based-api).
## Prerequisites
- Authenticated CLI and a managed business repository
- A business definition with a frontend surface
## Define the product
Create this declaration in `business/`, or split it across sibling modules. The
folder must contain exactly one default-exported `fs.business()` result.
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
fs.plan("pro", {
kind: fs.plan.kind.flat,
price: fs.money.usd(19).monthly(),
limits: [requests.perMinute(60)],
});
export default fs.business();
```
## Validate and launch
```bash
farthershore build --format json
farthershore validate --format json
git add business/ && git commit -m "define Quillby Pro" && git push
farthershore business publish quillby --dry-run --format json
# After explicit approval of the first draft activation:
farthershore business publish quillby --format json --idempotency-key
farthershore business status quillby --format json
```
Publishing is asynchronous; poll the last command until `status` is `ACTIVE`
and `live` is `true`. If a prerequisite blocks the dry run, follow its stable
remediation hint and repeat the preview before publishing.
## Verify
- The account page shows billing and plans.
- Checkout activates Pro and updates the subscriber's pinned plan.
- `farthershore plan list quillby --format json` shows the published version.
## Common failures and recovery
| Symptom | Fix |
| ---------------------------------------- | ---------------------------------------------------- |
| Publish succeeds but the app is not live | Poll business status and inspect the Apply Timeline. |
Correct contract mistakes in `business/`, then build, push, and publish the
next GitHub Release from the managed repository. A repriced plan reaches new
subscriptions; existing subscribers keep their recurring-price pin.
## Next steps
- [Gate frontend UI](/frontend/access-aware-ui)
- [Freemium](/cookbook/freemium)
- [Change a price](/cookbook/change-a-price)
## Agent prompt
```text
Define Quillby as a frontend-only Farther Shore business with a $19/month Pro
plan and an authenticated account page. Build, validate, push, inspect the
repository checks, and show the publish dry run. Ask before
publishing, then poll until ACTIVE and live. Verify checkout pins the Pro plan.
```
---
# Environment variables
Canonical URL: https://docs.farthershore.com/reference/env-vars
Business behavior is not configured with environment variables. Plans, prices,
meters, routes, limits, policies, and surfaces remain deterministic declarations
under `business/`.
This page covers three unrelated configuration channels that are easy to
confuse.
## CLI process configuration
| Variable | Default | Purpose |
| ---------------------- | ------------------------------- | -------------------------------------------------------------------------- |
| `FARTHERSHORE_API_URL` | `https://core.farthershore.com` | Override the control-plane API, primarily for development or stage testing |
| `FARTHERSHORE_ENV` | production | Default environment for commands that accept `--env` |
| `FARTHERSHORE_TOKEN` | unset | Ephemeral, pre-issued organization-scoped MakerToken for this process |
Normal authentication is user-bound and persisted by `farthershore login`.
`FARTHERSHORE_TOKEN` is the narrow automation override; it is not persisted
automatically and must never be committed or printed.
## Backend runtime bootstrap
`@farthershore/backend` reads one runtime credential:
| Variable | Default | Purpose |
| ------------------ | ------------------------------- | --------------------------------------------------------------------------------------- |
| `FS_RUNTIME_TOKEN` | required | Bootstrap request verification, runtime identity, transport configuration, and metering |
| `FS_CORE_URL` | `https://core.farthershore.com` | Runtime bootstrap URL override |
```ts
import { fartherShore } from "@farthershore/backend";
const fs = fartherShore.initFromEnv();
```
A runtime token may be business-wide, environment-scoped, or backend-scoped.
It can also restrict operations, meter names, and route identities. The token
value is shown once, stored hash-only by the platform, and belongs in the
backend host's secret manager.
Rotation is a hard cutover: the predecessor is revoked immediately. Deploy the
new secret as one coordinated change; there is no period where both tokens are
valid. See [Runtime tokens](/backend/runtime-tokens).
## Business variables: the name is the class
Business variables are platform-owned values addressed by business,
environment, and key. An environment-specific value overrides the production
value for that environment. There is no delivery setting — a name starting
with `FS_PUBLIC_` is public; any other name is a write-only secret.
| Name | Read behavior | Where it goes | Rebuild? |
| ------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `FS_PUBLIC_*` | readable plaintext | inlined into the hosted frontend bundle; visible to every visitor | yes |
| anything else | write-only | the hosted frontend build (leak-scanned), and injected by the gateway into a compiled `fs.frontendIntegration()` that references it | no |
A secret variable does **not** set a process environment variable on your
backend host, and it is unrelated to `FS_RUNTIME_TOKEN`.
```bash
farthershore variables list quillby --format json
printf %s "$VALUE" | farthershore variables set quillby API_REGION \
--idempotency-key --format json
farthershore variables status quillby --format json
```
`FS_PUBLIC_*` values are returned by reads because they are intentionally
public. Every other variable stays write-only; list/status responses expose
metadata, not plaintext. Use `rotate`, `revoke`, and `rm` for lifecycle changes.
Read [Variables](/frontend/variables) for frontend usage and
[Frontend integrations](/define/frontend-integrations) for the only edge
injection path.
## Hosted frontend bootstrap
`@farthershore/farthershore-js` reads connection and business context from the
`window.__FS_CONFIG__` shim injected by the hosted shell. Application code does
not embed a Core URL or platform credential. Local preview commands inject the
same shape:
```bash
farthershore frontend dev
farthershore frontend preview
```
## Ownership test
If a value changes what the business sells or permits, put it in deterministic
`business/` code. If it is an environment-specific credential, public setting,
or runtime binding with no contract representation, operate it through the CLI.
---
# Glossary
Canonical URL: https://docs.farthershore.com/reference/glossary
Use this page when a term in a guide is unfamiliar.
## Agents
**Coding agent** — your agent working in the business repository. It authors
product and application changes, opens pull requests, and uses the CLI or MCP
for platform operations.
**Farther Shore Agent** — the platform-run operating agent for a live business.
The currently enableable role is the **Operator**. It observes bounded business
metrics, composes research and analysis, posts to the Bulletin, and may take
only explicitly enabled marketing or customer-experience actions.
**Operator** — the single Farther Shore Agent role available today. It is off
by default and is enabled per business through the dashboard, CLI, or MCP.
**Bulletin** — the business feed where the Operator publishes insights,
warnings, and change requests for a human or coding agent to review.
**Agent run** — one scheduled Operator execution. A run records its status,
composed launches, usage, trace summary, and action receipts.
## Product definition
**Business** — the software product Farther Shore manages. Its code-managed
definition lives in the repository's `business/` folder.
**Business program** — all TypeScript modules under `business/`. The compiler
loads the folder and requires exactly one exported, declared `fs.business()`
result. The managed starter convention is `business/business.ts`, but filenames
are not part of the contract and declarations may be split across modules.
**Manifest** — the deterministic output of building the business program. Core
accepts this output; it does not run arbitrary repository code.
**Surface** — a way customers use the product, such as a hosted frontend or an
API.
**Route** — a path-first HTTP operation declaration. Its branded ref is the
unit a plan grants.
**Access group** — a reusable bundle of route or group refs created with
`fs.group()`.
## Plans and usage
**Plan** — a versioned offer with a declared **kind** (`free`, `flat`, `usage`,
`prepaid`, `hybrid`, `trial`, `custom`) and up to five economic controls:
`price`, `usagePricing`, `funding`, `lifecycle`, `spendPolicy`. Access grants
and structural limits sit beside them.
**Plan kind** — the declared classification of a plan (`fs.plan.kind.*`). The
compiler validates the controls against the kind; nothing is inferred from
shape.
**Grant** — access a plan gives to a stable route identity.
**Measure** — one observed quantity (`fs.measure("input_tokens")`); the key a
backend sends under `values`.
**Dimension** — a selector axis for measurements (`fs.dimension("model")`);
the key a backend sends under `dims`.
**Meter** — a measurement declaration: a set of measures reported together plus
the dimensions they may be reported under. Never a price.
**Pricing catalog** — an immutable, versioned family of exact rates for one
meter (`fs.pricing()`), with structured items `(provider, model, modality?)`,
`where` conditions, modifiers, tiers, and bounded backend quotes.
**Rating context** — the resolved, immutable input to rating: catalog version,
selector match sets, agreement terms, tier semantics, exact rational rates.
**RatedCharge** — the exact nanodollar value of one measurement under its
rating context.
**Funding bucket** — value that pays rated charges before anything is owed:
`fs.included`, `fs.prepaid`, `fs.promo`, `fs.referral`.
**Allowance** — an included bucket, denominated in rated value and reissued
each period.
**Disclosure** — `spendPolicy.disclosure`: `transparent` surfaces show rates
and totals; `opaque` surfaces show only allowance remaining.
**Exhaustion** — `spendPolicy.onExhaustion`: `fs.exhaustion.block` denies at
zero; `fs.exhaustion.overage(binding)` continues as amount due.
**Economic agreement** — a confirm-gated binding of one subscription to a
pricing family with negotiated terms; created and amended through the CLI.
**Commercial release** — the immutable, content-addressed per-business bundle
that binds structure and money; activated per business by appending to a
release log.
**Admission descriptor** — the compiled per-route linear bound the gateway
evaluates to reserve a request's economic maximum before forwarding it.
**Bill preview** — the subscriber money surface, computed by the invoicing
engine over the ledger; honors disclosure.
**Resource** — a countable object such as projects or team members.
**Limit** — a structural plan rule that caps request rate, resources,
concurrency, or per-request capacity. Money bounds come from funding and spend
policy, not from limits.
**Overage** — usage rated as amount due after an allowance is exhausted, on a
plan with `fs.exhaustion.overage`.
**Settlement rail** — Stripe: collects amount due and recurring fees, pays
refunds, and remits tax. It never rates usage or owns a balance.
## Runtime and operations
**Core** — the control plane and source of truth for businesses,
subscriptions, rating, funding, the monetary ledger, and operational state.
**Gateway** — the edge service that authenticates requests and enforces
route grants and limits before forwarding them.
**Backend** — your upstream service registered with a business. It can use a
direct public origin or a managed tunnel.
**Runtime token** — an `FS_RUNTIME_TOKEN` used by
`@farthershore/backend` to obtain runtime configuration and authenticate
usage reports. A token can cover a business, environment, or backend and can
further restrict operations, meters, and routes.
**User CLI session** — the credential created by `farthershore login`. It acts
as the approving user and reloads current organization membership and role on
every request.
**MakerToken** — a separately issued, organization-scoped automation
credential with fixed exact permissions and optional selected-business scope.
It is used when automation must be narrower than a user CLI session.
**Preview environment** — a non-production environment bound to a branch for
testing product changes.
**Apply** — Core validating and applying a built manifest to an environment.
**Release** — the production GitHub Release that publishes deferred economic
contract changes (as a new commercial release) and triggers the production
hosted-frontend build.
**Business rollback** — a new forward publish workflow that re-applies the
captured manifest snapshot from an earlier workflow. It does not rewrite Git
history.
**Contract operation** — a change represented in the business program, such as
a plan, route, meter, or limit. Make it in code and push it.
**Operate action** — runtime state with no business-program representation,
such as rotating a token or rolling back a frontend. Use the CLI, MCP, or
dashboard.
---
# The fs.business() program
Canonical URL: https://docs.farthershore.com/define/business-class
The `business/` folder is the contractual source of truth. Farther Shore loads
all of its supported source modules in canonical order and requires exactly one
default-exported `fs.business()` result.
The managed repository begins without a source file. Create
`business/business.ts` as the conventional starter, or split declarations into
sibling modules. Discovery is by folder, not by filename.
```ts
import * as fs from "@farthershore/business";
fs.backend("api");
const requests = fs.requests();
const status = fs.route("/v1/status", {
get: { costs: [requests.fixed(1)] },
});
fs.plan("starter", {
kind: fs.plan.kind.free,
grants: [status],
limits: [requests.perMinute(60)],
});
export default fs.business();
```
## Functional declarations
Import the SDK as a namespace:
```ts
import * as fs from "@farthershore/business";
```
Start with `business`, `route`, and `plan`. Add `measure`, `dimension`, `meter`,
`pricing`, and `meterRoutes` for measured usage; `requests` and `resource` for
structural limits; `backend` and `frontendIntegration` for runtime bindings.
`group` composes routes and can declare custom permission subjects. Managed RBAC
enablement is operating state, not an `fs.rbac()` declaration.
Use the [generated Business SDK exports](/generated/business-sdk/root) for the
complete signatures and types. The [SDK guide](/reference/business-sdk) explains
how to choose and combine them.
Declarations return immutable branded refs. Pass those refs to routes, groups,
and plans; do not reconstruct reference-shaped objects or join by string.
## Split a growing program
Only one module may finalize the registry:
```text
business/
business.ts # imports declarations and default-exports fs.business()
routes.ts # exports route refs
plans.ts # declares plans using imported refs
package.json
tsconfig.json
```
Every discovered source module executes, even when the entry module does not
import it explicitly. The normal isolated folder loader defers finalization
until all discovered modules have imported, so an alphabetically early business
module does not discard declarations in later files. Outside that loader,
`fs.business()` finalizes immediately and later declarations fail.
For portable, easy-to-review programs, explicitly import declaration modules
before the single default-exported `fs.business()` call. Do not depend on
alphabetical filenames to establish dependencies: import the refs you use.
Calling `fs.business()` twice is invalid even in deferred loader mode.
### What folder discovery includes
Supported source extensions are `.ts`, `.tsx`, `.mts`, and `.cts`. The folder
walk skips declaration files and files named with `.test`/`.spec` suffixes, and
prunes `node_modules`, `dist`, `__tests__`, `__fixtures__`, and `__mocks__`.
Symlink entries are not followed by the walk. These are discovery rules, not a
sandbox for arbitrary code imported by your program.
`--entry business/single-file.ts` selects that file and its imports; it does not
discover all its siblings. Prefer the normal folder build when verifying what
the platform will compile. Ensure the business package uses ESM module semantics
so its default export is exposed to the loader as intended.
## Contract options
`fs.business()` accepts these business-wide contract option families:
| Option | Responsibility |
| ------------------ | -------------------------------------------------------------- |
| `visibility` | Public/private business visibility intent |
| `authHeader` | API-key header the gateway reads; default `x-api-key` |
| `upstreamAuth` | Upstream-auth contract; never paste credentials into source |
| `billOn4xx` | Business-level treatment of client-error responses for billing |
| `operatorPolicies` | Platform operator-policy intent |
| `customerContext` | `contextTokens` and `customerAuth` controls |
| `billing` | Limit-upgrade timing and subscriber-change policy |
Unknown top-level options are rejected. Business identity, display name,
description, icons, concrete backend origins, environment variables, and
release state are platform-owned and do not belong in this call.
## Determinism
Treat the program as a pure declaration graph. Do not read environment
variables, make network requests, inspect the filesystem, use current time, or
generate random values while declaring the contract. The compiler builds twice
and rejects different hashes.
## Change loop
```bash
farthershore build
farthershore validate
git add business
git commit -m "Update business contract"
git push
```
After push, inspect the repository validation/apply check for the same commit
and intended environment. The accepted contract changes only after apply
succeeds. A local build does not bind an origin, deploy your backend, or prove a
subscriber can call the route.
Before handing off, exercise one allowed operation and one denied operation with
a preview subscriber. Record the commit, environment, applied contract, and
observed response. If compilation fails after splitting modules, check that the
entry imports every declaration before sealing; if apply fails, correct the
source and inspect the next apply instead of editing the generated artifact.
---
# Meters & measures
Canonical URL: https://docs.farthershore.com/define/meters
A meter is a **measurement**, never a price. It names the measures your backend
observes (tokens, jobs, rows) and the dimensions those observations are
produced under (model, modality, cache status). Money lives in a
[pricing catalog](/reference/pricing-catalogs) that references the meter; a
[plan](/define/plans) binds the catalog. That separation is what lets one
measurement be rated differently per plan, contract, and release without a
backend change.
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const input = fs.measure("input_tokens");
const output = fs.measure("output_tokens");
const model = fs.dimension("model");
const modelUsage = fs.meter("model_usage", {
measures: [input, output],
dimensions: [model],
});
const api = fs.backend("api", {
transport: { mode: "direct" },
default: true,
});
const chat = fs.route("/v1/chat", {
post: { backend: api, costs: [requests.fixed(1)], reports: [modelUsage] },
});
fs.meterRoutes("chat-model-usage", chat, {
reports: [modelUsage],
maxOutputUnits: output.atMost(8192),
});
fs.plan("free", {
kind: fs.plan.kind.free,
grants: [chat],
limits: [requests.perMinute(60)],
});
export default fs.business();
```
## Declarations
- `fs.measure(key)` — one observed quantity. Its key is what the backend sends
under `values`.
- `fs.dimension(key)` — one selector axis. `dimension.value("text")` mints a
catalog selector; `dimension.is("fast")` is a modifier condition.
- `fs.provider(key)` and `provider.model(key)` — an owned provider namespace and
its models, used as catalog items `(provider, model, modality?)`.
- `fs.meter(key, { measures, dimensions? })` — the meter: a set of measures the
backend reports together, plus the dimensions it may report them under. A
meter may carry several measures (input and output tokens on one report).
- `fs.requests()` — the platform-managed request counter used for structural
bounds (`requests.perMinute(n)`) and gateway-known fixed `costs`. It is
admission policy, not a rated measurement.
Keys are plain strings at the wire boundary: the backend reports
`{ meter: "model_usage", values: { input_tokens: 1200 }, dims: { model: "acme-4" } }`
and the gateway validates every key and value against the served release's
measurement schema. Unknown measures or dimensions are rejected loudly, never
silently dropped.
## Attach a meter to routes
Routes are unattached by default. `fs.meterRoutes(key, route, options)` binds a
meter to a route with an author-supplied stable key — the key survives releases
so agreements and admission bounds can reference it:
```ts
fs.meterRoutes("chat-model-usage", chat, {
reports: [modelUsage],
maxOutputUnits: output.atMost(8192),
});
```
Options:
- `reports` — the meters this route's backend reports.
- `maxOutputUnits: measure.atMost(n)` — a declared bound on an unbounded output
measure. The gateway clamps the request's output knob (`max_tokens` and its
aliases) to the bound before signing it upstream, so billing truncation and
product truncation are the same event. Required on prepaid plans for any
unbounded measure.
- `chunkPolicy: { bound: measure.atMost(n), chunkUnits }` — the alternative to a
hard bound: reserve in chunks of `chunkUnits` up to a cumulative per-operation
ceiling. Exclusive with `maxOutputUnits`.
- `caps: [measure.atMost(n)]` — finite admission bounds for measures the client
cannot declare.
- `postStream: { settlementMax: [measure.atMost(n)] }` — declares that the
authoritative measurement arrives after the response is on the wire, with a
finite settlement maximum per measure (required for prepaid plans).
A route that reports no meter creates no rated usage for that dimension, even
when a plan binds a catalog that could price it. The build reports such
operations.
### Concrete bindings and structural overlays are different
The **target shape** selects the `meterRoutes` API:
| Target | Purpose | Options |
| ------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| One `RouteRef` directly | Commerce binding with a stable key and admission bounds | Nonempty `reports` of meters with measures; optional `maxOutputUnits`, adapter, `caps`, `chunkPolicy`, `postStream` |
| Group, wildcard path, or target array | Structural metering overlay across matching operations | Structural reports/costs/status policy; not commerce admission bounds |
A direct route binding attaches its reports to every operation declared on that
route ref. If only `POST` should report usage or have an output bound, use a
separate route ref for `POST`; do not accidentally bind a combined GET/POST ref.
A one-element array is still an overlay target, not a concrete commerce binding.
On a concrete binding, put fixed `costs` and `onStatusCodes` on the route's
operation; those are not accepted as concrete-binding options. Group/wildcard
overlays do not replace the stable concrete bindings needed for commerce
admission. Wildcards select already declared routes; they do not create routes.
### Bounds must describe the reported measures
- Every `measure.atMost(n)` maximum must be a positive safe integer and refer
to a measure in this binding's reported meters.
- `caps` may name each measure once and cannot duplicate `maxOutputUnits`.
- `chunkPolicy` is mutually exclusive with `maxOutputUnits`. Its bound cannot
duplicate a cap, and `chunkUnits` must be a positive safe integer no greater
than its cumulative maximum. Invalid chunk combinations reject with
`ADMISSION_CHUNK_POLICY_INVALID`.
- `postStream.settlementMax` may name each reported measure once. The SDK's
shape validation is not the whole admission proof: prepaid/x402 compatibility
also depends on the compiler's economic-mode and measurement-timing checks.
- `maxOutputUnitsAdapter` requires `maxOutputUnits`. The supported adapter
names are `knob: "max_output_units"` and
`parser`/`mutator: "json_body_max_output_units_v1"`; this is not an arbitrary
JSON-path mapping API.
The chunk ceiling is a cumulative per-operation maximum, not permission to
produce unlimited output while reserving only the first chunk. Keep request
clamping, actual backend output, reported measurements, and settlement bounds
consistent. See [Monetary admission](/reference/monetary-admission).
## Backend reporting
Every measurement reaches the platform through one verb on the verified
context: `req.fartherShore.report({ meter, values, dims?, quote? })`. Before the
response is sent it rides signed response headers; after (streams, background
jobs) the SDK routes through the post-stream channel automatically. See
[Metering & verification](/backend/metering).
Backends report **measurements, never money**. The single exception is the
bounded quote channel for catalog rules declared `fs.rate.backendQuoted(...)`.
## Structural bounds
`fs.requests()` limits bound the request rate; `fs.resource()` limits bound
persistent inventory; `capacity` bounds one request. None of these price
anything. A rated meter is commercially bounded by its plan's funding and spend
policy; add a structural bound only when use must stop regardless of money.
## Status policy
By default, usage is associated with successful responses. Use
`onStatusCodes` on a route only when the commercial contract intentionally
counts a different explicit set or range.
---
# Counted resources
Canonical URL: https://docs.farthershore.com/define/resources
A resource counts things a subscriber owns. Unlike usage, the count can go down
when an object is deleted.
```ts
import * as fs from "@farthershore/business";
fs.backend("api");
const requests = fs.requests();
const projects = fs.resource("projects", {
cap: fs.scope.subscription,
countSource: "action_inferred",
});
const createProject = fs.route("/v1/projects", {
post: { creates: projects },
});
const deleteProject = fs.route("/v1/projects/{id}", {
delete: { deletes: projects },
});
fs.plan("starter", {
kind: fs.plan.kind.flat,
price: fs.money.usd(9).monthly(),
grants: [createProject, deleteProject],
limits: [requests.perMinute(600), projects.max(3)],
});
export default fs.business();
```
## Scope
Use `fs.scope.subscription` for inventory owned by the subscribing customer.
Use `fs.scope.subject` only when each verified principal has an independent
count. Pick the same owner that your backend uses for the underlying rows.
Subscription is the default scope. Subject scope requires a `subjectType`;
subscription scope rejects `subjectType`. `cap` selects the scope, not the
numeric maximum; the plan's `projects.max(3)` sets that maximum. If both `cap`
and `scope` are supplied, `cap` wins; prefer one spelling.
## Count source
Set `countSource: "action_inferred"` when using `creates` and `deletes`.
The default is `"reported"`, not automatic route inference. These fields
let the platform infer changes from successful route
operations. This is suitable only when one successful call has one unambiguous
effect.
For batch, asynchronous, imported, or out-of-band changes, report the
authoritative count through the resource-count operation instead of pretending
the route effect is exact. Use `countSource: "reported"` for that model;
an absolute count replaces the stored count, rather than adding a delta.
## Enforcement behavior
The gateway checks a create mutation before calling the backend. A subscriber
at its cap is denied. Deletes remain callable so the customer can return below
the cap.
Lowering a limit does not delete customer data. Existing customers may become
over-limit and cannot create more until their count falls or their plan
changes. Treat a lower resource cap as a restrictive plan change and inspect
its subscriber impact before publish.
For action-inferred creates, Core reserves one count before forwarding, using
the subscription's frozen compiled plan and environment. A 2xx result commits
that reservation; a non-2xx result releases it. Successful deletes decrement
the count, floored at zero. Repeated finalization of the same platform mutation
is idempotent; this does not make separate client requests idempotent in your
application. A 202 response is also 2xx, so do not model an asynchronous job as
a completed inventory creation unless that is genuinely its meaning.
The count is authoritative in Core, not an edge-local counter. Reservation and
cap checking share a guarded database write; do not implement a separate
read-count-then-create check and assume it has the same concurrency guarantee.
Counts are keyed by business, environment, subscription, resource and optional
subject. Platform reservation does not atomically commit your external database;
keep reconciliation and application idempotency in your integration tests.
## Correctness rules
- Use `creates` or `deletes` only with an SDK-created resource ref.
- One operation cannot contain both `creates` and `deletes`, even if they name
different resources. These fields describe one resource effect, not a batch
or a transfer between inventories.
- `resource.max(n)` accepts non-negative integers only. Negative, fractional,
infinite and `NaN` values are rejected; zero is a valid authored maximum.
- A resource cap belongs in plan `limits`, not plan `grants`. It does not replace
the complete request-rate limit required by the build, as the example shows.
- Keep the reported count and backend source of truth consistent.
- Make create handlers idempotent because client and gateway retries can
repeat a request.
## Inspect the compiled effect
The SDK lowers `creates`/`deletes` to an action with the resource ID and a
`create`/`delete` effect, then links the route operation to that action. If you
omit an explicit `action` ID, it derives one from the HTTP method and path.
Inspect both the route and action in the built IR; merely declaring a resource
does not attach a mutation to an endpoint. References must come from the same
SDK registry generation and still resolve to a declared resource.
---
# Routes & access groups
Canonical URL: https://docs.farthershore.com/define/routes
`fs.route(path, operations)` declares concrete HTTP operations and returns one
grantable ref for the path.
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const tokens = fs.measure("tokens");
const tokenUsage = fs.meter("token_usage", { measures: [tokens] });
const api = fs.backend("api", { meters: [requests, tokenUsage] });
const chat = fs.route("/v1/chat", {
post: {
backend: api,
costs: [requests.fixed(1)],
reports: [tokenUsage],
timeout: "30s",
requireMember: true,
surfaces: [fs.surfaces.api],
},
});
fs.meterRoutes("chat-tokens", chat, { reports: [tokenUsage] });
fs.plan("pro", {
kind: fs.plan.kind.flat,
price: fs.money.usd(49).monthly(),
grants: [chat],
limits: [requests.perMinute(60)],
});
export default fs.business();
```
## Paths and methods
Paths start with `/` and may use named parameters such as `{id}` or `:id`.
Declare the exact methods that exist: `get`, `post`, `put`, `patch`, `delete`,
`head`, or `options`.
Wildcards do not declare callable routes. Use them only as non-granting
`fs.meterRoutes()` selectors over separately declared operations.
Calling `fs.route()` more than once for the same normalized path merges
different methods. Declaring the same method twice with different options is a
conflict.
## Authentication and subject
Routes require customer authentication by default. `public: true` removes that
requirement and cannot be combined with a member/service subject requirement.
- `requireMember: true` admits only a verified person or personal key.
- `requireService: true` admits only an organization-owned service credential.
- omit both when either verified subject is valid.
Subject requirements are useful when the backend's data model is inherently
per-person or machine-only.
## Callability and visibility
`surfaces` is a callability allowlist. Use typed `fs.surfaces.api` and
`fs.surfaces.ui` values. Omitting the field permits all supported authenticated
surfaces.
An empty list is invalid, not deny-all. Surface sets are deduplicated and
canonically ordered. `public: true` cannot be combined with a surface restriction
because there is no credential to classify; `hidden: true` is still allowed.
A route that does not admit the API surface is omitted from generated API
discovery. Do not confuse hiding a route with denying an otherwise eligible
caller.
`hidden: true` removes an otherwise API-callable operation from generated API
discovery and returns a not-found response to wrong-surface callers. It is a
visibility control, not an authorization replacement.
## Metering and limits
- `costs` records a fixed, gateway-known structural amount (`requests.fixed(1)`).
- `fs.meterRoutes(key, route, { reports, ... })` binds the measurement meters
the backend reports on this route, plus admission bounds (`maxOutputUnits`,
`caps`, `chunkPolicy`, `postStream`).
- `onStatusCodes` narrows which response outcomes count.
- `rateLimit` and `quota` create route-scoped bounds.
Every bounded dimension must be attached to the operation. For the
default requests dimension, combine the bound with a request cost:
```ts
const requests = fs.requests();
fs.route("/v1/ping", {
get: {
costs: [requests.fixed(1)],
rateLimit: "10/min",
},
});
```
## Timeout and retry
`timeout` and `idleTimeout` accept milliseconds or values such as `"500ms"`,
`"30s"`, or `"2m"`, up to ten minutes. `retry: true` expands to the platform's
bounded safe default. Use retries only for operations whose upstream behavior
is idempotent, or supply your own idempotency mechanism.
The exact `retry: true` expansion is two total attempts (one retry), on network
errors and 5xx responses, with `{ base: 100, max: 1000, jitter: 0.2 }` backoff
and `budgetRatio: 0.1`. These are bounds, not a guarantee that every request is
retried. An explicit retry policy may use one to three total attempts; its
`retryOn` must contain `network` and/or `5xx`, backoff base must be nonnegative,
maximum must be at least base, and jitter and budget ratio must be between zero
and one.
Duration numbers are whole milliseconds from 1 to 600,000. Strings use `ms`,
`s`, or `m` and must resolve to whole milliseconds: `"0.5s"` is valid,
`"0.5ms"` is rejected rather than rounded.
### Raw policy and shorthand precedence
The advanced `policy` object is the base; top-level shorthand options override
it. For example, `retry: false` removes a retry configured inside `policy`, and
`public: false` overrides `policy.authMode: "public"`. Do not specify conflicting
forms casually: the compiler resolves them deterministically, not by object-key
order. Equivalent shorthand and canonical policy normalize to the same IR.
## Backend and resource effects
`backend` binds an operation to a logical `fs.backend()` ref. The concrete
origin is environment-owned and configured after deployment.
Every business that declares gateway routes must declare at least one logical
backend. With exactly one backend, it is the implicit default and route entries
may omit `backend`. A backendless business is valid only when it declares no
gateway routes, such as a hosted frontend-only product. This makes an
unroutable route a compile error (`BACKEND_REQUIRED_FOR_ROUTE`) instead of a
product that appears ready but returns `Unknown project` at runtime.
`creates` and `deletes` associate a successful operation with one counted
resource effect. Use reported counts for batch or asynchronous changes.
## Importing OpenAPI
The business program remains authoritative. `farthershore import openapi` is a
local scaffold tool that generates route declarations for review; it does not
publish an OpenAPI document or create a second source of truth. Inspect and
edit the generated refs, policies, grants, and metering before committing.
---
# Route groups & grants
Canonical URL: https://docs.farthershore.com/define/groups
`fs.group(id, members)` creates a reusable bundle of route refs and other group
refs. Plans grant the group directly.
```ts
import * as fs from "@farthershore/business";
fs.backend("api");
const requests = fs.requests();
const listReports = fs.route("/v1/reports", { get: {} });
const createReport = fs.route("/v1/reports", { post: {} });
const reporting = fs.group("reporting", [listReports, createReport]);
fs.plan("pro", {
kind: fs.plan.kind.flat,
price: fs.money.usd(29).monthly(),
grants: [reporting],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
A plain group is authoring-time composition. It does not create a
customer-visible feature, permission namespace, or route. The compiled plan
contains the concrete operation grants reached through the group.
A group declared with the `permission` option is different: its id becomes a
**custom permission subject** that gates its member routes at the gateway.
See [Custom permission subjects](/define/team-rbac#custom-permission-subjects).
## Good uses
- Reuse a stable set of operations across several plans.
- Compose a broad plan from smaller route families.
- Target the same route family with `fs.meterRoutes(key, group, options)`.
## Rules
- Members must be authentic refs from the current compilation.
- Cycles and unused plain groups fail validation (a permission group counts
as used by its permission declaration — it needs no plan grant).
- A group does not grant anything until a plan references it.
- Permission groups may not nest inside each other; plain groups may nest.
- Adding a route to a group changes every plan that grants the group; inspect
the commercial-release diff before activation.
Use separate `fs.route()` declarations when methods have different policies or
economics. A group is not a substitute for method-level route design.
---
# Plans & pricing
Canonical URL: https://docs.farthershore.com/define/plans
`fs.plan(id, options)` declares one sellable plan. Every plan states its
**kind** — `fs.plan.kind.free | flat | usage | prepaid | hybrid | trial | custom`
— and only the economic controls that kind allows. The compiler validates the
controls against the declared kind; a mismatch is a build error
(`PLAN_KIND_CONTROL_MISMATCH`), never a silent reclassification.
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const units = fs.measure("units");
const usage = fs.meter("api_usage", { measures: [units] });
const usagePricing = fs.pricing("api_usage", {
meter: usage,
catalog: [fs.rate.perUnit(fs.money.usd(0.01))],
});
const api = fs.backend("api", {
transport: { mode: "direct" },
default: true,
});
const search = fs.route("/v1/search", {
get: { backend: api, costs: [requests.fixed(1)], reports: [usage] },
});
fs.meterRoutes("search-usage", search, { reports: [usage] });
fs.plan("pro", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(30).monthly(),
usagePricing: usagePricing.current(),
funding: { buckets: [fs.included(fs.money.usd(10))] },
spendPolicy: {
onExhaustion: fs.exhaustion.overage(usagePricing.current()),
},
grants: [search],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
That plan is $30/month, includes $10 of rated usage every period, prices
everything past the allowance at the current `api_usage` catalog, and lets
subscribers keep calling after the allowance is spent (overage). Access
(`grants`) and structural bounds (`limits`) sit beside the economics but are
independent mechanisms — see
[Entitlements vs economics](/concepts/entitlements-vs-economics).
## The five controls
| Control | Type | What it decides |
| -------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `price` | `fs.money.usd(n).monthly()` / `.yearly()` | Recurring economics — the fee a subscription pins when it is created. |
| `usagePricing` | `pricing.current()` / `.withContractTerms()` / `.fixedVersion(n)` | Which [pricing catalog](/reference/pricing-catalogs) rates this plan's measurements, and how it is bound. |
| `funding` | `{ buckets: [fs.included(…), fs.prepaid(…), fs.promo(…), fs.referral(…)] }` | Value that pays for rated usage before anything is owed — see [Funding & allowances](/reference/funding-and-allowances). |
| `lifecycle` | `{ trialDays: n }` | Trial gating; during the trial no obligation accrues. |
| `spendPolicy` | `{ onExhaustion, disclosure?, rail? }` | What happens when funding is exhausted, and whether surfaces disclose rates. |
Every value is an SDK constructor or ref: `fs.plan.kind.*`, `fs.exhaustion.block`
/ `fs.exhaustion.overage(binding)`, `fs.disclosure.opaque | transparent`,
`fs.display.multiplier({ factor })`, `fs.rail.x402`. Hand-written objects and
bare strings are rejected at build time.
## Kinds and their controls
| Kind | Requires | Allows | Typical shape |
| --------- | ----------------------------------------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------- |
| `free` | nothing | nothing economic | `fs.plan("free", { kind: fs.plan.kind.free })` — bound it with `limits`. |
| `flat` | `price` | `price` | A $30/month subscription with no metered charges. |
| `usage` | `usagePricing` | `spendPolicy` only for `rail.x402` | Postpaid pay-as-you-go against a catalog. |
| `prepaid` | `usagePricing`, `funding` (only `fs.prepaid` buckets), `spendPolicy` with `fs.exhaustion.block` | those three | A wallet: usage draws down purchased value and stops at zero. |
| `hybrid` | `price`, `usagePricing`, `funding` (≥1 bucket), `spendPolicy` with `fs.exhaustion.overage(...)` | those four | Subscription plus an included allowance plus overage. |
| `trial` | `price`, `lifecycle` | `usagePricing` | A trial that converts to the recurring price; usage pricing rates post-trial use. |
| `custom` | `usagePricing`, `funding`, `spendPolicy` (`block` or `overage`) | all five | `fs.plan.kind.custom` — bespoke shapes that still select exactly one economic mode. |
The overage binding on a `hybrid` plan must name the same pricing family as
`usagePricing`; the compiler rejects a mismatch.
## Archetypes
The fragments below illustrate economic controls, not complete buildable
programs. Declare their referenced pricing first and add route grants and a
rate-limit rule to every plan, then seal the program once. The complete example
above shows those required pieces together. A plan that bills usage must bound
it somehow — a `limits` rule, `maxMonthlySpendCents`, or
`spendPolicy: { onExhaustion: fs.exhaustion.block }` — or the build fails with
`PLAN_UNBOUNDED_SPEND`.
```ts
fs.plan("free", { kind: fs.plan.kind.free });
fs.plan("flat", {
kind: fs.plan.kind.flat,
price: fs.money.usd(30).monthly(),
});
fs.plan("usage", {
kind: fs.plan.kind.usage,
usagePricing: usagePricing.current(),
});
fs.plan("prepaid", {
kind: fs.plan.kind.prepaid,
usagePricing: usagePricing.current(),
funding: { buckets: [fs.prepaid(fs.money.usd(25), { topUp: true })] },
spendPolicy: { onExhaustion: fs.exhaustion.block },
});
fs.plan("trial", {
kind: fs.plan.kind.trial,
price: fs.money.usd(30).monthly(),
lifecycle: { trialDays: 14 },
});
```
An opaque-allowance plan (the "5x / 20x" shape) keeps rates hidden and shows
subscribers only allowance remaining:
```ts
fs.plan("max-5x", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(100).monthly(),
usagePricing: usagePricing.current(),
funding: {
buckets: [
fs.included(fs.money.usd(25), {
display: fs.display.multiplier({ factor: 5 }),
}),
],
},
spendPolicy: {
disclosure: fs.disclosure.opaque,
onExhaustion: fs.exhaustion.overage(usagePricing.current()),
},
});
```
The multiplier's base is always `amount / factor` ($5 here) — an inconsistent
display cannot be authored. Two plans on the same catalog with $25 and $100
allowances are exactly 5x and 20x of the same $5 base, so the marketing label
is honest by construction. See the
[enterprise LLM archetype](/cookbook/ai-token-metering) for multi-measure,
multi-dimension pricing.
## Price values
Create prices with the SDK so money is represented exactly:
```ts
fs.money.usd(29).monthly();
fs.money.usd(290).yearly();
```
Amounts passed to `fs.money.usd()` are human major units with at most two
decimal places. Sub-cent per-unit rates are authored on the catalog with
`fs.rate.per(1000, fs.money.usd(5))` (half a cent per unit) or
`fs.rate.perMillion(...)`, never as fractional dollars on the plan.
## Grants
Plans accept route refs, group refs, and frontend-integration refs under
`grants`. Grants are access declarations only; monetary values are rejected. A
declaration is not customer-accessible merely because it exists — access comes
from the subscriber's compiled plan grants, except for explicitly public
routes.
## Limits and capacity
Every plan needs at least one complete rate-limit rule at build time, including
paid plans. A resource count cap alone does not satisfy that requirement.
Plan `limits` accept typed structural bounds:
```ts
limits: [requests.perMinute(600), projects.max(25)];
```
`fs.requests()` provides per-window request bounds; `fs.resource()` provides
inventory caps. `capacity` bounds one request (input tokens or payload bytes).
Structural bounds are admission policy; they never change what a measurement
costs. Prepaid and hybrid plans are additionally bounded by their funding: the
gateway reserves each request's economic maximum against the subscriber's
buckets before forwarding it (see
[Monetary admission](/reference/monetary-admission)).
### Window strategies and compiler constraints
Temporal limits default to `fixed_window`. The SDK also accepts
`sliding_window` and, for sub-day limits, `token_bucket`. Choose the algorithm
explicitly when burst behavior matters; do not confuse a billing allowance with
a request-rate limit.
| Authored rule | Compiler behavior or rejection |
| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Window shorter than 86,400 seconds | Emits a rate-limit constraint |
| Window of 86,400 seconds or longer | Emits a quota constraint, including custom windows |
| Quota-length window with `token_bucket` | Rejected; use fixed/sliding window or a sub-day token bucket |
| Token bucket without both `bucketCapacity` and `refillRatePerSecond` | Rejected by the SDK |
| Token bucket burst capacity greater than the limit capacity | Rejected; the burst must not widen the authored limit |
| Bucket fields on a non-token-bucket rule | Rejected by the SDK |
| `MAX`, `LATEST`, or `UNIQUE_COUNT` with non-fixed accounting | Rejected for nonzero capacity; these aggregations are not additive over a rolling horizon |
Bucket capacity and refill rate must be positive finite numbers. Zero limit
capacity is a deny-all policy, not an unlimited sentinel; Core normalizes its
algorithm to fixed-window accounting. This does not excuse invalid bucket fields
or a token bucket on a quota-length window.
The SDK's named day/week/month durations are 86,400 / 604,800 / 2,592,000
seconds. A named month is a 30-day duration; do not assume it means a calendar
month or the subscription's billing period. Keep structural windows and billing
cadence distinct when explaining the product to subscribers.
## Availability and retirement
`selfServeEnabled: false` removes a plan from customer self-service while
preserving existing assignments. `archive` can schedule retirement and point at
a successor plan ref.
Do not delete a plan declaration while subscribers still depend on it. Existing
subscriptions stay pinned to the release they bought; see
[Commercial releases](/reference/commercial-releases).
## Product bounds
The active contract supports at most six plans. Keep the plan set legible;
prefer clear plans over a matrix of tiny variations.
## Before releasing a change
```bash
farthershore build
farthershore commercial-release diff
git push
farthershore apply-timeline inspect \
--env production \
--format json
```
A published release affects **new** subscriptions. Existing subscriptions keep
their recurring-price pin; subscribers bound to `pricing.current()` pick up the
usage rates of each newly activated release, forward only — "current" means the
catalog version in the release being served, not a rate that moves before you
publish, and work already admitted is never rerated. For an active
repository-managed
business, production publication happens through a GitHub Release for the
reviewed commit. `farthershore business publish` is only for first activation
while the business is still `DRAFT`.
---
# The build output
Canonical URL: https://docs.farthershore.com/define/build-output
`farthershore build` loads the `business/` program, validates the declaration
graph, and writes a canonical Manifest IR envelope.
```bash
farthershore build
farthershore build --format json
farthershore build --entry business/ --out business-build.json
```
Normal folder discovery is preferred. Use `--entry` only when deliberately
building another folder or file.
## What is in the artifact
Manifest IR contains the normalized business contract: logical backends,
routes and policies, meter attachments, plans, grants, limits, resources,
frontend integrations, and permission-group declarations. The business-wide
RBAC enablement flag is operating state. The artifact also carries
SDK and IR versions plus a deterministic hash.
It does not contain arbitrary repository code, concrete backend origins,
decrypted secrets, customer rows, active deployment state, or presentation
metadata.
## Authenticity and validation
Only an authentic `fs.business()` result from the loaded SDK instance compiles.
Raw objects and schema-shaped JSON cannot impersonate a built program.
The build executes TypeScript through the SDK loader; it is **not a TypeScript
type checker**. Run the business repository's separate typecheck command before
the build (for example, its configured `tsc --noEmit` script). Neither gate
replaces the other: a type-correct program can still violate runtime invariants,
and an ill-typed program can execute successfully after TypeScript is transpiled.
The build validates runtime invariants: duplicate declarations,
dangling refs, unmatched metering selectors, invalid plan combinations,
unattached limited dimensions, conflicting route methods, and secret-bearing
contract values.
## Determinism gate
The loader compiles the program in isolated workers and compares hashes. An
observed difference is rejected. Matching hashes are not proof of source purity:
an environment variable, clock value, file, or network response could happen to
return the same value in both runs. Do not depend on those external inputs in
contract declarations, even if a particular build passes.
Computed constants and helper functions are fine when their result is purely a
function of source-controlled inputs.
## Local build vs accepted contract
A successful local build proves only that your checkout compiles. Push the
source and wait for the repository validation/apply check. Core accepts the IR,
computes the semantic release impact, and applies environment-specific state.
```bash
git push
farthershore apply-timeline list
farthershore business contract
```
If the pushed apply fails, the previous accepted contract remains authoritative.
Fix the source and push another commit rather than editing generated IR.
Do not commit build artifacts unless the business repository's own instructions
explicitly require them. The TypeScript program is the reviewable source.
---
# Team RBAC
Canonical URL: https://docs.farthershore.com/define/team-rbac
Managed RBAC adds a route-permission check for credentials inside a subscriber
organization, including organization API keys, not only member credentials. RBAC
enablement is platform-owned: turn it on once for the business in the dashboard
(the **Access control (RBAC)** card in business settings) or from the CLI, and
declare routes normally:
```bash
farthershore business rbac enable # or: disable
farthershore business rbac # read the current setting
```
Effective enforcement requires **both** the business flag and each subscriber
organization's own RBAC setting to be enabled. If either flag is off,
credential permission resolution can return `['*']`, even when a key retains
restrictive bindings. Do not use a disabled flag to test least-privilege
access.
The subscriber flag has two owners. The subscribing organization turns it on
from its portal's **Settings → Team** page (`/settings/team`, linked in the
portal nav). You can also turn it on — and read or repair its roles — from the
builder plane:
```bash
farthershore consumer list # find the subscriber id
farthershore consumer rbac enable \
--default-role reader # or: disable
farthershore consumer rbac roles list
```
The product flag comes first: with `business rbac` off, every per-subscriber
call answers `400 RBAC_NOT_ENABLED_BY_PRODUCT`. If the subscriber organization
has mandatory change control on, a direct enable/disable answers
`409 GOVERNED_BY_CHANGE_SET` and the change must go through its governed
ChangeSet instead.
The business flag applies to every environment and takes effect
at the edge automatically, with no republish of the business. Disabling removes
the managed member-role restriction across all environments; members still face
plan, subject, and other gateway checks. Role configuration is preserved for
re-enable. Treat disabling as an access expansion, not a routine repair for a
single denied member.
```ts
import * as fs from "@farthershore/business";
fs.backend("api");
const reports = fs.route("/v1/reports", {
get: {},
post: {},
});
const requests = fs.requests();
fs.plan("team", {
kind: fs.plan.kind.flat,
price: fs.money.usd(49).monthly(),
grants: [reports],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
The platform derives the grantable permission catalog from compiled route
operations. Customer organization owners configure roles and assign members at
runtime; those assignments are not business contract source.
## Role ownership and defaults
The Business SDK and compiler produce a raw permission catalog. They never
produce customer roles. Enabling RBAC also creates no roles and chooses no
default. Each subscribing organization uses its customer access-control
surface to:
1. create one or more `CUSTOM` product roles from the current catalog;
2. choose a default role, or deliberately leave the default unset;
3. assign roles and optional direct grants to its members and credentials.
As the builder you can drive the same rows for support — `farthershore
consumer rbac roles create|update|delete` and `farthershore consumer rbac
assign` — over one shared service layer, so a change made from either plane is
the same change. `--permissions` is set-replace: the list you pass becomes the
role's whole grant. The reserved key `owner` can never be created, edited, or
deleted (`409 OWNER_ROLE_IMMUTABLE`): owners always hold every permission.
A permission outside the derived catalog is rejected with
`400 UNKNOWN_PERMISSION`.
With no explicit role and no configured default, a nonowner receives no product
permissions. There is no platform `Member` or `Admin` product-role template to
fall back to. This makes the subscriber organization's choices authoritative
and prevents a newly declared SDK permission from silently entering an existing
role.
Account roles (`owner`, `admin`, and `member` — the canonical lowercase
vocabulary every API, CLI, and MCP surface accepts; `OWNER`/`ADMIN`/`VIEWER`
are the stored values, with `VIEWER` shown as `member`) are a separate
permission plane
for subscriber-workspace administration. The subscribing organization governs
who holds those account roles. Product-role bindings resolve `CUSTOM` roles
only; passing an account-role key as a product role is rejected. The account
owner remains the explicit full-access authority, so owner success never proves
that a nonowner product role is correct.
Product roles are scoped to one subscribing organization within one managed
business. They are not authored by the builder and are not separate catalogs
per deployment environment; an ephemeral environment selects test identities
and entitlements, while the subscriber's role definitions remain subscriber
governance state.
## Two independent checks
Plan access and credential permission both have to pass:
1. the subscriber's plan must grant the route;
2. the calling credential's effective permissions must allow the route operation.
RBAC cannot widen a plan. A role that names a route absent from the subscriber's
plan does not make it callable.
## Subject choice
RBAC also constrains organization keys. Use `requireMember: true` when an
operation specifically needs member identity: permission to call a route is not
proof that the credential represents a person. Plan entitlement, member identity,
and permission are separate checks. Backend tenant and record ownership checks
remain necessary even after all gateway checks pass.
## Effective grants and key types
With both RBAC flags enabled:
| Caller or binding | Effective product permissions |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Organization owner | `*`; owner success does not prove a nonowner role works |
| Member with explicit product roles | Union of those roles plus direct grants |
| Member with no explicit role keys | Configured default role, if any, plus direct grants |
| Member with only stale/deleted explicit role keys | No fallback to default; only any direct grants remain |
| Ordinary API key with no roles and no restrictions | `*`, not deny-all |
| Ordinary API key with role bindings | Union of the current CUSTOM role grants; stale keys contribute nothing |
| API key with nonempty restrictions | Wildcard-aware intersection of role grants and restrictions; without roles, restrictions narrow full access |
| SERVICE credential | Frozen service-account grant snapshot, not a live role binding; an empty snapshot denies all |
An empty ordinary-key restriction list means no additional restriction, not
deny-all. Role unions are additive: a narrower second role cannot subtract access
granted by another role. Narrow the granting role or credential restriction.
Role edits republish live-bound key claims and member authorization overlays.
This propagation is asynchronous; test existing credentials after the change has
reached the edge, not just newly issued credentials. SERVICE snapshots have
different semantics and must not be assumed to track later role edits.
## Custom permission subjects
Route-derived and managed permissions cover API calls and platform surfaces.
For your product's own domain vocabulary (for example a `reports` subject with
`generate` and `purge` verbs), declare a **permission group** in the business
program — a route group that carries a `permission` block:
```ts
import * as fs from "@farthershore/business";
fs.backend("api");
const list = fs.route("/v1/reports", { get: {} });
const generate = fs.route("/v1/reports/generate", {
post: { permission: "generate" },
});
const requests = fs.requests();
fs.group("reports", [list, generate], {
permission: {
verbs: ["generate", "purge"], // extra domain verbs beyond read/write
escalatory: ["purge"], // excluded from write-class expansion
description: "Report management",
},
});
fs.plan("team", {
kind: fs.plan.kind.flat,
price: fs.money.usd(49).monthly(),
grants: [list, generate],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
The group id becomes the permission subject. Member routes are gated at the
gateway by `:read` / `:write` (derived from the HTTP
method), replacing their per-route subjects; metering and grant identity are
untouched. An operation may override the derived verb with
`permission: ""` — the gateway then requires `:`
exactly. Custom permissions are grant-by-exact-name only: `:*` is
never grantable for a custom subject, and escalatory verbs never enter any
write-class expansion.
`GET`, `HEAD`, and `OPTIONS` derive `read`; other methods derive `write`.
At runtime `reports:write` does **not** imply `reports:read` or
`reports:generate`. Give a reader/writer both exact read and write grants.
An operation override must name an extra verb declared by its containing
permission group; `permission: "read"` and `permission: "write"` are invalid
overrides. Omit them to use the method-derived default.
The rules: `read` and `write` may not be re-declared as extra verbs (the
group's route gating implies them); a route belongs to at most one permission
group; permission groups may not nest inside each other (plain groups may
nest); names and verbs are lowercase snake (2–32 chars, starting with a
letter) and the subject may not collide with a managed or platform subject,
`custom`, or any route id. A permission group needs no plan grant to be
declared — but as always, RBAC cannot widen a plan.
The platform syncs this business-wide vocabulary on **every apply, including
environment-scoped preview applies**. A preview permission change is not isolated
from production vocabulary. Deleting a permission group strips its grants from
every role and API key and republishes edge claims; shrinking the verb list
strips the removed strings the same way. In the grantable catalog the group's
read/write pair appears with the route-derived permissions and extra verbs
appear in the `custom` group. No existing or future subscriber role receives
them automatically; a subscriber owner or admin grants every permission by
explicit role composition or direct assignment.
Treat group deletion, rename, verb removal, and moving an existing route into a
permission group as permission migrations. Grouping replaces the route's former
permission subject, so old per-route grants no longer satisfy the new subject.
Inspect affected roles, keys, and UI gates across environments before applying;
reintroducing a deleted subject does not reconstruct grants that were removed.
The listing surfaces are read-only: the dashboard shows the synced subjects,
and so does the CLI —
```bash
farthershore business rbac subjects # read-only list of synced subjects
```
The API mutation endpoints (`PUT`/`DELETE /businesses/:id/rbac/subjects/*`)
are gone; the business program is the only author.
To gate your portal UI on a custom subject — and let subscriber orgs
configure that gate like the managed components — register a
`custom:` component id: see
[Custom components](/frontend/custom-components).
The strings flow end to end: subscriber roles grant them, they ride the
signed context claim, and the gateway enforces them at the boundary. Because
the extra verbs ride the claim, your backend may additionally check them with
the same `requirePermission(ctx, "reports:generate")` it uses for any other
permission — defense in depth on top of the gateway, never a substitute for
it.
Use `hasPermission` or `requirePermission` only on a verified backend request
context. Missing permission claims deny in these carrier helpers. Do not use the
lower-level `permissionSatisfies` predicate as a request authorization boundary:
its raw missing-claim behavior differs. Require concrete permission keys;
requiring `reports:*` is not a way to require every reports verb.
## Component access panel
`FsComponentAccessPanel` (from the frontend SDK's component kit) is the
org-admin surface for per-subscriber **component gate policies**. It lists
every managed SDK component plus any `custom:` component with an
existing policy, and lets an admin override each component's required
permission (picker fed by the same derived permission catalog the role editor
uses) and its gate mode (`hide`, `disable`, `readOnly`, or `denied`).
The panel self-gates on the subscriber `team:manage_rbac` permission
(overridable via the `canManage` prop) — non-managers render nothing. These
policies drive the SDK's client-side component gating only; the gateway's
permission check remains the security boundary.
## Operate customer roles
The business program owns which routes exist; whether managed RBAC is enabled
is a platform-owned business setting (dashboard, CLI, or
`PUT /businesses/:id/rbac`), and the per-subscriber enforcement flag is
operational state you can also drive (`farthershore consumer rbac
enable|disable`, `PUT /businesses/:id/consumers/:subscriberId/rbac`). Customer
team membership, role definitions, and assignments are platform-owned
operational state. See
[Customer operations](/operate/customer-operations).
Before removing or renaming a route, inspect its active dependents so an
existing role or frontend permission gate is not silently stranded.
---
# Tenancy & identity
Canonical URL: https://docs.farthershore.com/define/tenancy
Every customer tenancy is a subscriber organization, including a solo user.
Plans, subscriptions, billing, entitlements, and business-level limits attach
to that organization.
Inside it, requests carry a verified principal:
- a member principal identifies a person;
- a service principal identifies an organization-owned machine credential.
Do not trust customer-supplied organization or user ids in headers or request
bodies. Verify the Farther Shore runtime context in the backend SDK and derive
ownership from the signed principal.
## Shared-workspace data
Key shared rows by the verified subscriber/organization identifier. Every query
must include that key, even when another id appears globally unique.
```ts
// ctx comes from fs.handler() after fs.middleware() verifies the request.
const projects = await db.project.findMany({
where: { orgId: ctx.principal.org.id },
});
```
This keeps billing, entitlements, and the backend's data boundary aligned.
## Per-member data
For private rows inside the organization, include both boundaries:
```ts
import { requireMember } from "@farthershore/backend";
const member = requireMember(ctx);
const documents = await db.document.findMany({
where: {
orgId: ctx.principal.org.id,
ownerMemberId: member.memberId,
},
});
```
Require a member subject on routes that need `memberId`:
```ts
fs.route("/v1/me/documents", {
get: { requireMember: true },
});
```
## Create user state on demand
Identity and webhook delivery can race with the first API request. Do not make a
webhook the prerequisite for a user row. Use a database unique key on the
verified identity and an atomic upsert/find-or-create path on first use.
See [Storing per-user data](/backend/user-data) for the transaction and retry
pattern.
## Collaboration
Sharing a row is a domain authorization rule in your backend. Store an explicit
membership or ACL keyed by verified member ids and scope it to the subscriber.
Managed RBAC answers whether a member may call an operation; it does not decide
which individual document that operation may return.
## Avoid boundary confusion
- Builder organizations own Farther Shore businesses; subscriber organizations
buy and use one business. They are different domains.
- A plan grant is subscriber-wide; managed RBAC narrows member operations.
- A service credential is not a person and has no member id.
- Environment ids route preview data and configuration; they are not customer
tenancy ids.
If production and previews share a database, isolate their rows or databases
explicitly as well. Organization scoping alone is not an environment isolation
strategy. Test with two organizations and two members: changing a body, URL, or
query parameter to another owner's id must never widen the verified scope.
---
# Frontend integrations
Canonical URL: https://docs.farthershore.com/define/frontend-integrations
`fs.frontendIntegration()` declares a narrow edge proxy for a browser-facing
third-party call. Use it when the operation is small and declarative; use your
own backend for domain workflows, arbitrary proxying, or durable processing.
```ts
import * as fs from "@farthershore/business";
fs.backend("api");
const requests = fs.requests();
const account = fs.route("/v1/account", { get: {} });
const invitations = fs.frontendIntegration("invitations", {
upstream: "https://api.example.com",
request: {
operations: [
{
method: "POST",
path: "/v1/invitations",
headers: ["x-api-version"],
body: { kind: "json", maxBytes: 16_384 },
},
],
},
injection: {
secretRef: "INVITATIONS_API_KEY",
location: "header",
name: "authorization",
template: "Bearer {value}",
},
response: {
kind: "json",
contentTypes: ["application/json"],
maxBytes: 65_536,
jsonPointers: ["/id", "/status"],
responseHeaders: ["x-request-id"],
},
});
fs.plan("pro", {
kind: fs.plan.kind.flat,
price: fs.money.usd(49).monthly(),
grants: [account, invitations],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
Every request and response field is an allowlist. The integration above can
call one method/path, accept one browser-provided header and a bounded JSON
body, and return only two JSON pointers plus one safe response header.
## Secret names, not secret values
`secretRef` is an uppercase variable name. The value is platform-owned,
encrypted operational state and is delivered only at runtime.
```bash
printf '%s' "$INVITATIONS_API_KEY" | \
farthershore variables set INVITATIONS_API_KEY --idempotency-key
```
The contract cannot contain the secret value. The upstream must be a public,
bare HTTPS origin; userinfo, private hosts, arbitrary ports, query strings, and
fragments are rejected. Browser-controlled headers cannot overlap the injected
credential or unsafe hop-by-hop headers.
A granted integration can apply in an environment only once the secret it
references has been published to the edge for that environment. A public
(`FS_PUBLIC_`) variable is baked into the bundle, not injected at the edge, so
it is never an integration's injection secret.
## Plan access
An integration is reachable only when the caller's compiled plan grants its
ref. Declaring it alone is inert. Like route grants, the compiled integration
policy is versioned with the subscriber cohort, so narrowing an existing
operation is a contract change that must be previewed.
## No subscriber economics
Frontend integrations are platform operations, not customer-billable route
usage. Their type rejects route economics and resource effects such as `costs`,
`reports`, `usagePolicy`, `creates`, and `deletes`. If a third-party call should
consume a subscriber meter, put the workflow behind a declared backend route
instead.
## Response projection
The edge buffers a bounded response, validates its content type, and constructs
a new JSON result from the allowed pointers. It does not blindly proxy the
upstream body or headers. Missing pointers are omitted. Oversized or
wrong-content-type responses fail closed.
This boundary prevents a provider from unexpectedly widening the data exposed
to browser code. Keep projections minimal and test upstream error responses as
well as success responses.
## Browser use
The frontend SDK calls the integration through the business origin. Browser
code never receives the upstream credential, injection rule, or decrypted
secret. See [Variables](/frontend/variables) for the frontend call and error
handling surface.
---
# @farthershore/business
Canonical URL: https://docs.farthershore.com/reference/business-sdk
`@farthershore/business` is the Business-as-Code SDK. This guide explains the
3.2 authoring model; the [generated export reference](/generated/business-sdk/root)
contains the complete signatures and types. Use a single namespace
import and declaration functions that return immutable branded refs:
```ts
import * as fs from "@farthershore/business";
fs.backend("api");
const requests = fs.requests();
const tokens = fs.measure("tokens");
const tokenUsage = fs.meter("token_usage", { measures: [tokens] });
const tokenPricing = fs.pricing("token_usage", {
meter: tokenUsage,
catalog: [fs.rate.perMillion(fs.money.usd(2))],
});
const seats = fs.resource("seats", { cap: fs.scope.subscription });
const chat = fs.route("/v1/chat", {
post: { costs: [requests.fixed(1)], reports: [tokenUsage] },
});
const admin = fs.route("/v1/admin", {
post: { surfaces: [fs.surfaces.api] },
});
fs.meterRoutes("chat-tokens", chat, { reports: [tokenUsage] });
fs.plan("pro", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(49).monthly(),
usagePricing: tokenPricing.current(),
funding: { buckets: [fs.included(fs.money.usd(10))] },
spendPolicy: {
onExhaustion: fs.exhaustion.overage(tokenPricing.current()),
},
grants: [chat, admin],
limits: [requests.perMinute(60), seats.max(25)],
});
export default fs.business();
```
## Rules
- Plans grant route refs directly. Use `fs.group()` to reuse a route bundle.
- Cross-references use refs, never declaration-name strings.
- Every plan declares `kind: fs.plan.kind.*`; the compiler validates the five
economic controls (`price`, `usagePricing`, `funding`, `lifecycle`,
`spendPolicy`) against it.
- Money constructors accept human major units and lower to exact minor units;
catalog rates lower to exact rationals.
- Platform vocabulary is grouped under `fs.surfaces`, `fs.scope`, `fs.money`,
`fs.rate`, `fs.modifier`, `fs.plan.kind`, `fs.exhaustion`, `fs.disclosure`,
`fs.display`, and `fs.rail`.
- Managed RBAC enablement is platform-owned — toggle it in the dashboard or
with `farthershore business rbac enable`, not in code.
- The SDK declares the raw product-permission vocabulary through route
operations and permission-bearing groups. Subscribing organizations compose
that vocabulary into their own roles, defaults, assignments, and direct
grants at runtime; no customer role is authored in `business/` or seeded by
RBAC enablement.
- Exactly one module under `business/` default-exports `fs.business()`.
The compiler imports business modules in canonical order inside a fresh,
compile-scoped worker. Ordinary TypeScript values and extra exports are inert;
only registered SDK declarations affect Manifest IR. An import that throws is
reported with its source file.
## Routes and metering families
Routes follow the OpenAPI Path Item model: a concrete or `{parameter}` path
lists the HTTP operations that exist on that path. Wildcards do not declare
routes. `fs.meterRoutes(key, route, options)` binds a measurement meter to a
declared route under an author-supplied stable key.
```ts
const tokens = fs.measure("tokens");
const tokenUsage = fs.meter("token_usage", { measures: [tokens] });
const tokenPricing = fs.pricing("token_usage", {
meter: tokenUsage,
catalog: [fs.rate.perMillion(fs.money.usd(2))],
});
const createChat = fs.route("/v1/chat", {
post: { surfaces: [fs.surfaces.api] },
});
const getChat = fs.route("/v1/chat/{id}", {
get: { surfaces: [fs.surfaces.api, fs.surfaces.ui] },
});
const createEmbedding = fs.route("/v1/embeddings", {
post: { surfaces: [fs.surfaces.api] },
});
fs.meterRoutes("chat-tokens", createChat, {
reports: [tokenUsage],
maxOutputUnits: tokens.atMost(8192),
});
fs.meterRoutes("embedding-tokens", createEmbedding, { reports: [tokenUsage] });
fs.plan("pro", {
kind: fs.plan.kind.usage,
usagePricing: tokenPricing.current(),
grants: [createChat, getChat, createEmbedding],
});
```
The binding key (`"chat-tokens"`) is what economic agreements and admission
bounds reference; keep it stable across releases. `maxOutputUnits`,
`chunkPolicy`, `caps`, and `postStream` declare the admission bounds described
in [Meters and measures](/define/meters). A route with no binding reports no
rated usage.
## Declarations
| Function | Purpose |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `fs.requests()` | Platform request counter for structural bounds and gateway-known fixed costs. |
| `fs.measure(key)` | One observed quantity a backend reports. |
| `fs.dimension(key, options?)` | A selector axis; `.value(v)` mints a catalog selector, `.is(v)` a modifier condition. |
| `fs.provider(key)` | An owned provider namespace; `.model(key)` mints a catalog item. |
| `fs.meter(key, { measures, dimensions? })` | A measurement meter — what the backend reports together and under which dimensions. |
| `fs.pricing(key, { meter, catalog })` | A versioned catalog of exact rates for one meter; `.current()` / `.withContractTerms()` / `.fixedVersion(n)` bind it. |
| `fs.meterRoutes(key, route, options)` | Bind a meter to a declared route under a stable key, with admission bounds. |
| `fs.route(path, operations)` | Path-first HTTP operations and their policy, surface, action, and backend bindings. |
| `fs.plan(key, options)` | Declared `kind` plus `price`, `usagePricing`, `funding`, `lifecycle`, `spendPolicy`, grants, limits, and archival behavior. |
| `fs.resource(name, options)` | Counted resource; its ref exposes `max(count)` for typed plan limits. |
| `fs.backend(id, options)` | Backend declaration returning a route-bindable ref. |
| `fs.frontendIntegration(id, options)` | Browser-to-provider integration whose credential is held and injected by the platform. |
| `fs.group(key, routes)` | Reusable route bundle. |
| `fs.business(options?)` | Seal the registry; use as the sole default export. |
Value constructors: `fs.money.usd(n).monthly() / .yearly()`, `fs.rate.perUnit /
per / perMillion / rational / graduated / volume / backendQuoted`,
`fs.modifier.multiplier(n, d).when(...)`, `fs.included / prepaid / promo /
referral`, `fs.display.multiplier({ factor })`, `fs.disclosure.opaque |
transparent`, `fs.exhaustion.block | overage(binding)`, `fs.rail.x402`, and
`fs.plan.kind.*`.
The namespace values `fs.surfaces` and `fs.scope` supply typed platform
vocabulary. Use the generated reference for the exhaustive export inventory;
this guide concentrates on the declarations used in the change loop.
Business identity, origin, and presentation are platform-owned operating state.
Frontend page routes and navigation are authored in the editable frontend
application, not in `business.ts`. Route `surfaces` control credential
callability only.
The source program must be deterministic; Farther Shore compiles it twice and
rejects differing hashes.
## Choose an authoring workflow
| Goal | Workflow |
| -------------------------- | -------------------------------------------------------------------------------------------------- |
| Create or split a program | [Business program](/define/business-class), [build output](/define/build-output) |
| Make an operation callable | [Routes](/define/routes), [groups and grants](/define/groups) |
| Sell measured usage | [Meters](/define/meters), [plans](/define/plans), [metered route recipe](/cookbook/metered-routes) |
| Bound stored inventory | [Resources](/define/resources), [resource-limit recipe](/cookbook/add-resource-limit) |
| Model customer access | [Tenancy](/define/tenancy), [team RBAC](/define/team-rbac) |
| Connect code and providers | [Backend](/backend/overview), [frontend integrations](/define/frontend-integrations) |
The [`/codegen` entrypoint](/generated/business-sdk/codegen) is tooling for
source generation. It does not replace the authored `business/` program or
activate a contract. In particular, reconstructing source from structural IR
cannot recover commerce data that is absent from that IR; unsupported shapes
fail rather than inventing prices. Review generated source, run the local
build, and verify apply in the selected environment before testing traffic.
---
# Add metered routes
Canonical URL: https://docs.farthershore.com/cookbook/metered-routes
You want to add a billable dimension to a product — a new measure and meter, a
pricing catalog for it, a `fs.meterRoutes()` binding on the route that reports
it, and a plan that binds the catalog. This is the most common change you'll
make: it pairs the business declaration change with backend reporting and a
preview settlement check.
The running example is CronCloud, the product used throughout these recipes.
It already declares `fs.requests()` (the platform-managed request counter), a
`cron-jobs` route, and `starter`/`pro` plans. Here we add a `compute` measure
in milliseconds, report it from the create route, and bill it as overage on
the Pro plan after a $5 included allowance.
## Outcome
One route reports a new measurement, and the Pro plan prices that measurement
after an included allowance.
## Prerequisites
- A backend using `@farthershore/backend`
- A preview environment and test persona
- A confirmed unit and per-unit price
## Edit the functional business program
Product state lives in the composable `business/` program. It may span modules,
but the folder must contain exactly one default-exported `fs.business()` result. There is no
YAML: edit TypeScript and push.
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const computeMs = fs.measure("compute_ms");
const compute = fs.meter("compute", { measures: [computeMs] });
const computePricing = fs.pricing("compute", {
meter: compute,
catalog: [fs.rate.per(1000, fs.money.usd(0.05))],
});
const api = fs.backend("api", {
transport: { mode: "direct" },
meters: [requests, compute],
default: true,
});
const createJob = fs.route("/v1/cron-jobs", {
post: { backend: api, costs: [requests.fixed(1)], reports: [compute] },
});
fs.meterRoutes("cron-jobs-compute", createJob, {
reports: [compute],
caps: [computeMs.atMost(60_000)],
});
fs.plan("pro", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(49).monthly(),
usagePricing: computePricing.current(),
funding: { buckets: [fs.included(fs.money.usd(5))] },
spendPolicy: {
onExhaustion: fs.exhaustion.overage(computePricing.current()),
},
grants: [createJob],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
`fs.rate.per(1000, fs.money.usd(0.05))` is $0.00005 per millisecond, exactly.
The $5 included bucket covers the first 100,000 ms each period; overage is
rated at the same catalog. `caps` declares a finite per-request bound the
gateway uses when reserving spend.
A meter bound with `fs.meterRoutes()` is a *reported* measurement: the
upstream sends its value per request. `costs: [requests.fixed(1)]` is a
gateway-known structural count used for rate limits. Money never appears on
either — it lives in the pricing catalog.
## Report the value from your backend
A bound meter needs the upstream to send the measured value. Use
[`ctx.report()`](/cookbook/ai-token-metering) from `@farthershore/backend` —
before the response is sent it signs the usage into the platform response path
with no extra network call.
```ts
import { fartherShore } from "@farthershore/backend";
const fs = fartherShore.initFromEnv(); // derives everything from FS_RUNTIME_TOKEN
// Use the scaffold's raw-body capture before fs.middleware(), then JSON parsing.
// The middleware supplies the response adapter for in-band reporting.
app.post(
"/v1/cron-jobs",
fs.handler(async (ctx, req, res) => {
const result = await createCronJob(req.body);
const report = await ctx.report({
meter: "compute",
values: { compute_ms: result.computeMs },
});
if (!report.ok) console.error("Usage delivery failed", report.reason);
res.json(result);
}),
);
```
The platform rates `result.computeMs` from the signed response evidence under
the release the request was admitted with.
For a Fetch handler, use the [response-sink example](/reference/backend-sdk).
Calling `verifyRequest()` without that adapter chooses post-stream delivery;
returning a `Response` does not attach reporting headers automatically.
## Build, push, verify
```bash
# Validate the manifest locally to confirm the edit is valid.
farthershore build --format json
```
`build` runs the same deterministic validation the platform does. Push
`business/**` and the GitHub bot validates and applies it. Wait until `env list`
contains the target preview before creating or binding its matching `api` row;
if automatic branch-prefix creation did not occur, create the preview explicitly
first. Then confirm the new meter shows up on real traffic:
```bash
farthershore env list croncloud --format json
farthershore backend create croncloud --env \
--name "Preview API" --slug api --transport direct \
--idempotency-key \
--origin-url https://preview-api.example.com --default --format json
farthershore backend list croncloud --format json
# After traffic flows, the new dimension appears in the usage summary.
farthershore usage summary croncloud --format json
```
Filter the structured backend list by the preview's environment id.
## Verify it works
- `farthershore build` succeeds and the validated business lists a `compute`
meter with a `compute_ms` measure.
- A `POST /v1/cron-jobs` call is allowed and `compute_ms` rises by the reported
value.
- `requests` increments by 1 because the route explicitly attaches its fixed
cost.
- The Pro subscriber's bill preview shows the $5 included allowance draining,
then `receivableNanos` growing at exactly $0.00005 per millisecond.
## Common failures
- Usage never appears: the backend's meter and measure keys must exactly
match the declared keys.
- Usage is doubled: do not report the same observation twice. Batch distinct
measures once, inspect the result, and keep one transport per served request.
## Recover
Stop test traffic and revert the meter, route binding, and plan price together in
preview. If incorrect usage reached billing, diagnose it before replaying or
adjusting any event.
## Agent prompt
> Add a `compute` meter with a `compute_ms` measure to the existing create
> route, price it in a catalog, bind it on Pro as a hybrid plan with a $5
> included allowance and overage, and report actual usage through
> `ctx.report()`. Build and verify one preview request and its usage delta. Do
> not publish or change live billing.
## Related
- [Meter AI tokens](/cookbook/ai-token-metering) — the same `meterRoutes` + `report()` loop for LLM tokens.
- [Prepaid wallet](/cookbook/prepaid-credits) — meter a dimension down against a balance instead of overage.
- [Gate API routes by plan](/cookbook/grant-routes) — grant the route only to the intended plans.
---
# Add a resource limit
Canonical URL: https://docs.farthershore.com/cookbook/add-resource-limit
## Outcome
Each plan caps a persistent object such as projects or jobs. Use this for counts, not request rates.
## Prerequisites
- Existing create and delete routes
- A stable resource key
## Define the resource
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const api = fs.backend("api", {
transport: { mode: "direct" },
meters: [requests],
default: true,
});
const projects = fs.resource("projects", {
display: "Projects",
countSource: "action_inferred",
cap: fs.scope.subscription,
});
const projectRoutes = fs.route("/v1/projects", {
get: { backend: api, costs: [requests.fixed(1)] },
post: { backend: api, creates: projects, costs: [requests.fixed(1)] },
});
const deleteProject = fs.route("/v1/projects/{id}", {
delete: { backend: api, deletes: projects, costs: [requests.fixed(1)] },
});
fs.plan("starter", {
kind: fs.plan.kind.free,
grants: [projectRoutes, deleteProject],
limits: [requests.perMinute(600), projects.max(5)],
});
```
Run `farthershore build --format json`, then push to an `env/*` branch. Wait
until `env list` contains the preview environment before creating or binding
the same logical `api` backend slug there. If automatic branch-prefix creation
did not occur, create the preview explicitly first:
```bash
farthershore env list --format json
farthershore backend create --env \
--name "Preview API" --slug api --transport direct \
--idempotency-key \
--origin-url https://preview-api.example.com --default --format json
farthershore backend list --format json
```
Filter the structured backend list by the preview's environment id.
## Verify
Create five projects with a preview subscriber. The sixth create must be denied; deleting one must make one slot available.
## Common failures
- Counts never change: the create/delete operation is missing `creates` or
`deletes`, or the backend did not complete successfully.
- Deletes consume capacity: use `deletes: projects` on the delete operation.
- Build reports an unknown resource: reuse the branded `projects` ref in route
effects and `projects.max(5)`; do not substitute matching strings.
## Recover
Revert the new resource, action bindings, and cap together. Do not remove or reduce a live limit without reviewing subscriber impact.
## Next steps
Read [limits](/operate/limits) and [Business SDK reference](/reference/business-sdk).
## Agent prompt
> Add an action-inferred `projects` resource limit to the existing Farther Shore business. Bind create and delete actions, cap the relevant plans, run the local build, and show how to verify the boundary in preview. Do not publish.
---
# Add team RBAC
Canonical URL: https://docs.farthershore.com/cookbook/add-team-rbac
## Outcome
Organization members receive roles, and the edge enforces route read/write
permissions. Use this for team products with different member responsibilities.
## Prerequisites
- Routes already declared in the business
- A preview environment with a team organization
## Enable RBAC
RBAC enablement is a platform-owned business setting, not code. Turn it on in
the dashboard (the **Access control (RBAC)** card in business settings) or from
the CLI:
```bash
farthershore business rbac enable
farthershore business rbac # confirm the current setting
```
Then enable each subscriber organization's own RBAC setting. Both flags must be
on; business enablement alone is not least-privilege enforcement. The
subscribing org can do it from its portal's **Settings → Team** page
(`/settings/team`), and you can do it from the builder plane:
```bash
farthershore consumer list # find the subscriber id
farthershore consumer rbac enable --default-role reader
farthershore consumer rbac roles list # verify
```
Order matters: with the product flag still off, the per-subscriber call answers
`400 RBAC_NOT_ENABLED_BY_PRODUCT`. The business flag is shared across
environments. Do not toggle it off for tests.
Declare a stable permission group so the exact product grants are explicit:
```ts
import * as fs from "@farthershore/business";
fs.backend("api");
const requests = fs.requests();
const reports = fs.route("/v1/reports", {
get: { requireMember: true },
post: { requireMember: true },
});
const generate = fs.route("/v1/reports/generate", {
post: { requireMember: true, permission: "generate" },
});
const reportAccess = fs.group("reports", [reports, generate], {
permission: { verbs: ["generate"] },
});
fs.plan("team", {
kind: fs.plan.kind.flat,
price: fs.money.usd(49).monthly(),
grants: [reportAccess],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
RBAC is business-level and applies to every environment. Tier-specific API
access remains explicit in each plan's route or group refs. The flag takes
effect at the edge automatically — no rebuild or republish is needed.
## Verify
Create explicit CUSTOM product roles — from the subscriber's own portal
(`/settings/team`) or from the builder plane:
```bash
farthershore consumer rbac roles create reader \
--name Reader --permissions "reports:read"
farthershore consumer rbac assign \
--roles reader
```
The platform does not seed product roles or select a default. Assign separate
nonowner test members exactly these grants:
| Role | Exact grants | GET reports | POST reports | POST generate |
| --------- | ---------------------------------- | ----------- | ------------ | ------------- |
| Reader | `reports:read` | Allow | Deny | Deny |
| Writer | `reports:read`, `reports:write` | Allow | Allow | Deny |
| Generator | `reports:read`, `reports:generate` | Allow | Deny | Allow |
All allow outcomes also require an active subscription whose plan grants the
route and successful identity, scope, and limit checks. Test direct API requests,
not just whether a frontend button is visible. Then verify these negative cases:
- Remove the plan's route grant: a matching role still cannot grant plan access.
- Try an organization-owned credential: the example requires member identity
independently of its permission grants. For machine-callable routes that omit
`requireMember`, test restricted organization keys separately; RBAC still applies.
- Give a role only `reports:write`: it must not gain `GET` or `generate` access.
- Remove all explicit grants and default-role access from a nonowner: it must
not gain access. Deleted explicit role keys must not fall back to the default.
- Narrow a role and test an existing credential after edge propagation. A new
token alone does not prove existing credentials have been updated.
- Attempt cross-tenant/private-record access: the backend must still enforce
record ownership using verified context. Route permission is not row access.
Keep the permission vocabulary identical across environments while testing role
assignments. Preview applies reconcile the shared business-wide vocabulary:
renaming/deleting a group or shrinking its verbs can remove grants outside the
preview. Plan that as a permission migration, not an isolated preview experiment.
## Common failures
- Everyone remains unrestricted: **both** flags are required. Confirm the
business flag (`farthershore business rbac`) and the subscriber flag — the
`rbac.enabled` field on that subscriber's `farthershore consumer list` row;
turn it on with `farthershore consumer rbac enable `
or have the org do it at its portal's `/settings/team`.
- A route remains unavailable: confirm the subscriber plan grants its route or group ref.
- UI hides correctly but API allows: enforce backend access through the gateway, not UI checks alone.
## Recover
Repair the affected role or route grant first and repeat the denied call. The
business-wide `farthershore business rbac disable` removes managed role
restrictions in every environment, including production; it is an access
expansion, not a preview-only rollback. Roles are preserved for re-enable.
Keep backend record-level authorization active throughout recovery.
## Next steps
See [team RBAC](/define/team-rbac), [customer operations](/operate/customer-operations), and [permission gates](/frontend/permission-gates).
## Agent prompt
> Enable managed RBAC for this Farther Shore business with
> `farthershore business rbac enable` (it is a platform setting, not code),
> then enable it for the test subscriber with
> `farthershore consumer rbac enable ` — enforcement
> needs both flags. Preserve routes and plans, test read versus write access in
> preview, and do not publish production.
---
# Bring your own backend
Canonical URL: https://docs.farthershore.com/backend/overview
A backend is your always-running HTTP application behind Farther Shore's gateway.
You need one when a route must run your code: query your database, call a private
provider, process a job, or calculate dynamic usage. You do not need one for
platform-owned operations such as subscriptions, API keys, hosted portal data,
or request counting at the gateway.
## Three separate responsibilities
| Responsibility | Owner | How it changes |
| ---------------------------------- | ------------------------ | --------------------------------------------------- |
| Logical backend and route binding | Repository | `fs.backend()` and route refs in `business/` |
| Concrete origin for an environment | Platform operating state | `farthershore backend create` or `backend bind` |
| Application process and database | Your infrastructure | Deploy with your normal host and migration workflow |
`fs.backend()` is a contract declaration. It says which logical backend a route
uses, which transport it expects, whether verification is required, and which
meters it may report. An origin URL is **environment-owned** operating state and
is intentionally rejected in the Business SDK.
```ts business/business.ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const api = fs.backend("api", {
name: "Application API",
transport: { mode: "direct" },
verification: { required: true },
default: true,
});
const jobs = fs.route("/v1/jobs", {
get: { backend: api, requireMember: true },
post: { backend: api, requireMember: true },
});
fs.plan("starter", {
kind: fs.plan.kind.free,
grants: [jobs],
limits: [requests.perMinute(60)],
});
export default fs.business();
```
Verification defaults to required, so the explicit line above is useful for
readers but not necessary. A single declared backend is the implicit default.
With multiple backends, bind every route explicitly or mark exactly one backend
as `default: true`.
Build and push the repository change before binding traffic:
```bash
pnpm build
git push
farthershore apply-timeline list my-business
```
## Create or bind the origin
Backends use a stable logical slug across environments. Previews inherit
production backends by default; a preview row with a concrete target overrides
only that slug in that environment. Concrete overrides and their runtime
credentials remain per-environment.
This fallback applies only to the backend's concrete target. The preview's
plans, prices, routes, permissions, meters, limits, and policies always come
from the Business SDK compiled on its own `env/*` branch; none of that contract
state is inherited from production.
Published routes also address backends by this stable slug, rather than by an
environment-specific database ID. The gateway resolves the slug against the
effective backend set for the request environment, so binding an override takes
effect without changing the Business program or duplicating routes.
Create a direct production backend and bind the origin in one operation:
```bash
farthershore backend create my-business \
--name "Application API" \
--slug api \
--transport direct \
--origin-url https://api.example.com \
--idempotency-key \
--default
```
Do nothing when the preview should use the production service. To override it,
create the corresponding preview row before binding it:
```bash
farthershore backend create my-business \
--name "Application API" \
--slug api \
--env staging \
--idempotency-key \
--transport direct
farthershore backend bind my-business api \
--env staging \
--origin-url https://api-staging.example.com
```
`backend create` is useful when the row does not exist. `backend bind` updates a
direct backend's origin for one environment. A manifest-created preview row with
no target remains a placeholder and does not hide the usable production row.
Resolution is preview override, then production, by exact logical slug. It
never consults another preview. An environment-only backend with no usable
target fails closed with `origin_unavailable` (HTTP 503).
The same typed `503 origin_unavailable` is what callers see whenever the origin
cannot be reached — while it is redeploying, stopped, or failing to resolve —
and `504 origin_timeout` when it accepts the connection but never sends response
headers within the route's budget. Your hosting provider's own error page is
never relayed, so an outage is never mistaken for one of your application's
404s. Your backend's own JSON responses pass through untouched. See
[response codes](/reference/response-codes).
For tunnel transport, the platform provisions the origin hostname, so do not
pass `--origin-url`:
```bash
farthershore backend create my-business \
--name "Private API" \
--slug api \
--transport tunnel \
--runner embedded \
--idempotency-key \
--default
```
See [Transport modes](/backend/transport-modes) before choosing a tunnel.
## Give the process a runtime token
The backend SDK bootstraps from `FS_RUNTIME_TOKEN`. For one deployment serving
the business across multiple environments, the simplest default is a
business-scoped token: omit both `--env` and `--backend`.
```bash
farthershore backend tokens create my-business --format json --idempotency-key
```
Store the one-time secret in your host's secret manager, restart or redeploy the
service, then verify the binding:
```bash
farthershore backend list my-business --format json
farthershore backend tokens list my-business --format json
```
Use an environment-scoped or backend-scoped token only when the deployment
boundary needs that narrower scope. The complete lifecycle is in [Runtime
tokens](/backend/runtime-tokens).
## What the gateway does
For both `direct` and `tunnel` transport, the gateway:
1. resolves the current environment and its compiled route;
2. selects a concrete environment override or the inherited production backend;
3. enforces the plan, limits, subject, and route permission;
4. signs the exact method, path, query, raw body hash, route, backend, and
verified principal context;
5. forwards the request to the resolved origin.
Your process verifies that signature with `@farthershore/backend` before reading
identity or handling the request. Transport changes how packets reach the
process; it does not change the trust model.
## Next steps
- [Scaffold a backend](/backend/scaffold)
- [Verify identity and store user data](/backend/user-data)
- [Report dynamic usage](/backend/metering)
- [Deploy on Railway](/backend/deploy-railway) or [Render](/backend/deploy-render)
---
# Scaffold a backend service
Canonical URL: https://docs.farthershore.com/backend/scaffold
This walkthrough produces a small Express service with the current backend SDK.
The generated service verifies Farther Shore gateway signatures before handler
code runs and is ready for response-bound metering.
Before you start, confirm you have somewhere to run a long-lived HTTP service on
a public HTTPS URL, the ability to set environment variables there, and a way to
read its logs. Farther Shore is the gateway in front of your service; it does not
host it, and a production publish fails until a real origin is bound.
## 1. Generate the application
Run this from the **root of the managed business repository** — the directory
containing `business/`. It writes the service into `api/` and appends build
output entries to the root `.gitignore`.
```bash
farthershore create api --help # the current language list
farthershore create api --node
cd api
npm install
```
`--node` is the default and today the only language; run `--help` rather than
assuming, and use whatever it lists. `--path ` points the command at a
different repository root, and `--force` overwrites an existing `api/`. Nothing
binds the service to that directory afterwards — move or rename it freely, since
the platform only ever sees the deployed origin URL.
The Node template targets Node 22 or newer and includes
`@farthershore/backend`. It listens on `PORT`, defaulting to **8080**. Do not
hand-write the raw-body capture or the `fs.middleware()` verification chain; use
the template's. Keep the generated body-processing order intact:
1. unsigned health route;
2. raw-body capture;
3. `fs.middleware()` signature verification;
4. JSON parsing;
5. verified handlers.
The signature covers the original bytes. If `express.json()` consumes and
re-serializes the body before verification, otherwise-valid requests fail with a
body-hash error.
```ts
import type { IncomingMessage } from "node:http";
import express from "express";
import { fartherShore, requireMember } from "@farthershore/backend";
import { RUNTIME_BODY_HASH_CONTRACT } from "@farthershore/backend/runtime";
const fs = fartherShore.initFromEnv();
const app = express();
app.get("/healthz", (_req, res) => res.json({ ok: true }));
app.use(
express.raw({
type: (req: IncomingMessage) => {
const value = req.headers["content-type"] ?? "";
const type = Array.isArray(value) ? (value[0] ?? "") : value;
return !RUNTIME_BODY_HASH_CONTRACT.streamingExemptContentTypes.includes(
type.split(";")[0]!.trim().toLowerCase(),
);
},
limit: RUNTIME_BODY_HASH_CONTRACT.maxBodyBytes,
}),
);
app.use((req, _res, next) => {
if (Buffer.isBuffer(req.body) && req.body.length > 0) {
(req as typeof req & { rawBody?: Buffer }).rawBody = req.body;
}
next();
});
app.use(fs.middleware());
app.use((req, res, next) => {
const rawBody = (req as typeof req & { rawBody?: Buffer }).rawBody;
if (!rawBody) return next();
try {
req.body = JSON.parse(rawBody.toString("utf8"));
next();
} catch {
res.status(400).json({ error: "invalid_json" });
}
});
app.post(
"/v1/jobs",
fs.handler(async (ctx, req, res) => {
const member = requireMember(ctx);
res.status(201).json({ ownerId: member.memberId, input: req.body });
}),
);
app.listen(Number(process.env.PORT ?? 8080));
await fs.start();
process.on("SIGTERM", () => void fs.shutdown());
```
The template in the CLI is the canonical implementation; the excerpt above
shows why the ordering matters.
`fs.start()` performs the bootstrap call that exchanges `FS_RUNTIME_TOKEN` for
routing and metering configuration, and it rejects when the token is missing,
revoked, or scoped to a different environment. Start the HTTP listener **before**
bootstrap, as above, so `/healthz` answers while bootstrap is still retrying and
the host does not mark the deployment crashed. Log the bootstrap failure and let
it retry rather than exiting; `fs.ready` reports whether bootstrap has completed,
so verified routes can fail closed until it has.
## 2. Declare the backend contract
Add the logical backend and route in a module under `business/`. The compiler
discovers the whole folder; `business/business.ts` is the conventional entry
file, not a required filename.
```ts business/business.ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const api = fs.backend("api", {
transport: { mode: "direct" },
default: true,
});
const jobs = fs.route("/v1/jobs", {
post: { backend: api, requireMember: true },
});
fs.plan("starter", {
kind: fs.plan.kind.free,
grants: [jobs],
limits: [requests.perMinute(60)],
});
export default fs.business();
```
Validate before pushing:
```bash
farthershore validate
git add business api
git commit -m "Add the application backend"
git push
```
A direct push reports the `farthershore/build` and `farthershore/apply` checks on
that commit. `farthershore/validate` is the pull-request check and does not
appear on a plain push; do not wait for it.
## 3. Deploy the process
Deploy `api/` to any long-running Node host. It must listen on `PORT`, expose
`/healthz` before verification, and receive `FS_RUNTIME_TOKEN` from the host's
secret manager. Do not put the runtime token in the repository or build output.
For a first service, the host's own CLI is the shortest path — `railway up`,
`render deploys create`, `flyctl deploy`, or `gcloud run deploy`. Once you need a
preview environment and a production environment to stay in step, describe the
hosting declaratively instead: see
[Infrastructure with OpenTofu](/backend/infrastructure-opentofu).
For direct transport, note the public HTTPS origin printed by the provider. The
origin may accept unauthenticated network connections because the SDK still
rejects requests that do not carry a valid gateway signature.
## 4. Register the environment origin
```bash
farthershore backend create my-business \
--name "Application API" \
--slug api \
--transport direct \
--origin-url https://your-service.example.com \
--idempotency-key \
--default
```
Previews inherit this production backend automatically. Use `--env
` only when a preview needs a different origin: create the same
logical slug there, bind it, and give that deployment a matching scoped token.
`--transport tunnel` requires the Scale plan. The `fs.backend()` declaration
accepts `transport: { mode: "tunnel" }` on any plan, but the operate surface
refuses to create the tunnel backend, so use `direct` unless the workspace is on
Scale.
## 5. Create and deliver the runtime token
Mint the token **after** step 4. A runtime token is scoped to backend rows, so
creating one before the row exists leaves it with nothing to resolve. If the
target environment has more than one backend, add `--backend ` so the
token resolves to the intended row.
For a deployment that can serve this business in every environment:
```bash
farthershore backend tokens create my-business --format json --idempotency-key
```
Copy the one-time token into `FS_RUNTIME_TOKEN`, restart or redeploy the service,
and inspect the result:
```bash
farthershore backend list my-business --format json
farthershore backend tokens list my-business --format json
```
Neither list command takes `--env`: both are business-wide. Read each row's
environment field instead of expecting a filter flag.
Then call the Farther Shore business gateway with a real test subscriber
credential. Calling the direct origin without a platform signature should fail
with `missing_signature`; that is the expected security posture. If the origin is
down, the gateway surfaces the upstream failure rather than a typed
`origin_unavailable`, so check the deployment's own logs before assuming a
binding problem.
## 6. Before going live
Every backend declared in `business/` must have a concrete production binding, or
publishing fails with `BACKEND_TARGET_REQUIRED`. With more than one backend,
exactly one must be the default or it fails with `DEFAULT_BACKEND_REQUIRED`.
```bash
farthershore backend list my-business --format json
farthershore backend bind my-business \
--env production \
--origin-url https://your-production-service.example.com \
--format json
```
## Next steps
- [Store user and organization data safely](/backend/user-data)
- [Report dynamic usage](/backend/metering)
- [Choose direct or tunnel transport](/backend/transport-modes)
- [Provision multiple environments with OpenTofu](/backend/infrastructure-opentofu)
---
# Transport modes
Canonical URL: https://docs.farthershore.com/backend/transport-modes
Transport controls how Farther Shore reaches your process. It does not control
who the process trusts: the gateway signs forwarded requests in both modes, and
`@farthershore/backend` verifies them in both modes.
| | Direct | Tunnel |
| ---------------------- | ------------------------------------ | ------------------------------------------------------ |
| Network path | Gateway to your public HTTPS origin | Backend opens an outbound Cloudflare tunnel |
| Inbound public service | Required | Not required |
| Origin ownership | You bind `originUrl` per environment | Platform provisions `originHostname` |
| Scaling fit | Serverless or long-running HTTP | Long-running process with stable outbound connectivity |
| SDK process | `fs.start()` is a no-op | `fs.start()` supervises embedded `cloudflared` |
| Availability | Always available | Subject to workspace entitlement |
## Direct HTTPS
Use direct mode for Railway, Render, Cloud Run, an ALB, or any other provider
that gives the application a stable public HTTPS URL.
```ts business/business.ts
const api = fs.backend("api", {
transport: { mode: "direct" },
default: true,
});
```
```bash
farthershore backend create my-business \
--name "Application API" \
--slug api \
--transport direct \
--origin-url https://api.example.com \
--idempotency-key \
--default
```
Previews inherit the production origin by stable slug. To override one later:
```bash
farthershore backend bind my-business api \
--env staging \
--origin-url https://api-staging.example.com
```
The preview backend row must already exist. Create it with `backend create
--env staging` if necessary.
A direct origin is network-public, but its business routes are not trusted
public API endpoints. Strict SDK middleware rejects an unsigned request before
handler code runs. Put unsigned provider health checks before the verifier.
## Outbound tunnel
Use tunnel mode when the process should have no public ingress and can maintain a
long-running outbound connection on port 443.
```ts business/business.ts
const api = fs.backend("api", {
transport: { mode: "tunnel", runner: "embedded" },
default: true,
});
```
```bash
farthershore backend create my-business \
--name "Private API" \
--slug api \
--transport tunnel \
--runner embedded \
--idempotency-key \
--default
```
Do not pass an origin URL. The platform provisions the tunnel and returns its
connection data through runtime bootstrap.
The application still listens locally, normally on `PORT` or port 3000:
```ts
const fs = fartherShore.initFromEnv();
app.listen(Number(process.env.PORT ?? 3000));
await fs.start();
```
`fs.start()` launches and supervises the SDK's optional, platform-specific
`cloudflared` binary for an embedded tunnel. Install dependencies on the target
OS and do not omit optional dependencies. On direct transport it is a no-op, so
the same application startup can support either contract.
Tunnel tokens need the `tunnel` operation in addition to verification,
metering, and health:
```bash
farthershore backend tokens create my-business \
--backend \
--operations gateway_verification,metering,health,tunnel \
--idempotency-key \
--format json
```
### Embedded versus sidecar
- `embedded` is the self-contained path: the SDK starts `cloudflared` from the
same process/container and receives the credential during bootstrap.
- `sidecar` is for an independently supervised tunnel process. Provisioning and
lifecycle wiring are operator-managed; `fs.start()` does not start it.
Use embedded unless your infrastructure already has a clear sidecar lifecycle.
## Failure behavior
- A preview with no concrete override inherits the matching Main backend.
- An environment-only row or selected override with an unusable direct origin
fails with `origin_unavailable`; traffic never falls through to another slug
or another preview.
- A disconnected tunnel cannot receive traffic until it reconnects.
- `fs.start()` is fail-open by default for process startup. Set the runtime
tunnel option to fail closed only when crashing the application is the desired
response to tunnel startup failure.
- Request verification remains fail-closed independently of tunnel startup.
Inspect the current environment rows and derived status with:
```bash
farthershore backend list my-business --format json
```
---
# Infrastructure with OpenTofu
Canonical URL: https://docs.farthershore.com/backend/infrastructure-opentofu
Farther Shore is the gateway, billing, and entitlement plane in front of an HTTP
service that you run. It does not host that service. Creating it with a host's
CLI is the fastest way to a first working preview; describing it with
[OpenTofu](https://opentofu.org) is what makes a second environment, a rebuild,
and a handover reproducible.
Use this page once the product is real enough to need more than one environment.
For the first hour, [Scaffold a backend](/backend/scaffold) is the shorter path.
## What OpenTofu owns, and what it does not
| Thing | Owner |
| ---------------------------------------------------------- | --------------------------------------------------- |
| The host project, service, deployment, and public hostname | OpenTofu |
| `FS_RUNTIME_TOKEN` and other process secrets in the host | OpenTofu (as a sensitive variable it does not mint) |
| Plans, pricing, routes, meters, limits, `fs.backend()` | The `business/` program in the managed repository |
| The Farther Shore environment row and its origin binding | The `farthershore` CLI |
OpenTofu never authors contract state, and the CLI never provisions hosting.
Runtime tokens are minted by the CLI and _delivered_ by OpenTofu.
## Map platform environments to infrastructure environments
Keep the mapping one-to-one and name both sides identically.
| Farther Shore | Git branch | Infrastructure |
| ----------------------------- | -------------- | ---------------------- |
| preview environment `preview` | `env/preview` | workspace `preview` |
| production | default branch | workspace `production` |
Each side stays independent: the contract comes from the branch, the origin
comes from the infrastructure, and `farthershore backend create --env` /
`farthershore backend bind --env` is the single join between them.
Give every environment its own deployment and its own `FS_RUNTIME_TOKEN`. A
token minted with `--env preview` cannot bootstrap production, which is the
point: a compromised preview deployment cannot serve live customers.
## Keep state where a second machine can read it
Shared infrastructure must not use local state. Configure a remote backend with
locking before the first `tofu apply`, and keep one state file per environment —
either separate workspaces or separate backend keys.
```hcl
terraform {
required_version = ">= 1.10"
backend "s3" {
bucket = "acme-tofu-state"
key = "farthershore/api.tfstate"
region = "us-east-1"
# Native S3 conditional-write locking. On older OpenTofu, lock with
# `dynamodb_table` instead; both mechanisms remain supported.
use_lockfile = true
}
}
```
State contains secret values in plaintext. Encrypt the bucket, restrict who can
read it, and never commit a `.tfstate` file. OpenTofu's built-in [state
encryption](https://opentofu.org/docs/language/state/encryption/) is worth
enabling on top of the backend's own encryption.
## A concrete example: Railway
The community Railway provider models a project, its environments, one service
per environment, that service's domain, and its variables. Substitute your own
provider if you deploy elsewhere — the shape below is what matters, not the
vendor.
```hcl
terraform {
required_providers {
railway = {
source = "terraform-community-providers/railway"
version = "~> 0.5"
}
}
}
# RAILWAY_TOKEN comes from the environment; never write it into a .tf file.
provider "railway" {}
variable "environment_name" {
type = string
description = "preview or production; matches the Farther Shore environment"
}
variable "fs_runtime_token" {
type = string
sensitive = true
description = "Minted by farthershore backend tokens create for THIS environment"
}
resource "railway_project" "api" {
name = "acme-api"
private = true
}
resource "railway_environment" "this" {
name = var.environment_name
project_id = railway_project.api.id
}
resource "railway_service" "api" {
name = "api"
project_id = railway_project.api.id
source_repo = "acme/acme-api"
source_repo_branch = var.environment_name == "production" ? "main" : "env/preview"
root_directory = "/api"
}
resource "railway_service_domain" "api" {
subdomain = "acme-api-${var.environment_name}"
environment_id = railway_environment.this.id
service_id = railway_service.api.id
}
resource "railway_variable" "runtime_token" {
name = "FS_RUNTIME_TOKEN"
value = var.fs_runtime_token
environment_id = railway_environment.this.id
service_id = railway_service.api.id
}
output "origin_url" {
value = "https://${railway_service_domain.api.domain}"
}
```
Pass the secret from your own secret store rather than a variable file:
```bash
tofu workspace select preview
TF_VAR_fs_runtime_token="$(read-from-your-secret-store)" \
tofu apply -var environment_name=preview
```
If you use a different host, the four resources to look for are the same: a
project or account scope, one service per environment, a public HTTPS hostname,
and a secret variable bound to that service and environment.
## Order of operations
The runtime token is scoped to a backend row, so the row must exist first.
1. Declare the logical backend in `business/` and push the branch, so the
environment has an accepted contract:
```ts
const api = fs.backend("api", {
transport: { mode: "direct" },
default: true,
});
```
2. Provision the hosting for that environment and read its hostname:
```bash
tofu workspace select preview
tofu apply -var environment_name=preview
tofu output -raw origin_url
```
3. Register the origin for that environment:
```bash
farthershore backend create acme \
--env preview \
--name api --slug api \
--transport direct \
--origin-url "$(tofu output -raw origin_url)" \
--default \
--idempotency-key \
--format json
```
4. Mint the environment's token **after** that row exists, then deliver it:
```bash
farthershore backend tokens create acme \
--env preview \
--idempotency-key \
--format json
```
Pass `--backend ` as well when the environment has more than one
backend, so the token resolves to the intended row. Store the one-time value
in your secret store and re-apply so the host receives it.
5. Repeat for production with `--env production` omitted (production is the
default target) and a production-scoped token.
Steps 3 and 4 cannot be inverted, and they cannot be moved into OpenTofu: the
token is a one-time secret returned by a CLI write, not a declarable resource.
## What to re-run when something changes
| Change | Re-run |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Plans, pricing, routes, meters, limits — anything in `business/` | `git push` only. No `tofu` run. |
| Backend application code in `api/` | Your host's deploy (often automatic from the branch). |
| A **new** `fs.backend()` slug | `tofu apply`, then `backend create` + `tokens create`. |
| A new environment | `farthershore env create`, then `tofu apply` in a new workspace, then `backend create` + `tokens create`. |
| Hostname, region, replica count, resource sizing | `tofu plan` then `tofu apply`, then `backend bind --env --origin-url ` if the hostname moved. |
| Token rotation | `backend tokens create`, update the secret, re-apply, verify, then revoke the predecessor. |
Always read `tofu plan` before `tofu apply`. A plan that proposes replacing the
service or its domain will change the origin URL, which requires a matching
`farthershore backend bind` or the gateway will return `origin_unavailable`.
## Verify
- `tofu output -raw origin_url` returns an HTTPS URL that serves `/healthz`
unauthenticated.
- `farthershore backend list acme --format json` shows the row for each
environment with a concrete target. This command is business-wide and takes no
`--env`; read the environment off each returned row.
- A signed gateway request reaches the backend, and a direct call to the origin
without a platform signature fails with `missing_signature`.
- Publishing production succeeds. If it fails with `BACKEND_TARGET_REQUIRED`, a
declared backend has no production binding — see
[Production releases](/operate/releases).
## Next steps
- [Runtime tokens](/backend/runtime-tokens) — scope, delivery, and rotation.
- [Preview environments](/operate/environments) — branches, applies, and teardown.
- [Scaffold a backend](/backend/scaffold) — the single-service starting point.
---
# Deploy on Railway
Canonical URL: https://docs.farthershore.com/backend/deploy-railway
Railway's long-running service model supports either a public direct origin or
an embedded outbound tunnel. Start with [Scaffold a backend](/backend/scaffold)
and commit the application before creating the Railway service.
## Direct HTTPS
1. Create a Railway service from the managed business repository.
2. Set its root directory to the backend folder, for example `api/`.
3. Use Node 22 or newer and start the application with its normal production
command.
4. Make the application listen on Railway's `PORT`.
5. Generate a Railway public domain and copy its HTTPS URL.
Create the runtime token before the first real request:
```bash
farthershore backend tokens create my-business --format json --idempotency-key
```
Add the returned one-time value as Railway variable `FS_RUNTIME_TOKEN`, then
redeploy. Do not make it a frontend/build variable in Farther Shore; this is a
secret consumed by the running backend process.
Bind the Railway domain to the production environment:
```bash
farthershore backend create my-business \
--name "Railway API" \
--slug api \
--transport direct \
--origin-url https://your-service.up.railway.app \
--idempotency-key \
--default
```
By default a branch environment uses the production Railway service. When a
branch needs an isolated service, deploy it and create a concrete
per-environment override with the same logical slug:
```bash
farthershore backend create my-business \
--name "Railway API" \
--slug api \
--env staging \
--transport direct \
--idempotency-key \
--origin-url https://your-staging-service.up.railway.app
```
## Embedded tunnel
For no public ingress, declare and create a tunnel backend instead:
```bash
farthershore backend create my-business \
--name "Railway Private API" \
--slug api \
--transport tunnel \
--runner embedded \
--idempotency-key \
--default
farthershore backend tokens create my-business \
--backend \
--operations gateway_verification,metering,health,tunnel \
--idempotency-key \
--format json
```
Set that token as `FS_RUNTIME_TOKEN`. The application must call `await
fs.start()` after listening locally. Do not generate or bind a public domain for
the tunnel path.
## Verify
```bash
farthershore backend list my-business --format json
farthershore backend tokens list my-business --format json
```
Call the Farther Shore gateway with a test subscriber key. For direct mode, an
unsigned request to the Railway domain should be rejected by strict SDK
middleware. Keep Railway health checks on `/healthz`, mounted before the
verifier.
When rotating the token, remember that `backend tokens rotate` revokes the old
token immediately. For planned zero-downtime changes, create a second token,
update Railway, wait for every replica to redeploy, and only then revoke the old
one. See [Runtime tokens](/backend/runtime-tokens).
---
# Deploy on Render
Canonical URL: https://docs.farthershore.com/backend/deploy-render
Use a Render Web Service for a direct HTTPS backend. Start with [Scaffold a
backend](/backend/scaffold), then push the application to the managed repository.
## Create the service
1. Create a Web Service from the repository.
2. Set the root directory to the backend folder, such as `api/`.
3. use Node 22 or newer;
4. configure the repository's install/build command and production start
command;
5. listen on `process.env.PORT`;
6. keep `/healthz` before `fs.middleware()` and use it as the health-check path.
Create a runtime token:
```bash
farthershore backend tokens create my-business --format json --idempotency-key
```
Store the returned one-time value as a secret environment variable named
`FS_RUNTIME_TOKEN`, then deploy the service. The value belongs to the running
process, never the repository or browser bundle.
## Bind Render's URL
After Render prints the service's HTTPS URL:
```bash
farthershore backend create my-business \
--name "Render API" \
--slug api \
--transport direct \
--origin-url https://your-service.onrender.com \
--idempotency-key \
--default
```
Previews inherit the production Render service. A preview needs its own Render
service or equivalent origin plus an override row only when isolation is desired:
```bash
farthershore backend create my-business \
--name "Render API" \
--slug api \
--env staging \
--transport direct \
--idempotency-key \
--origin-url https://your-staging-service.onrender.com
```
Changing the Render URL later is an operating-state update:
```bash
farthershore backend bind my-business api \
--env staging \
--origin-url https://replacement.onrender.com
```
## Verify and operate
```bash
farthershore backend list my-business --format json
```
Call the Farther Shore gateway with a subscriber test key. Calling the Render
origin directly without a gateway signature should return `missing_signature`.
If the service scales to zero, expect the provider's cold-start latency to be
part of the route's upstream latency. Set route timeouts in the business
contract based on measured behavior; do not disable verification to hide a
startup or routing problem.
For secret rotation, create a second matching token, update Render and redeploy,
then revoke the old token. The one-command rotate path revokes the predecessor
immediately. See [Runtime tokens](/backend/runtime-tokens).
---
# Deploy on AWS
Canonical URL: https://docs.farthershore.com/backend/deploy-aws
ECS Fargate is a good fit for the Node backend SDK because it runs a normal
long-lived process and supports either transport mode. The Farther Shore pieces
are the same in both cases: Node 22+, `FS_RUNTIME_TOKEN`, strict request
verification, and graceful `fs.shutdown()` on termination.
## Build the container correctly
Build and install dependencies on the target Linux architecture. The embedded
tunnel runner uses a platform-specific optional `cloudflared` package, so do not
copy a macOS `node_modules` directory into the image or omit optional
dependencies.
```bash
docker build --platform linux/amd64 -t application-api .
docker tag application-api:latest \
.dkr.ecr..amazonaws.com/application-api:latest
docker push .dkr.ecr..amazonaws.com/application-api:latest
```
The container should listen on port 3000 (or its configured `PORT`) and expose
an unsigned `/healthz` before SDK middleware.
## Direct mode behind an ALB
1. Run the task as an ECS service behind an Application Load Balancer.
2. Terminate HTTPS with an ACM certificate for a domain you control.
3. Point the target-group health check at `/healthz`.
4. Give only the ALB permission to reach the container port where practical.
5. Copy the stable HTTPS origin.
```bash
farthershore backend create my-business \
--name "AWS API" \
--slug api \
--transport direct \
--origin-url https://api.example.com \
--idempotency-key \
--default
farthershore backend tokens create my-business --format json --idempotency-key
```
Store the one-time token in AWS Secrets Manager and map it to the container as
`FS_RUNTIME_TOKEN` through the ECS task definition. The task execution role
needs permission to read only that secret.
## Tunnel mode without inbound traffic
Run the task without a load balancer or inbound security-group rule. It still
needs outbound HTTPS connectivity so the embedded runner and SDK can reach the
platform.
```bash
farthershore backend create my-business \
--name "AWS Private API" \
--slug api \
--transport tunnel \
--runner embedded \
--idempotency-key \
--default
farthershore backend tokens create my-business \
--backend \
--operations gateway_verification,metering,health,tunnel \
--idempotency-key \
--format json
```
Store the tunnel-capable token as `FS_RUNTIME_TOKEN`. Start the HTTP listener,
then call `await fs.start()`; no ALB origin is bound for this mode.
## Environments and deployments
Each branch environment inherits the production backend unless you create a
concrete override with `--env `. A business-scoped runtime token can
bootstrap all environment rows for the same business from one deployment; an environment-scoped or
backend-scoped token isolates them when you run separate ECS services.
```bash
farthershore backend list my-business --format json
```
When changing an ECS-injected token, force a new deployment so every running
task receives the new value. For a planned cutover, create a second token,
deploy it, verify all tasks, then revoke the old token. `backend tokens rotate`
revokes the old token immediately.
Use AWS-native database migrations and deployment health gates for your
application. Farther Shore provisions routing and verifies traffic; it does not
run your application migrations.
---
# Deploy on Google Cloud
Canonical URL: https://docs.farthershore.com/backend/deploy-gcp
Cloud Run naturally fits direct transport: it provides a stable HTTPS service
URL, and `@farthershore/backend` supplies application-level verification for the
gateway's signed requests.
## Deploy the service
The application must use Node 22 or newer, listen on `PORT`, and mount an
unsigned `/healthz` before `fs.middleware()`.
Create the runtime token first:
```bash
farthershore backend tokens create my-business --format json --idempotency-key
```
Store its one-time value in Secret Manager. Deploy the backend from its source
directory or container and expose the secret to the process as
`FS_RUNTIME_TOKEN`. Grant the Cloud Run service account access only to that
secret.
```bash
gcloud run deploy application-api \
--source . \
--region us-central1 \
--allow-unauthenticated \
--set-secrets FS_RUNTIME_TOKEN=fs-runtime-token:latest
```
`--allow-unauthenticated` makes the HTTPS network endpoint reachable by Farther
Shore; it does not make your business handlers trusted-public. Strict SDK
middleware rejects requests that do not carry a valid gateway signature.
## Bind the Cloud Run URL
Use the HTTPS URL from the deploy output:
```bash
farthershore backend create my-business \
--name "Cloud Run API" \
--slug api \
--transport direct \
--origin-url https://application-api-.run.app \
--idempotency-key \
--default
```
Previews use the production service by default. To isolate one, deploy the
preview revision/service and create a matching concrete override:
```bash
farthershore backend create my-business \
--name "Cloud Run API" \
--slug api \
--env staging \
--transport direct \
--idempotency-key \
--origin-url https://application-api-staging-.run.app
```
Cloud Run revisions behind one service share a public origin. If you need a
hard environment boundary, use separate services and bind each environment to
the corresponding URL.
## Verify and rotate
```bash
farthershore backend list my-business --format json
```
Test through the Farther Shore gateway with a subscriber key. An unsigned call
straight to the Cloud Run business route should fail with `missing_signature`.
Secret Manager updates do not change environment variables in already-running
instances. Deploy a new revision when the token changes. For a zero-downtime
planned cutover, create a second matching token, deploy it, verify the new
revision, move traffic, and revoke the old token. The rotate command itself
revokes the predecessor immediately.
Cloud Run's request-driven lifecycle is optimized for direct transport. An
embedded tunnel needs an always-running instance and stable outbound CPU; use
long-running compute for tunnel mode unless you have deliberately configured
those Cloud Run lifecycle constraints.
---
# Metering & verification
Canonical URL: https://docs.farthershore.com/backend/metering
Install the backend SDK in a Node 22 or newer application:
```bash
pnpm add @farthershore/backend
```
```ts
import { fartherShore } from "@farthershore/backend";
const fs = fartherShore.initFromEnv();
```
`initFromEnv()` reads `FS_RUNTIME_TOKEN` and lazily bootstraps on first use. In a
normal deployment you do not set a Core URL; the token determines the business,
backend, environment scope, verification keys, routes, transport, and metering
configuration.
## Verify before parsing or handling
Farther Shore signs the exact raw body hash. In Express, capture raw bytes before
the SDK middleware, then parse JSON after verification:
```ts
app.get("/healthz", (_req, res) => res.json({ ok: true }));
app.use(express.raw({ type: shouldCaptureRawBody, limit: "10mb" }));
app.use((req, _res, next) => {
if (Buffer.isBuffer(req.body) && req.body.length > 0) {
(req as typeof req & { rawBody?: Buffer }).rawBody = req.body;
}
next();
});
app.use(fs.middleware());
app.use(parseVerifiedJson);
```
Use the generated Node template for `shouldCaptureRawBody` and
`parseVerifiedJson`; it shares the platform's streaming content-type and body
size contract instead of hard-coding its own version.
Strict middleware is fail-closed by default. It verifies the request signature
and signed context, attaches `req.fartherShore`, and strips every inbound
`x-fs-*` header before your handler runs. Never read identity from an ordinary
header.
`fs.handler()` is the ergonomic verified-handler boundary:
```ts
import { requireMember, requirePermission } from "@farthershore/backend";
app.post(
"/v1/jobs",
fs.handler(async (ctx, req, res) => {
const { memberId } = requireMember(ctx);
requirePermission(ctx, "jobs:create");
res.status(201).json({ memberId });
}),
);
```
The gateway is still the route-level authorization boundary. Backend helpers are
useful for narrowing the verified subject and for finer record- or field-level
checks.
## Choose the correct metering channel
There is one verb — `ctx.report({ meter, values, dims?, quote? })` — and the
SDK chooses the transport from the response adapter and _when_ you call it:
| Moment | Transport | Network call from handler | Effect |
| --------------------------------------------- | ------------------------------------------------------ | ------------------------------------- | ----------------------------------------------------------------------------------------- |
| Structural request count | `fs.requests()` in the business contract | None | Counted by the gateway; never reported by the backend. |
| Before the response is sent | signed in-band `x-fs-metering` headers | None | Settles the request's monetary reservation on the way out. |
| After the response is on the wire (streams) | attested post-stream channel | One callback | Rated under the request's served identity; billing-only. |
| Deferred work for the original served request | post-stream channel with its retained verified context | One callback total per served request | Rated under that request's served identity; billing-only, not independent cron reporting. |
Do not report `requests` from backend code. Backends report **measurements,
never money**: the platform owns what a measurement costs.
Express middleware attaches the response adapter automatically. A
framework-neutral `verifyRequest()` call needs a `responseSink` to stamp
in-band headers; without one it uses post-stream delivery even if the handler
has not returned. See the [Fetch adapter example](/reference/backend-sdk).
### Report from the verified context
`ctx.report()` is the one reporting verb. `meter` is the meter key declared
with `fs.meter()` in the business; `values` are keyed by measure key
(`fs.measure()`); `dims` are optional catalog dimension selectors keyed by
dimension key (`fs.dimension()`). Identity rides the verified context — there
is no subscription or request id argument to forget, and the served identity
(subscription, commercial release, rating context) is stamped by the gateway
inside the signed usage event.
```ts
app.post(
"/v1/complete",
fs.handler(async (ctx, req, res) => {
const result = await model.complete(req.body.prompt);
await ctx.report({
meter: "model_usage",
values: {
input_tokens: result.inputTokens,
output_tokens: result.outputTokens,
},
dims: { model: "acme-4" },
});
res.json({ text: result.text });
}),
);
```
The meter, measure, and dimension keys are plain strings validated by the
gateway against the served release's measurement schema; an unknown key is
rejected loudly, never silently dropped. Values must be non-negative finite
numbers.
Transport is automatic. Before the response is sent, the measurement rides
signed `x-fs-metering` headers — the gateway removes and verifies them, then
settles the request using the served plan and policy, with no extra network
call. A malformed report throws; a delivery fault resolves `{ ok: false }` so a
metering hiccup never breaks your endpoint. For a pricing rule declared
`backendQuoted`, an optional `quote: { currency, amountNanos }` field carries a
proposed, non-negative rate input that the platform clamps; backends never
report money otherwise.
Always inspect the result, record delivery failures with request correlation,
and investigate missing usage. `ok: true` with `transport: "in_band"` proves
headers were attached, not that the gateway accepted or settled the report.
Do not blindly report the same usage again: the reporting verb is not an
application-level deduplication key.
For a quote, `amountNanos` is the proposed **per-unit** rate, not the total cost.
Prefer a decimal integer string so large exact amounts survive JavaScript.
### When the gateway cannot trust a report
Your response is never rewritten because of a metering report. By the time the
gateway inspects the signed headers your endpoint has already run and committed
its work, so a report the gateway cannot verify is **discarded**, not escalated
into an error for your caller. The request is then settled on the route's
declared or fixed costs — the same settlement a route with no in-band report at
all receives.
A discarded report is visible on the response itself:
```http
HTTP/1.1 201 Created
x-fs-metering-report: rejected;reason=token_backend_mismatch
```
The header appears **only** when a report was rejected; an accepted report
leaves it absent. The reason names the exact check that failed — it never
contains a token, hash, or identifier:
| `reason` | What it means |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token_not_published` | The runtime token is not present in edge state. Newly minted tokens take a moment to propagate; a token for an environment that is not live never propagates at all. |
| `token_revoked` | The token exists at the edge but is revoked. |
| `token_kind_mismatch` | The token's `fsrt_live_` / `fsrt_test_` prefix disagrees with the kind it was minted as. |
| `token_business_mismatch` | The token belongs to a different business than the request's credential. |
| `token_missing_metering_operation` | The token was minted without the `metering` operation. |
| `token_environment_mismatch` | The token is scoped to one environment and the request authenticated into another. |
| `token_backend_mismatch` | The token is bound to a specific backend and the request resolved to a different one — commonly an environment override row versus the production row of the same slug. |
| `token_hash_mismatch` | The published record does not match the presented token. Report this. |
| `signature_mismatch` | The signature did not verify against the presented token. |
| `request_context_mismatch` | The report's `method`/`path` do not match the request the gateway served. Most often the backend sits behind an origin path prefix, so it observes a different path than the caller sent. |
| `partial_headers` | Some but not all of the three metering headers arrived — usually a proxy stripping headers. |
| `invalid_measurements` | The measurement lane was present but malformed. |
| `parse_or_verify_error` | The payload could not be parsed or verified at all. |
Every rejection also emits a `metering_report_invalid` warning carrying the same
`reason` alongside the request id, so support can correlate it.
Reporting a meter that the route does not declare, or that the runtime token is
not scoped to, is a different and stricter case: that is a scope violation
rather than an untrusted report, and it still returns `400
metering_report_not_allowed` naming the offending routes or meters.
### Post-stream usage
Declare a streaming binding explicitly, with a finite settlement maximum when
the plan can run out of money:
```ts business/business.ts
const stream = fs.route("/v1/stream", { post: { backend: api } });
fs.meterRoutes("stream-model-usage", stream, {
reports: [modelUsage],
postStream: { settlementMax: [outputTokens.atMost(8192)] },
});
```
After the stream closes, call the same verb on the verified request context:
```ts
await ctx.report({
meter: "model_usage",
values: { output_tokens: finalOutputTokens },
});
```
Once the response is on the wire, `ctx.report()` automatically switches to the
attested post-stream channel, carrying the same served identity that rode the
signed request context. Post-stream reports write the billable measurement
but do not retroactively change real-time enforcement windows.
#### Delivery is asynchronous and order-independent
The platform's own record of the served request — the gateway receipt — reaches
the billing plane asynchronously. A fast background job routinely finishes and
reports **before** its receipt lands. That is expected, and it is not your
problem to handle:
- A verified report that arrives ahead of its receipt is **accepted** (`202`)
and held, then settled automatically once the receipt arrives. It is not
rejected and it is not lost.
- Settlement is **exactly once**. Redelivering the identical report is a no-op;
reporting _different_ measurements under the same served request is rejected,
because the first accepted report is that request's economic identity.
- A held report is bounded at 24 hours. In the (platform-fault) case where the
receipt never arrives, it is dropped with an internal billing-divergence
alert rather than silently.
So `await ctx.report(...)` resolving `{ ok: true }` means the measurement is
**durably accepted**, not necessarily already rated. There is nothing to poll.
##### Reading a failed delivery
Delivery faults resolve `{ ok: false }` (they never throw — a metering hiccup
must not break your endpoint) and carry the platform's own diagnosis, so you do
not need to wrap `fetch` to see why:
```ts
const result = await ctx.report({ meter: "pages", values: { pages } });
if (!result.ok) {
logger.warn(
{ code: result.code, status: result.status, message: result.message },
result.reason,
);
}
```
`code` and `message` are the platform's error code and message when the failure
came back in a response body, and `status` is the HTTP status of the final
attempt; all three are absent for local validation faults and transport errors,
where `reason` alone describes the problem. The SDK has already retried
everything worth retrying by the time you see `ok: false`.
Choose one transport for the entire served request. If any report already
stamped in-band headers, later post-stream reporting fails with `ok: false`.
For streaming work, collect the final measurements and send one final batch;
do not send an input-token report in-band followed by output tokens post-stream.
A served request owns exactly **one** post-stream callback, so report every
post-response meter in **one call** using the array form — sequential
single-meter calls after the first flush resolve `{ ok: false }` and that
usage is not delivered:
```ts
await ctx.report([
{ meter: "model_usage", values: { output_tokens: finalOutputTokens } },
{ meter: "jobs", values: { jobs: 1 } },
]);
```
All entries of a batch share one quote (two different quotes throw) and one
`dims` tuple — the request receipt rates under `(route, dims)`, so report each
dims tuple on its own request. The same one-quote / one-dims rule applies to
in-band accumulation before the response is sent. For bounded
streaming — the gateway clamping the client's `max_tokens` up front — declare
`maxOutputUnits` on the binding instead; see
[Monetary admission](/reference/monetary-admission).
### Background usage
For asynchronous work originating in a gateway request, retain the verified
context in the same running process and report the final batch after the
response. This is still reporting for that served request, not an independent
cron or arbitrary historical usage API:
```ts
async function runBatch(ctx: FartherShoreRequestContext, job: BatchJob) {
const elapsedSeconds = await executeBatch(job);
await ctx.report({
meter: "compute_seconds",
values: { seconds: elapsedSeconds },
});
}
```
The context includes runtime behavior and cannot be JSON-serialized into a
durable job queue and reconstructed as a trusted context. Post-stream delivery
requires the originating request's valid attestation and supports one callback.
If work can outlive that delivery window or process, design and verify its
settlement workflow before using this pattern. Call `fs.shutdown()` during
graceful termination; shutdown is not durable queue storage.
## Contract pairing
Backend code cannot invent a billable dimension. Declare the measures and
meter, bind the meter to the route, and price it in a catalog a plan binds:
```ts business/business.ts
import * as fs from "@farthershore/business";
const inputTokens = fs.measure("input_tokens");
const outputTokens = fs.measure("output_tokens");
const model = fs.dimension("model");
const modelUsage = fs.meter("model_usage", {
measures: [inputTokens, outputTokens],
dimensions: [model],
});
const modelPricing = fs.pricing("model_usage", {
meter: modelUsage,
catalog: [
fs.rate.perMillion(fs.money.usd(3)).for(inputTokens),
fs.rate.perMillion(fs.money.usd(15)).for(outputTokens),
],
});
const api = fs.backend("api", { default: true });
const complete = fs.route("/v1/complete", {
post: { backend: api },
});
fs.meterRoutes("complete-model-usage", complete, {
reports: [modelUsage],
maxOutputUnits: outputTokens.atMost(8192),
});
fs.plan("payg", {
kind: fs.plan.kind.usage,
usagePricing: modelPricing.current(),
grants: [complete],
maxMonthlySpendCents: 50_000,
});
export default fs.business();
```
Then build and push the contract before deploying code that reports the new
meter. A runtime token can narrow the allowlist further with `--meters` or
`--routes`; it cannot widen the compiled business contract.
---
# Runtime tokens
Canonical URL: https://docs.farthershore.com/backend/runtime-tokens
A runtime token (`fsrt_…`) authenticates your running backend to Farther Shore.
`@farthershore/backend` reads it from `FS_RUNTIME_TOKEN` to fetch bootstrap
configuration, verify gateway requests, report health, and report configured
meters. It is not a subscriber API key or a CLI login credential.
## Persisted-kind upgrade
The persisted-kind upgrade revokes every existing runtime token created before
the migration, and its stored authentication hash is destroyed. Those tokens
cannot be recovered and must be reissued. Create a replacement for each
deployment, replace `FS_RUNTIME_TOKEN` in the host, and restart or redeploy it.
The cutover does not infer continuing `live` or `test` authority from an old
token's environment scope.
## Create the backend row first
A runtime token is scoped to backend rows. Register the backend with
`farthershore backend create` **before** minting its token; a token created
against an environment that has no backend row yet has nothing to resolve. When
an environment has more than one backend, pass `--backend ` so the
token resolves to the intended row rather than depending on the environment's
backend set staying singular.
## Choose the deployment scope
| Scope | Create command | What it can bootstrap |
| ---------------------- | ---------------------------- | ---------------------------------------------------- |
| **business-scoped** | omit `--env` and `--backend` | This business's backend rows across all environments |
| **environment-scoped** | add `--env ` | Backend rows in one environment |
| **backend-scoped** | add `--backend ` | One backend row; combine with `--env` when needed |
The recommended default for one deployment serving the same business across
Main and previews is a business-scoped token:
```bash
farthershore backend tokens create my-business --format json --idempotency-key
```
The bootstrap response supplies every backend id the deployment may serve, so
request verification remains bound to the business while accepting the signed,
environment-specific backend id. The token does not grant access to another
business.
Use a narrower scope when deployment isolation requires it:
```bash
# All backends in one preview environment.
farthershore backend tokens create my-business \
--env staging \
--idempotency-key \
--format json
# One backend row.
farthershore backend tokens create my-business \
--backend \
--idempotency-key \
--format json
```
## Operations and meter restrictions
The default operations are:
- `gateway_verification`
- `metering`
- `health`
Add `tunnel` for an embedded tunnel backend. You can also restrict dynamic usage
to named meters or routes:
```bash
farthershore backend tokens create my-business \
--operations gateway_verification,metering,health,tunnel \
--meters tokens,compute_seconds \
--routes post-v1-jobs \
--idempotency-key \
--format json
```
An empty meter or route allowlist means all corresponding meters or routes in
the token's business/backend scope. A route-scoped token requires a matching
route id on background metering events.
## Secret delivery
The plaintext token is returned once at creation or rotation. Store it directly
as `FS_RUNTIME_TOKEN` in the deployment platform's secret manager. Do not put it
in the repository, a Docker image, build arguments, logs, or frontend variables.
```bash
# Metadata only; plaintext tokens are never listed again.
farthershore backend tokens list my-business --format json
```
The list includes the persisted `kind` (`live` or `test`) so an operator can
verify the runtime classification without seeing the secret.
Neither `backend list` nor `backend tokens list` accepts `--env`: both are
business-wide reads. Filter on each returned row's environment rather than
expecting a flag.
When infrastructure is described declaratively, keep the mint separate from the
delivery: the CLI mints the one-time value and the infrastructure tool writes it
into that environment's secret store as a sensitive variable. See
[Infrastructure with OpenTofu](/backend/infrastructure-opentofu).
Most hosts inject secrets when a process starts. Restart or redeploy after
changing `FS_RUNTIME_TOKEN`; a running SDK instance keeps its bootstrapped
credential and configuration.
## Rotation semantics
`backend tokens rotate` creates a successor with the same scope and then marks
the old token **revoked immediately**. There is no overlap window:
```bash
farthershore backend tokens rotate my-business --format json --idempotency-key
```
That hard cutover is useful for a suspected leak, but it can interrupt running
replicas that still hold the old value. Replace the secret and restart or
redeploy every replica immediately.
For a planned zero-downtime change, create a second token with the same explicit
scope instead:
1. create a new token;
2. store the new value in the host;
3. deploy or restart every replica;
4. verify requests and health with the new deployment;
5. revoke the old token.
```bash
farthershore backend tokens create my-business --format json --idempotency-key
# update FS_RUNTIME_TOKEN and redeploy
farthershore backend tokens revoke my-business --yes
```
Deleting a backend also revokes runtime tokens bound to that backend.
## Token kinds
If `--kind` is omitted, an environment-scoped token defaults to `test` and an
unscoped token defaults to `live`. Pass `--kind live` or `--kind test` only when
you intentionally need to override that default. The kind is part of the token's
runtime classification; it does not broaden its business, environment, backend,
meter, or route scope. Rotation preserves the stored kind exactly, including an
explicit override.
## Recovery checklist
If bootstrap or verification fails:
1. confirm the process has a nonempty `FS_RUNTIME_TOKEN`;
2. list token metadata and check that it is active;
3. check that its scope includes the selected environment/backend;
4. check `gateway_verification`, plus `tunnel` when applicable;
5. redeploy after any host secret change;
6. inspect backend status and application verification diagnostics without
logging the token itself.
---
# Storing per-user data
Canonical URL: https://docs.farthershore.com/backend/user-data
Farther Shore authenticates the subscriber, resolves the organization and
subject, enforces the route contract, and sends your backend a signed principal.
Your application still owns its domain data: projects, documents, jobs, user
preferences, and any provider records that do not have a platform
representation.
## Trust only the verified context
Strict `fs.middleware()` verifies the gateway signature, attaches
`req.fartherShore`, and strips inbound `x-fs-*` headers. Use `fs.handler()` when
the handler requires a principal:
```ts
import { requireMember, requireService } from "@farthershore/backend";
app.get(
"/v1/profile",
fs.handler(async (ctx, _req, res) => {
const subject = requireMember(ctx);
const profile = await findOrCreateMember({
orgId: ctx.principal.org.id,
memberId: subject.memberId,
});
res.json(profile);
}),
);
```
The verified principal has one organization and exactly one subject:
```ts
type Principal = {
org: { id: string };
subject:
| {
kind: "member";
memberId: string;
via: "session" | "api_key";
keyId?: string;
}
| {
kind: "service";
serviceAccountId: string;
keyId: string;
};
};
```
- `memberId` is the stable Farther Shore member id, not an email address or
identity-provider subject.
- `serviceAccountId` is the stable organization-owned machine identity; `keyId`
identifies the rotating credential used for audit.
- `org.id` is the subscriber organization boundary. Include it in every tenant
query even when the subject id is expected to be globally unique.
After a contextual request is verified, the signed lifecycle identifiers are
available on `ctx.signedContext`:
| Application need | Verified accessor |
| ---------------------- | ---------------------------------- |
| Business | `ctx.signedContext.businessId` |
| Accepted compiled plan | `ctx.signedContext.compiledPlanId` |
| Subscription | `ctx.signedContext.subscriptionId` |
| Subscriber/customer | `ctx.signedContext.subscriberId` |
| Environment | `ctx.signedContext.environmentId` |
`signedContext` is optional on the raw `fs.middleware()` context because a
valid signed request may intentionally carry no customer context. It is present
inside `fs.handler()` after contextual verification succeeds. Never substitute
body, query, or ordinary header values for these signed identifiers.
If a route accepts only people or only service accounts, declare that in the
business program with `requireMember: true` or `requireService: true`. The
gateway rejects the other subject type before forwarding, and the SDK helper
narrows the TypeScript type inside the handler.
## Find or create on the first request
A platform identity may reach your backend before your database has a local
row. Treat the first verified request as an idempotent synchronization point.
Do not implement this as `SELECT`, then an unguarded `INSERT`: two concurrent
requests can both observe “missing” and race.
First, enforce the invariant in the database:
```prisma
model AppMember {
id String @id @default(cuid())
orgId String
memberId String
createdAt DateTime @default(now())
@@unique([orgId, memberId])
}
```
Then use the database's atomic upsert or insert-on-conflict primitive:
```ts
async function findOrCreateMember(identity: {
orgId: string;
memberId: string;
}) {
return db.appMember.upsert({
where: {
orgId_memberId: {
orgId: identity.orgId,
memberId: identity.memberId,
},
},
create: identity,
update: {},
});
}
```
In SQL, the equivalent shape is `INSERT ... ON CONFLICT (org_id, member_id) DO
UPDATE/NOTHING RETURNING ...`. The unique constraint is essential; application
locking alone does not protect multiple replicas.
Use the same pattern for organization records:
```prisma
model AppOrganization {
id String @id @default(cuid())
orgId String @unique
}
```
```ts
const appOrg = await db.appOrganization.upsert({
where: { orgId: ctx.principal.org.id },
create: { orgId: ctx.principal.org.id },
update: {},
});
```
This lets an application work on the first request without waiting for a
webhook. Webhooks remain useful for asynchronous projections, cleanup, and
prewarming, but they should not be the only path that makes a verified request
usable.
## Authorize the record, not only the route
A route permission proves the subject may invoke an operation; it does not prove
that an arbitrary record id belongs to the same organization. Scope every lookup
to the signed organization:
```ts
const project = await db.project.findFirst({
where: {
id: req.params.projectId,
orgId: ctx.principal.org.id,
},
});
if (!project) return res.status(404).json({ error: "not_found" });
```
Use `requirePermission(ctx, "projects:write")` only for checks finer than the
compiled route policy. It reads the verified permission claim and fails closed
when the claim is absent. Hiding a frontend control is not a replacement for
either gateway enforcement or tenant-scoped database queries.
## Avoid identity drift
- Never accept an organization, member, service-account, subscription, or plan
id from the request body as authoritative.
- Never trust copied `x-fs-*` headers; the middleware removes them intentionally.
- Do not key application users by email. Emails can change and may not be unique
across identity providers.
- Store the stable ids you need, plus your own application metadata. Resolve
current plan or permission state from `ctx.signedContext` rather than
permanently copying it into an authorization column.
- For jobs queued after the request, persist the verified tenant/subject ids and
an immutable job id; recheck any mutable authorization needed when the job
executes.
---
# @farthershore/backend
Canonical URL: https://docs.farthershore.com/reference/backend-sdk
`@farthershore/backend` is the runtime SDK for your upstream. Install one package,
set one runtime credential (`FS_RUNTIME_TOKEN`), and Farther Shore handles signed
platform-to-upstream request verification plus response-bound usage reporting.
This guide targets the current 0.21 runtime API. Use the
[generated export reference](/generated/backend-sdk/root) for every public
signature and type. It versions independently from [`@farthershore/business`](/reference/business-sdk)
and [`@farthershore/farthershore-js`](/reference/frontend-sdk).
```bash
npm install @farthershore/backend
```
Mint the token with the CLI — it is returned **once**. Tokens may be scoped to
the business, one environment, or one backend, and may further restrict
operations, meters, and route identities:
```bash
farthershore backend tokens create croncloud --backend --format json --idempotency-key
```
`fartherShore.initFromEnv()` derives **everything** — business/backend ids, the
JWKS URL, the metering endpoint, verification config, transport — from
`FS_RUNTIME_TOKEN` via `POST /v1/runtime/bootstrap` (cached in memory, refreshed
lazily). The builder configures exactly one thing. See
[environment variables](/reference/env-vars).
## Quick start (Fetch handlers)
```ts
import { fartherShore } from "@farthershore/backend";
const fs = fartherShore.initFromEnv();
export async function POST(request: Request) {
const url = new URL(request.url);
const body = new Uint8Array(await request.clone().arrayBuffer());
const responseHeaders = new Headers();
// Fail-closed: throws FartherShoreError (→ 401) on any verification failure.
const ctx = await fs.verifyRequest(
{
method: request.method,
path: url.pathname,
query: url.search,
headers: request.headers,
body,
},
{
responseSink: {
canStampHeaders: () => true, // non-streaming: the handler has not returned
stampHeaders: (headers) => {
for (const [name, value] of Object.entries(headers)) {
responseHeaders.set(name, value);
}
},
},
},
);
const result = await runWorkflow(await request.json());
const report = await ctx.report({
meter: "workflow_usage", // matches an fs.meter() key in the business
values: { tokens_used: result.tokensUsed },
});
if (!report.ok) console.error("Usage delivery failed", report.reason);
return Response.json(result, { headers: responseHeaders });
}
```
The example assumes `runWorkflow` is your application function and a route bound
to `workflow_usage`. Map thrown verification errors to their typed HTTP status
in your framework; an uncaught exception alone does not produce a 401. The
response sink is required for Fetch-style in-band reporting. Without it,
`verifyRequest()` reports over the post-stream channel even before you return a
`Response`. Streaming adapters must stop permitting header stamps once headers
have actually been sent. Express middleware supplies this adapter for you.
## Express
Start with the [backend scaffold](/backend/scaffold), which creates `app` and
`fs` and installs raw-body capture → `fs.middleware()` → JSON parsing, in that
order. Preserve that pipeline: verification needs the original request bytes;
installing the verifier alone will reject signed nonempty bodies. The excerpt
below replaces only the scaffold's handler and startup section.
```ts
import { requireMember } from "@farthershore/backend";
// app and fs are initialized by the scaffold; body verification runs first.
app.post(
"/v1/cron-jobs",
fs.handler((ctx, req, res) => handler(requireMember(ctx).memberId, req, res)),
);
await fs.ready(app); // register routes first; reconciliation is diagnostic
app.listen(3000);
await fs.start(); // starts the configured embedded tunnel, if any
process.on("SIGTERM", () => void fs.shutdown());
```
## Verification
The platform signs each request with Ed25519 and stamps a signed `X-Fs-Context`
whose hash is bound into that signature. The SDK recomputes the canonical signing
string from the **actual** request and verifies the signature against a
JWKS-resolved key. Identity comes **only** from the verified context — the
plaintext `X-FS-*` headers are untrusted, and `fs.middleware()` **strips every
inbound `x-fs-*` header** after verification so a handler cannot read a spoofable
one. Every failure (missing / malformed / bad-signature / stale / clock-skew /
wrong-route / body-hash-mismatch / replayed-nonce / unknown-kid /
jwks-unavailable) throws and maps to **HTTP 401** (413 for oversized bodies).
`fs.middleware()` is **strict by default** — it verifies every request
fail-closed and the verified `req.fartherShore` is a guaranteed, non-optional
presence. Pass `{ always: false }` only for a backend that intentionally consumes
no identity, to defer to the bootstrapped `verification.required` flag. A direct
call to `verifyRequest()` always verifies.
### Guaranteed context: `fs.handler`
`fs.handler((ctx, req, res) => …)` wraps a route handler so it runs only with a
GUARANTEED verified context: `ctx` is a non-optional `FartherShoreRequestContext`
(read `ctx.principal` / `requireMember(ctx)` with no optional-chaining), else it
fails closed with a `401`. A thrown `FartherShoreError` /
`FartherShorePermissionError` (e.g. from `requireMember`) is mapped to its typed
status.
## Permission checks (Managed RBAC)
For products with [`rbac` enabled](../define/team-rbac), the verified context
carries the acting user's resolved permissions so you can do fine-grained,
in-handler checks beyond the route-level enforcement the edge already applied:
```ts
import { requirePermission, hasPermission } from "@farthershore/backend";
const ctx = await fs.verifyRequest({ ... }); // throws on any verification failure
requirePermission(ctx, "reports:write"); // throws FartherShorePermissionError (403) if missing
if (hasPermission(ctx, "exports:run")) {
// …
}
```
`ctx.permissions` comes **only** from the signed `X-Fs-Context` token — the
unsigned `x-fs-permissions` / `x-fs-roles` header fallback is gone, and those
headers are stripped before your handler runs. A `"*"` entry grants everything;
otherwise `subject:*` and exact `subject:verb` keys are supported. A missing
permission fails closed. `FartherShorePermissionError` carries `status: 403` and
code `permission_denied`.
## Metering
One verb on the verified context: `ctx.report({ meter, values, dims?, quote? })`.
The meter key is **not** hardcoded by the SDK — it must match an `fs.meter()`
declared in the [business](/reference/business-sdk); `values` are keyed by
measure key, and `dims` are optional catalog dimension selectors (string
values). Identity rides the verified context — there is no subscription or
request id argument to forget.
Transport is automatic and invisible:
| Moment | Transport | Network call? |
| ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| Before the response is sent | Signed in-band `x-fs-metering` headers; the platform verifies, settles, and strips them. | No. |
| After the response is on the wire: streams or deferred work for the original served request | The attested post-stream channel, carrying that request's served identity. Billing-only; not reusable for independent cron jobs. | One callback total per served request. |
```ts
await ctx.report({
meter: "model_usage",
values: { input_tokens: 1200, output_tokens: 850 },
dims: { model: "acme-4" }, // catalog dimension selectors
});
```
For a pricing rule declared `backendQuoted`, an optional
`quote: { currency, amountNanos }` field carries a proposed, non-negative rate
input that the platform clamps; backends never report money otherwise.
The quote is a **per-unit** rate, never a total: the platform multiplies it by
the measured quantity. Prefer a decimal integer string for `amountNanos` to
preserve precision.
One request uses one reporting transport. After a successful in-band report,
a later post-stream report is rejected with `ok: false`; accumulate the full
measurement before sending, or report the entire final batch after streaming.
Check the returned result. `ok: true` for an in-band report means headers were
stamped, not that billing settlement has already completed.
Plain request counting (from `fs.requests()`) is platform-managed and needs no
upstream code.
Only response-bound settlement can affect the request that is currently being
served. Post-stream and background reports arrive later and are billing-only;
they can affect later requests, not retroactively deny the completed one.
## The `fartherShore` instance
| Member | Description |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fartherShore.initFromEnv(options?)` | Construct an instance; derive everything from `FS_RUNTIME_TOKEN`. Throws `missing_token` / `invalid_token` eagerly. |
| `fs.middleware(options?)` | Express middleware, **strict by default**: verifies fail-closed, attaches `req.fartherShore`, and strips inbound `x-fs-*`. `{ always: false }` defers to the contract flag. |
| `fs.handler(handler)` | Wrap a route handler so it runs only with a GUARANTEED verified context — `handler(ctx, req, res)` with a non-optional `ctx`, else `401`. |
| `fs.verifyRequest(input)` | Framework-neutral verification primitive (`{ method, path, query, headers, body }`). |
| `ctx.report(input)` | THE reporting verb, on the verified request context: `{ meter, values, dims?, quote? }`. Transport is chosen automatically (in-band headers or post-stream). |
| `fs.ready(app?)` | Boot-time bootstrap and route reconciliation; reports drift and a ready heartbeat. Fail-open — never blocks boot. |
| `fs.start()` | Start the embedded `cloudflared` runner for a `tunnel` backend; no-op otherwise. |
| `fs.health()` | Local runtime health report. |
| `fs.shutdown()` | Graceful: flush metering + send a `stopping` heartbeat. |
| `fs.onShutdown(hook)` | Register an additional shutdown hook. |
`initFromEnv(options)` accepts `runtimeToken`, `coreUrl`, `env`, `fetchImpl`,
`verification: { enabled }`, `metering: { enabled }`, `tunnel`, and `instanceId`
for tests and advanced opt-outs — but the default DX is everything on, token only.
## Choose a workflow and entrypoint
| Task | Start here | API details |
| ---------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------- |
| Add an Express application | [Scaffold](/backend/scaffold), then [verification](/backend/metering) | [Express adapter](/generated/backend-sdk/express) |
| Bind an existing service | [Named backend](/cookbook/add-backend), [runtime tokens](/backend/runtime-tokens) | [Runtime contract](/generated/backend-sdk/runtime) |
| Record customer data | [Verified identity](/backend/user-data), [sharing](/cookbook/share-with-a-member) | [Root exports](/generated/backend-sdk/root) |
| Report usage or stream output | [Metering](/backend/metering) | [Root reporting types](/generated/backend-sdk/root) |
| Receive signed events | [Webhook recipe](/cookbook/add-webhook-consumer) | [Webhook receiver](/generated/backend-sdk/webhooks) |
| Check declared versus implemented routes | Call `fs.ready(app)` after route registration; inspect drift | [Reflection](/generated/backend-sdk/reflect) |
| Exercise a handler without credentials | Use the local test workflow below | [Testing](/generated/backend-sdk/testing) |
Reflection is diagnostic: it does not install routes or enforce permissions.
Express 5 mounted subrouters can be reported as `unreflectable`; a partial
reflection result is not proof of complete route coverage. Exercise those paths
explicitly in preview.
## Test without platform credentials
The `/testing` entrypoint supplies `createDevRuntime`, signed persona clients,
usage/trace sinks, and webhook signers. Use `mode: "simulated"` for local
verification and permission tests. Passthrough mode deliberately skips checks
and cannot prove authorization. Dev tooling rejects `NODE_ENV=production`.
Separate three proofs: your handler tests show business behavior, simulated
signed requests show verification/permission behavior, and preview traffic shows
the accepted route, backend binding, and settlement actually agree. The local
dev gateway accepts arbitrary meter keys; a green local report does not prove
the deployed measurement schema accepts them.
Test a valid member, a service principal on a member-only handler, a missing
permission, a changed signed body, and duplicate usage handling. Always shut
down the test runtime. Never deploy fixture keys or dev-mode configuration.
## Common public exports
| Export | What it is |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `fartherShore`, `initFromEnv` | The conceptual entrypoint and its top-level convenience twin. |
| `FartherShore`, `FartherShoreInstance` | The runtime class and its augmented type. |
| `ReportInput`, `ReportResult`, `ReportTransport`, `Measurement`, `MeasurementValues`, `MeasurementDimensions`, `QuoteInput`, `QuoteProposal`, `MEASUREMENTS_VERSION` | The `ctx.report()` verb's types. |
| `computeMeteringHeaders`, `MeteringError` | The framework-neutral signed-header wire recipe (for non-JS backends). |
| `FartherShoreError`, `statusForCode` | The typed verification error and its HTTP-status mapper. |
| `verifyRequest`, `FartherShoreRequestContext`, `VerifyRequestInput` | The standalone verification primitive + types. |
| `createExpressMiddleware`, `createExpressHandler`, `ExpressMiddleware`, `MiddlewareOptions`, `VerifiedExpressHandler` | Express adapter (strict middleware + guaranteed-context handler). |
| `JwksClient`, `NonceCache`, `BootstrapClient` | The lower-level clients `initFromEnv` composes. |
| `CloudflaredSupervisor`, `nodeSpawn`, `FartherShoreTunnelOptions` | The embedded tunnel runner (BYO-backend). |
| `buildHealthReport`, `reportHealth`, `ShutdownManager` | Health + shutdown helpers. |
| `FS_RUNTIME_TOKEN_ENV`, `RUNTIME_TOKEN_OPERATIONS`, `RUNTIME_HEADER_NAMES`, `MAX_BODY_BYTES`, `RUNTIME_CLOCK_SKEW_SECONDS`, `RUNTIME_REPLAY_WINDOW_SECONDS`, `RUNTIME_ERROR_CODES` | Shared contract constants (mirrors `@farthershore/contracts/runtime`). |
| `hashBody`, `buildCanonicalSigningString`, `signCanonicalString`, `verifyCanonicalSignature`, `canonicalizeQuery` | Signing primitives (one source of truth shared with the platform). |
| `METERING_PAYLOAD_HEADER`, `METERING_SIGNATURE_HEADER`, `METERING_TOKEN_HEADER`, `DEFAULT_TOKEN_ENV` | Response-metering header-contract constants. |
## Declaring a backend
A backend is declared in the product via `fs.backend()`. Bind routes to it with a
route's `backend`; a single backend is the default, otherwise mark one
`default: true`.
```ts
import * as fs from "@farthershore/business";
const prodOrigin = fs.backend("prod-origin", {
transport: { mode: "direct" },
verification: { required: true },
default: true,
});
const cronJobs = fs.route("/v1/cron-jobs", {
post: { backend: prodOrigin },
});
fs.plan("pro", {
kind: fs.plan.kind.flat,
price: fs.money.usd(29).monthly(),
grants: [cronJobs],
});
```
`fs.backend()` options: `name`, `slug`, `transport: { mode: "direct" | "tunnel",
runner }`, `verification: { required }`, `meters` (allow-list), and `default`.
Concrete origins are bound per environment during deployment. A route that
meters a dimension the backend's `meters` allow-list excludes is rejected at
build time.
---
# Add a backend
Canonical URL: https://docs.farthershore.com/cookbook/add-backend
Backend identity and route selection are repo-owned contract. The concrete
origin, environment row, readiness, and runtime token are platform-owned.
## Outcome
A named logical backend owns the intended routes, and the matching preview
environment has a verified concrete origin and scoped runtime token.
## Prerequisites
- A managed business repository and preview environment.
- A reachable HTTPS backend origin.
- The backend id returned by `backend list` after the contract is accepted.
## Declare the backend and route
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const analytics = fs.backend("analytics", {
transport: { mode: "direct" },
verification: { required: true },
});
const analyze = fs.route("/v1/analyze", {
post: { backend: analytics },
});
fs.plan("pro", {
kind: fs.plan.kind.flat,
price: fs.money.usd(29).monthly(),
grants: [analyze],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
Build and push to a preview branch. Once its apply is accepted, the platform
has an environment-scoped backend row with slug `analytics`.
```bash
farthershore build --format json
git push -u origin HEAD:env/backend-preview
farthershore apply-timeline inspect acme "$(git rev-parse HEAD)" \
--env backend-preview \
--format json
farthershore backend list acme --format json
```
## Bind runtime state
```bash
farthershore backend bind acme analytics \
--env backend-preview \
--origin-url https://analytics-preview.example.com \
--format json
farthershore backend tokens create acme \
--backend \
--env backend-preview \
--idempotency-key \
--format json
```
Store the one-time secret as `FS_RUNTIME_TOKEN` in the preview backend. Binding
is environment-specific. Until an environment-specific backend has a concrete
target, the preview inherits the matching production backend by logical slug.
To override it, create or accept the same logical backend in the preview, bind
its origin, and deploy its own scoped runtime token.
### Different upstream URLs per environment
Keep the same logical backend slug in the Business program. Bind a different
concrete upstream URL for each environment through the CLI:
```bash
farthershore backend bind acme analytics \
--env staging \
--origin-url https://analytics-staging.example.com \
--format json
farthershore backend bind acme analytics \
--env feature-preview \
--origin-url https://analytics-feature.example.com \
--format json
```
These commands assume `analytics` already has an environment row in each
selected environment. They change only that environment's binding, not the
logical route declaration or another environment's URL. Use `--dry-run` to
inspect a proposed binding without applying it. **Omitting `--env` targets
production**; agents should pass the intended environment explicitly and obtain
approval for production changes. Store each overridden environment's matching
runtime token in its own upstream service.
Preview resolution is `targeted environment override -> production backend` by
stable slug. A manifest-created preview placeholder with no origin does not
shadow a usable production backend. Partial overrides do not hide inherited
sibling backends, another preview is never consulted, and an environment-only
backend with no target fails safely with `origin_unavailable`. Deleting a
preview override reveals the inherited production backend again. Changing a
production binding queues republication for active previews that inherit it.
Verify one request through the preview runtime, confirm the backend reports
healthy, and confirm an unsigned direct request is rejected. Deleting a backend
revokes its runtime tokens; remove route references from the Business program
before deletion.
## Verify
```bash
farthershore backend list acme --format json
farthershore analytics log acme --env --range 1h --domain usage --format json
```
Require the expected backend slug, environment, origin, and ready status, then
prove one signed request through the preview gateway.
## Recovery
If binding fails because the inherited backend has no preview row, create the
preview override with the same slug first, then bind it. If a secret is exposed,
rotate the runtime token and deploy the replacement immediately. Remove
repository route references before deleting an environment-only backend.
## Agent prompt
```text
Add the named backend to the Business program, build and push it to preview,
bind only that environment's concrete origin, create a scoped runtime token,
and verify one signed gateway request. Stop before production changes.
```
See [Backends](/backend/overview) and
[Preview environments](/operate/environments).
---
# Connect a direct backend
Canonical URL: https://docs.farthershore.com/cookbook/direct-backend
## Outcome
The preview gateway reaches one public HTTPS backend only through a verified,
environment-specific direct binding.
## Prerequisites
- A preview environment and reachable HTTPS origin.
- Backend code that verifies FartherShore-signed requests.
- A safe secret store for the one-time runtime token.
## Create the preview runtime target
```bash
farthershore backend create acme \
--name "Preview API" \
--slug core \
--env preview \
--transport direct \
--origin-url https://preview-api.example.com \
--default \
--idempotency-key preview-core \
--format json
```
The first backend in an environment becomes its default unless you choose
another. Reusing the same slug in the same environment updates that runtime row.
## Declare the matching contract
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const core = fs.backend("core", {
transport: { mode: "direct" },
verification: { required: true },
meters: [requests],
default: true,
});
const health = fs.route("/health", {
get: { backend: core, costs: [requests.fixed(1)] },
});
fs.plan("free", {
kind: fs.plan.kind.free,
grants: [health],
limits: [requests.perMinute(60)],
});
export default fs.business();
```
`verification.required` defaults to true when omitted. The concrete origin URL
must not appear in `fs.backend()`; it belongs to the environment binding.
```bash
farthershore build --format json
git push -u origin HEAD:env/preview
farthershore backend tokens create acme \
--backend \
--env preview \
--idempotency-key \
--format json
```
Store the returned secret once as `FS_RUNTIME_TOKEN`, restart the backend, and
verify `backend list` reports a healthy status. Send one request through the
preview runtime and one unsigned request directly to the origin; only the
gateway-signed request should pass application verification.
If the token is exposed, rotate it and immediately deploy the new value. Token
rotation revokes the old token rather than providing a dual-validity window.
See [Backend request verification](/backend/metering) and
[Runtime tokens](/backend/runtime-tokens).
## Verify
Require a ready backend row and a successful gateway request. Send one unsigned
request directly to the origin and require rejection.
## Recovery
If the origin changes, bind the exact environment again and read it back. If a
runtime token is exposed, rotate it and deploy the replacement immediately; the
old token stops working without a dual-validity window.
## Agent prompt
```text
Create and declare one signed direct backend in preview, keep the concrete
origin out of the Business program, store the runtime token safely, and prove a
gateway-signed request succeeds while an unsigned origin request fails.
```
---
# Add a webhook consumer
Canonical URL: https://docs.farthershore.com/cookbook/add-webhook-consumer
Webhook endpoint lifecycle is platform-owned. Receiver code, raw-body signature
verification, idempotency, and asynchronous processing belong in your backend
repository.
## Outcome
The endpoint accepts a signed test delivery, records it idempotently, and can be
paused without changing the Business contract.
## Prerequisites
- A public HTTPS receiver that preserves raw request bytes.
- A durable event-id deduplication store and asynchronous work queue.
- The exact event types the receiver is prepared to process.
## Develop locally first
Use the Backend SDK's dedicated receiver. Webhooks have their own signing
secret; they do not carry gateway request signatures and do not require
`FS_RUNTIME_TOKEN`.
```ts
import express from "express";
import { createWebhookHandler } from "@farthershore/backend/webhooks";
const app = express();
const secret = process.env.FS_WEBHOOK_SECRET;
if (!secret) throw new Error("FS_WEBHOOK_SECRET is required");
const webhooks = createWebhookHandler({
secret,
on: {
"subscription.created": async (event) => {
await enqueueIdempotently(event); // your durable transaction/queue adapter
},
"payment.failed": async (event) => {
await enqueueIdempotently(event);
},
},
});
// Mount before gateway verification and before any JSON body parser.
app.post(
"/webhooks/farthershore",
express.raw({ type: "*/*" }),
webhooks.express(),
);
```
For a Fetch framework export `webhooks.fetch` as the handler. The receiver
verifies raw bytes and timestamp, parses the envelope, and dispatches known
events. Handler failures return a retriable failure. Unknown future event types
are acknowledged; use `onUnknown` for observability. A successful handler must
mean the event is durably recorded or completed, not merely scheduled in memory.
The default deduplication store is in-process. For multiple replicas or crash
recovery, implement the [WebhookNonceStore](/generated/backend-sdk/webhooks)
claim/settle contract and retain application-level idempotency in your durable
write. Do not substitute a check-then-insert cache; concurrent deliveries and
lease expiry can otherwise run side effects twice. Use
[`signWebhookForTesting`](/generated/backend-sdk/testing) to test modified bytes,
expired signatures, duplicates, and a handler that fails once then succeeds.
While the receiver still runs on your machine, let the CLI tunnel to it instead
of deploying to test:
```bash
farthershore webhook listen acme \
--forward-to http://localhost:3000/webhooks/farthershore \
--print-secret --trigger subscription.created
```
Export the printed `FS_WEBHOOK_SECRET=` line into the receiver's environment,
then watch the tail (`time type status responseStatus id`) while you fire
more samples from a second shell with
`farthershore webhook trigger acme --type payment.failed --idempotency-key `. Ctrl-C
deletes the temporary endpoint; if the listener was killed instead, remove the
leftover `*.trycloudflare.com` endpoint with `webhook delete --yes`.
## Create and test
```bash
farthershore webhook create acme \
--url https://api.example.com/webhooks/farthershore \
--events subscription.created,payment.failed \
--idempotency-key primary-webhook \
--format json
```
Capture any one-time secret without logging or committing it. Verify the
signature against the raw request bytes before JSON parsing, deduplicate by the
event identifier, return a `2xx` promptly, and queue slow work.
```bash
farthershore webhook test acme --idempotency-key --format json
farthershore webhook trigger acme --type payment.failed --idempotency-key --format json
farthershore webhook deliveries acme --limit 20 --format json
```
`webhook test` sends the plain `webhook.test` ping; `webhook trigger --type`
sends a signed, realistic sample of one catalog event so each handler branch is
exercised and logged under its own event type. A successful send is not proof
the receiver processed it; inspect the delivery response and your receiver's
durable record.
Pause deliveries while repairing a failing receiver:
```bash
farthershore webhook update acme --disable --format json
farthershore webhook deliveries acme --limit 20 --format json
```
Rotate the signing secret when it may have leaked or on a schedule. The new
secret is returned once; deliveries carry both signatures for 24 hours so the
receiver can switch without a gap:
```bash
farthershore webhook rotate acme --format json --idempotency-key
```
Re-enable only after a signed test succeeds. Deletion is destructive and
requires `--yes`:
```bash
farthershore webhook delete acme --yes --format json
```
Webhook delivery is independent of a person's notification preferences. See
[Notifications](/operate/notifications).
## Verify
Require a successful test delivery, a matching receiver-side durable record,
and a recent delivery row with the expected response. A `2xx` alone does not
prove downstream work completed.
## Recovery
Disable the endpoint while repairing repeated failures. Re-enable only after a
new signed test succeeds. Use `delete --yes` only when permanent removal is the
reviewed intent; deletion has no restore command.
## Agent prompt
```text
Create the webhook for only the listed events, capture the one-time secret
without logging it, send a signed test, and verify both the platform delivery
record and the receiver's durable deduplication record. Pause on failure.
```
---
# Frontend SDK
Canonical URL: https://docs.farthershore.com/frontend/overview
`@farthershore/farthershore-js` connects a browser UI to Farther Shore platform
state and to the business routes behind the gateway. The frontend never needs a
backend origin, Core URL, business id, environment id, or auth endpoint in its
source code when it is hosted by Farther Shore.
The business contract lives in `business/`; editable UI code lives in
`frontend/`. Plans, routes, meters, and permissions are repository contract
state. Navigation, page composition, CSS, and browser interactions belong to the
frontend application.
## Start with the managed frontend
The managed repository intentionally starts without sample frontend source. The
platform's standard subscriber experience remains available until you add a
custom `frontend/` application. A custom hosted frontend is an opt-in part of
the managed repository; create that project, install
`@farthershore/farthershore-js`, and keep its package and Vite configuration
under `frontend/`.
The local commands below require that `frontend/package.json` already exists:
```bash
pnpm --dir frontend install
farthershore frontend dev
```
`frontend dev` starts Vite with hot reload. With no live configuration it uses
deterministic mock data and bypassed auth for local page work. To serve a
production build locally:
```bash
farthershore frontend preview
```
Opt into real platform data with `--live`, `--core-url`, or
`VITE_FS_CORE_URL`. The CLI injects a temporary `window.__FS_CONFIG__` shim; it
does not edit your Vite config or frontend source.
## Zero-config client
Hosted HTML receives an environment-specific `window.__FS_CONFIG__` from the
edge. The SDK reads it lazily, so client creation is safe at module scope:
```tsx
import { createFartherShoreClient } from "@farthershore/farthershore-js";
import { FartherShoreRoot } from "@farthershore/farthershore-js/components";
const fs = createFartherShoreClient();
export function App() {
return (
);
}
```
The public client factory accepts application concerns such as organization
selection, mock mode, retry behavior, injected `fetch`, and error callbacks.
Platform routing and authentication are intentionally not part of custom hosted
frontend configuration; the platform supplies them.
`FartherShoreRoot` mounts the client provider, bootstrap gate, managed auth,
customer-readiness and legal gates, error boundary, and environment badge. A
signed-in customer does not reach application children until the selected
organization has an `ACTIVE` subscription with a non-null compiled plan. When
that entitlement is missing, the root presents the managed organization picker
and plan onboarding flow, then refetches the subscriber record before mounting
the application. A catalog entry alone is never treated as enrollment.
The root also includes the managed legal Markdown renderer and its GFM runtime
dependencies. A frontend using the root must not install `react-markdown` or
`remark-gfm` separately. The SDK ships no CSS; the frontend repository owns
presentation.
## Request boundaries
| SDK surface | Destination | Credential |
| ------------------------------------------------------- | ---------------------------------------- | ---------------------------------- |
| `fs.bootstrap()` and public business resolution | Farther Shore platform | none |
| keys, usage, billing, plans, account and team resources | Farther Shore platform | subscriber session |
| `fs.route.get/post/…` | business gateway route | SDK-managed signed browser context |
| `fs.integration(id)` | compiled same-origin integration gateway | signed-in subscriber session |
The platform injects environment routing and the SDK attaches its managed
browser context. Your code does not concatenate an environment hostname, select
a backend origin, receive a bearer, or supply an access key. A route call is
still enforced against the subscriber's current plan, permission, subject type,
subscription state, and limits at request time.
```ts
const jobs = await fs.route.get("/v1/jobs");
```
Use typed errors and stable denial codes for UI remediation; never treat a hidden
button as authorization.
## Git-triggered hosting
Hosted frontend releases follow repository events. There is no separate manual
deployment trigger.
| Scope | Build trigger | Activation |
| ------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Production | Publish a GitHub Release from the managed repository | Successful release build becomes the production frontend according to release policy |
| Preview environment | Push to that environment branch | Successful build becomes the frontend for that environment |
| Local | `farthershore frontend dev` or `frontend preview` | Local Vite process only |
After a push or GitHub Release, inspect the latest known build for that exact
source revision:
```bash
farthershore frontend status my-business \
--ref "$(git rev-parse HEAD)" \
--wait
```
For a preview environment:
```bash
farthershore frontend status my-business \
--env \
--ref "$(git rev-parse HEAD)" \
--wait \
--timeout 900
```
`status` reports the current release plus recent builds and failure reasons.
`--ref` limits the observation to the latest known build for that immutable
source revision; it does not identify a particular webhook delivery. Exact
attempt proof requires the build id returned by an enqueue response when one is
available. `--wait` exits nonzero if the observed build fails or the timeout
elapses.
A change to an `FS_PUBLIC_` Variable also enqueues a frontend rebuild because
its value is baked into the bundle. A write-only secret does not rebuild the
frontend — it never enters the bundle; it is published to the edge for a named
integration that references it.
## Roll back
Find a previously successful release id in `frontend status`, then reactivate
it:
```bash
farthershore frontend rollback my-business --release-id
```
For production, omitting `--env` changes the active hosted artifact and pins the
production target. A later Release can build the repository fix, but the
successful build does not autoactivate while the pin remains. After proving the
new release id is healthy, explicitly reactivate it with the same `frontend
rollback` command and read status back. There is no separate unpin command.
For a preview environment, add `--env `. Preview rollback
changes the active artifact but **does not pin it**: the next successful build
for that environment autoactivates. Treat preview rollback as temporary
containment, stop or fix the source that is producing bad builds, and verify the
active release again after every preview build.
## React hooks
For custom presentation, mount `FartherShoreProvider` directly or use it through
`FartherShoreRoot`. Hooks follow the common shape `{ data, error, isLoading,
isError, isSuccess, refetch, queryKey }` and add domain mutations:
```tsx
import { useApiKeys, useUsage } from "@farthershore/farthershore-js/react";
function Usage() {
const usage = useUsage();
if (usage.isLoading) return
Loading…
;
if (usage.error) return
Usage unavailable.
;
return
{JSON.stringify(usage.data, null, 2)}
;
}
```
See [Root and data components](/frontend/components), [Auth and
sessions](/frontend/auth), and [Variables](/frontend/variables) for the three
integration boundaries most custom portals need.
---
# Root & data components
Canonical URL: https://docs.farthershore.com/frontend/components
The `/components` entrypoint supplies a managed root plus headless subscriber
components. They fetch through the same client and render stable `.fs-*` class
hooks, but the package ships no component stylesheet and owns no application
router.
## The managed root
```tsx
import { createFartherShoreClient } from "@farthershore/farthershore-js";
import {
ApiKeysPanel,
BillingSummary,
FartherShoreRoot,
FsSignIn,
PlansTable,
SignedIn,
SignedOut,
UsageCard,
} from "@farthershore/farthershore-js/components";
const fs = createFartherShoreClient();
function Application() {
return (
);
}
export function Portal() {
return (
);
}
```
`FartherShoreRoot` provides:
- `FartherShoreProvider` and one cached bootstrap boundary;
- the environment-selected auth provider;
- a signed-in organization + subscription readiness gate;
- payment and legal-consent gates;
- an error boundary and configurable resolve/crash fallbacks;
- a `.fs-app` shell and an automatic test-environment badge.
The root mounts the selected auth provider and context; it does not implicitly
render a sign-in form. Custom portals must render a managed signed-out surface,
such as ``, and keep private application UI inside ``.
Its public customization props are `clerk`, `splash`, `renderError`,
`renderCrash`, `envBadge`, `skipAppShell`, `skipAuth`, and `skipBootGate`.
Hosted frontends normally provide only `client` and `children`.
For a signed-in customer, application children mount only after the root has
loaded subscription contexts, selected a subscribed organization when one is
available, and confirmed an `ACTIVE` subscriber whose `compiledPlanId` is
non-null. Otherwise the root renders the managed workspace picker and plan
onboarding surface. Free onboarding may omit `compiledPlanId`; Core selects the
current free offer, and the root refetches `/me` before exposing the app. Paid
onboarding still uses the selected compiled offer. Do not recreate this state
machine from catalog labels or a successful checkout response.
```tsx
}
renderError={(error, retry) => (
)}
renderCrash={(error, reset) => (
)}
envBadge
>
```
`envBadge` defaults on and renders nothing in production. The `skip*` options
are for hosts deliberately replacing a platform layer; skipping the boot gate
also means `useBoot()` is unavailable.
## Read resolved bootstrap data
Children render after the first successful resolve. Within the root, `useBoot()`
returns the resolved business, branding, environment, and plans without a null
state:
```tsx
import { useBoot } from "@farthershore/farthershore-js/react";
function Header() {
const boot = useBoot();
return
{boot.branding.displayName}
;
}
```
During a background refresh the root keeps the last good bootstrap value instead
of unmounting the application.
## Self-managed data components
Under the root, these components work with zero required data props:
| Component | Subscriber surface |
| ------------------------------------------- | ----------------------------------------------------- |
| `PlansTable` | plan catalog and subscription action |
| `UsageCard` | current metered usage |
| `BillingSummary` | subscription and billing state |
| `ApiKeysPanel` | create, rotate, and revoke subscriber keys |
| `BillPreviewCard` | the subscriber's bill preview (transparent or opaque) |
| `ResourceLimitUsageCard` / `ResourcesPanel` | counted resource usage |
| `TeamPanel` / `FsAccessControl` | team and managed RBAC |
| `TrialBanner` / `CancelSubscription` | subscription lifecycle |
| `BusinessDocs` | published business docs |
Each component accepts `className` and exports a typed props interface. Many
offer optional slots or render callbacks, but the default data source remains
the current SDK client.
`BusinessApiReference` is a separate rendering primitive and requires a
`document` prop containing your OpenAPI document. It does not fetch a reference
document automatically. See the [API reference example](/reference/frontend-sdk#api-reference-primitive)
for the typed input and rendering call.
`UsageCard` shows recent usage, not a settled invoice. Its snapshot is a bounded
sample marked `exact: false`. Use `BillPreviewCard` for platform-rated money and
preserve its transparent/opaque disclosure behavior. Do not sum sampled events
or multiply a displayed allowance label into an amount owed.
## Component gating
The components in the managed vocabulary resolve their own render permission
through the component-policy resolver before showing anything — `UsageCard`
requires `usage:read`, `ApiKeysPanel` requires `apikey:read`, `TeamPanel`
requires `team:read`, and so on. A member whose role lacks the permission
sees an explicit "you don't have access" panel by default; the
security-sensitive components (`AuditLog`, `ApiKeysPanel`, `TeamPanel`) hide
entirely instead. Subscriber organizations can re-gate or change the deny
rendering per component from the access-control panel. Re-gating is
**additive** for two families: the security-sensitive set keeps its managed
permission as a confidentiality floor, and data-floor components
(`FsUsageLimits`) keep the permission their data fetch is server-gated on —
in both cases the selected permission is required _in addition to_ the
managed one, never instead of it (the access panel labels these rows).
A few components deliberately do NOT self-gate: presentational surfaces
(`TrialBanner`, `UpgradePrompt`, docs/reference views) and inline summaries
designed to compose inside an already-gated page (`BillPreviewCard`,
`ResourceLimitUsageCard` / `ResourcesPanel`, `CancelSubscription` — its
mutation still gates per-control on `subscription:cancel`). Mount those
inside a gated route or wrap them yourself. Your own components can join the
managed system — see [Permission gates](/frontend/permission-gates) and
[Custom components](/frontend/custom-components).
## Limit presentation
Catch typed `LimitExceededError` values from route calls. You can render a
specific `LimitNotice` or `UpgradePrompt`, or mount one global
`FsLimitBoundary` and report caught errors. **Initial prepaid plan purchase** is
already handled by `PlansTable` and `FsOnboardingPlanRail` through
`fs.plans.subscribe()` / `fs.plans.startOnboarding()`. For a later refill there
is no managed component or typed `fs.billing` method yet: render a
[subscriber-owned refill control](/cookbook/prepaid-credits#add-the-subscriber-refill-control)
that uses the public `fs.core()`
[top-up endpoint](/generated/commerce/http#createportalconsumerbalancetopup).
Do not substitute a builder CLI/MCP call; the subscriber session owns the
purchase.
```tsx
import {
FsLimitBoundary,
useLimitHandler,
} from "@farthershore/farthershore-js/components";
import { LimitExceededError } from "@farthershore/farthershore-js";
function CreateJob() {
const limits = useLimitHandler();
async function create() {
try {
await fs.route.post("/v1/jobs", {});
} catch (error) {
if (error instanceof LimitExceededError) limits.report(error);
else throw error;
}
}
return ;
}
;
```
The boundary changes presentation only. The gateway made the actual limit
decision.
## Navigation and styling
Create frontend routes, sidebar entries, layout, and CSS in `frontend/`.
Business route `surfaces` constrain which credential surfaces may call an API
operation; they do not generate browser pages or navigation.
Use the stable `.fs-*` class names as hooks or wrap components with your design
system. Do not expect a package CSS import: the managed repository's stylesheet
is ordinary editable application code.
For a thinner integration, mount `FartherShoreProvider` from `/react` directly
and use hooks without the root's boot/auth/shell gates.
---
# Auth & sessions
Canonical URL: https://docs.farthershore.com/frontend/auth
`` selects the subscriber auth strategy from the environment's
bootstrap data and mounts the matching provider:
- `clerk` uses the platform-injected Clerk connection and installs the current
subscriber session token on SDK requests;
- `test-personas` reads Core's server-owned browser session through the SDK;
- local mock mode supplies a deterministic signed-in owner without making
network requests.
These are subscriber sessions inside the hosted frontend. They are separate
from builder CLI login and backend runtime tokens.
`` mounts the provider and auth context, but it does not render
a sign-in surface for signed-out visitors. A custom portal must render a managed
auth component explicitly and keep its application under the signed-in gate.
## Mount auth once
```tsx
import { createFartherShoreClient } from "@farthershore/farthershore-js";
import {
FartherShoreRoot,
FsSignIn,
SignedIn,
SignedOut,
} from "@farthershore/farthershore-js/components";
const fs = createFartherShoreClient();
export function App() {
return (
);
}
```
Hosted environments normally need no Clerk prop: the edge injects the public
connection into `window.__FS_CONFIG__`. A custom non-hosted shell can pass a
public Clerk configuration to the root, but it must never embed a secret key.
## Read the normalized auth surface
`useFsAuth()` presents the same shape for either live strategy:
```tsx
import { useFsAuth } from "@farthershore/farthershore-js/react";
function AccountButton() {
const auth = useFsAuth();
if (!auth.loaded) return ;
return auth.signedIn ? (
) : null;
}
```
The important fields are:
| Field | Meaning |
| ----------------------------------------- | ------------------------------------ |
| `strategy` | `clerk` or `test-personas` |
| `loaded` | auth initialization has completed |
| `signedIn` | a subscriber session is present |
| `user` | normalized current user or `null` |
| `signOut()` | revokes the active server session |
| `roles`, `permissions`, `hasPermission()` | server-resolved member authorization |
| `authzLoaded` | permission resolution has completed |
`useFsAuth()` must be under the managed auth provider. Use
`useOptionalFsAuth()` only for a shared component that intentionally renders
outside it.
The component subpath also exports `SignedIn`, `SignedOut`, `AuthLoading`,
`FsSignInButton`, `FsSignOutButton`, and `FsUserButton` for declarative chrome.
## Preview persona browser sessions
Create a persona, then start its browser session from the authenticated CLI:
```bash
farthershore persona bootstrap my-business \
--env preview \
--idempotency-key \
--plan starter
farthershore persona login my-business --env preview
```
`persona login` opens the platform-owned `/persona-sign-in` page on the hosted
portal origin. Its single-use handoff secret is carried only in the URL fragment,
which the page clears before making a same-origin exchange. Core then sets a
server-owned HttpOnly cookie and the page returns to the requested portal route.
The custom frontend bundle never receives the handoff secret, a bearer, or an
access key.
The bootstrap key is returned once for CLI and gateway testing only. Do not paste
it into a web form, add it to frontend configuration, or persist it in browser
storage. A persona key belongs to its selected preview environment and is not a
production credential.
Use the safe identity projection on the SDK session to render custom
persona-aware UI:
```tsx
import { useSession } from "@farthershore/farthershore-js/react";
function PersonaName() {
const session = useSession();
const persona = session.data?.authSession;
if (!persona) return null;
return {persona.displayName ?? persona.userId};
}
```
## Local live preview as a persona
While building the frontend, run the checkout itself against the real
environment, already signed in, with hot reload (CLI 0.33.5+):
```bash
farthershore frontend dev --live --business my-business --env preview \
--persona --port 5173 --format json
```
The CLI resolves the business, environment, and persona, issues a short-lived
local-preview lease that never leaves the CLI process, starts Vite on a private
loopback port, and serves `http://localhost:5173` through a CLI-owned proxy:
`/_fs/api/*` calls carry the lease to Core, `/_fs/secure/*` (`fs.fetch`) goes to
the environment's gateway, and everything else reaches Vite with the session
cookie, `Authorization`, and all trust headers stripped — including HMR
WebSocket upgrades. The browser opens a platform-owned `/persona-sign-in` page
on `localhost` whose URL carries no secret; the proxy attaches the one-time
handoff itself and Core sets the same server-owned HttpOnly cookie as the hosted
portal. `frontend preview` serves the production bundle the same way.
The JSON envelope is emitted only once the preview is signed in and carries
`localUrl`, `signInUrl`, and `vitePort`. The lease renews itself while the
command runs; SIGINT, SIGTERM, or SIGHUP stops Vite and revokes it, and the
cookie stops authenticating immediately. Lifecycle notices (a renewal that keeps
failing, Vite exiting) are written to stderr in every output mode. `--mock` and
`--core-url` cannot be combined with `--persona`. The
`persona.local_preview.issue|renew|revoke` operations exist only for this
command; never call them by hand.
`authSession` contains only verified, non-secret session metadata. It is not a
token source. For a custom logout control, call `fs.auth.signOut()` (or the
`signOut()` returned by `useSession()`); it asks Core to revoke the server cookie
before the SDK clears its local read caches. If revocation fails, retain the
authenticated UI and surface the failure rather than pretending the browser is
signed out.
## Gate an application route
The SDK is router-agnostic. `` renders only after auth is allowed;
your router performs navigation through `onRedirect`:
```tsx
import { RequireAuth } from "@farthershore/farthershore-js/components";
import { useNavigate } from "react-router-dom";
function JobsPage() {
const navigate = useNavigate();
return (
Signing in…
}
redirectTo="/"
onRedirect={(target) => navigate(target)}
>
);
}
```
For custom routing, `useAuthGuard({ requireAuth: true })` returns `loading`,
`allowed`, `redirecting`, or `denied` plus a redirect target when relevant. It
does not navigate. By default it stores the current path in `sessionStorage`
under `fs-return-to`; this is a return-location hint, never a credential. Pass
`returnToKey: null` to disable that behavior.
`RequireAuth` and `useAuthGuard` can also require one permission, but that check
only controls presentation. The gateway remains the security boundary for the
route call, and the backend must still scope application records to the verified
organization.
## Session failure behavior
The SDK uses the active provider for every request instead of asking application
code to cache credentials. When Core no longer accepts the persona cookie, the
managed provider returns the UI to its signed-out flow. Application code should
handle the typed 401/permission/limit errors from the requested operation and
must never copy a bearer or access key into browser storage.
---
# Access-aware UI
Canonical URL: https://docs.farthershore.com/frontend/access-aware-ui
A subscriber can be unable to use a control for different reasons. Keep those
reasons separate so the UI offers the right remedy:
| Axis | Question | UI response | Security boundary |
| ----------------- | ------------------------------------------------------------------------------------- | ---------------------------------------- | ----------------------------------- |
| Authentication | Is there a current subscriber session? | sign in | session validation |
| Member permission | May this member perform the action? | hide, disable, or request access | gateway route permission |
| Plan/subscription | Does the subscriber's pinned plan grant the route and is access active? | choose/upgrade/repair subscription | gateway plan and subscription check |
| Dynamic limit | Is this request within quota, rate, capacity, concurrency, spend, or adaptive limits? | retry, reduce, queue, top up, or upgrade | gateway admission decision |
The frontend can present observed state, but the route call is authoritative
because plan, role, subscription, and usage can change after render.
## Compose authentication and permission presentation
```tsx
import {
AccessDenied,
PermissionGate,
RequireAuth,
} from "@farthershore/farthershore-js/components";
export function CreateReportButton() {
return (
Sign in to create reports.}>
}
>
);
}
```
`PermissionGate` uses the server-resolved current member claim. It is a
presentation primitive, not a client-side grant database.
## Handle the actual call
```tsx
import {
FartherShoreApiError,
LimitExceededError,
retryWhileThrottled,
} from "@farthershore/farthershore-js";
async function createReport(input: unknown) {
try {
return await retryWhileThrottled(() => fs.route.post("/v1/reports", input));
} catch (error) {
if (error instanceof LimitExceededError) {
showLimitNotice(error);
return;
}
if (error instanceof FartherShoreApiError) {
showStableDeny(error.code);
return;
}
throw error;
}
}
```
Only retry when the denial envelope says the request is retry-safe. Preserve an
application idempotency key for writes and bound retry attempts. Quota/spend
upgrade reactions and capacity reductions are not fixed by blind backoff.
`LimitNotice` maps the current limit class to the correct explanation.
`FsLimitBoundary` can show a global prompt for a caught `LimitExceededError`.
Use `UpgradePrompt` only for plan or funding remedies; a permission denial should
offer access-request or administrator guidance instead.
## Do not duplicate the contract
Plan grants are route/group refs in `business/`. Permission constraints and
subject requirements also live on the route contract. Do not create a second
hard-coded client feature map and treat it as authority.
It is reasonable to use bootstrap, entitlement, resource-limit, usage, and
permission hooks to reduce dead-end interactions. Every mutation must still
handle a deny from the gateway, and backend record queries must still use the
verified organization.
## Avoid content flashes
- Hold protected content while auth is loading.
- Hold permission-gated content until `authzLoaded` or
`usePermissionGate()` resolves.
- Do not optimistically reveal plan-restricted functionality from a stale local
cache.
- After plan, role, or organization changes, let the SDK invalidate/refetch its
resources instead of manually mutating several copies of access state.
These rules keep the UI responsive without moving authorization into the
browser.
---
# Permission gates
Canonical URL: https://docs.farthershore.com/frontend/permission-gates
Permission gates answer one question: does the signed-in member's current role
grant a permission? They do not answer whether the plan grants a route or whether
a usage limit currently admits the request.
Permission keys use the same grammar as gateway enforcement: exact
`subject:verb`, subject wildcard `subject:*`, or global `*`. There is no implicit
verb widening.
## Gate a component
```tsx
import {
AccessDenied,
PermissionGate,
} from "@farthershore/farthershore-js/components";
}
>
;
```
While auth and authorization resolve, the component renders an aria-busy
placeholder instead of flashing protected content.
`PermissionGate` supports four deny modes (the contracts gate-mode
vocabulary):
- `hide` renders nothing;
- `denied` renders the supplied `fallback` node, or a default `AccessDenied`
panel naming the missing permission;
- `disable` wraps children in an inert disabled fieldset;
- `readOnly` renders children while `usePermissionReadOnly()` returns `true`.
For a bare `permission` gate the default is `hide` — hiding an affordance the
member cannot use. For a `component` gate (below) the default comes from the
resolved component policy: `denied` for most components, `hide` for the
security-sensitive set (`audit_log`, `api_keys_panel`, `team_panel`), unless
the builder registration or a subscriber-org override configured one.
Components that support read-only composition can consume the context:
```tsx
import {
PermissionGate,
usePermissionReadOnly,
} from "@farthershore/farthershore-js/components";
function ReportEditor() {
const readOnly = usePermissionReadOnly();
return ;
}
;
```
## Gate by component id
Pass `component` instead of `permission` and the gate resolves the permission
AND the deny render through the fail-closed component-policy resolver —
builder registration, then the subscriber organization's override, then the
managed defaults:
```tsx
```
The same resolution backs `withGate` (wrap once, gate everywhere the component
mounts) and the `useComponentGate` hook:
```tsx
import {
useComponentGate,
withGate,
} from "@farthershore/farthershore-js/components";
const GatedReports = withGate(ReportsPanel, "custom:reports");
function ReportsNavLink() {
const gate = useComponentGate("custom:reports");
if (gate.status !== "granted") return null;
return Reports;
}
```
Register your own component ids and let subscriber orgs configure them — see
[Custom components](/frontend/custom-components).
## Check imperatively
```tsx
import { usePermissionGate } from "@farthershore/farthershore-js/components";
function ExportButton() {
const permission = usePermissionGate("reports:export");
if (permission.status !== "granted") return null;
return ;
}
```
Or use the current auth surface when several checks share one component:
```tsx
const auth = useFsAuth();
if (auth.authzLoaded && auth.hasPermission("team:invite")) {
// render invite action
}
```
The claim is resolved by the server. Do not decode a token in the browser or
replace it with a locally cached list.
## Combine with authentication
`RequireAuth` can also require a permission. A signed-out visitor follows the
auth redirect path; a signed-in but under-permissioned member sees the fallback
without a redirect:
```tsx
}
onRedirect={(target) => navigate(target)}
>
```
`AccessDenied` can receive an `onRequestAccess` callback when your product wants
to create an access request. Keep that workflow distinct from an upgrade prompt:
roles are controlled by the subscriber organization; plans are commercial
entitlements.
## Mirror fine-grained checks in the backend
Route-level permission requirements are enforced before forwarding. For a
record- or field-level rule inside a handler, use the verified backend context:
```ts
import { requirePermission } from "@farthershore/backend";
fs.handler(async (ctx, req, res) => {
requirePermission(ctx, "reports:export");
// still scope the record query to ctx.principal.org.id
});
```
Every frontend gate is UX. A user can call an endpoint without rendering your
component. The compiled gateway policy, verified backend context, and
tenant-scoped database query are the authorization boundary.
---
# Custom components
Canonical URL: https://docs.farthershore.com/frontend/custom-components
The managed data components self-gate through the component-policy resolver:
each id maps to a render permission, a mutating permission, and a deny
render. A subscriber organization can override the RENDER permission and the
deny render from its access-control panel (the mutating `writePermission` is
builder/managed-owned — overlays do not carry it). Your own components can
join the same system — one registration makes a builder component
permission-aware, org-configurable, and consistent with everything the SDK
ships.
## 1. Declare the permission vocabulary
Custom permissions use builder-defined subjects, declared as a **permission
group** in the business program (see
[Custom permission subjects](/define/team-rbac#custom-permission-subjects)):
```ts
fs.group("reports", [listReports, generateReport], {
permission: { verbs: ["generate"] },
});
```
Publish the business and the subject syncs on apply. The verbs available for
UI gating are the group's `reports:read` / `reports:write` pair (derived from
its routes' HTTP methods) plus any declared extra verbs, such as
`reports:generate`. Roles grant them like any managed permission — the pair
and every extra verb must be selected explicitly by the subscribing
organization when it composes a role. There is no role seeding and no verb
widening — a role holds exactly the strings it was granted.
## 2. Register the component id
Component ids are namespaced `custom:` — the slug starts with a
lowercase letter followed by 1–31 more of `a-z`, `0-9`, `-`, `_` (2–32
characters total, the same budget as custom permission subjects, so the
slug-derived subject is always declarable). Register the
gate policy once at module scope, in a module that is **eagerly imported
during app bootstrap** (your entry module, or a `component-registry.ts`
imported from it):
```tsx
import { registerComponent } from "@farthershore/farthershore-js/components";
registerComponent({
id: "custom:reports",
writePermission: "reports:write",
});
```
The resolver derives sensible defaults when fields are omitted — a
`custom:reports` registration with no permission resolves `reports:read`
(from the slug), and the deny render defaults to an explicit `AccessDenied`
panel (`"denied"`).
> **Registration must run before the first gate resolves.** The registry
> is module-global and NOT reactive: a gate that resolves `custom:reports`
> before the registering module is imported falls back to the derived
> defaults, and nothing re-renders when the registration arrives later. In
> a code-split app, do not rely on the lazy page's own import to register
> its component — a sidebar item or an outer route gate resolves the policy
> before the chunk loads, and a pinned `permission`/`gateMode` would not
> apply. Keep registrations in an eagerly-imported bootstrap module.
Registration precedence matters: a field you SET in `registerComponent` is
authoritative — the resolver reads registration → subscriber-org override →
defaults, in that order. The example deliberately omits `permission` and
`gateMode` so subscriber orgs can re-gate the component and change its deny
rendering from their access panel; register a field only when your product
must pin it.
## 3. Gate the component
```tsx
import { withGate } from "@farthershore/farthershore-js/components";
const ReportsPanel = withGate(ReportsPanelImpl, "custom:reports");
```
`withGate` resolves registration → subscriber-org override → derived defaults
on every mount. ``,
`useComponentGate("custom:reports")`, and
`usePermissionAction("custom:reports")` (for mutating controls inside the
panel) read the same resolution — see
[Permission gates](/frontend/permission-gates).
## 4. Subscriber organizations configure it
The access-control panel lists every managed component, plus each
`custom:` key that already carries an override row (registrations live
in your bundle, so the first override announces the key to the server — write
it with `fs.rbac.componentPolicies.update({ componentKey: "custom:reports",
... })` or the `/me/component-policies` API). From then on an org admin can
re-gate the component onto a different permission or change how a deny
renders (`hide`, `denied`, `disable`, `readOnly`); the change reaches every
member's portal through the same `/me` policy overlay the managed components
use, with a governed audit trail.
## How the pieces travel
Registration lives in your bundle; the org's overrides live server-side and
arrive on the authenticated `/me` document; role grants propagate to the edge
for enforcement. UI gating updates when the member's `/me` document is
re-fetched — on sign-in, an organization switch, or a page reload. A member
who keeps the portal open sees the OLD gate state until one of those
refreshes happens; edge enforcement updates independently (and first), so a
stale UI never widens what the gateway actually allows.
Component gating is UX. A member can call an endpoint without rendering your
component, so the route's permission constraint at the gateway — and your
backend's verified context — remain the authorization boundary. Gate the data,
then gate the pixels.
---
# Variables
Canonical URL: https://docs.farthershore.com/frontend/variables
Variables are platform-owned, environment-resolved values stored outside the
repository. There is nothing to configure about how a value is delivered: **the
name is the class.**
| Name | Class | Browser readable? | Where it goes | Rebuild after a change? |
| ------------- | ------ | ------------------- | --------------------------------------------------------------------------------------------- | ----------------------- |
| `FS_PUBLIC_*` | public | Yes — every visitor | the frontend build environment, then the shipped bundle | Yes |
| anything else | secret | No — write-only | the frontend build environment (leak-scanned), and the edge when an integration references it | No |
A secret is never returned by the API, CLI, or dashboard after you set it; you
can rotate it, not read it back. A public value is returned by variable listing
because anyone can already read it from the shipped bundle.
Farther Shore Variables do not set environment variables in your backend
application process. Put `FS_RUNTIME_TOKEN`, database URLs, and other backend
process secrets in that deployment provider's secret manager.
## Public: `FS_PUBLIC_` values ship to every visitor
Use the `FS_PUBLIC_` prefix only for values the provider explicitly documents
as safe for every visitor: a PostHog project key, a Stripe publishable key, a
Sentry DSN, a Supabase anonymous key.
```bash
printf '%s' "$POSTHOG_PROJECT_KEY" | \
farthershore variables set my-business FS_PUBLIC_POSTHOG_KEY --idempotency-key
```
The value enters the frontend build and is inlined into the bundle. Read it in
the managed frontend as `import.meta.env.FS_PUBLIC_POSTHOG_KEY`. It is
deliberately excluded from secret leak scanning, because appearing in the
bundle is its purpose.
The dashboard asks for one confirmation before creating a public variable.
Never put a secret key behind the prefix, regardless of what the value is
called elsewhere.
The retired bundler prefixes (`PUBLIC_`, `NEXT_PUBLIC_`, `VITE_PUBLIC_`) are
rejected as names, so a public-looking name can never be a secret by accident.
## Secret: everything else stays private
A variable without the prefix is a secret. It is available to the isolated
frontend build (the runner exposes it to the build command, removes the
temporary assignment, and fails the build if the value survives into the
output in any supported encoding), and it is injected at the edge for any
compiled `fs.frontendIntegration()` that references it by name.
```bash
printf '%s' "$SENTRY_AUTH_TOKEN" | \
farthershore variables set my-business SENTRY_AUTH_TOKEN --idempotency-key
printf '%s' "$PROVIDER_SECRET" | \
farthershore variables set my-business PROVIDER_SECRET --idempotency-key
```
```ts
const response = await fs.integration("provider").fetch("/v1/messages", {
method: "POST",
body: { kind: "json", value: { message } },
});
```
You do not declare where a secret is used. When a compiled integration names
it in `secretRef`, the platform publishes it to the edge; when no active
integration references it any more, it is retired from the edge. Browser code
never receives the secret or chooses the upstream URL. Referencing an
`FS_PUBLIC_` name from `secretRef` is a compile error.
A secret is not a runtime server environment. Hosted frontends are static
artifacts; if browser behavior needs a private credential at request time, use
a backend route or a compiled integration.
## Environment resolution
Variables are resolved by name for the selected business environment:
- a branch environment value overrides Main;
- when the branch has no row for that name, it inherits Main;
- changing a branch value does not modify Main;
- deleting/revoking the branch override reveals the inherited effective state
only after the lifecycle operation completes.
Use `--env` with a preview environment name or id:
```bash
printf '%s' "$STAGING_KEY" | \
farthershore variables set my-business FS_PUBLIC_POSTHOG_KEY --env staging --idempotency-key
farthershore variables list my-business --env staging --format json
```
The frontend build and the edge path resolve the same environment selection.
Environment routing is platform-owned; frontend code should not switch values
by hostname itself.
## Rebuild behavior
A public value is part of the bundle, so creating, rotating, revoking, or
deleting one enqueues a rebuild for that environment once the new generation is
effective.
A secret does not rebuild the frontend. It never enters the bundle; the edge
uses the new generation as soon as it is published, and the next build picks
it up on its own.
Monitor the mutation and any resulting build:
```bash
farthershore variables status my-business --format json
farthershore frontend status my-business --wait
```
Because a variable mutation has no Git commit identity, this unpinned wait is
advisory only. Add `--env ` to `frontend status` for a preview
build. For a Git-triggered build, use `--ref "$(git rev-parse HEAD)" --wait`
instead.
## Rotate, revoke, and delete
Values are read from stdin, never from a CLI argument:
```bash
printf '%s' "$NEW_VALUE" | \
farthershore variables rotate my-business PROVIDER_SECRET --idempotency-key
farthershore variables revoke my-business PROVIDER_SECRET --yes --idempotency-key
farthershore variables rm my-business PROVIDER_SECRET --yes --idempotency-key
```
Names are immutable. Because the name is the class, turning a secret into a
public value (or back) means revoking, deleting, and creating it under the new
name — a value can never silently cross that boundary.
`revoke` removes a value from future builds and, for a secret an integration
references, from the edge once the platform acknowledges it. `rm` permanently
destroys ciphertext and requires the variable to be revoked first. Use
`variables status` when a mutation reports publication pending.
## Choosing the name
- If every browser user may inspect the value, start the name with
`FS_PUBLIC_`.
- Otherwise it is a secret — the build can use it, a compiled integration can
inject it, and no one can read it back.
- If your own server process needs it, use the server host's secret manager,
not a frontend Variable.
---
# @farthershore/farthershore-js
Canonical URL: https://docs.farthershore.com/reference/frontend-sdk
This guide targets `@farthershore/farthershore-js` 0.32.0. See the generated
[client exports](/generated/frontend-sdk/root), [React exports](/generated/frontend-sdk/react),
and [component exports](/generated/frontend-sdk/components) for exact signatures.
`@farthershore/farthershore-js` is the Frontend SDK — the browser integration
layer between a static frontend artifact and the platform. Your frontend code
never sees platform URLs or auth endpoints; it **expresses intent** and the SDK
decides where each request goes (platform-management calls vs your product's own
features), how it's authenticated, and the host/env scoping.
It versions independently from [`@farthershore/business`](/reference/business-sdk)
and [`@farthershore/backend`](/reference/backend-sdk).
```bash
pnpm add @farthershore/farthershore-js # react is an optional peer (for /react)
```
## Client
```ts
import { createFartherShoreClient } from "@farthershore/farthershore-js";
const fs = createFartherShoreClient(); // zero config on platform-served portals
const { business, plans } = await fs.bootstrap(); // → platform (discover this host)
const session = await fs.auth.getSession(); // → safe identity/session metadata
const usage = await fs.usage.snapshot(); // → platform
const forecast = await fs.route.get("/forecast?city=NYC"); // → your product API
```
In a test-persona environment, start a browser session with
`farthershore persona login`; the CLI opens the platform-owned
`/persona-sign-in` bridge. The one-time secret remains fragment-only until its
same-origin exchange sets a server-owned HttpOnly cookie. Custom or template
JavaScript reads `session.authSession` and never receives or stores a bearer or
access key. Call `fs.auth.signOut()` to revoke that server session.
`createFartherShoreClient(config?)` never throws at construction — it
needs **no config** at all: the platform injects `window.__FS_CONFIG__` into
served portals, and `farthershore frontend dev|preview` injects the same shim
during local development, so the SDK reads its connection lazily in every
environment. Platform-infrastructure values (`coreUrl`, `portalHost`,
`businessId`, `gatewayUrl`, `environmentId`) are injected by the edge, never
passed here.
Builder config fields (all optional): `getToken`, `organizationId`, `apiKey`,
`mock`, `retry`, `fetch`, `onError`, `onLimitExceeded`, `onUnauthorized`.
`apiKey` and `fs.setApiKey()` remain available for deliberate Gateway API-key
clients, such as a headless integration exercising product routes. They are not
persona browser sign-in APIs: hosted portal code must use the server-owned
session and must not place an API key in browser storage.
## Client namespaces
The client routes each namespace to the platform's management API or your
product's own API:
| Namespace | Routes to | Methods |
| -------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------- |
| `fs.bootstrap()` | platform (public resolve) | discover business and environment metadata (memoized) |
| `fs.business` | bootstrap | `get()`, `resources()` |
| `fs.auth` | platform | `getSession()`, `signOut()`, `setToken()`, `gatewayContextToken()` |
| `fs.keys` | platform | `list()`, `create()`, `revoke()`, `rotate()` |
| `fs.usage` | platform | `summary()`, `events()`, `snapshot()` |
| `fs.billing` | platform | Subscription, billing portal, cancellation, plan-change, and `getBillPreview()` |
| `fs.plans` | platform / bootstrap | `list()`, `getPlanOffers()`, `subscribe()`, `startOnboarding()` |
| `fs.entitlements` | platform / bootstrap | Read the subscriber's enforced limits and current usage |
| `fs.resources(type)` | platform | `list()`, `get()`, `create()`, `update()`, `delete()`, `count()` |
| `fs.organizations`, `fs.team`, `fs.rbac` | platform | Organization contexts, membership, role assignment, and RBAC catalog/settings/roles |
| `fs.notifications`, `fs.auditLogs` | platform | Notification preferences and paginated audit history |
| `fs.route.get(path)` / `fs.route.post(path, body)` | **your product API** | `fetch(path)`, `json(path)` |
Platform-management and product-API calls use the SDK-managed browser context.
In a managed portal, let the SDK obtain the gateway context; do not copy browser
credentials to your backend or construct unsigned identity headers. Use
`apiKey`/`setApiKey()` only when intentionally building an API-key-authenticated
Gateway client, never as a substitute for customer or persona browser sign-in.
## React (`/react`)
Hooks live in a subpath; `react` is an optional peer. Each hook returns
`{ data, error, isLoading, isError, isSuccess, refetch, queryKey }` plus its
mutations.
```tsx
import { createFartherShoreClient } from "@farthershore/farthershore-js";
import {
FartherShoreProvider,
useApiKeys,
} from "@farthershore/farthershore-js/react";
const fs = createFartherShoreClient(); // config arrives via the injected channel
function App() {
return (
);
}
function ApiKeys() {
const { data, isLoading, create, revoke, rotate } = useApiKeys();
// …
}
```
| Hook | Returns |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `useFartherShore()` | The raw client. |
| `useBootstrap()`, `useBusiness()`, `useDeclaredResources()` | Bootstrap + product + declared resources. |
| `useSession()` | Session and safe `authSession` identity metadata (+ `signOut`). |
| `useApiKeys()` | Keys (+ `create` / `revoke` / `rotate`). |
| `useUsage()`, `usePinnedUsageRows()` | Usage snapshot + pinned billing rows. |
| `useBilling()` | Subscription (+ `openBillingPortal`). |
| `usePlans()` | The plan catalog (kind, recurring price, trial — never per-unit rates). |
| `useBillPreview()` | The subscriber's [bill preview](/reference/bill-preview-api); switch on `disclosure`. |
| `useEntitlements()`, `useResourceLimitUsage()` | Subscriber-state and enforced-limit usage. |
| `useRouteRateLimit()` | Rate-limit snapshot for a product API route. |
| `useMe()`, `useSubscriptionContexts()` | The current subscriber + their subscription contexts. |
| `useTeam()`, `useAuditLogs()`, `usePaginatedAuditLogs()` | Team + audit logs. |
| `useResourcesList()`, `useResource()`, `useResourceUsage()` | Counted resources. |
| `useUpgrade()`, `useResourceCap()`, `useReconcileAfterCheckout()` | Upgrade targets; checkout-return reconciliation. |
| `useOrganization()` | Multi-org reactivity (the provider mounts the layer automatically). |
### Usage is not a bill
`useUsage()` reads the usage snapshot, a bounded event sample marked
`exact: false`. It is useful for recent activity and troubleshooting, not an
invoice total or proof of settlement. Use `useBillPreview()` for platform-rated
money and honor its disclosure mode. Loading, empty, and failed are separate UI
states: a failed query must not display a zero balance. See
[bill preview](/reference/bill-preview-api).
### Managed RBAC
When a product has [`rbac` enabled](../define/team-rbac), `useFsAuth()` also
returns the signed-in user's resolved access:
```tsx
const { roles, permissions, hasPermission } = useFsAuth();
if (hasPermission("reports:write")) {
// show the edit control
}
```
- `roles` / `permissions` are **server-resolved** (read from `/me`, never
decoded from the token client-side); `permissions` of `["*"]` grants
everything (owner / RBAC-disabled / personal org).
- `…` renders its
fallback when the user lacks the permission.
- `fs.rbac.settings` / `fs.rbac.catalog()` / `fs.rbac.roles` and
`useTeam().assignRoles` drive the customer's role-management UI.
Client-side checks are **UX only** — hide controls, never enforce. The edge
`permission` constraint is the security boundary.
## Components (`/components`)
The managed component kit. Subscriber data components such as `PlansTable`,
`UsageCard`, `BillingSummary`, and `ApiKeysPanel` work with zero data props under
`` through SDK hooks. Rendering primitives can require input:
`BusinessApiReference` requires an OpenAPI `document`, as shown below. Consult
each component's props rather than assuming it fetches its own data. The
components accept `className`, are headless, and do not ship a CSS
entrypoint. Style their
rendered markup with your application's CSS.
```tsx
import {
FartherShoreRoot,
PlansTable,
UsageCard,
BillingSummary,
ApiKeysPanel,
} from "@farthershore/farthershore-js/components";
;
```
| Group | Components |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Mount + chrome | `FartherShoreRoot`, `useBoot` (from `/react`), `FsErrorBoundary`, `FsFooter`, `FsSplash` |
| Auth (managed) | `FsAuthProvider`, `useFsAuth` + `useOptionalFsAuth` (from `/react`), `FsSignIn`, `FsSignInButton`, `FsSignOutButton`, `FsUserButton`, `SignedIn`, `SignedOut`, `AuthLoading` |
| Auth-gated routing | `RequireAuth`, `useAuthGuard`, `planAuthGuard`, `resolveSignInDestination` |
| Data | `PlansTable`, `ApiKeysPanel`, `UsageCard`, `BillingSummary`, `RoutePanel`, `ResourcesPanel`, `DocsLegal` |
| Gating + prompts | `PermissionGate`, `UpgradePrompt`, `OrgSwitcher`, `FsLimitBoundary`, `useLimitHandler` |
| Data-rich | `TeamPanel`, `CancelSubscription`, `TrialBanner`, `ResourceLimitUsageCard`, `OnboardingView`, `AutoKeyBanner`, `AuditLogTable`, `RateLimitDisplay`, `BillPreviewCard` |
| Composite | `FartherShoreApp`, `FsPricing` — one-tag portal |
| Docs | `BusinessDocs`, `BusinessApiReference`, `useBusinessDocs`, plus the composable shell parts, markdown renderers, and OpenAPI helpers |
`` combines the provider, bootstrap gate, theme and branding
chrome, and managed auth strategy in one wrapper.
### API reference primitive
`BusinessApiReference` is the docs-styled API reference renderer. Feed it an OpenAPI 3.x document, or a compatible subset with `info`, `servers`, `tags`, `paths`, request bodies, responses, and `components.schemas`. It is intentionally separate from `BusinessDocs`: prose docs and endpoint reference can live side by side, like a managed gateway docs portal.
```tsx
import {
BusinessApiReference,
type BusinessOpenApiDocument,
} from "@farthershore/farthershore-js/components";
const spec = {
openapi: "3.1.0",
info: { title: "Weather API", version: "2026-06-28" },
servers: [{ url: "https://api.weather.example" }],
paths: {
"/v1/forecast": {
get: {
summary: "Get forecast",
tags: ["Forecasts"],
responses: { "200": { description: "Forecast returned." } },
},
},
},
} satisfies BusinessOpenApiDocument;
;
```
## Errors and deny codes
Branch on `FartherShoreApiError.code` against the platform deny vocabulary. See
[response & deny codes](/reference/response-codes).
```ts
import {
FartherShoreApiError,
LimitExceededError,
FartherShoreRateLimitedError,
FS_DENY_CODES,
} from "@farthershore/farthershore-js";
import { isThrottled, isRetryable } from "@farthershore/farthershore-js/errors";
try {
await fs.route.get("/v1/cron-jobs");
} catch (err) {
if (
err instanceof FartherShoreApiError &&
err.code === FS_DENY_CODES.rate_limited
) {
// back off and retry
}
}
```
The root exports five common classes: `FartherShoreError`,
`FartherShoreApiError`, `LimitExceededError`,
`FartherShoreRateLimitedError`, and `FartherShoreConfigError`. The `/errors`
subpath also exports the network, abort, not-ready, request-too-large, spend,
concurrency, provider, adaptive-throttle, feature, permission, lifecycle,
subscription-conflict, changed-offer, and billing-not-configured subclasses.
It also provides `isRetryable`, `isThrottled`, `isBillingNotConfigured`, and the
deny-envelope parsers. Import specialized errors and guards from `/errors`.
## Choose a frontend workflow
| Task | Guide | Full API |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
| Mount a hosted portal | [Frontend overview](/frontend/overview), [components](/frontend/components) | [Components](/generated/frontend-sdk/components) |
| Build custom subscriber screens | [Auth](/frontend/auth), [access-aware UI](/frontend/access-aware-ui) | [React hooks](/generated/frontend-sdk/react) |
| Add org-configurable controls | [Custom components](/frontend/custom-components), [permission gates](/frontend/permission-gates) | [Components](/generated/frontend-sdk/components) |
| Call your API and handle denials | [Consume an API](/backend/consume) | [Errors](/generated/frontend-sdk/errors) |
| Call a provider without exposing a secret | [Integrations](/define/frontend-integrations), [variables](/frontend/variables) | [Client](/generated/frontend-sdk/root) |
| Compose a documentation shell | Use `BusinessDocs` or `BusinessApiReference`, then customize layout | [Docs chrome](/generated/frontend-sdk/components-docs-chrome) |
| Test rendering without live credentials | Use the test workflow below | [Test utilities](/generated/frontend-sdk/test-utils) |
## Test custom UI before preview
`/test-utils` exports `createMockFartherShoreClient`,
`FartherShoreTestProvider`, and typed error builders such as `mockApiError` and
`mockLimitExceeded`. Override the particular client method your component uses,
then render it under the test provider. Assert loading, success, empty, and
failure states separately; include the denied mutation path even if the button
usually hides.
Mock mode proves presentation, not gateway enforcement, pricing, or hosted
configuration. Repeat critical flows in a preview with real persona sessions:
sign in, switch organizations, call a denied route, and sign out. Confirm stale
data from the previous organization disappears and that usage failures do not
render a zero bill. Never keep test API keys in shipped source.
## Catalog helpers
Pure display helpers so any frontend renders the catalog identically:
`isFreePlan`, `formatPlanPrice`, `formatDate`, `entitlementBullets`,
`describePlanLimit`, `subscriptionStatusChip`, `trialDaysRemaining`,
`paymentHealth`, plus the CSV exporters (`usageToCsv`, `downloadCsv`) and
subscription helpers. Plans expose their declared `kind`; there is no
client-side classification and no client-side bill math — money comes only
from the [bill preview API](/reference/bill-preview-api).
Pin this SDK exactly or with a patch-only range while it is pre-1.0. It
versions independently from `@farthershore/business` and
`@farthershore/backend`; SemVer 0.x minor bumps may break, so pin the current
release exactly or use only a patch-level range until `1.0.0`.
---
# Share with another member
Canonical URL: https://docs.farthershore.com/cookbook/share-with-a-member
You want the everyday collaboration move: a member opens a document they own and
shares it with a specific teammate, who then sees it in their own list. This is
not a platform feature you turn on — it's ordinary application data you store,
exactly like every real app does. The platform's job is narrower and load-bearing:
it hands your backend a **verified member id** on every request, and that id is
the **same** id you listed in the share menu. That one correlation is all you need
to build sharing safely.
The three tenancy boundaries stay exactly as they are — the
[org is the billing boundary, the member is the visibility boundary, and roles
are the capability boundary](/define/tenancy). Sharing lives on the **member**
axis: it decides who can _see_ a document, and it needs **no** [Team RBAC](/define/team-rbac)
at all (more on that [below](#why-no-platform-rebac)).
## Outcome
A member can share a document they own with another member of the same org, and
the recipient sees it in their document list — enforced entirely by your backend,
keyed on the platform's verified `memberId`, and never leaking across orgs.
## Prerequisites
- A Farther Shore business with a [backend](/backend/overview) you run
- A backend that verifies platform requests (see [metering & verification](/backend/metering))
- A `documents` table you own, and somewhere to store shares
- A portal or custom frontend built with [`@farthershore/farthershore-js`](/frontend/overview)
## Pick the recipient — `member.id`, not `userExternalId`
The share menu lists the org's members with [`useTeam()`](/frontend/permission-gates).
Each `TeamMember` carries **two** identifiers, and picking the wrong one is the
single most common way to break sharing:
- **`member.id`** — the internal **Membership** id. This is what you store and
send, because it **equals `ctx.principal.subject.memberId`** on the recipient's
next request. It is the only field that correlates.
- **`member.userExternalId`** — the identity-provider id (Clerk/SAML subject).
Never store this as a share key: it does **not** match anything your backend
sees on a request, so the recipient's reads would silently return nothing.
**Store `member.id`, never `member.userExternalId`.** `member.id` is the
Membership id and is exactly the `memberId` your backend reads from
`ctx.principal.subject.memberId`. `userExternalId` is the IdP's id — it never
appears on a verified request, so a share keyed on it matches no one. This is
the load-bearing rule of the whole recipe.
```tsx
import {
useTeam,
useFartherShore,
type TeamMember,
} from "@farthershore/farthershore-js/react";
function ShareMenu({ docId }: { docId: string }) {
const fs = useFartherShore();
const team = useTeam();
const members: TeamMember[] = team.data?.members ?? [];
async function shareWith(memberId: string) {
// Send the Membership id. This is the value the recipient's request will
// carry as ctx.principal.subject.memberId — the correlation that makes the
// share resolve on read.
await fs.route.post(`/documents/${docId}/share`, { memberId });
}
// Roster rows carry a display name and email when the identity provider
// supplies them; fall back to a short id rather than showing a raw UUID.
// (This mirrors what the SDK's own components render for members.)
function label(
m: TeamMember & { name?: string | null; email?: string | null },
) {
return m.name ?? m.email ?? `Member ${m.id.slice(0, 8)}`;
}
return (
{members.map((m) => (
{label(m)} {/* display only — never the share key */}
{/* ^^^^ m.id, NOT userExternalId */}
))}
);
}
```
Display identity comes from the roster row itself — `name`/`email` when the
provider supplies them. `useFsAuth()` / `useSession()` describe only the
**caller**, so they cannot label teammates. The share key is always `member.id`.
## Declare the routes as member-only
Sharing is a human action and private data is per-member, so both the write and
the reads must be **member** subjects. Declare that in your business program and
the gateway rejects machine (service-key) traffic at the edge with
`403 member_subject_required` — before it ever reaches your handler:
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const api = fs.backend("api", {
transport: { mode: "direct" },
meters: [requests],
default: true,
});
const documents = fs.route("/documents", {
get: { backend: api, requireMember: true, costs: [requests.fixed(1)] },
post: { backend: api, requireMember: true, costs: [requests.fixed(1)] },
});
const documentShare = fs.route("/documents/{id}/share", {
post: { backend: api, requireMember: true, costs: [requests.fixed(1)] },
});
// Grant both refs in the plan that sells this feature:
// fs.plan("team", { kind: fs.plan.kind.flat, …, grants: [documents, documentShare] });
```
Create or bind the same `api` logical slug in every environment you test. A
preview never borrows the production origin. Wait until `env list` contains the
pushed branch's preview before creating its backend row; if automatic
branch-prefix creation did not occur, create the preview explicitly first:
```bash
farthershore env list --format json
farthershore backend create --env \
--name "Preview API" --slug api --transport direct \
--idempotency-key \
--origin-url https://preview-api.example.com --default --format json
farthershore backend list --format json
```
Filter the structured backend list by the preview's environment id.
## Store the share
The frontend `fs.route.post(...)` above forwards through the gateway to your
backend, which writes an ordinary shares row. Two rules make it safe: the sharer
must **own** the document, and you stamp the **org id** onto the row so reads can
never cross tenants.
```ts
import { fartherShore, requireMember } from "@farthershore/backend";
const fs = fartherShore.initFromEnv();
// POST /documents/:id/share body: { memberId }
// Register fs.middleware() first; fs.handler requires its verified context.
app.post(
"/documents/:id/share",
fs.handler(async (ctx, req, res) => {
const me = requireMember(ctx); // the sharer, narrowed to a member subject
const orgId = ctx.principal.org.id;
const documentId = req.params.id;
const memberId: unknown = req.body?.memberId;
if (typeof memberId !== "string" || memberId.length === 0) {
return res.status(400).json({ error: "invalid_member_id" });
}
// Application-owned trusted directory; never trust the browser roster.
if (!(await membershipDirectory.isActiveMember({ orgId, memberId }))) {
return res.status(404).json({ error: "not_found" });
}
// The sharer must own the document, within their own org.
const doc = await db.documents.findFirst({
where: { id: documentId, orgId, ownerId: me.memberId },
});
if (!doc) return res.status(404).json({ error: "not_found" });
// Stamp orgId on the share row — the read query always filters by it.
await db.documentShares.upsert({
where: { documentId_memberId: { documentId, memberId } },
create: { documentId, memberId, orgId, grantedBy: me.memberId },
update: {},
});
res.json({ ok: true });
}),
);
```
The browser can submit any `memberId`, regardless of what `useTeam()` displayed.
`membershipDirectory.isActiveMember` above is an application-owned adapter, not
an SDK export: implement it against a trusted server-side membership source and
fail closed when membership cannot be established. Keep removals synchronized,
enforce ownership and membership within your database transaction where
possible, and retain the organization filter on every read. A roster dropdown
is never membership validation.
## Enforce on read
The recipient's request carries their own verified `memberId`. The list query
returns documents they **own** or that are **shared with them** — and is
**always** scoped by `ctx.principal.org.id`, so a share can never resolve across
orgs:
```ts
import { requireMember } from "@farthershore/backend";
// GET /documents → mine OR shared-with-me, never cross-org
app.get(
"/documents",
fs.handler(async (ctx, req, res) => {
const me = requireMember(ctx);
const orgId = ctx.principal.org.id;
const docs = await db.documents.findMany({
where: {
orgId, // ALWAYS scope by the principal's org — the tenancy floor
OR: [
{ ownerId: me.memberId }, // documents I own
{ shares: { some: { memberId: me.memberId, orgId } } }, // shared with me
],
},
});
res.json(docs);
}),
);
```
That is the whole loop: the recipient's `me.memberId` came from the same signed
principal the platform verified, and it matches the `memberId` the sharer picked
from `useTeam()` — so the shared row resolves, and only for the right person in
the right org.
## Why no platform ReBAC
The platform does **not** model your sharing graph, and it doesn't need to. The
who-shared-what-with-whom relationships are ordinary rows in **your** database —
the same way Google Drive stores its ACLs. What you cannot build yourself is a
trustworthy answer to "who is calling?"; that is exactly what the platform
supplies, unforgeably, as `ctx.principal.subject.memberId`, correlatable to the
`member.id` you rendered in the UI.
So there is nothing to turn on:
- **No relationship engine (ReBAC/Zanzibar).** Your `documentShares` table _is_
the relationship store. The platform's contribution is the verified,
correlatable id — not the graph.
- **No [Team RBAC](/define/team-rbac).** RBAC is the **capability** axis and is
strictly **opt-in** (a dashboard toggle, or `farthershore business rbac enable`).
Sharing is the **visibility** axis: a member
with only a member role can still be granted a share, because "may open this
document" is not a role. The two are orthogonal — leave RBAC disabled in the
dashboard and sharing still works.
Reach for RBAC only when you also need org-wide _capability_ rules (who may
export, who may delete) layered on top — see
[Shared and private in one portal](/cookbook/hybrid-portal), which combines an
RBAC-gated shared section with member-keyed private data in one frontend.
## Verify it works
1. Sign in as member A, create a document, and share it with member B from the
`ShareMenu` (confirm the request body carries B's `member.id`).
2. Sign in as member B and load `GET /documents` — the shared document appears.
3. Sign in as member C (same org, no share) — the document is **absent**.
4. Sign in as a member of a **different** org — the document is absent even if
you replay B's id, because the query is scoped by `ctx.principal.org.id`.
5. Call `POST /documents/{id}/share` with a **service** key — the gateway denies
it `403 member_subject_required` before your handler runs.
6. Submit another organization's member id directly, bypassing the dropdown;
require rejection and no share row. Repeat for a removed member and for a
caller who does not own the document.
## Common failures
- **Nothing shows up for the recipient.** You stored `member.userExternalId`
instead of `member.id`. The recipient's request carries `memberId` (the
Membership id), which never equals the IdP `userExternalId` — so the join
finds nothing. Store `member.id`.
- **A recipient in another org can see the document.** A read query dropped the
`orgId` filter. Every member-keyed read must include `orgId:
ctx.principal.org.id`; the shares join must carry it too.
- **The share write 404s for a legitimate owner.** The ownership check keys on
the wrong id — confirm `ownerId` is stored as the owner's `memberId`, not
their `userExternalId`.
- **Service traffic reaches the handler.** The route is missing
`requireMember: true` in the manifest; add it and rebuild.
## Recover
Sharing lives entirely in your data model, so recovery is a data operation, not
a platform one: delete the offending `documentShares` rows (or add the missing
`orgId` filter and redeploy your backend). No contract change or republish is
involved — the manifest only declares the routes as member-only.
## Agent prompt
> In this Farther Shore repo, add member-to-member document sharing. In the
> frontend, list recipients with `useTeam()` and send `member.id` (NOT
> `userExternalId`) to `fs.route.post('/documents/:id/share', { memberId })`. In
> the backend, wrap handlers with `fs.handler(async (ctx, req, res) => …)`, call
> `requireMember(ctx)`, and write a shares row stamped with
> `ctx.principal.org.id`. Make `GET /documents` return owned-or-shared rows
> always scoped by the principal's org.
> Validate recipient membership server-side; the browser roster is untrusted.
> Declare the routes with `fs.route(path, { post: { requireMember: true } })` and grant
> their refs in the plan. Leave RBAC disabled in the dashboard — sharing is the
> visibility axis, not a capability. Build and report the preview test steps; do not
> publish.
## Related
- [Tenancy & identity](/define/tenancy) — the org/member/role boundaries and the verified principal this recipe stands on.
- [Team RBAC](/define/team-rbac) — the opt-in capability axis, orthogonal to sharing.
- [Metering & verification](/backend/metering) — how `fs.middleware()` verifies the request that carries `ctx.principal`.
- [Permission gates](/frontend/permission-gates) — gate shared, org-wide surfaces by role in your UI.
---
# Shared and private in one portal
Canonical URL: https://docs.farthershore.com/cookbook/hybrid-portal
Most real portals are **both** tenancy shapes at once: a shared, org-wide area
that a member's **role** governs, and a private area that belongs to the
individual member. Farther Shore builds both on the same verified principal, so
you don't choose one model — you place both in one frontend. This recipe wires a
`Workspace` with a shared, RBAC-gated **Reports** section and a member-keyed
private **Notes** section, and shows why the two use different axes.
The [three tenancy boundaries](/define/tenancy) map straight onto the two
sections:
- **Shared section → the capability axis.** Reports belong to the **org**; who
may see them is a **role** question, so gate it with
[Team RBAC](/define/team-rbac) and key the data on the org id.
- **Private section → the visibility axis.** Notes belong to the **member**, so
key the data on `ctx.principal.subject.memberId` and gate nothing — every
member simply sees their own.
- **One org → the billing axis.** Both sections are one subscription: every call
draws down the **same** org plan.
## Outcome
A single portal renders an org-shared section (visible only to members whose
role grants it) alongside a member-private section (each member sees only their
own rows) — both enforced at the edge and both billed to one org.
## Prerequisites
- A Farther Shore business with a [backend](/backend/overview) and a portal
- [Team RBAC](/define/team-rbac) enabled for the business (dashboard toggle or
`farthershore business rbac enable`) for the role gate
- A `reports` table keyed by org and a `notes` table keyed by member
## Two sections, two axes
The shared section is **org-keyed** and **role-gated**: the data is the whole
org's, and a member sees it only if their role grants `reports:read`. The private
section is **member-keyed** and **ungated**: there is nothing to gate, because
the query already returns only the caller's own rows. Putting them side by side
makes the distinction concrete — and shows that [RBAC is opt-in](/define/team-rbac):
you enable it (in the dashboard) for the shared section, while the private
section needs none.
```tsx
import { PermissionGate } from "@farthershore/farthershore-js/components";
function Workspace() {
return (
<>
{/* SHARED: org-wide data. RBAC (the capability axis) decides who sees it. */}
{/* PRIVATE: member-keyed. No gate — every member sees only their own. */}
>
);
}
```
`` hides the shared section for members whose role lacks
`reports:read`. It is a **UX** convenience — the gateway enforces the same
`reports:read` permission at the edge — see [permission gates](/frontend/permission-gates).
The private `` carries no gate at all.
## The shared section — gate by role
Declare both routes, and enable RBAC for the business (the **Access control
(RBAC)** card in the dashboard, or `farthershore business rbac enable`) so the
platform derives route permissions. The backend keys the shared data on the
**org**, not the member:
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const api = fs.backend("api", {
transport: { mode: "direct" },
meters: [requests],
default: true,
});
// SHARED: org-wide data. RBAC derives reports:read from this route.
const reports = fs.route("/reports", {
get: { backend: api, requireMember: true, costs: [requests.fixed(1)] },
});
fs.group("reports", [reports], { permission: {} });
// PRIVATE: member-keyed. requireMember rejects service-key traffic at the edge.
const notes = fs.route("/notes", {
get: { backend: api, requireMember: true, costs: [requests.fixed(1)] },
});
fs.group("notes", [notes], { permission: {} });
fs.plan("team", {
kind: fs.plan.kind.flat,
price: fs.money.usd(49).monthly(),
grants: [reports, notes],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
After pushing the preview branch, wait until `env list` contains its environment
before creating or binding that environment's `api` row. The production origin
is never a preview fallback. If automatic branch-prefix creation did not occur,
create the preview explicitly first:
```bash
farthershore env list --format json
farthershore backend create --env \
--name "Preview API" --slug api --transport direct \
--idempotency-key \
--origin-url https://preview-api.example.com --default --format json
farthershore backend list --format json
```
Filter the structured backend list by the preview's environment id.
The permission groups above explicitly establish `reports:read` and
`notes:read`; do not guess permission names from route URLs. Because
[RBAC is **business-wide**](/define/team-rbac), not
per-route: enabling the dashboard flag derives a `notes:read` permission for the
private route too. Keep the private section universally reachable by granting
`notes:read` to **every** role, member included — the "ungated" feel comes from
that universal grant, while the member-keyed query keeps each member's rows
private. The role gate you actually curate is `reports:read`.
```ts
import { fartherShore, requireMember } from "@farthershore/backend";
const fs = fartherShore.initFromEnv();
// SHARED: org-keyed. The edge already enforced the reports:read role gate;
// the data is the whole org's.
app.get(
"/reports",
fs.handler(async (ctx, req, res) => {
const rows = await db.reports.findMany({
where: { orgId: ctx.principal.org.id },
});
res.json(rows);
}),
);
```
## The private section — key by member
The private route is declared `requireMember: true`, so the gateway rejects
service-key traffic at the edge (`403 member_subject_required`). The handler
scopes to the caller's own member id — no role check, because visibility, not
capability, is the axis here:
```ts
import { requireMember } from "@farthershore/backend";
// PRIVATE: member-keyed. Same org bill, scoped to the one member.
app.get(
"/notes",
fs.handler(async (ctx, req, res) => {
const me = requireMember(ctx);
const rows = await db.notes.findMany({
where: { orgId: ctx.principal.org.id, memberId: me.memberId },
});
res.json(rows);
}),
);
```
On the frontend, the private section is just a call through the gateway — the
member subject rides the request automatically:
```tsx
import { useFartherShore } from "@farthershore/farthershore-js/react";
import { useEffect, useState } from "react";
function MyNotes() {
const fs = useFartherShore();
const [notes, setNotes] = useState([]);
useEffect(() => {
// fs.route.get carries this member's principal; the backend filters by
// ctx.principal.subject.memberId, so the response is already just theirs.
void fs.route.get("/notes").then(setNotes);
}, [fs]);
return ;
}
```
## One bill: it all draws down the org
There is one subscription behind both sections. Every `fs.route.*` call — shared
or private — meters against the **org's** plan limits and is attributed to the
calling member (see [usage & billing policy](/operate/usage-billing-policy)). A
member reading their private notes and an admin reading the shared reports both
spend from the same org allowance; the split between shared and private is a
_visibility_ concern, never a billing one.
## Verify it works
- A member with a role that grants `reports:read` sees the Reports section; a
member without it does not (and the gateway denies `GET /reports` directly).
- Every member sees the Notes section, and each sees only their own rows.
- `GET /notes` called with a service key is denied `403 member_subject_required`.
- Usage from both sections lands on one org's plan, attributed per member.
## Common failures
- **The shared section renders for everyone.** `` hides UI only —
confirm RBAC is enabled for the business (`farthershore business rbac`) and the route's `reports:read` gate denies at
the edge. UI gating is never the boundary.
- **The private section 403s for some members.** RBAC is business-wide, so a
role that omits `notes:read` is denied `permission_denied` at the edge before
your member-keyed query runs. Grant `notes:read` to every role.
- **A member sees another member's notes.** The `/notes` query dropped the
`memberId` (or the `orgId`) filter. Member-keyed reads must include both.
- **The private section is empty for everyone.** The query keyed on
`userExternalId` instead of `me.memberId`; use the Membership id from the
principal.
## Recover
Both sections are your own data and manifest. Revert the route and handler
changes together on a preview branch and rebuild; production is unchanged until
you publish. RBAC itself is a platform flag — `farthershore business rbac disable`
removes role restrictions in every environment (roles are preserved for
re-enable). Prefer repairing the affected role to expanding access globally.
A data-only mistake (a missing `orgId`/`memberId` filter) is
fixed by redeploying the backend, with no contract change.
## Agent prompt
> In this Farther Shore repo, build a `Workspace` with two sections. Shared:
> enable RBAC for the business with `farthershore business rbac enable` (it is a
> platform setting, not code), declare a `reports` route with `fs.route`, gate its UI with
> ``, and key the backend on
> `ctx.principal.org.id`. Private: declare a `notes` route with
> `{ get: { requireMember: true } }`, grant `notes:read` to every role, and key
> the backend on `requireMember(ctx).memberId` (the Membership id, not
> `userExternalId`). Use the `fs.handler(async (ctx, req, res) => …)` wrapper on
> both routes. Build and report preview test steps; do not publish.
## Related
- [Tenancy & identity](/define/tenancy) — the org/member/role boundaries this recipe realizes.
- [Share with another member](/cookbook/share-with-a-member) — member-to-member sharing, which needs no RBAC.
- [Team RBAC](/define/team-rbac) — enable and configure the capability axis.
- [Permission gates](/frontend/permission-gates) — make any component role-aware in your UI.
---
# Design and operate commerce
Canonical URL: https://docs.farthershore.com/commerce/overview
Commerce connects measurements to customer obligations. Keep the plan's access grants, structural bounds and monetary controls distinct; they answer different questions.
## Define the commercial model
Choose a declared [plan kind](/define/plans): free, flat, usage, prepaid, hybrid, trial or custom. Define measurements with measures and dimensions, attach meters to routes, and connect a [pricing catalog](/reference/pricing-catalogs). Catalogs contain exact rates, selectors, tiers and modifiers. A backend reports measured values through its verified context; the platform rates them.
[Funding buckets](/reference/funding-and-allowances) pay rated usage from included, prepaid, promotional or referral value. Exhaustion policy either blocks or selects an overage binding. [Monetary admission](/reference/monetary-admission) bounds work before the gateway forwards it. A displayed allowance is not permission to bypass admission.
Use [economic agreements](/reference/economic-agreements) for negotiated subscription terms. Checkout promo codes are operational customer offers; they are distinct from repository-defined funding buckets.
## Change a live offering
Inspect the semantic diff and [commercial release](/reference/commercial-releases). A release is immutable; subscriptions retain their served commercial identity and pricing binding. Read [plan transitions](/concepts/plan-transitions) before promising a migration. The operation catalog states which migration operations are actually available.
Test a free path, a funded path, exhaustion, an upgrade and an existing subscriber on an older pin. Use the same environment when comparing results. Publish the reviewed artifact through the [release workflow](/operate/releases).
## Read and explain money
Use the [bill preview](/reference/bill-preview-api) for customer usage money. Honor transparent versus opaque disclosure and unavailable values. Do not calculate a bill in the browser by multiplying usage counts by a current price: pins, funding, modifiers and corrections can change the result.
When numbers disagree, follow [billing diagnosis](/cookbook/diagnose-billing-usage) and [ledger and settlement](/operate/ledger-and-settlement). Preserve request, subscription and release identifiers. An admission reservation, measured usage, a rated ledger entry and Stripe settlement represent different stages.
---
# Entitlements vs economics
Canonical URL: https://docs.farthershore.com/concepts/entitlements-vs-economics
A plan places access, economics, and limits next to each other because they must
be reviewed together. They remain independent mechanisms.
## Entitlement: may the customer do this?
A route or frontend-integration grant admits the operation for subscribers on
that compiled plan. Managed RBAC may narrow a member further. A grant alone does
not say the operation is metered or billed.
## Measurement: what happened?
A `fs.meterRoutes()` binding says the backend reports a meter on that route; the
backend's `report({ meter, values, dims })` supplies the facts. `fs.requests()`
`costs` are gateway-known structural counts used for admission bounds. An
unbound route creates no rated usage for that meter, even when the plan binds a
catalog that could price it.
## Rating: what is it worth?
The plan's `usagePricing` binding resolves to a rating context — an immutable
catalog version plus contract modifiers — and the rating engine turns each
measurement into an exact nanodollar `RatedCharge`. Rating is a pure function
of the measurement and the served context; backends never supply money, and
Stripe never rates.
## Funding: who pays first?
Funding buckets (`fs.included`, `fs.prepaid`, `fs.promo`, `fs.referral`) pay
rated charges before anything is owed. The remainder is amount due, settled by
Stripe. A recurring `price` is rail-settled independently. Trials gate accrual;
taxes are a settlement concern on amount due.
## Bounds: when does the platform deny?
Structural: `requests` limits bound the request rate; resource limits bound
persistent inventory; capacity limits bound one request. Monetary: on a `block`
plan the gateway reserves each request's economic maximum against available
funding and denies `credit_exhausted` when it cannot. An included allowance on
an `overage` plan is not a wall — it defines where amount due begins.
## Platform cost is another axis
Farther Shore can account for the infrastructure work required to serve the
builder without charging the subscriber for that same operation. Frontend
integrations are the clearest example: they consume platform operations but
cannot carry customer meter costs.
Do not infer subscriber billing from platform accounting or vice versa.
## Review every operation as five questions
1. Which plan grants it?
2. Which meter binding records it, and which measures and dimensions arrive?
3. Which catalog entry rates it, under which binding?
4. Which funding pays first, and what happens on exhaustion?
5. Which structural or monetary bound can deny it?
The compiler catches structural mistakes and kind/control mismatches, but it
cannot decide whether those five commercial answers match your intended
product. Verify them in a preview environment — including the
[bill preview](/reference/bill-preview-api) — before production publish.
---
# Cohorts & releases
Canonical URL: https://docs.farthershore.com/concepts/cohorts-and-versions
A plan key such as `pro` is a lineage. Each material contract change mints a
new immutable compiled plan and a new immutable
[commercial release](/reference/commercial-releases) that contains it.
Subscribers still served under the same release form a cohort.
## Why releases are immutable
A customer's invoice, entitlement, route policy, measurement schema, and rating
must be explainable from the terms that applied when the request was
admitted. Editing one live row in place would erase that evidence and make
retries or delayed events ambiguous.
A release is therefore content-addressed: its id is derived from the hash of
every artifact it binds — compiled plans, route grants, measurement schema,
admission descriptors, rating contexts, commercial policy. Republishing
identical content yields the same id. Activation appends to a per-business
release log; it never rewrites.
## Usage is rated under the admitting release
The gateway stamps the served identity — subscription, release, rating context
— inside the signed usage event. Core rates against that stamped release, not
against whatever release is current when the event arrives. A later publish
or rollback cannot invalidate already-admitted work.
## A diff compares two releases
```bash
farthershore commercial-release diff --format json
```
The output names compiled plans added, removed, and changed; rating-context
versions added and removed; and every changed manifest path.
## Cohorts are not product plans
Do not create a new plan key merely to represent every historical price. Keep a
stable key for one intended plan lineage and let releases preserve its
history. Create a distinct plan key when the product offer is genuinely
different.
## Old does not mean mutable or abandoned
An older cohort can remain active indefinitely. Usage, limits, and rating
continue against its frozen release. Runtime code must resolve the subscriber's
served release rather than assuming the latest source contract applies to
everyone.
## Operational implications
- Inspect active dependents before removing routes, backends, integrations, or
meters.
- Do not delete source declarations while retained cohorts still reference
them.
- Treat delayed settlement events as events about the release recorded in
their durable state, not whatever is current now.
- Treat move-to-latest as a separate post-launch operation; release
activation never changes subscriber pins.
Continue with [Plan transitions](/concepts/plan-transitions).
---
# Plan transitions
Canonical URL: https://docs.farthershore.com/concepts/plan-transitions
Publishing a new [commercial release](/reference/commercial-releases) never
edits a subscription in place. Each subscription carries two pins, and every
change is classified by what those pins say.
## The two pins
| Pin | Set at | Moves when |
| --------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Recurring-price pin | subscription start | Never silently. Only an explicit (post-launch, deferred) move-to-latest operation repoints it. |
| Usage-pricing binding | plan declaration | `pricing.current()` — follows every activated catalog forward. `withContractTerms()` — follows activated catalogs while terms change only on agreement amendment. `fixedVersion(n)` — never. |
## What each change does to an existing subscriber
- **Recurring price change** — nothing. New subscriptions pay the new price.
- **Catalog change under `current()`** — applies from the activation of the new
release, forward. Usage already admitted under the old release is rated
under the old release. Nothing is rerated retroactively.
- **New rating context** (new catalog version, modifier, or tier table) — opens
a new rating segment for the subscriber. Graduated tier position carries
across segments; volume-retroactive selection is computed per segment at
window close.
- **New meter or measure** — plain subscriptions rate it at the catalog. Subjects
under an [economic agreement](/reference/economic-agreements) apply the
agreement's `NewMeterPolicy` / `NewMeasurePolicy`: `adopt_at_current_rate`,
`exclude_until_renewal`, or `require_amendment`.
- **Structural change** (grants, limits, routes) — applies with the release the
subscriber is served under; the gateway admits under one release cohort at a
time.
- **Funding or exhaustion policy change** — issued buckets are unchanged; the
next issuance follows whichever plan version the subscription is on.
## Preview before release
```bash
farthershore build
farthershore commercial-release diff
git push
farthershore apply-timeline inspect \
--env production \
--format json
```
Review every changed compiled plan and every added or removed rating-context
version. For an active repository-managed business, create a GitHub Release
for the exact reviewed commit only after approval. The initial `DRAFT`
activation is the sole repository-managed state that uses
`farthershore business publish`; active businesses reject that command,
including dry-run, with `MANAGED_BY_CODE`.
## Bespoke terms are agreements, not releases
A negotiated discount, floor, cap, or pinned catalog version for one customer
is an economic agreement created through the confirm-gated CLI. Releases carry
the public catalog; agreements bind a subject to it with terms. Fixed-version
agreements are `require_amendment` by definition and never move with a release.
## Recovery
A rollback appends a release-log entry that points at an older immutable
release. Requests already admitted under the rolled-back release complete and
are rated under it. Business A's rollback never touches business B.
---
# Connect Stripe
Canonical URL: https://docs.farthershore.com/monetize/stripe
Farther Shore uses your Stripe account to **settle**: subscriptions, recurring
fees, invoices for amount due, prepaid top-up payments, refunds, and tax. The
business program is the source of truth for plan economics; the platform's
rating engine and ledger are the source of truth for what a subscriber owes.
Stripe never rates usage, never owns a subscriber balance, and never receives
per-unit usage.
## Human setup
An organization owner connects and verifies Stripe from the signed-in
dashboard. This browser flow establishes provider ownership and account
requirements; it is not a CLI automation step.
Publishing currently requires a verified Stripe connection even for a
free-only business. Complete setup before the first production publish.
## What the platform creates
When a release is published, Farther Shore materializes the subscription and
recurring-price identities the release needs in Stripe. Usage pricing, funding
buckets, and allowances have no Stripe counterpart: they are rated and
allocated locally and reach Stripe only as amount due on an invoice or as a
top-up payment.
Your application initiates checkout through the Farther Shore frontend SDK or
API and reads subscription state from Farther Shore. Do not create a parallel
Stripe product/price model for the same plan.
Initial prepaid plan purchase uses the same managed plan checkout. A later
refill is a subscriber-portal operation: implement the public `fs.core()`
handoff in the [prepaid wallet cookbook](/cookbook/prepaid-credits#add-the-subscriber-refill-control),
which targets the documented
[top-up endpoint](/generated/commerce/http#createportalconsumerbalancetopup).
There is no builder CLI or MCP mutation for a subscriber's purchase.
## What stays in your code
Author these in `business/`:
- plan kinds, recurring prices, and trials;
- pricing catalogs (rates, tiers, modifiers, backend-quoted bounds);
- funding buckets and their display; disclosure and exhaustion policy;
- meter-route bindings and admission bounds.
Do not put Stripe credentials, connected-account ids, price ids, or webhook
secrets in the business program.
## How Stripe events are treated
Every Stripe webhook is an **input to a ledger posting**, idempotent on the
Stripe event id — never a direct balance mutation. Core is the single monetary
writer for a subject.
- A top-up payment moves a prepaid bucket `pending → available`; the gateway
reserves only against `available` value.
- A refund drains buckets first (prepaid → rail-refundable; promo, referral, and
included value are restored only if unexpired, else written off).
- A rail-initiated refund or dispute that matches no expected local operation
moves the settlement account to `RECONCILIATION_REQUIRED`, which the gateway
treats as a monetary admission denial until scheduled reconciliation clears
it.
Read [Ledger & settlement](/operate/ledger-and-settlement) for the posting
templates and reconciliation checks.
## Operational checks
Before a production release:
1. Build and inspect `farthershore commercial-release diff `.
2. Push the reviewed commit and inspect its production Apply Timeline.
3. Verify the organization payments connection in the dashboard.
4. In a preview environment, test checkout, a top-up (prepaid plans), a
metered request, the bill preview, and cancellation.
5. Confirm the business status and Apply Timeline after publish.
Use `farthershore business publish` only for first activation while the
repository-managed business is still `DRAFT`. Later production releases are
GitHub Releases for an exact reviewed commit; active businesses reject the
publish command, including dry-run, with `MANAGED_BY_CODE`.
Stripe events are asynchronous. A checkout redirect is not proof that the
subscription is active or that a top-up is available; wait for the platform's
durable state.
## Ownership boundary
Stripe owns payment-method collection, invoices, tax, and payouts. Farther
Shore owns rating, funding allocation, the ledger, and the mapping from the
compiled release to settlement objects. Your backend owns only its domain data
and the measurements it is authorized to report.
Continue with [Subscriptions and usage](/monetize/subscriptions) and
[Plan changes](/monetize/plan-changes).
---
# Subscriptions & usage
Canonical URL: https://docs.farthershore.com/monetize/subscriptions
A subscription assigns one subscriber organization to one compiled plan inside
one [commercial release](/reference/commercial-releases). That assignment drives
gateway grants and limits, selects the rating context that prices the
subscriber's measurements, and issues the plan's funding buckets.
## Define the contract
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const units = fs.measure("units");
const usage = fs.meter("api_usage", { measures: [units] });
const usagePricing = fs.pricing("api_usage", {
meter: usage,
catalog: [fs.rate.perUnit(fs.money.usd(0.01))],
});
const api = fs.backend("api", {
transport: { mode: "direct" },
default: true,
});
const account = fs.route("/v1/account", {
get: { backend: api, costs: [requests.fixed(1)], reports: [usage] },
});
fs.meterRoutes("account-usage", account, { reports: [usage] });
fs.plan("pro", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(29).monthly(),
usagePricing: usagePricing.current(),
funding: { buckets: [fs.included(fs.money.usd(5))] },
spendPolicy: {
onExhaustion: fs.exhaustion.overage(usagePricing.current()),
},
grants: [account],
limits: [requests.perMinute(120)],
});
export default fs.business();
```
The grant authorizes the route. The `meterRoutes` binding says the backend
reports `api_usage` there. The catalog prices each unit. The included bucket
pays for the first $5 of rated usage each period; overage is rated at the same
catalog and settled on the invoice. The recurring fee charges independently of
usage.
## The pipeline
```text
Measurement → Rating context → RatedCharge → Funding → Ledger → Settlement
```
1. **Measurement.** The backend reports facts (`values`, `dims`) through
`report()`. The gateway stamps the served identity — subscription, release,
rating context — inside the signed usage event.
2. **Rating context.** Core resolves the plan's `usagePricing` binding to an
immutable rating context: catalog version, selector match sets, contract
modifiers, tier semantics, exact rational rates.
3. **RatedCharge.** The rating engine computes an exact nanodollar charge per
measurement. Rating is order-insensitive; a deterministic re-rating pass at
window close handles cumulative tiers and late events.
4. **Funding.** Eligible buckets pay the charge in a fixed total order
(`priority`, soonest expiry, `promo < referral < included < prepaid`,
bucket id). The remainder becomes amount due.
5. **Ledger.** Every step is a balanced posting; bucket balances are a view of
the ledger and are reconciled against it.
6. **Settlement.** Stripe collects amount due and recurring fees. Stripe never
rates usage or owns a balance.
Read [Ledger & settlement](/operate/ledger-and-settlement) for the operator
view.
## What a subscription pins
A subscription holds two pins:
- the **recurring-price pin** — the fee promised when it was created; a later
release never changes it silently;
- the **usage-pricing binding** — normally `pricing.current()`, so metered
rates track the live catalog automatically. `withContractTerms()` also tracks
activated catalog releases while preserving the subject's agreement terms;
those terms change only through an agreement amendment. `fixedVersion(n)`
never moves.
Publishing a release with a new recurring price affects new subscriptions.
Moving an existing subscriber to the latest plan is a deferred post-launch
operation.
## Subscription lifecycle
Checkout creates settlement intent, but access changes only when the platform
records the authoritative lifecycle transition. Payment events can arrive later,
repeat, or be delivered out of order; the control plane deduplicates them by
event id and applies them to durable subscription state.
Render pending, active, past-due, trial, scheduled-transition, and canceled
states rather than assuming checkout is synchronous. A trial (`lifecycle:
{ trialDays }`) suppresses obligation while it runs; rating still records the
served context.
## What the subscriber sees
The only money surface for subscribers is the
[bill preview API](/reference/bill-preview-api), which runs the same rating
engine and ledger reads as invoicing — preview equals invoice by construction.
Plans with `fs.disclosure.opaque` return allowance-remaining only; no rate can
be recovered from the response.
## Limits are not invoices
The edge may deny use because of rate, capacity, resource, or funding state
(`credit_exhausted` on a `block` plan). An invoice can still include already
accepted usage. Conversely, successful payment does not bypass a route or
member permission deny. Handle the stable
[deny envelope](/reference/response-codes) in the client rather than inferring
state from Stripe.
## Cancellation and renewal
Period-end cancellation keeps the current compiled entitlements until the
effective boundary. Included buckets are reissued each period by the plan's
posting template; unused included value expires as a contra reversal, unused
prepaid value is the subscriber's until it expires or is refunded per the
bucket's rules. Delayed settlement events resolve against the release the usage
was admitted under.
Use subscription state and transition timestamps from Farther Shore for UI;
Stripe ids are implementation details.
---
# Plan changes
Canonical URL: https://docs.farthershore.com/monetize/plan-changes
Change a plan by editing its `fs.plan()` declaration — or the pricing catalog it
binds — and pushing the managed repository. Do not mutate live plan, price, or
catalog rows through an API. Bespoke per-customer terms are
[economic agreements](/reference/economic-agreements), not repo edits.
## Safe workflow
```bash
farthershore build
farthershore commercial-release diff
git push
farthershore apply-timeline inspect \
--env production \
--format json
```
Inspect the apply check and publish only after reviewing the current-head
result. For an active repository-managed business, create a GitHub Release for
that exact commit. `farthershore business publish` is only the first-activation
command for a business that is still `DRAFT`.
## What the diff answers
`farthershore commercial-release diff` compares two compiled release manifests
and reports:
- compiled plans added, removed, or changed — a compiled plan changes when its
route grants, structural limits or recurring fee change, **not** when you
edit a catalog rate;
- rating-context versions added or removed — a catalog or modifier change mints
a new rating context and leaves the compiled plan alone, so a price-only edit
shows up here and only here;
- every changed manifest path and any compatibility-fence change;
- `unchanged: true` when both manifests hash identically — republishing
identical content reuses the same release id.
## Who a change reaches
| Change | New subscriptions | Existing subscriptions |
| ---------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------- |
| Recurring `price` up or down | new price | keep their recurring-price pin |
| Catalog rate change under a `pricing.current()` plan | new rate | new rate from the activation of the release, forward only — never rerated retroactively |
| Catalog rate change under a fixed-version agreement | n/a | untouched; the agreement is `require_amendment` by definition |
| New meter or measure | rated per catalog | agreements apply their `NewMeterPolicy` / `NewMeasurePolicy`; plain subscriptions rate it |
| Funding bucket amount or `onExhaustion` | new policy | next issuance follows the new plan version they are moved to; current buckets are unchanged |
| Route grants and structural limits | applies | applies with the release they are served under |
The generalized "move every subscriber to the latest plan" operation is
deferred to post-launch: it is strictly a pricing rebind (repoint the recurring
pin, refresh non-`current` bindings, open a new rating segment), and it is not
part of prepare, publish, or activation.
## Subscriber-initiated plan changes
Everything above is a BUILDER change — a new release of the catalog. A
subscriber changes their own plan from the portal, and that path has its own
timing rule:
| Move | Endpoint | When it takes effect |
| -------------------------------------- | ---------------------------------------- | ------------------------------------ |
| Free floor → a chargeable plan | `POST /me/subscription/checkout-session` | immediately, once checkout completes |
| Cheaper plan → dearer plan (upgrade) | `POST /me/change-plan` | immediately |
| Dearer plan → cheaper plan (downgrade) | `POST /me/change-plan` | at the end of the current period |
| Any paid plan → back to the free floor | `POST /me/change-plan` | at the end of the current period |
The rule is a single comparison of the recurring fee: a strictly dearer target
is applied immediately, and everything else is scheduled for the period
boundary. A downgrading subscriber therefore keeps the plan they paid for
until it expires — there is no mid-period repricing and no refund to compute.
This means **cancelling is not the only way down**. The portal's plan list
offers every plan except the one the subscriber is already on, including the
free floor, so "I want to spend less" does not have to become "I want to
leave". Cancelling ends the subscription; downgrading to the free floor keeps
the account, its keys, and its history.
Moving a live subscription onto the free floor can only be scheduled, never
forced through immediately: asking for an immediate move is refused with
`409 PAID_TO_PROVIDERLESS_REQUIRES_PERIOD_END`.
## Activation is per business
Publication writes an immutable release; activation appends an entry to the
business's release log and promotes a per-business pointer. Business A's
publish or rollback never touches business B. Requests already admitted under
the previous release complete under it. See
[Commercial releases](/reference/commercial-releases).
## Recovery
Publishing is idempotent. Inspect workflow and Apply Timeline state before
issuing another mutation.
A rollback appends a new release-log entry that points at an older immutable
release; it never rewrites a release, never re-rates usage that was admitted
under the rolled-back release, and does not erase invoices or ledger history.
---
# Billing strategies
Canonical URL: https://docs.farthershore.com/monetize/strategies
Start with the simplest economic model. Every plan declares its kind up front,
and the compiler only accepts the controls that kind needs — so the shape you
pick is the shape you get. Access (`grants`) and structural bounds (`limits`)
sit beside every kind and are not repeated below. Any kind that bills usage
must carry one of them — a `limits` rule, `maxMonthlySpendCents`, or
`spendPolicy: { onExhaustion: fs.exhaustion.block }` — or the build fails with
`PLAN_UNBOUNDED_SPEND`.
The examples share one measurement and one catalog:
```ts
const units = fs.measure("units");
const usage = fs.meter("api_usage", { measures: [units] });
const usagePricing = fs.pricing("api_usage", {
meter: usage,
catalog: [fs.rate.perUnit(fs.money.usd(0.01))],
});
```
## Free with a real bound
```ts
fs.plan("free", {
kind: fs.plan.kind.free,
grants: [status],
limits: [requests.perMinute(60)],
});
```
A free plan carries no economic controls. Bound it structurally: attach
`requests` to the granted operations and give it a rate limit.
## Flat subscription
```ts
fs.plan("flat", {
kind: fs.plan.kind.flat,
price: fs.money.usd(30).monthly(),
});
```
Customers buy access, not a quantity. Do not invent a meter to justify the fee.
## Pay as you go
```ts
fs.plan("usage", {
kind: fs.plan.kind.usage,
usagePricing: usagePricing.current(),
});
```
Postpaid: every reported unit is rated at the bound catalog and settled on the
invoice. Value scales directly with measured consumption. There is no funding
control because nothing is prepaid; add `spendPolicy: { rail: fs.rail.x402,
onExhaustion: fs.exhaustion.block }` only for a pay-per-call rail where each
operation is funded before it runs.
## Prepaid wallet
```ts
fs.plan("prepaid", {
kind: fs.plan.kind.prepaid,
usagePricing: usagePricing.current(),
funding: { buckets: [fs.prepaid(fs.money.usd(25), { topUp: true })] },
spendPolicy: { onExhaustion: fs.exhaustion.block },
});
```
The subscriber buys $25 of rated value; usage draws it down; `topUp: true` lets
the platform offer replenishment; the gateway blocks at zero
(`credit_exhausted`, 402). Prepaid plans require every unbounded measure on
their routes to declare `maxOutputUnits`, `chunkPolicy`, or a post-stream
`settlementMax` so admission can reserve a finite economic maximum.
## Subscription plus included allowance and overage
```ts
fs.plan("hybrid", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(30).monthly(),
usagePricing: usagePricing.current(),
funding: { buckets: [fs.included(fs.money.usd(10))] },
spendPolicy: {
onExhaustion: fs.exhaustion.overage(usagePricing.current()),
},
});
```
An included bucket is $10 of rated value reissued each period, not "N units":
the allowance is worth the same across models and dimensions, and its cost is
tracked as contra-revenue in the ledger. Overage after exhaustion is rated at
the same catalog. Use `fs.exhaustion.block` only on `prepaid` and `custom`
plans.
## Trial
```ts
fs.plan("trial", {
kind: fs.plan.kind.trial,
price: fs.money.usd(30).monthly(),
lifecycle: { trialDays: 14 },
});
```
Obligation is suppressed during the trial; the recurring price applies after.
Add `usagePricing` when post-trial usage should be rated.
## Opaque allowances (5x / 20x)
```ts
fs.plan("max-5x", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(100).monthly(),
usagePricing: usagePricing.current(),
funding: {
buckets: [
fs.included(fs.money.usd(25), {
display: fs.display.multiplier({ factor: 5 }),
}),
],
},
spendPolicy: {
disclosure: fs.disclosure.opaque,
onExhaustion: fs.exhaustion.overage(usagePricing.current()),
},
});
```
`disclosure.opaque` makes every subscriber surface — including the bill
preview API — show allowance remaining as display units or a fraction, never
rates or dollar totals. Rating stays exact underneath, so the ledger reconciles
and a "20x" plan is exactly four times the "5x" bucket on the same catalog.
## Tiered and dimension-dynamic pricing
Tiers, provider/model catalogs, and modifiers are catalog concerns, not plan
concerns: see [Pricing catalogs](/reference/pricing-catalogs). Use
`fs.rate.graduated([...])` when each band keeps its own rate and
`fs.rate.volume([...])` when crossing a threshold reprices the whole window at
close.
## Resource-priced products
Use `fs.resource()` limits for persistent inventory such as projects or seats.
Bill seats by reporting the active count as a measure from your backend rather
than summing seat-change events.
## Decision checklist
- What does the customer believe they are buying: access, use, prepaid value,
or an allowance? That answer is the plan `kind`.
- Can the quantity be measured authoritatively from the backend?
- Does the customer need to stop at zero (`prepaid` + `block`) or keep going
(`hybrid` + `overage`)?
- Should subscribers see rates (`transparent`, the default) or only allowance
(`opaque`)?
- What happens at renewal, cancellation, failed payment, and plan change?
- Can support explain the invoice from the bill preview, the release, and the
ledger?
If the answer requires several exceptions, simplify the plan before reaching
for `custom`.
---
# Pricing catalogs
Canonical URL: https://docs.farthershore.com/reference/pricing-catalogs
`fs.pricing(key, { meter, catalog })` declares an immutable, versioned family of
rates for one meter. Plans bind a family through `pricing.current()`,
`pricing.withContractTerms()`, or `pricing.fixedVersion(n)`; the platform
resolves that binding — plus any economic-agreement terms — into a **rating
context**, the immutable input the rating engine prices measurements against.
```ts
import * as fs from "@farthershore/business";
fs.backend("api");
const requests = fs.requests();
const input = fs.measure("input_tokens");
const out = fs.measure("output_tokens");
const providerName = fs.dimension("provider");
const model = fs.dimension("model");
const modality = fs.dimension("modality");
const cacheStatus = fs.dimension("cache_status");
const mode = fs.dimension("mode");
const text = modality.value("text");
const uncached = cacheStatus.value("uncached");
const cached = cacheStatus.value("cached");
const acme = fs.provider("acme");
const acme4 = acme.model("acme-4");
const modelUsage = fs.meter("model_usage", {
measures: [input, out],
dimensions: [providerName, model, modality, cacheStatus, mode],
});
const llmPricing = fs.pricing("llm", {
meter: modelUsage,
catalog: [
fs.rate.perMillion(fs.money.usd(3)).for(input, acme4, text, uncached),
fs.rate.perMillion(fs.money.usd(15)).for(out, acme4, text, uncached),
fs.rate.perMillion(fs.money.usd(0.3)).for(input, acme4, text, cached),
fs.rate.perMillion(fs.money.usd(15)).for(out, acme4, text, cached),
fs.modifier.multiplier(3, 2).when(mode.is("fast")),
],
});
const chat = fs.route("/v1/chat", { post: {} });
fs.meterRoutes("chat-model-usage", chat, {
reports: [modelUsage],
maxOutputUnits: out.atMost(8192),
});
fs.plan("enterprise", {
kind: fs.plan.kind.usage,
usagePricing: llmPricing.current(),
grants: [chat],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
## Rates are exact
Authors write human units; the compiler serializes exact rationals
(`{ num: "3", den: "1000000" }`) and the rating engine never touches a float.
| Constructor | Meaning |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| `fs.rate.perUnit(fs.money.usd(0.01))` | $0.01 per unit. |
| `fs.rate.per(1000, fs.money.usd(5))` | $5 per 1,000 units — half a cent per unit, exactly. |
| `fs.rate.perMillion(fs.money.usd(3))` | $3 per 1,000,000 units. |
| `fs.rate.rational(1, 3, fs.money.usd(1))` | $1/3 per unit — reserved for genuinely non-dyadic rates. |
| `fs.rate.graduated([{ upTo, rate }, ...])` | Tiered: each unit rated by the tier its cumulative window position falls in. |
| `fs.rate.volume([{ upTo, rate }, ...])` | Tiered, retroactive: every unit in the window rated at the tier the window total selects at close. |
| `fs.rate.backendQuoted({ min, max })` | The backend proposes a per-unit rate per report; the platform clamps it into `[min, max]`. |
Tier brackets use `upTo` as the inclusive cumulative upper bound and end with
one open `upTo: null` tier:
```ts
fs.rate.graduated([
{ upTo: 1_000_000, rate: fs.rate.perMillion(fs.money.usd(3)) },
{ upTo: null, rate: fs.rate.perMillion(fs.money.usd(2)) },
]);
```
Each finite tier boundary must be a positive safe integer, strictly greater
than the previous boundary. Only the final tier may be open-ended, and it must
be open-ended. Tier rates must be SDK-created flat rates; do not nest a tier
table or backend-quoted rate inside a tier.
Graduated position carries across rating segments; volume-retroactive
selection is computed per segment at window close, and a late measurement into
a closed window becomes a correction posting that reruns the same deterministic
selection.
## Structured items and selectors
`.for(measure, ...selectors)` binds a rate to a measure and a catalog tuple:
- a **provider-owned model** (`acme.model("acme-4")`) becomes the item
`(provider, model)`; models are declared inside their provider's namespace, so
a selector can never match another provider's model;
- **dimension values** (`modality.value("text")`, `cacheStatus.value("cached")`)
become `where` conditions on the entry.
A single-measure meter may carry an unbound default rate
(`fs.rate.perUnit(...)` with no `.for`); a multi-measure meter must bind every
rate with `.for(measure)`.
At rating time the backend's `dims` select the entry: `{ model: "acme-4",
modality: "text", cache_status: "cached" }` resolves to the $0.30/M input rate.
Selector match sets are snapshotted per rating context — adding a catalog entry
applies forward only, never retroactively.
### Matching precedence is not array order
For a given measure, matching entries have this precedence:
1. Provider/model item plus dimension conditions.
2. Provider/model item alone.
3. Dimension conditions without an item.
4. A namespace-free default for the measure.
Adding more conditions does not create another specificity level. Two
overlapping entries at the same level are rejected with
`CATALOG_ENTRY_AMBIGUOUS`; neither the first nor last array entry wins. For
example, a `mode=fast` rule and a `cache_status=cached` rule can both match the
same report. Give them disjoint selectors or model the shared behavior as a
modifier instead of relying on declaration order.
No matching entry is not a zero-price fallback. Cover all intended report
combinations and test uncovered combinations explicitly. An explicit unbound
default is available for a single-measure meter; for multiple measures, declare
each default with `.for(theMeasure)`.
### Authoring rejection checklist
- A catalog must contain at least one rate; an empty or modifier-only catalog
is invalid.
- A rate's measure must belong to the catalog's meter. Each dimension selector
and modifier condition must use a dimension declared on that meter.
- `.for()` takes one measure, at most one provider-owned model, and at most one
value for each dimension. Extra measures and duplicate dimension selections
are rejected.
- Use actual SDK refs from the same compilation. Equal-looking objects, copied
refs, or refs from another registry generation are not substitutes.
- Provider/model components cannot contain slashes or glob metacharacters.
Catalog item declarations are exact; agreement selector patterns are a
different API.
- Two identical semantic entries do not become valid by changing their order.
The SDK derives stable entry keys from the measure/item/conditions, sorts
them canonically, and validates duplicates and overlaps.
## Modifiers
`fs.modifier.multiplier(numerator, denominator).when(dimension.is(value))`
scales every matching charge by an exact rational. `multiplier(3, 2)` is 1.5x
without a float. Contract discounts from economic agreements compose on top of
modifiers; the true-up base for minimums is post-modifier rated usage.
## Backend quotes
For rules that only the backend can price (dynamic upstream resale, bespoke
jobs), declare bounds in the repo:
```ts
const jobs = fs.measure("jobs");
const jobUsage = fs.meter("jobs", { measures: [jobs] });
const jobPricing = fs.pricing("jobs", {
meter: jobUsage,
catalog: [
fs.rate.backendQuoted({
min: fs.rate.perUnit(fs.money.usd(0.5)),
max: fs.rate.perUnit(fs.money.usd(50)),
}),
],
});
```
The backend then passes `quote: { currency: "usd", amountNanos }` to
`report()` — a **per-unit** rate in nanodollars, never a total. Out-of-range
quotes are clamped and dispute-flagged; the ledger records only core-rated
charges. A quote sent against a rule that is not backend-quoted is ignored.
Both bounds must be SDK-created flat rates and `min` must not exceed `max`.
Tier tables and nested quote rules are not valid bounds. Invalid bounds reject
with `BACKEND_QUOTE_BOUNDS_REQUIRED`; do not repair them by trusting an
unbounded amount from the backend.
## Bindings
| Binding | Rating context | Moves when |
| ----------------------------- | ------------------------------------------------------- | ------------------------------------------- |
| `pricing.current()` | The catalog version active in the served release. | Every activated release, forward. |
| `pricing.withContractTerms()` | The current catalog plus the subject's agreement terms. | Release activation and agreement amendment. |
| `pricing.fixedVersion(n)` | Exactly version `n`. | Never — `require_amendment` by definition. |
`withContractTerms` and `fixedVersion` are set on subjects through
[economic agreements](/reference/economic-agreements); repo plans normally bind
`current()`.
## Versions
Every material catalog change mints a new immutable pricing-policy version and
a new rating-context version in the next commercial release.
A catalog edit does **not** mint a new compiled plan, and it is not meant to.
The compiled plan freezes the plan's route grants, structural limits and its
own recurring fee — the contract a live subscriber is enforced against — and a
rate edit changes none of those. So the identity of a price-only change is the
pair: the release's membership for the plan keeps the **same** compiled-plan id
and moves to a **new** rating-context version. A repricing that re-sealed the
compiled plan would rebuild every live subscriber's enforcement contract for a
change that never touched it.
`farthershore commercial-release diff` lists rating-context versions added and
removed — that, not a compiled-plan entry, is where a price change shows up. Overlays and bindings carry author-supplied keys (`fs.meterRoutes`'s
first argument, the pricing family key), so reordering declarations never
rebinds a pinned rate.
---
# Funding & allowances
Canonical URL: https://docs.farthershore.com/reference/funding-and-allowances
A funding bucket is rated value the subscriber can spend before amount due
begins. Buckets are denominated in money internally (nanodollars), so an
allowance is worth the same across models, dimensions, and rate changes. The
bucket kind **is** the constructor — there is no label heuristic and no
plan-level credit policy.
```ts
funding: {
buckets: [
fs.included(fs.money.usd(10)),
fs.prepaid(fs.money.usd(25), { topUp: true }),
fs.promo(fs.money.usd(5)),
fs.referral(fs.money.usd(5)),
],
}
```
Constructors take money first and never take a string label.
## Bucket kinds
| Kind | Constructor | Source | Cash-backed | Refundable to the rail | Ledger contra account |
| ---------- | ----------------------------------- | --------------- | ----------- | ---------------------- | ------------------------- |
| `included` | `fs.included(amount, { display? })` | plan issuance | no | no | `IncludedAllowanceContra` |
| `prepaid` | `fs.prepaid(amount, { topUp? })` | subscriber pays | yes | yes | none — `PrepaidLiability` |
| `promo` | `fs.promo(amount)` | platform issues | no | no | `PromoContra` |
| `referral` | `fs.referral(amount)` | platform issues | no | no | `ReferralContra` |
- **included** — reissued each period by the plan. Only kind that accepts a
`display`.
- **prepaid** — purchased value. `topUp: true` lets the platform offer
replenishment; a top-up moves `pending → available` when the rail confirms
payment, and admission reserves only against `available` value. Required (and
the only kind allowed) on `prepaid` plans. **Initial purchase** uses the
managed plan checkout. **Refills use the signed-in subscriber** session and
the public Frontend SDK `fs.core()` handoff; see the
[prepaid wallet cookbook](/cookbook/prepaid-credits#add-the-subscriber-refill-control)
and [top-up HTTP contract](/generated/commerce/http#createportalconsumerbalancetopup).
- **promo** and **referral** — issued value that offsets charges and is tracked
as contra-revenue. Never cash, never rail-refundable.
## Allocation order
When a rated charge lands, eligible buckets pay it in one total order:
```text
(priority, expiresAt ASC nulls-last, promo < referral < included < prepaid, bucketId)
```
Cheapest-to-the-builder value goes first, soonest-expiring first within a
priority, and the bucket id is the final tie-break — never database order or
arrival time. Allocation runs against an epoch-versioned snapshot of the
subject's buckets so concurrent charges cannot double-spend.
## Holds
The gateway's monetary reservation is an admission bound; the durable funding
claim is a bucket **hold**:
```text
availableNanos = balanceNanos − heldNanos
```
Reserving moves value from available to held; capturing decreases both; releasing
returns value to available unless the bucket expired while held, in which case
the expiry posting runs atomically. An expiry sweep can never take a hold.
## Expiry and refunds
- **Included** value that is unused at period end expires as a contra reversal
(the builder's cost is unwound). It is never refunded.
- **Prepaid** value that expires is breakage (`UsageRevenue`, role
`BREAKAGE`).
- **Refunds** unwind pro-rata per source in reverse: prepaid → rail-refundable;
promo, referral, and included value are restored only if their bucket is
unexpired, else written off. A refund of a charge that was 60% promo / 40%
prepaid returns 40% to the customer and restores the promo portion.
- A rail-initiated refund or dispute that matches no expected local operation
puts the settlement account into `RECONCILIATION_REQUIRED`, which blocks
monetary admission until reconciliation clears it.
## Display and disclosure
An included bucket may carry a multiplier display:
```ts
fs.included(fs.money.usd(25), {
display: fs.display.multiplier({ factor: 5 }),
});
```
The display accepts only `factor`; the base is derived as `amount / factor`
($5 here). Combined with `spendPolicy.disclosure: fs.disclosure.opaque`, every
subscriber surface — the portal and the
[bill preview API](/reference/bill-preview-api) — shows the allowance as
`factor` display units with remaining and consumed units, and never a
nanodollar amount. Without a display, an opaque plan shows the consumed and
remaining fraction in basis points. `transparent` (the default) exposes
per-window rated totals and bucket balances.
## Exhaustion
`spendPolicy.onExhaustion` decides what happens when no eligible bucket can
pay:
- `fs.exhaustion.block` — the gateway denies `credit_exhausted` (402) until a
top-up lands or the next issuance. Required on `prepaid` plans.
- `fs.exhaustion.overage(pricing.current())` — usage continues and is rated at
the named catalog as amount due. Required on `hybrid` plans; the binding must
name the same pricing family as `usagePricing`.
Overage after exhaustion happens mid-period with no plan transition and no
release event.
## No direct balance mutation
A top-up on a `topUp: true` prepaid bucket is a Stripe payment; the bucket
becomes spendable only when the rail confirms it (`pending → available`).
There is no builder or subscriber API that mutates a bucket balance directly —
every balance change is a ledger posting, and bucket balances are reconciled
against the ledger on a schedule.
---
# Economic agreements
Canonical URL: https://docs.farthershore.com/reference/economic-agreements
An economic agreement binds one subject (a subscription) to one pricing family
with a specific binding mode and optional contract terms. Agreements are how a
bespoke discount, floor, cap, or pinned catalog version reaches one customer
without touching the public catalog. They are platform operations run through
the CLI, not declarations in `business/`.
## Binding modes
| `--binding` | Meaning | Follows releases? |
| ----------------------------- | --------------------------------------------------------------------------------------- | ----------------- |
| `current` | The live catalog version in each served release. | yes |
| `current_with_contract_terms` | The live catalog plus this agreement's terms (discount, floor, cap, scope). | yes |
| `fixed_version` | Exactly `--policy-version `; `require_amendment` by definition. | no |
## Two-step, confirm-gated
Every write previews first and commits only with the preview's token:
```bash
farthershore agreements create acme-llm \
--subject sub_01J... \
--binding current_with_contract_terms \
--policy pricing_llm \
--terms-json '{"percentDiscount":{"num":"1","den":"10"},"floorNanos":"500000000000","selectorScope":[{"kind":"catalog_item","item":{"provider":"acme","model":"acme-4"}}]}' \
--new-meter-policy adopt_at_current_rate \
--format json
```
The preview prints the resolved **effect** — which pricing rules the terms
touch, the resulting rating-context change, bounds violations — plus a
server-minted `confirmationToken` (`agc_..`). Nothing
is written. Re-run the same command with `--confirm ` to commit. If the
inputs or the policy changed in between, the effect digest no longer matches
and the CLI refuses with `AGREEMENT_CONFIRMATION_STALE` before any write; the
server enforces the same gate.
```bash
farthershore agreements create acme-llm \
--subject sub_01J... \
--binding current_with_contract_terms \
--policy pricing_llm \
--terms-json '{"percentDiscount":{"num":"1","den":"10"},"floorNanos":"500000000000","selectorScope":[{"kind":"catalog_item","item":{"provider":"acme","model":"acme-4"}}]}' \
--new-meter-policy adopt_at_current_rate \
--confirm agc_... \
--idempotency-key \
--format json
```
Amendments append a new contract-pricing version; they never edit one in
place:
```bash
farthershore agreements amend acme-llm agr_01J... \
--terms-json '{"percentDiscount":{"num":"3","den":"20"},"floorNanos":"500000000000","selectorScope":[{"kind":"catalog_item","item":{"provider":"acme","model":"acme-4"}}]}' \
--effective-at 2026-09-01T00:00:00Z
```
Read commands:
```bash
farthershore agreements list acme-llm --subject sub_01J...
farthershore agreements show acme-llm agr_01J...
farthershore agreements pins acme-llm sub_01J...
```
`pins` reads a subject's recurring-price pin and usage-pricing binding. It is
read-only; moving a subscriber to the latest plan is deferred post-launch.
## Contract terms
`--terms-json` is a JSON object with:
| Field | Type | Meaning |
| ----------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `percentDiscount` | exact rational `{num,den}` | Discount applied after catalog modifiers; at most 1. |
| `floorNanos` | decimal nanodollar string | Per-window minimum (true-up base is post-modifier rated usage). |
| `capNanos` | decimal nanodollar string | Per-window cap on rated usage; must not be below the floor. |
| `selectorScope` | array, ≥1 | Which catalog items the terms apply to: `{ kind: "catalog_item", item }` or `{ kind: "provider_model_glob", provider, modelGlob, modality? }`. |
Selectors match only within the named provider's namespace — a glob can never
cross providers. A pricing policy may declare permitted bounds for negotiated
terms; terms outside them are rejected with
`CONTRACT_TERMS_EXCEED_POLICY_BOUNDS`. A percent discount paired with
`adopt_at_current_rate` requires a floor.
## New meters and measures
When a release introduces pricing the agreement never negotiated,
`--new-meter-policy` (a meter the agreement has no rule for) and
`--new-measure-policy` (a new measure on an agreed meter) decide what happens:
| Policy | Effect |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `adopt_at_current_rate` | Rate the new basis at the live catalog; extend and audit the agreement's scope. |
| `exclude_until_renewal` | Rate at the live catalog with agreement terms withheld until the next negotiated version. |
| `require_amendment` | Rate at the live catalog with agreement terms withheld and record an amendment requirement (the default; mandatory for `fixed_version`). |
Auto-apply is limited to changes with the same measurement-rule key, the same
measure set, and items inside an agreed namespace; anything else routes
through these policies.
## Where agreements show up
- The plan's `usagePricing` binding is the default for every subject; an
agreement overrides it for one subject.
- Rating contexts compiled into a release include agreement modifiers, so
`farthershore commercial-release diff` shows a new rating-context version
when an agreement is created or amended.
- The bill preview reflects agreement pricing automatically.
---
# Commercial releases
Canonical URL: https://docs.farthershore.com/reference/commercial-releases
A **commercial release** is one immutable bundle per business and environment
that binds every artifact the gateway uses at admission to every artifact core
uses at rating: compiled plans, route grants, entitlements, measurement schema,
admission descriptors, commercial policy (funding, disclosure, exhaustion), and
rating contexts. Its id is derived from the hash of its content; identical
content yields the same release.
## Publication is not activation
Building and pushing a business compiles a release and **publishes** it: the
immutable `CommercialReleaseVersion` and its membership index are persisted,
every artifact is written to the edge under versioned keys and read back.
**Activation** is a separate, per-business step: an entry is appended to the
business's release log and the per-business active pointer is promoted to the
new sequence. Only activated releases admit traffic.
```text
content-hash → health-probe → sequence reserve → versioned keys
→ readback → per-business two-phase pointer promotion → readback → watermark
```
A stale publisher (one that did not observe the newest release) cannot publish
over it. Business A's activation or rollback never touches business B.
## Diff two releases
```bash
farthershore commercial-release diff --format json
```
The command compares two compiled release manifests locally and reports
`unchanged`, compiled plans added / removed / changed, rating-context versions
added / removed, every changed manifest path, and compatibility-fence changes.
## Active at admission
"Active" is a property of an admission, not of the current pointer. A request
is admitted under exactly one release cohort — the route table, grants,
entitlement, measurement schema, admission descriptor set, policy, and rating
context all from that release; nothing is ever mixed across releases. The
gateway stamps the served identity into the signed usage event:
```text
{ subjectId, compiledPlanId, commercialReleaseVersionId,
ratingContextVersionId, routeKey, descriptorSetHash, commercialSequence }
```
For plans authored with `pricing.current()`, a compatible commercial publish
does not rewrite every subscriber. If the current global release still contains
the subscriber's immutable compiled plan, the gateway serves that release and
selects its rating context by stable economic-agreement id, or by the unique
public/default context when there is no agreement. The subscriber's exact older
release remains only as a retained structural fallback when the current release
no longer contains the plan. Missing or ambiguous current context selection
fails closed; the gateway never mixes it with an artifact from the fallback.
`pricing.withContractTerms()` follows the same current-release path while
selecting the context for the subscriber's stable agreement id.
`pricing.fixedVersion(n)` never follows current G: the exact retained release
named by the subscriber remains authoritative even when current G contains the
same compiled plan.
Core rates the event under **that** release after verifying the signature, the
append-only release log, and the persisted membership index. It never asks
whether the release is current now, so a later publish or rollback cannot
invalidate already-admitted work. Operations already in flight when a pointer
moves complete under the release they were admitted with (bounded by lease TTL
plus reap slack, about 75 seconds).
## Fail-closed proof states
| State | Structure | Money | Deny code |
| ------------------------ | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------- |
| `verified-active` | Full verified cohort. | Normal monetary reservation under the release's rating context. | — |
| `verified-stale-bounded` | Only the complete last-verified cohort of the same release. | Bounded emergency reservation from the last-verified descriptor; stamped `releaseProof: "emergency"`. | — |
| `unprovable-closed` | Deny before upstream; no partial or fallback artifacts. | No reservation. | `commercial_release_unprovable` (503) |
Absence is never interpreted as proof: a missing, incomplete, hash-invalid, or
scope-mismatched bundle closes admission.
## Rollback
Rollback appends a new release-log entry with a **greater** sequence that
points at an older immutable release. It never rewrites a release, never
decrements a sequence, and never re-rates usage that was admitted under the
rolled-back release. A later deliberate roll-forward names the rolled-back
release as its expected source and receives another greater sequence.
## Subscriptions and releases
A release selects the contract for new subscriptions. Existing
subscriptions stay pinned to their release for structure and to their
recurring-price pin for the fee; their `usagePricing` binding decides whether
metered rates follow the new catalog (`current()`) or not (`fixedVersion`).
Publication, activation, and rollback never realign subscriber pins; that is a
deferred post-launch operation.
## What a release is not
- Not a Stripe object. Stripe receives subscription and recurring-price
identities and settles amount due; catalogs and buckets have no Stripe
counterpart.
- Not a subscriber-migration mechanism. There are no bridge roles, migration
batches, or plan-release legs.
- Not mutable. Fix forward by publishing a new release.
Read [Plan changes](/monetize/plan-changes) for the builder workflow and
[Releases](/operate/releases) for the GitHub Release process.
---
# Bill preview API
Canonical URL: https://docs.farthershore.com/reference/bill-preview-api
The bill preview is a core API over the rating engine and the ledger. It uses
the same window projection and posting reads that invoicing uses, so **preview
equals invoice by construction** — there is no client-side bill math to keep in
sync, and no per-unit price is exposed for a subscriber to recompute.
```http
GET /portal/businesses/{businessId}/me/bill-preview
```
- Consumer (portal) authentication; the environment header selects a preview
environment.
- A public business with no subscriber or subscription yet receives the empty
transparent preview (all zeros). Private businesses return 403 like every
sibling read.
- Every monetary amount is a **decimal string of nanodollars**
(`"1234567890"` = $1.23456789). Never parse them as floats.
- `recurringFeeCents` is the subscription's recurring-price pin, in cents.
The response shape is chosen by the served plan's `spendPolicy.disclosure`,
read from the release the subscription is actually served under. Any lookup
miss — no activated release for the plan, member absent from that release,
projection missing or hash-invalid — resolves to **opaque**, never to the
disclosing branch.
## `disclosure: "transparent"` (default)
```json
{
"currency": "usd",
"disclosure": "transparent",
"recurringFeeCents": 3000,
"windows": [
{
"windowId": "rw_2026_08",
"windowStart": "2026-08-01T00:00:00.000Z",
"windowEnd": "2026-09-01T00:00:00.000Z",
"chargeCount": 1842,
"ratedNanos": "18420000000"
}
],
"totals": {
"ratedNanos": "18420000000",
"fundedNanos": "10000000000",
"receivableNanos": "8420000000"
},
"allowances": [
{
"kind": "included",
"state": "AVAILABLE",
"remainingNanos": "0",
"heldNanos": "0",
"consumedNanos": "10000000000",
"expiresAt": "2026-09-01T00:00:00.000Z"
}
]
}
```
| Field | Meaning |
| ------------------------ | -------------------------------------------------------------------------------------------------------- |
| `windows[]` | One entry per rating window: engine-exact recognized total (per-event floors plus the window remainder). |
| `totals.ratedNanos` | Sum of window totals. |
| `totals.fundedNanos` | Rated value paid by funding buckets (ledger postings). |
| `totals.receivableNanos` | Amount due through the settlement rail after funding (debits minus credits). |
| `allowances[]` | Every bucket for the subject: kind, state, remaining / held / consumed nanos, expiry. |
`ratedNanos − fundedNanos` may differ from `receivableNanos` when refunds,
corrections, or true-ups have posted; the ledger is authoritative for each.
## `disclosure: "opaque"`
```json
{
"currency": "usd",
"disclosure": "opaque",
"recurringFeeCents": 10000,
"allowances": [
{
"kind": "included",
"state": "AVAILABLE",
"expiresAt": "2026-09-01T00:00:00.000Z",
"display": {
"kind": "multiplier",
"allowanceUnits": 5,
"remainingUnits": "3.25",
"consumedUnits": "1.75"
}
}
]
}
```
No windows, no totals, no nanodollar balances — nothing a subscriber could
divide by a known request count to recover a rate. `display` is either:
- `{ kind: "multiplier", allowanceUnits, remainingUnits, consumedUnits }` when the
bucket was authored with `fs.display.multiplier({ factor })` — units are the
authored factor, remaining and consumed are two-decimal strings; or
- `{ kind: "fraction", consumedBasisPoints, remainingBasisPoints }` (0..10000)
when the plan authored no display.
Amounts owed on an opaque plan surface through the invoice, not the preview.
## Using it from the frontend SDK
`@farthershore/farthershore-js` exposes the preview as
`fs.billing.getBillPreview()` and the `useBillPreview()` React hook; render
either branch by switching on `disclosure`. Do not compute a bill from
`usePlans()` or usage rows — the catalog is not returned to subscribers, and a
client-side estimate can never match the ledger.
## Guarantees
- Preview and invoice share one code path; a preview/invoice mismatch is a
platform defect, not a rounding artifact.
- Rating floors per event; the window remainder is recognized separately and
the dropped fraction posts to `RoundingDifference` — the customer never pays
more than the exact rational total.
- The response reflects rated events. Measurements still in flight (queued,
post-stream, DLQ) are not yet in `windows`; they will be rated under the
release they were admitted under, never at today's price.
---
# Usage & billing policy
Canonical URL: https://docs.farthershore.com/operate/usage-billing-policy
The Business program defines what is metered, billable, limited, and granted.
The platform records the runtime decisions and settlements produced by that
contract. Keep those ownership boundaries separate while diagnosing.
## Start with the accepted contract
```bash
farthershore business status acme --format json
farthershore business routes acme --env production --format json
farthershore business contract acme --env production --format json
farthershore plan list acme --format json
```
Confirm the expected release is live and the route, meter, and plan version you
expect were actually accepted. A local source file or pending Git commit is not
runtime evidence.
## Follow live traffic
```bash
farthershore analytics timeseries acme --range 24h --domain usage --format json
farthershore analytics top acme --range 24h --by type --domain usage --format json
farthershore analytics latency acme --range 24h --format json
farthershore analytics log acme --range 1h --domain usage --limit 100 --format json
farthershore usage summary acme --format json
```
- Timeseries shows volume over time.
- Top groups current activity by the selected dimension.
- Latency distinguishes application slowness from missing traffic.
- Log is the newest-first request and metering evidence.
- Usage summary is the coarser 30-day business view.
Use `--env ` on analytics when you need an exact environment.
Do not combine preview usage with production billing conclusions.
## Explain a denial
```bash
farthershore denial show acme --format json
```
Correlate the denial with the request id and decision id from the gateway
response. See [Diagnose limits and denials](/operate/limits).
## Compare the full chain
When served requests and billed usage disagree, compare in order:
1. Accepted commercial release, route match, and environment.
2. Gateway request/decision record (its signed usage event carries the served
identity: subscription, release, rating context).
3. Backend `report()` for backend-measured dimensions — measure keys and
dimension values must match the release's measurement schema.
4. Aggregated usage summary.
5. The rating context the event was admitted under, its `RatedCharge`, and
the funding postings — the subscriber's [bill preview](/reference/bill-preview-api)
is computed from exactly these.
A missing event near the start of that chain cannot be repaired by changing a
price. An unexpected price cannot be repaired by replaying traffic — usage is
rated under the release it was admitted with, never at today's catalog. Find
the first boundary where evidence diverges. If the ledger and Stripe disagree,
the subject is in `RECONCILIATION_REQUIRED` and monetary admission is blocked
until reconciliation clears it; see
[Ledger & settlement](/operate/ledger-and-settlement).
The CLI also exposes a **preview-only** historical billing replay:
```bash
farthershore workflow-control replay acme \
--period-start 2026-08-01T00:00:00Z \
--period-end 2026-09-01T00:00:00Z \
--format json
```
This previews historical replay work; it does not execute a charge or mutate a
provider. Treat its result as diagnostic evidence.
If the accepted contract is wrong, fix `business/`, test in preview, and publish
forward. If the contract and request evidence are correct but settlement or
provider state diverges, capture request ids, decision ids, subscription ids,
period bounds, and the live release for support.
---
# Ledger & settlement
Canonical URL: https://docs.farthershore.com/operate/ledger-and-settlement
Every monetary fact on the platform is a balanced ledger posting in
nanodollars. Funding buckets, amount due, and the bill preview are **views**
of the ledger, reconciled against it on a schedule. Stripe is the settlement
rail: it collects and pays out, and its webhooks are idempotent ledger inputs
— never direct balance mutations. Core is the single monetary writer for a
subject.
## Chart of accounts
| Account | Class | Meaning |
| ---------------------------- | ---------------- | ------------------------------------------------------------------------------------------------- |
| `CashClearing` | asset | Gross funds the rail has confirmed collected, less confirmed refunds or payouts. |
| `PrepaidLiability` | liability | Rated-value obligation represented by every live bucket (prepaid, included, promo, referral). |
| `IncludedAllowanceContra` | contra-revenue | Cost of included value a plan issued; unused expiry credits it back. |
| `PromoContra` | contra-revenue | Cost of promotional value issued. |
| `ReferralContra` | contra-revenue | Cost of referral value issued. |
| `UsageRevenue` | revenue | Rated usage recognized from `RatedCharge` rows; prepaid breakage posts here with role `BREAKAGE`. |
| `RecurringRevenue` | revenue | Recurring plan fees. |
| `RoundingDifference` | contra / expense | Customer-favorable value dropped by rating-floor or settlement-quantum flooring. |
| `SettlementReceivable(rail)` | asset | Exact amount due through a named rail after funding. |
| `TaxPayable(rail)` | liability | Tax the rail collected and must remit; never rewrites local revenue. |
| `RefundsPayable` | liability | Cash-backed value approved for return, not yet confirmed paid. |
| `WriteOff` | expense | Uncollectible receivables, expired non-cash funding on refund, final dispute loss. |
## Posting templates
One template per event type; every transaction balances to zero:
- **Rated charge** — debit funding bucket(s) in allocation order and/or the
rail receivable; credit `UsageRevenue`.
- **Recurring fee / commitment true-up** — the local agreement clock recognizes
it; exactly one settlement export carries it to the rail.
- **Top-up** — `pending → available`: the bucket is created pending on
checkout, funded (`CashClearing` ↔ `PrepaidLiability`) when the rail confirms
payment.
- **Grant issuance / expiry** — included, promo, referral value debits its
contra account on issuance; unused value on expiry credits it back. Prepaid
expiry is breakage.
- **Refund** — reverse the original allocation pro-rata per source: prepaid to
`RefundsPayable`, non-cash restored if unexpired else `WriteOff`; a rail
payout later debits `RefundsPayable`, credits `CashClearing`.
- **Settlement export / receipt / void / write-off** — the exported receivable
is matched byte-for-byte to the Stripe invoice; receipts post gross cash plus
tax.
- **Rating-floor rounding** — per-event floors are exact; the window remainder
is recognized once and the dropped fraction posts to `RoundingDifference`.
Rating corrections (late measurements, volume-tier reselection at window
close) are correction transactions on the closed window, not refunds.
## Machine-checked invariants
Scheduled reconciliation asserts, and alerts on:
1. every transaction balances;
2. `sum(bucket balances) == ledger liability` per subject and source kind;
3. per rating window, `sum(RatedCharge) == revenue + contra postings`;
4. settlement rounding stays within `settlements × quantum`;
5. rating-floor rounding equals the exact rational target per window;
6. gateway value reconciliation: folded per-lease spend equals the usage
meter's per-dimension totals.
Nothing edits historical postings to make a predicate pass. Corrections are
explicit balanced transactions after the discrepancy is understood.
## Two kinds of stop
| State | Scope | Effect | Cleared by |
| --------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `SettlementAccount.state = RECONCILIATION_REQUIRED` | one subject | Monetary admission denied for that subject; rating continues. | Scheduled reconciliation proving ledger, buckets, expected rail operations, and Stripe agree. |
| `SettlementHold(reason)` | one rating window or event | Export for that window blocked; unrelated rating and admission continue. | The reason-specific reconciler (`admission_bound_breached`, `accounting_divergence`, quarantine, …). |
Triggers for `RECONCILIATION_REQUIRED`: a rail-initiated refund, dispute, or
early-fraud warning matching no expected local operation; a top-up success
after cancellation; a Stripe invoice that does not match its local export;
bucket/liability or rail/cash divergence.
Sequence gaps in the usage pipeline **hold settlement, never rating**: a
quarantined event writes a terminal record that closes its hole; late events
are correction transactions.
## What Stripe never does
Stripe never rates usage, chooses funding, owns a balance, or mutates a bucket.
No subscriber balance and no meter lives in Stripe. `invoice.upcoming` is
never a monetary input. Only one webhook family is authoritative for each
transition (Checkout success for top-ups, `invoice.paid` for cash receipt,
`refund.*` for payouts, `charge.dispute.*` for disputes).
## Reading the state
- Subscriber-facing totals: the [bill preview](/reference/bill-preview-api).
- Builder-facing usage: `farthershore usage summary ` and the
dashboard's usage and plans views (catalog and release, never subscriber
bills).
- Release identity for any admitted request rides the signed usage event; the
Apply Timeline and workflow views show publication and activation history.
Continue with [Diagnose billing and usage](/cookbook/diagnose-billing-usage).
---
# Subscription + overage
Canonical URL: https://docs.farthershore.com/cookbook/subscription-plus-overage
## Outcome
Sell Quillby for $29/month with a recurring usage allowance and per-word overage.
Customers under the allowance pay the base fee; heavier customers pay for the
extra usage.
Use this model when customers want predictable access but their usage varies.
## Prerequisites
- A managed business repository with an accepted plan
- A meter reported from your product
- Request-bound usage reported with `ctx.report()`
## Define the meter and plan
```ts
import * as fs from "@farthershore/business";
const words = fs.measure("words");
const usage = fs.meter("word_usage", { measures: [words] });
const usagePricing = fs.pricing("word_usage", {
meter: usage,
catalog: [fs.rate.per(1000, fs.money.usd(0.2))],
});
const api = fs.backend("api", {
default: true,
});
const generate = fs.route("/v1/generate", {
post: { backend: api },
});
const generateStream = fs.route("/v1/generate-stream", {
post: { backend: api },
});
fs.meterRoutes("generated-words", generate, {
reports: [usage],
maxOutputUnits: words.atMost(4_000),
});
fs.meterRoutes("generated-words-stream", generateStream, {
reports: [usage],
postStream: { settlementMax: [words.atMost(4_000)] },
});
fs.plan("author", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(29).monthly(),
usagePricing: usagePricing.current(),
funding: { buckets: [fs.included(fs.money.usd(10))] },
spendPolicy: {
onExhaustion: fs.exhaustion.overage(usagePricing.current()),
},
maxMonthlySpendCents: 50_000,
});
export default fs.business();
```
The exact rate prices each word at $0.0002 — 50,000 words per $10 allowance.
The branded `words` measure guarantees the pricing dimension resolves. The
included bucket pays the first $10 of rated usage every period; the declared
overage policy rates the rest at the same catalog as amount due. The `hybrid`
kind requires exactly this shape (`price`, `usagePricing`, `funding`,
`spendPolicy` with `exhaustion.overage`).
Overage is unbounded by construction, so the plan must also state an upper
bound or the build fails with `PLAN_UNBOUNDED_SPEND`. `maxMonthlySpendCents`
caps the subscriber's monthly bill at $500; a rate limit
(`limits: [requests.perMinute(600)]`) or a blocking policy
(`spendPolicy: { onExhaustion: exhaustion.block }`, which forgoes overage
entirely) satisfy the same requirement. See
[Billing strategies](/monetize/strategies) and
[Funding & allowances](/reference/funding-and-allowances).
Report request-bound usage from the backend:
```ts
await ctx.report({
meter: "word_usage",
values: { words: generatedWordCount },
});
```
For the streaming route, call the same verb after the stream closes:
```ts
await ctx.report({
meter: "word_usage",
values: { words: generatedWordCount },
});
```
Once the response is on the wire, `ctx.report()` automatically delivers over
the attested post-stream channel; the `postStream.settlementMax` bound on the
binding tells admission the most that report can be worth. It carries the
served identity of the original request and is billing-only; it does not
retroactively enforce the current request.
## Validate and launch
```bash
farthershore build --format json
farthershore commercial-release diff previous-release.json candidate-release.json --format json
farthershore validate --format json
git add business/ && git commit -m "add Author overage plan" && git push
# After explicit approval of the exact active-business release:
approved_sha="$(git rev-parse HEAD)"
git tag -a v1.2.0 "$approved_sha" -m "v1.2.0"
git push origin v1.2.0
gh release create v1.2.0 --verify-tag --target "$approved_sha" --title "v1.2.0" --generate-notes
farthershore business status quillby --format json
```
Replace the example version with the approved semantic version. Active
repo-managed businesses publish subsequent versions with GitHub Releases, not
`farthershore business publish`.
Poll status until `ACTIVE` and `live: true`.
## Verify
- `farthershore business status quillby --format json` reports `live: true`.
- Calls to `POST /v1/generate` complete and report `words`.
- `farthershore usage summary quillby --format json` shows `words` increasing
for both below-allowance and above-allowance tests.
- The subscriber's bill preview shows the included allowance's
`consumedNanos` rising to $10, then `receivableNanos` growing at exactly
$0.0002 per word.
## Common failures and recovery
| Symptom | Fix |
| ----------------- | --------------------------------------------------------------------------------------- |
| No usage appears | Match the `fs.meter()` key and measure key exactly; report through `ctx.report()`. |
| Wrong amount | Read the bill preview; confirm the catalog rate and the included amount in the release. |
| Duplicate usage | Reuse the request identity; request-bound callbacks are idempotent. |
| Unexpected denial | Inspect the rate/quota response metadata and usage summary. |
Correct meter or plan values in `business/` and publish a new version. A
catalog change reaches `current()`-bound subscribers from activation forward;
a recurring-price change reaches new subscriptions only.
## Next steps
- [Backend metering](/backend/metering)
- [Prepaid wallet](/cookbook/prepaid-credits)
- [Diagnose billing and usage](/cookbook/diagnose-billing-usage)
## Agent prompt
```text
Add a words meter and an Author plan to Quillby: $29/month, included funding,
and $0.0002 per word overage. Report request-bound usage through an attested SDK
channel. Build and verify in preview, then show the exact commit, semantic diff,
and proposed GitHub Release and ask for approval. After approval, release and
verify low- and high-usage calls. Do not use unattested background metering for
request-bound usage.
```
---
# Freemium that converts
Canonical URL: https://docs.farthershore.com/cookbook/freemium
## Outcome
Give Quillby a limited Free plan and a $19/month Pro plan. Free users can try the
product; Pro raises the limit and unlocks Brand Voice.
Use freemium when a bounded free experience helps customers understand the
product before checkout.
## Prerequisites
- A frontend business and authenticated CLI
- A reachable HTTPS origin for `POST /v1/generate`
- A usage meter or resource that can enforce the free boundary
## Define the plans and gate
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const words = fs.measure("words_generated");
const wordUsage = fs.meter("word_usage", { measures: [words] });
const wordPricing = fs.pricing("word_usage", {
meter: wordUsage,
catalog: [fs.rate.per(1000, fs.money.usd(0.02))],
});
const api = fs.backend("api", {
transport: { mode: "direct" },
default: true,
});
const generate = fs.route("/v1/generate", {
post: { backend: api, costs: [requests.fixed(1)], reports: [wordUsage] },
});
const brandVoice = fs.route("/v1/brand-voice", {
post: { backend: api, costs: [requests.fixed(1)] },
});
fs.meterRoutes("generate-words", generate, {
reports: [wordUsage],
maxOutputUnits: words.atMost(2_000),
});
fs.plan("free", {
kind: fs.plan.kind.free,
grants: [generate],
limits: [requests.perMinute(30)],
});
fs.plan("pro", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(19).monthly(),
usagePricing: wordPricing.current(),
funding: { buckets: [fs.included(fs.money.usd(10))] },
spendPolicy: {
onExhaustion: fs.exhaustion.overage(wordPricing.current()),
},
grants: [generate, brandVoice],
limits: [requests.perMinute(300)],
});
export default fs.business();
```
The origin handler for `POST /v1/generate` must report real usage:
```ts
await ctx.report({
meter: "words_generated",
values: { words_generated: generatedWordCount },
});
```
Direct route grants control access. Free is bounded structurally (30
requests/minute); Pro includes $10 of rated words each month — 500,000 words at
$0.02 per thousand — and rates anything past that as overage on the same
catalog. Subscribers see the allowance and amount due through the bill
preview, never a per-word price they must compute.
## Validate and launch
```bash
farthershore build --format json
farthershore validate --format json
git add business/ && git commit -m "add Free and Pro" && git push
farthershore backend create quillby \
--name "Quillby API" \
--slug api \
--transport direct \
--origin-url https://api.example.com \
--default \
--idempotency-key \
--format json
farthershore business publish quillby --dry-run --format json
# After explicit approval of the first draft activation:
farthershore business publish quillby --format json --idempotency-key
farthershore business status quillby --format json
```
Poll until `status: "ACTIVE"` and `live: true`.
## Verify
```bash
farthershore backend create quillby \
--env preview \
--name "Quillby API (preview)" \
--slug api \
--transport direct \
--origin-url https://preview-api.example.com \
--default \
--idempotency-key \
--format json
farthershore persona bootstrap quillby --env preview --plan free --format json --idempotency-key
PREVIEW_GATEWAY="https://preview.example.com" # replace with preview hostname
FSK_TEST_KEY="fsk_test_..." # replace with bootstrap output
curl -i "$PREVIEW_GATEWAY/v1/generate" \
-H "x-api-key: $FSK_TEST_KEY"
farthershore usage summary quillby --format json
```
- The Free persona activates without Stripe checkout.
- Repeated calls increase `words_generated`; a burst past 30/minute is denied
`rate_limited`.
- Brand Voice is hidden or upsold for Free.
- Bootstrap a `--plan pro` persona and confirm Brand Voice is unlocked; the
bill preview shows the included allowance draining and overage after it.
- The managed usage card matches the usage summary.
## Common failures and recovery
| Symptom | Fix |
| -------------------------- | ------------------------------------------------------------------------------ |
| Free plan fails validation | A `free` plan takes no economic controls; bound it with `limits`. |
| Publish dry run is refused | Follow the stable prerequisite hint, resolve it, and preview again. |
| Paid route leaks to Free | Grant the backend route only to the paid plan; UI hiding is presentation. |
| Usage never increases | Report the exact declared meter key through an attested request-bound channel. |
Fix the contract in `business/`, build, and publish forward. A new release
reaches new subscriptions; existing subscribers keep their recurring-price pin
and, under `wordPricing.current()`, follow catalog changes forward.
## Next steps
- [Metered routes](/cookbook/metered-routes)
- [Frontend route-aware UI](/frontend/access-aware-ui)
- [Plan changes](/monetize/plan-changes) — including the downgrade direction: a
subscriber can move from Pro back to Free from the portal, and that move
lands at the end of their current billing period.
## Agent prompt
```text
Add Free and Pro plans to Quillby. Free is kind free with an enforced rate
limit. Pro is kind hybrid at $19/month with a $10 included word allowance,
overage on the same catalog, and grants brand-voice. Add managed
usage, plans, and upsell UI; build and validate in preview; then verify Free
denial at limit and Pro access. Show the production publish dry run and ask
before publishing production.
```
---
# Add a trial
Canonical URL: https://docs.farthershore.com/cookbook/add-trial
## Outcome
New subscribers can try a paid plan before normal billing begins.
## Prerequisites
- A paid `fs.plan()`
- Approval for a billing change
## Change the plan
A trial is a plan **kind**: `fs.plan.kind.trial` with `lifecycle.trialDays` and
the recurring price that applies after the trial. Add it as a sibling of the
paid plan, or convert the paid plan itself:
```ts
fs.plan("pro-trial", {
kind: fs.plan.kind.trial,
price: fs.money.usd(49).monthly(),
lifecycle: { trialDays: 14 },
grants: [api],
limits: [requests.perMinute(600)],
});
```
During the trial no obligation accrues; rating still records the served
context, so a trial with `usagePricing` rates post-trial use correctly from
the first paid day. Preview the compiled contract:
```bash
farthershore build --format json
git push -u origin HEAD:env/trial-preview
farthershore business show acme --env trial-preview --format json
```
## Verify
Create a new preview subscription and confirm the returned plan has
`kind: "trial"` and `trial_days: 14`. Existing subscriptions are not a
substitute for this test.
## Common failures
- Build fails with `PLAN_KIND_CONTROL_MISMATCH`: a `trial` plan requires
`price` and `lifecycle` and allows only `usagePricing` beyond them.
- Trial is missing: confirm you edited the active plan key and pushed the
preview branch.
- Existing subscribers did not enter a trial: trials apply to new subscription
starts; existing subscriptions keep their pins.
## Recover
Remove the trial plan (or restore the previous kind) and rebuild the preview.
If already released, review active trials before releasing another billing
change.
## Next steps
See [plans](/define/plans), [subscriptions](/monetize/subscriptions), and
[plan changes](/monetize/plan-changes).
## Agent prompt
> Add a 14-day trial to the paid `pro` plan using `fs.plan.kind.trial` and
> `lifecycle.trialDays`. Treat this as a billing change requiring approval,
> validate with `farthershore build`, test a new preview subscription, and stop
> before production publish.
---
# Add a spend cap
Canonical URL: https://docs.farthershore.com/cookbook/add-spend-cap
## Outcome
A plan has an explicit spend ceiling: metered usage draws down a fixed amount
of value and the gateway denies further billable work at zero. Use this to
limit a subscriber's — and your — exposure on metered plans.
## Prerequisites
- A meter and pricing catalog the plan binds
- A chosen ceiling in dollars
- Approval for a billing change
## Change the plan
A spend cap in the new model is **funding plus a block policy**. Prepay (or
include) exactly the value the subscriber may spend and set
`fs.exhaustion.block`; the gateway reserves each request's economic maximum
against that value before forwarding it, so the cap holds before the origin is
called rather than after the invoice.
For a subscription that includes a fixed monthly usage budget and must stop
there, use `custom` (an included bucket with `block` is not a `hybrid` shape,
which requires overage):
```ts
fs.plan("pro-capped", {
kind: fs.plan.kind.custom,
price: fs.money.usd(49).monthly(),
usagePricing: tokenPricing.current(),
funding: { buckets: [fs.included(fs.money.usd(200))] },
spendPolicy: { onExhaustion: fs.exhaustion.block },
grants: [chat],
limits: [requests.perMinute(600)],
});
```
For a wallet the subscriber funds themselves, use `prepaid` with `topUp: true`
— the cap is whatever they have paid for. Every route the plan can spend on
must declare a bound (`maxOutputUnits`, `chunkPolicy`, or a post-stream
`settlementMax`) so the reservation is finite; the compiler rejects an unbounded
one.
Validate and test on preview:
```bash
farthershore build --format json
git push -u origin HEAD:env/spend-cap-preview
farthershore business show acme --env spend-cap-preview --format json
```
## Verify
Confirm the preview contract's `pro-capped` plan is `kind: "custom"` with a
$200 included bucket and `block` exhaustion. Exercise usage near the cap with a
preview subscriber: the bill preview's allowance `remainingNanos` falls with
each request, and the first request that cannot be covered is denied
`credit_exhausted` (402) before it reaches your origin.
## Common failures
- Build fails with `PLAN_KIND_CONTROL_MISMATCH`: `hybrid` requires
`exhaustion.overage`; a blocking allowance is `custom` or `prepaid`.
- Build fails with `ADMISSION_OUTPUT_BOUND_REQUIRED`: a route the plan can
spend on has an unbounded measure — add `maxOutputUnits`.
- Usage continues past the cap: the plan is `overage`, not `block`; read the
release's `spendPolicy`.
- Plan list is unchanged: the preview build may not have applied yet.
## Recover
Revert the preview commit. If released, the new bucket amount applies to new
issuances; existing subscribers keep their current-period bucket. Prepare and
approve a forward correction.
## Next steps
See [billing strategies](/monetize/strategies),
[funding & allowances](/reference/funding-and-allowances),
[monetary admission](/reference/monetary-admission), and
[plans](/define/plans).
## Agent prompt
> Give the metered `pro` plan a $200 monthly spend cap: a kind-custom plan
> with a $200 included bucket and exhaustion.block, every spend route bounded
> with maxOutputUnits. Validate in preview, prove the credit_exhausted denial,
> and require approval before release.
---
# Change a price
Canonical URL: https://docs.farthershore.com/cookbook/change-a-price
You want to change what a plan costs. Editing the price is one line; the real
question is what happens to subscribers already on the old price. A
subscription holds a **recurring-price pin** that a release never changes
silently, and a **usage-pricing binding** (`pricing.current()` by default)
that follows the live catalog. So a recurring-fee change reaches new
subscriptions only, while a catalog-rate change reaches every `current()`
subscriber from activation forward. Moving an existing subscriber to the latest
recurring price is a deferred post-launch operation.
## Outcome
New signups receive the new recurring price; existing subscribers keep their
pin. Metered rates on `current()` plans follow the new catalog.
## Prerequisites
- The current plan key and price
- Preview evidence for the new contract
- Billing-owner approval before production publish or migration
## Edit the price
Prices live on `fs.plan()` in the folder-discovered `business/` program. Money
constructors accept human major units and lower them to exact integer cents.
```ts
import * as fs from "@farthershore/business";
// Raise Pro from $199 to $249 per month. Preserve its existing route refs.
fs.plan("pro", {
kind: fs.plan.kind.flat,
price: fs.money.usd(249).monthly(),
grants: [managedJobs],
limits: [requests.perMinute(6000)],
});
```
To change a **usage** rate instead, edit the catalog the plan binds:
```ts
const apiPricing = fs.pricing("api_usage", {
meter: apiUsage,
catalog: [fs.rate.per(1000, fs.money.usd(6))], // was $5 per 1,000
});
```
Build, push, then cut the release. A price change is **economic**, so the push
validates and is accepted but its publish **defers to a release** — nothing
changes for subscribers until you cut one:
```bash
farthershore build --format json
farthershore commercial-release diff previous-release.json candidate-release.json --format json
git add business/ && git commit -m "raise Pro to $249" && git push
farthershore apply-timeline list croncloud --format json # economic → publish: skipped
# After explicit approval of the exact SHA, version, and cohort impact:
approved_sha="$(git rev-parse HEAD)"
git tag -a v2.0.0 "$approved_sha" -m "v2.0.0"
git push origin v2.0.0
gh release create v2.0.0 --verify-tag --target "$approved_sha" --title "v2.0.0" --generate-notes
```
Replace the example version with the approved next semantic version. An active
repo-managed business rejects `farthershore business publish`, including dry
run, with `MANAGED_BY_CODE`; later releases are GitHub Releases.
The release publishes a new commercial release containing the repriced `pro`
plan. Existing Pro subscribers keep their $199 recurring-price pin. Anyone
subscribing after activation pays $249. If the release also changed the
`api_usage` catalog, every subscriber bound to `apiPricing.current()` is rated
at the new rate from activation forward — usage already admitted stays at the
old rate.
Migration is **not** a declaration. An `fs.business()` result is a snapshot of
product state, not a description of how to move live subscribers. Commercial
release prepare, publish, and activation never change subscriber pins.
## Existing subscribers
Moving an existing cohort to the latest commercial release is intentionally
deferred until the post-launch migrate-to-latest workflow ships. Do not treat a
new release as consent to reprice or re-entitle an existing subscription.
## Verify it works
- `farthershore build` succeeds and the new price is in the validated plan.
- After publish, a fresh signup is charged the new price.
- Existing subscribers keep their recurring-price pin.
- `farthershore commercial-release diff` lists `pro` under changed compiled
plans (and a new rating-context version if a catalog changed).
## Common failures
- The amount is off by 100: recurring prices are integer cents.
- The push validates but does not deploy: economic changes wait for a release.
- A subscriber's usage rate did not change: they are on a `fixedVersion`
agreement; amend the agreement.
## Recover
Stop any pending manual migration. Correct the plan in code, validate in preview,
and publish a forward release after billing approval. Do not edit Stripe prices
or subscriber rows by hand.
## Agent prompt
> Change the existing Pro price from $199 to $249 in the `business/` program.
> Build, push to preview, show the semantic diff and production publish dry run,
> and explain which subscribers the change reaches. Ask before publishing.
## Related
- [Add metered routes](/cookbook/metered-routes) — change usage pricing, not just the recurring fee.
- [Publish a production release](/cookbook/release-production) — the gates a publish must pass.
- [CLI quickstart](/get-started/quickstart) — use the same build, push, and verification loop.
---
# Prepaid wallet
Canonical URL: https://docs.farthershore.com/cookbook/prepaid-credits
## Outcome
Sell a Quillby prepaid plan. A $5 prepaid funding bucket is issued by the
ledger, metered usage draws it down, the gateway blocks at zero, and customers
can replenish it (`topUp: true`) through a Stripe payment. There is no
recurring subscription charge.
Use a prepaid wallet when customers prefer a known spend before consumption, or
when usage is irregular enough that a recurring subscription is awkward.
## Initial purchase and later refills
**Initial purchase** chooses the prepaid plan and pays for its declared bucket.
The managed `PlansTable` and `FsOnboardingPlanRail` components already start
that checkout through `fs.plans.subscribe()` or `fs.plans.startOnboarding()`.
A **Refill** is different: an already-subscribed customer purchases more value
after the initial bucket has been issued. There is no managed refill component
or typed `fs.billing` method yet. Add a subscriber-owned control that calls the
public Core endpoint through `fs.core()` as shown below. This is not a builder
CLI or MCP action because the signed-in subscriber is the purchasing principal.
## Prerequisites
- A declared usage meter
- Attested request-bound usage reporting
## Define prepaid funding
```ts
import * as fs from "@farthershore/business";
const words = fs.measure("words");
const usage = fs.meter("word_usage", { measures: [words] });
const usagePricing = fs.pricing("word_usage", {
meter: usage,
catalog: [fs.rate.per(1000, fs.money.usd(0.2))],
});
const api = fs.backend("api", {
transport: { mode: "direct" },
default: true,
});
const generate = fs.route("/v1/generate", {
post: { backend: api },
});
fs.meterRoutes("generated-words", generate, {
reports: [usage],
maxOutputUnits: words.atMost(4_000),
});
fs.plan("prepaid", {
kind: fs.plan.kind.prepaid,
usagePricing: usagePricing.current(),
funding: {
buckets: [fs.prepaid(fs.money.usd(5), { topUp: true })],
},
spendPolicy: { onExhaustion: fs.exhaustion.block },
});
export default fs.business();
```
`prepaid` plans require every unbounded measure to declare a bound
(`maxOutputUnits`, `chunkPolicy`, or a post-stream `settlementMax`) so the
gateway can reserve each request's economic maximum — here at most 4,000 words
× $0.0002 = $0.80 — against available funding before forwarding it. The
compiler rejects an unbounded prepaid route (`ADMISSION_OUTPUT_BOUND_REQUIRED`).
Report usage with the exact `words` key:
```ts
await ctx.report({
meter: "word_usage",
values: { words: generatedWordCount },
});
```
## Add the subscriber refill control
Call the [top-up HTTP contract](/generated/commerce/http#createportalconsumerbalancetopup)
from a signed-in subscriber session. The member needs permission `invoice:pay`.
The amount is an integer from 100 through 100,000 cents ($1 through $1,000).
```ts
import type { FartherShoreClient } from "@farthershore/farthershore-js";
type TopUpCheckout = Readonly<{ ok: true; checkoutUrl: string }>;
export async function startPrepaidRefill(
fs: FartherShoreClient,
amountCents: number,
): Promise {
const boot = await fs.bootstrap();
const returnUrl = window.location.href;
const requestId = crypto.randomUUID();
const result = await fs.core({
method: "POST",
path: `/portal/businesses/${encodeURIComponent(boot.business.id)}/me/balance-top-up`,
body: {
amountCents,
requestId,
successUrl: returnUrl,
cancelUrl: returnUrl,
},
});
window.location.assign(result.checkoutUrl);
}
```
`fs.core()` supplies the session bearer and portal environment/organization
scope; do not call an internal route or send a builder credential. A POST is not
automatically retried by the SDK. If your UI lets a customer retry an ambiguous
attempt, retain the same `requestId` for that logical purchase instead of
creating a second intent.
## Validate and launch
```bash
farthershore build --format json
farthershore validate --format json
git add business/ && git commit -m "add prepaid wallet" && git push
farthershore backend create quillby \
--name "Quillby API" \
--slug api \
--transport direct \
--origin-url https://api.example.com \
--default \
--idempotency-key \
--format json
farthershore business publish quillby --dry-run --format json
# After explicit approval of the first draft activation:
farthershore business publish quillby --format json --idempotency-key
farthershore business status quillby --format json
```
Poll until `ACTIVE` and `live: true`.
## Verify
- Subscribing issues the declared $5 prepaid funding bucket once.
- A successful refill follows `pending → available` exactly once; a duplicate
Stripe webhook does not issue twice.
- Reported usage reserves, then captures, available funding; the bill preview's
allowance `remainingNanos` falls by exactly $0.0002 per word.
- After available funding reaches zero, the next request is denied
`credit_exhausted` (402) before it reaches the origin.
## Common failures and recovery
| Symptom | Fix |
| ----------------------- | ---------------------------------------------------------------------------------------------------- |
| Balance never changes | Match the meter and measure keys; report through `ctx.report()`. |
| Refill appears twice | Retain one `requestId` per logical purchase; do not create rail objects manually. |
| Units cost too much | Recheck the exact rate and human-unit money amount in the release. |
| Build rejects the route | Add `maxOutputUnits` (or `chunkPolicy` / `postStream.settlementMax`) to the binding. |
| Customer is locked out | `credit_exhausted` means available funding is zero — top up or move to a `hybrid` plan with overage. |
Correct the contract and publish forward. For an incorrect usage event, stop the
faulty producer and follow [billing diagnosis](/cookbook/diagnose-billing-usage)
before replaying or adjusting anything.
## Next steps
- [Subscription plus overage](/cookbook/subscription-plus-overage)
- [Backend metering](/backend/metering)
- [Funding & allowances](/reference/funding-and-allowances)
- [Monetary admission](/reference/monetary-admission)
## Agent prompt
```text
Add a kind-prepaid words plan to Quillby with no recurring fee, a $5 prepaid
funding bucket that permits top-ups, and exhaustion.block. Price words at
$0.0002 with fs.rate.per, bound the route with maxOutputUnits, report usage
through ctx.report(), and validate in preview. Show the production publish preview and ask for confirmation before
publishing. Then verify funding burn-down, exhaustion denial, top-up, and
idempotency.
```
---
# Meter AI tokens
Canonical URL: https://docs.farthershore.com/cookbook/ai-token-metering
An AI proxy bills on tokens, not request counts. The pattern is a two-part loop:
declare input and output token **measures** on one `model_usage` meter with the
dimensions you price by (provider, model, modality, cache status, mode), price
them in a catalog, bind the meter to the chat route with `fs.meterRoutes()`,
then send the real per-request token counts from your backend with one
`report()` call. The platform rates the signed measurement under the release it
was admitted with.
This is the same `meterRoutes` + `report()` mechanism as
[Add metered routes](/cookbook/metered-routes), specialized for LLM usage. We
build a small `llm-api` product to keep the example self-contained; it is the
platform's enterprise-LLM golden sample.
## Outcome
The gateway settles the model's signed actual token counts on each successful
response.
## Prerequisites
- A backend route that returns model usage
- A preview environment and runtime token
- A pricing decision in dollars per million tokens, per model and cache status
## Declare the token meters and the chat route
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const input = fs.measure("input_tokens");
const out = fs.measure("output_tokens");
const providerName = fs.dimension("provider");
const model = fs.dimension("model");
const modality = fs.dimension("modality");
const cacheStatus = fs.dimension("cache_status");
const mode = fs.dimension("mode");
const text = modality.value("text");
const uncached = cacheStatus.value("uncached");
const cached = cacheStatus.value("cached");
const acme = fs.provider("acme");
const acme4 = acme.model("acme-4");
const modelUsage = fs.meter("model_usage", {
measures: [input, out],
dimensions: [providerName, model, modality, cacheStatus, mode],
});
const llmPricing = fs.pricing("llm", {
meter: modelUsage,
catalog: [
fs.rate.perMillion(fs.money.usd(3)).for(input, acme4, text, uncached),
fs.rate.perMillion(fs.money.usd(15)).for(out, acme4, text, uncached),
fs.rate.perMillion(fs.money.usd(0.3)).for(input, acme4, text, cached),
fs.rate.perMillion(fs.money.usd(15)).for(out, acme4, text, cached),
fs.modifier.multiplier(3, 2).when(mode.is("fast")),
],
});
const api = fs.backend("api", {
transport: { mode: "direct" },
meters: [requests, modelUsage],
default: true,
});
const chat = fs.route("/v1/chat", {
post: { backend: api, costs: [requests.fixed(1)], reports: [modelUsage] },
});
fs.meterRoutes("chat-model-usage", chat, {
reports: [modelUsage],
maxOutputUnits: out.atMost(8192),
});
fs.plan("enterprise", {
kind: fs.plan.kind.usage,
usagePricing: llmPricing.current(),
grants: [chat],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
Cached input is $0.30/M instead of $3/M; output stays $15/M for both cache
states; `fast` mode multiplies every matching charge by exactly 3/2. Rates are
exact rationals end to end. `maxOutputUnits: out.atMost(8192)` bounds the
output measure — the gateway clamps the client's `max_tokens` to 8,192 before
signing the request upstream, so billing truncation and product truncation
are the same event.
A meter bound with `fs.meterRoutes()` is rated from signed backend evidence.
The request count is separate and structural: it bounds the request rate
because the route attaches `costs: [requests.fixed(1)]`.
## Report the real token counts from your backend
Install `@farthershore/backend`, set `FS_RUNTIME_TOKEN` (see
[CLI quickstart](/get-started/quickstart) for the agent workflow), verify the
platform signature, then report the model's `usage` figures with `ctx.report()`.
Before the response is sent it signs usage into the platform response path — no
extra network call.
```ts
import { fartherShore } from "@farthershore/backend";
const fs = fartherShore.initFromEnv(); // derives everything from FS_RUNTIME_TOKEN
export async function POST(request: Request) {
const url = new URL(request.url);
const body = new Uint8Array(await request.clone().arrayBuffer());
// Fail-closed verification: identity comes only from the platform's signature,
// never from the plaintext X-FS-* headers.
const ctx = await fs.verifyRequest({
method: request.method,
path: url.pathname,
query: url.search,
headers: request.headers,
body,
});
const completion = await callModel(await request.json());
await ctx.report({
meter: "model_usage",
values: {
input_tokens: completion.usage.prompt_tokens,
output_tokens: completion.usage.completion_tokens,
},
dims: {
provider: "acme",
model: "acme-4",
modality: "text",
cache_status: completion.usage.cached ? "cached" : "uncached",
mode: "standard",
},
});
return Response.json(completion);
}
```
The meter, measure, and dimension keys are plain strings that must match the
release's measurement schema (`model_usage`, `input_tokens`, `output_tokens`,
and the declared dimensions); an unknown key is rejected loudly at the gateway,
never silently dropped. Values are validated locally before signing — they must
be non-negative finite numbers. Identity is never an argument: it rides the
verified context. Backends report **measurements, never money**.
### Express upstreams
If your upstream is Express, verify with the middleware and report the same way:
```ts
const fs = fartherShore.initFromEnv();
app.use(express.raw({ type: shouldCaptureRawBody, limit: "10mb" }));
app.use((req, _res, next) => {
if (Buffer.isBuffer(req.body) && req.body.length > 0) {
(req as typeof req & { rawBody?: Buffer }).rawBody = req.body;
}
next();
});
app.use(fs.middleware()); // strict and fail-closed by default
app.use(parseVerifiedJson); // parse only after signature verification
```
Use the generated backend scaffold's `shouldCaptureRawBody` and
`parseVerifiedJson` helpers so streaming exemptions and size limits remain in
sync with the SDK. Only `fs.middleware({ always: false })` defers verification
to a backend contract that explicitly disables it.
## Build and verify
```bash
farthershore build --format json
```
Push `business/**`, then drive a real chat request through a
[test persona](/reference/cli#environments-and-personas) and read the breakdown.
Wait until `env list` contains the preview environment before creating its
backend row. If automatic branch-prefix creation did not occur, create the
preview explicitly first:
```bash
farthershore env list llm-api --format json
farthershore backend create llm-api --env \
--name "Preview API" --slug api --transport direct \
--idempotency-key \
--origin-url https://preview-api.example.com --default --format json
farthershore backend list llm-api --format json
# Token meters appear per-dimension once traffic flows.
farthershore usage summary llm-api --format json
```
Filter the structured backend list by the preview's environment id.
## Verify it works
- `farthershore build` succeeds and the validated business lists
`model_usage` with `input_tokens` + `output_tokens` measures.
- A `POST /v1/chat` call with `max_tokens: 100000` is forwarded with
`max_tokens: 8192`.
- `input_tokens` / `output_tokens` in the usage summary match the model's
`usage.prompt_tokens` / `usage.completion_tokens`.
- The subscriber's bill preview rates a 1,000-uncached-input / 500-output call
at exactly $0.003 + $0.0075 = $0.0105 (× 3/2 in `fast` mode).
## Common failures
- Usage stays at zero: the reported meter, measure, and dimension keys must
exactly match the declared keys.
- Token totals double: call `report()` once per request; the SDK picks the
channel.
- `422 admission_bound_exceeded`: the client's `max_tokens` exceeds the
declared bound and could not be rewritten.
## Recover
Stop test traffic, correct the meter bindings or backend report in preview, and
re-run one request. Do not replay or adjust billable usage until the mismatch is
understood.
## Agent prompt
> Add a model_usage meter with input and output token measures and
> provider/model/modality/cache_status/mode dimensions to the existing chat
> route, price it per million tokens per model and cache status in a catalog,
> bind it with meterRoutes and maxOutputUnits, and report the model's exact
> counts through one report() call. Build and verify one preview request. Do
> not publish production or print runtime tokens.
## Related
- [Add metered routes](/cookbook/metered-routes) — the general `meterRoutes` + `report()` loop.
- [Pricing catalogs](/reference/pricing-catalogs) — items, selectors, modifiers, tiers, quotes.
- [Prepaid wallet](/cookbook/prepaid-credits) — meter tokens down against a prepaid balance.
- [CLI reference](/reference/cli) — mint `FS_RUNTIME_TOKEN` and a test persona via the CLI.
---
# Understand gateway behavior
Canonical URL: https://docs.farthershore.com/gateway/overview
The gateway fronts the business's declared routes. It verifies the caller and enforces the accepted contract before work reaches the backend. A request must resolve to the intended business environment, route, credential, subscriber and plan.
## Trace a request
Start with the exact method, path, host and environment. [Routes](/define/routes) declare call surfaces and scopes; [tenancy](/define/tenancy) describes member and service identities. The plan must grant access. Managed RBAC can add a member permission requirement. The gateway then applies the relevant structural and monetary bounds and resolves the environment's backend.
[Usage limits](/operate/limits) distinguish rate, quota, concurrency, resource and other enforced bounds. [Monetary admission](/reference/monetary-admission) describes reservations, capacity and maximum settlement for work that can spend funding. Admission failure prevents upstream work; a later upstream failure is a different outcome.
## Verify the backend boundary
Use the [Backend SDK](/backend/metering) to verify signed raw-body and context data before handling. Ordinary caller headers cannot establish tenant identity. Application record ownership still belongs in the backend.
Fixed request costs are counted by the gateway. Dynamic measurements come from the verified request's report. Streaming or background reporting preserves the served identity and follows the [post-stream contract](/backend/metering). Do not replace a missing measurement with a guessed charge.
## Handle denial and retry
Read the structured code and deny envelope using the [response reference](/reference/response-codes). Retry only when the documented reaction permits it, respecting Retry-After and bounded attempts. A required upgrade, missing permission or resource cap needs a state change rather than a retry loop.
Use [denial diagnosis](/cookbook/diagnose-denied-request) to locate the first failed boundary. Keep the request and decision IDs and inspect the matching environment. Do not broaden a plan or role merely to make a reproduction pass.
For non-JavaScript consumers, use [API consumption](/backend/consume). Wire examples are not Python or Go SDKs.
---
# Consuming the API
Canonical URL: https://docs.farthershore.com/backend/consume
Subscribers call the Farther Shore business gateway, not your direct backend
origin. The gateway resolves the subscriber, plan, environment, route, limits,
and backend, then signs and forwards an admitted request.
## Send the subscriber key
The default business API-key header is `x-api-key`. If the business contract
sets a different `authHeader`, use that exact header instead.
```ts
async function createJob(apiKey: string, input: unknown) {
const response = await fetch("https:///v1/jobs", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify(input),
});
if (response.ok) return response.json();
const body = await response.json().catch(() => ({}));
throw new BusinessApiError(response.status, body);
}
```
Use a test key and preview gateway for preview environments and a live key for
Main. Do not send a runtime token, CLI credential, or provider secret to a
business route.
## Handle a denial as data
Denied requests return a stable machine-readable `code`. Usage-limit denials
also include an `_fs` envelope that tells a client what kind of limit fired and
what reaction is appropriate:
```json
{
"error": "Too many requests in flight.",
"code": "concurrency_limit_exceeded",
"_fs": {
"limitClass": "concurrency",
"reaction": "queue",
"limitOrigin": "platform",
"retrySafe": true,
"mustModify": false,
"decisionId": "dec_8a91…",
"requestId": "req_2c7f…",
"envelopeVersion": 1
}
}
```
Branch on `_fs.limitClass`, `_fs.reaction`, `retrySafe`, and `mustModify`; do not
infer the remedy only from the HTTP status.
| Limit class | Typical response |
| --------------------------------- | ---------------------------------------------------------------------------- |
| `rate`, `concurrency`, `adaptive` | Honor `Retry-After`; retry the same request only when `retrySafe` is true |
| `capacity` | Reduce or change the request before retrying |
| `quota`, `spend` | Upgrade, top up, or wait for the applicable reset; blind retries do not help |
```ts
async function handleDeny(response: Response) {
const body = await response.json().catch(() => ({}));
const detail = body._fs;
if (!detail) throw new Error(body.code ?? `HTTP ${response.status}`);
if (detail.retrySafe) {
const seconds = Number(response.headers.get("retry-after") ?? 1);
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
return { retry: true };
}
if (detail.reaction === "upgrade") return { upgrade: true };
if (detail.mustModify) return { modifyRequest: true };
return { code: body.code, decisionId: detail.decisionId };
}
```
Always bound retries, add jitter, and preserve the operation's own idempotency
key when retrying a write.
## Upstream responses
An admitted request normally returns your backend's response status, body, and
safe headers. Farther Shore consumes internal signing and metering headers; they
are not an application API. A 503 `origin_unavailable` means the selected
environment has no usable backend binding or tunnel connection. It is an
operator problem, not a subscriber authentication failure.
Use the denial `requestId` or `decisionId` when correlating a subscriber report
with platform and backend diagnostics. Never include API keys or runtime tokens
in logs.
---
# Monetary admission
Canonical URL: https://docs.farthershore.com/reference/monetary-admission
The gateway does no pricing. For plans that can run out of money (`prepaid`,
`hybrid` while an included or prepaid bucket is paying, `custom` with `block`,
`rail.x402`), core compiles an **admission descriptor** per route into the
commercial release. The descriptor is a per-category linear bound the gateway
evaluates with integer arithmetic; the result is the request's economic
maximum, reserved against the subscriber's available funding before the request
is forwarded.
```text
economicMaximumNanos = max( perOpFloor, Σ categoryBound_i × maxNanosPerUnit_i )
```
Coefficients are opaque compiler outputs derived from the rating context with
**ceiling** rounding; rating uses **floor**. So `final charge ≤ admitted
maximum` holds by construction, and a "20x cache-heavy" workload is not
over-reserved: each token category has its own bound.
## Where category bounds come from
| Bound source | Example | Authored with |
| ------------------ | --------------------------------------------------- | ------------------------------------------------------------ |
| `exact` | request count = 1; gateway-tokenized uncached input | nothing — the gateway knows it |
| `request_declared` | the client's `max_tokens` after clamping | `fs.meterRoutes(..., { maxOutputUnits: out.atMost(8192) })` |
| `route_cap` | max batch items; a post-stream settlement maximum | `caps: [measure.atMost(n)]`, `postStream: { settlementMax }` |
For prepaid-class plans **every** chargeable measure on the route must have a
finite bound. The compiler rejects an unbounded output measure
(`ADMISSION_OUTPUT_BOUND_REQUIRED`), a client knob that could raise a bound but
is not clamped (`ADMISSION_UNBOUNDED_KNOB_UNCLAMPED`), and prepaid post-stream
without a settlement max (`PREPAID_POST_STREAM_SETTLEMENT_MAX_REQUIRED`).
## Client-knob clamping
`maxOutputUnits: out.atMost(8192)` does two things: it bounds the admission
category, and it makes the gateway **rewrite the request** — `max_tokens` /
`max_output_tokens` and their protocol aliases are parsed as a non-negative
integer, clamped down to the bound, and set to the bound when missing, all
before the request is signed upstream. Billing truncation and product
truncation are therefore the same event. A malformed or conflicting knob is
`422 invalid_admission_knob`; a knob above the bound that cannot be safely
rewritten is `422 admission_bound_exceeded`. Neither reaches the upstream.
## Streaming
- **Bounded output** — declare `maxOutputUnits`; the whole operation is reserved
up front.
- **Chunked** — declare `chunkPolicy: { bound: out.atMost(65536), chunkUnits: 1024 }`;
the gateway reserves `chunkUnits` at a time up to the cumulative
per-operation ceiling. When a top-up cannot be reserved, the stream stops at
the preceding admitted chunk boundary; the final charge never exceeds the sum
of admitted chunks. Exclusive with `maxOutputUnits`.
- **Post-stream** — the authoritative measurement arrives after the response is
on the wire. Prepaid-class plans must declare `postStream: { settlementMax:
[measure.atMost(n)] }` so admission can reserve a finite maximum.
An actual measurement above its admitted bound is an invariant breach: core
rates what it can, opens a settlement hold (`admission_bound_breached`), and
alerts. It never silently over-debits.
## The monetary reservation
The reservation is a first-class strategy in the gateway's usage meter — the
same lease/spend/release/checkpoint algebra as structural rate limits, with
nanodollars as its unit — riding the same Durable Object call as every other
constraint. Reap releases `reserved − spent`. It replaces the retired credit
mirror: exact algebra against the ledger-derived funding projection instead of
a mirrored estimate.
## Deny codes
| Code | HTTP | When | Retry? |
| ------------------------------------------ | ---- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
| `credit_exhausted` | 402 | A `block` plan's available funding cannot cover the request's economic maximum. | After a top-up or issuance. |
| `credit_state_unavailable` | 503 | The funding projection for a `block` plan is absent or unreadable — fails closed. | Yes, transient. |
| `commercial_release_unprovable` | 503 | The per-business release bundle is absent, incomplete, hash-invalid, scope-mismatched, or the emergency budget cannot cover the request. | Yes, transient. |
| `admission_descriptor_unavailable` | 503 | The descriptor artifact is missing, corrupt, or from the wrong release / rating context. | Yes, transient. |
| `admission_descriptor_no_admissible_tuple` | 503 | No conditional dimension tuple is consistent with the request. | Yes, transient. |
| `invalid_admission_knob` | 422 | The client's output knob is malformed or has conflicting aliases. | No — fix the request. |
| `admission_bound_exceeded` | 422 | The client's knob exceeds the admitted bound and cannot be safely rewritten. | No — lower the knob. |
Every one of these fails closed **before** any upstream call. Full vocabulary:
[Response & deny codes](/reference/response-codes).
## Obsolete measurement payloads
After the platform's billing cutover, a usage payload from an outdated
`@farthershore/backend` that lacks the served-identity block receives a
permanent `410`/`422 unsupported_usage_schema` with
`{ expected_schema_version, received_schema_version }`. It is never
transformed, queued, quarantined, or backfilled. Upgrade the SDK.
## Release proof and emergency reserve
If the edge cannot refresh its proof of the active release, it may keep serving
the last-verified cohort of the **same** release with a bounded emergency
reservation (stamped `releaseProof: "emergency"`) for at most the lease-TTL
window; past that, or with an incomplete cohort, admission closes with
`commercial_release_unprovable`. See
[Commercial releases](/reference/commercial-releases).
---
# Usage limits
Canonical URL: https://docs.farthershore.com/operate/limits
Limit definitions are repo-owned Business contract. A denial and its recorded
usage are platform-owned runtime facts. Diagnose the fact before changing the
contract.
## Capture the request identifiers
On a denied gateway response, retain the response status, `code`, `limitCode`,
`X-FS-Decision-Id`, request id, environment, route, and caller. Limit-class
responses also carry a self-describing `_fs` block.
```json
{
"code": "rate_limited",
"limitCode": "rate_limit",
"_fs": {
"limitClass": "rate",
"reaction": "backoff_retry",
"retrySafe": true,
"decisionId": "dec_...",
"requestId": "req_..."
}
}
```
Then ask the platform to explain the recorded request:
```bash
farthershore denial show acme --format json
farthershore analytics log acme --domain denials --range 1h --limit 100 --format json
farthershore usage summary acme --format json
```
`denial show` is the narrow diagnostic. Analytics establishes whether the same
failure is isolated or widespread. Usage establishes the current business-level
picture; none of these commands changes a limit.
## React by limit class
| Class | Typical response | Safe reaction |
| ------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `quota` | Period budget exhausted | Wait for reset or offer an appropriate plan change. Do not hot-loop. |
| `rate` | Short-window velocity cap | Honor `Retry-After`, add jitter, and retry. |
| `concurrency` | Too many requests in flight | Queue or back off until a slot clears. |
| `capacity` | One request is too large | Reduce or split the request before retrying. |
| `spend` | Funding exhausted on a `block` plan (`credit_exhausted`) | Wait for a top-up or the next issuance, or offer an overage/plan change. |
| `adaptive` | Upstream provider throttle | Back off or use a safe fallback; the customer cannot raise the provider's cap. |
Do not branch only on HTTP status. `429` can mean rate or concurrency, and `402`
can mean quota or spend. `_fs.limitClass` and `_fs.reaction` carry the semantic
meaning for a classified limit denial.
A `503` ending in `_unavailable` or `_unprovable` is a fail-closed dependency
or release-proof failure rather than a limit. `422 invalid_admission_knob` and
`422 admission_bound_exceeded` mean the client's output knob (`max_tokens` and
aliases) is malformed or above the route's declared bound — fix the request.
See [Monetary admission](/reference/monetary-admission). Retrying a
destructive platform operation because a gateway dependency failed is not a
valid limit response.
## Resource counts are backend-reported state
If a plan caps persistent resources, the Business program declares the resource
and limit, while the running backend reports the authoritative count after its
own mutation:
```bash
farthershore resource-count report acme projects \
--subscription \
--count 42 \
--environment \
--format json
```
Use the backend runtime SDK for normal request-path reporting. The CLI command is
an operator surface for explicit reconciliation; it is not a substitute for a
race-safe create/delete transaction in your application.
## Change the contract only when the contract is wrong
Read the accepted route and contract first:
```bash
farthershore business routes acme --env production --format json
farthershore business contract acme --env production --format json
farthershore plan list acme --format json
```
If the declared capacity, route grant, meter, or spend policy is wrong, edit the
Business program, test in preview, and release forward. Do not search for an
imperative limit-update command.
See [Diagnose a denied request](/cookbook/diagnose-denied-request).
---
# Gate API routes by plan
Canonical URL: https://docs.farthershore.com/cookbook/grant-routes
Use direct route grants when only selected plans should reach an operation.
The gateway, not the UI, is the authority.
## Outcome
Each plan receives exactly the API operations it grants by ref.
## Prerequisites
- Existing plans and a preview environment
- Stable route paths or explicit action ids
- Test subscribers on both an allowed and denied plan
```ts
import * as fs from "@farthershore/business";
const requests = fs.requests();
const api = fs.backend("api", {
transport: { mode: "direct" },
meters: [requests],
default: true,
});
const listJobs = fs.route("/v1/jobs", {
get: { backend: api, costs: [requests.fixed(1)] },
});
const createJob = fs.route("/v1/jobs/create", {
post: { backend: api, costs: [requests.fixed(1)] },
});
const deleteJob = fs.route("/v1/jobs/{id}", {
delete: { backend: api, costs: [requests.fixed(1)] },
});
const managedJobs = fs.group("managed-jobs", [listJobs, createJob, deleteJob]);
fs.plan("starter", {
kind: fs.plan.kind.free,
grants: [listJobs],
limits: [requests.perMinute(60)],
});
fs.plan("pro", {
kind: fs.plan.kind.flat,
price: fs.money.usd(29).monthly(),
grants: [managedJobs],
limits: [requests.perMinute(600)],
});
export default fs.business();
```
Starter can list jobs but cannot mutate them. Pro grants the group and can use
all three operations.
## Verify in preview
After the push, wait for `route-grants-preview` to appear in `env list` before
creating the backend row. If automatic branch-prefix creation did not occur,
create the preview explicitly first:
```bash
farthershore build --format json
farthershore validate --format json
git push -u origin HEAD:env/route-grants-preview
farthershore env list --format json
farthershore backend create --env route-grants-preview \
--name "Preview API" --slug api --transport direct \
--idempotency-key \
--origin-url https://preview-api.example.com --default --format json
farthershore backend list --format json
```
Filter the structured backend list by the preview's environment id.
Test one subscriber on each plan:
- Starter: `GET /v1/jobs` succeeds.
- Starter: `POST /v1/jobs/create` and `DELETE /v1/jobs/{id}` return the stable authorization
denial.
- Pro: every declared operation succeeds when the request is otherwise valid.
Client-side hiding is optional presentation. Never treat it as authorization;
the route grant is enforced at the gateway.
## Common failures
- A plan has too much access: grant the narrow route ref instead of the group.
- A grant has no effect: reuse the exact ref returned by `fs.route()`.
- The build rejects a ref: do not forge refs or reuse a ref from another
compile.
## Recover
Change the affected plan's `grants`, rebuild, and publish a forward contract
revision. Keep stable route paths and action ids when possible.
## Agent prompt
> Grant these API operations by plan using direct `fs.route()` or `fs.group()`
> refs. Build and validate, verify both allowed and denied subscribers in
> preview, and do not publish production.
---
# Diagnose a denied request
Canonical URL: https://docs.farthershore.com/cookbook/diagnose-denied-request
## Outcome
The exact enforcement boundary and owning state are known before retrying or
changing a plan, permission, limit, or backend.
## Prerequisites
- The denied response's request id and environment.
- The original method, path, caller, and timestamp.
- Read access to denial, contract, route, analytics, and backend state.
## Capture evidence
Keep the response status, `code`, `limitCode`, `_fs`, `X-FS-Decision-Id`, request
id, method, path, caller, and environment. Do not retry yet.
```bash
farthershore denial show acme --format json
farthershore analytics log acme --range 1h --domain denials --limit 100 --format json
farthershore business status acme --format json
farthershore business routes acme --env production --format json
farthershore business contract acme --env production --format json
```
## Walk the boundaries in order
1. Confirm the credential belongs to this business and environment.
2. Confirm method and path matched the expected compiled route.
3. Confirm the active plan grants that route.
4. For a member request, confirm the effective Managed-RBAC permission.
5. Read `_fs.limitClass` for quota, rate, concurrency, capacity, spend, or
provider throttling.
6. Only after gateway admission passed, inspect backend health and application
logs.
```bash
farthershore backend list acme --format json
```
Reproduce at most once with the same customer and environment. A hard quota,
capacity, entitlement, or permission denial will not improve through rapid
retry. A rate or concurrency denial should follow `_fs.reaction` and
`Retry-After`.
Fix repository-owned causes in the Business program and test them in preview.
Fix customer state through the narrow consumer operation. Do not broaden a plan
or role merely to make one test pass.
See [Diagnose limits and denials](/operate/limits).
## Verify
After correcting the owning fact, reproduce once with the same customer,
environment, method, and path. Require either the intended success or the same
documented expected denial.
## Recovery
Fix repository-owned policy in `business/` and push it through preview. Fix
customer state with the narrow customer operation. Follow `Retry-After` only
for a retryable rate/concurrency decision; do not retry hard denials rapidly.
## Agent prompt
```text
Diagnose this one denied request by preserving its ids, reading denial details,
and walking credential, route, plan, role, and limit boundaries in order.
Report the first failing boundary before making any change.
```
---
# Response & deny codes
Canonical URL: https://docs.farthershore.com/reference/response-codes
When the platform denies a request it returns stable base fields: a
human-readable `error` plus a machine-readable `code`. Limit decisions may also
include `limitCode` and an `_fs` envelope with reaction, retry safety, origin,
decision id, and limit metadata. Branch on typed SDK helpers and `code`, never
the message. The frontend SDK exposes the canonical gateway vocabulary as
`FS_DENY_CODES`.
```json
{ "error": "Rate limit reached for this key.", "code": "rate_limited" }
```
```ts
import {
FartherShoreApiError,
FS_DENY_CODES,
} from "@farthershore/farthershore-js";
try {
await fs.route.get("/v1/cron-jobs");
} catch (err) {
if (err instanceof FartherShoreApiError) {
switch (err.code) {
case FS_DENY_CODES.credit_exhausted:
return showFundingExhausted();
case "route_not_enabled":
return promptUpgrade();
}
}
}
```
## Platform deny wire-codes (`FS_DENY_CODES`)
The canonical deny `code` values. Grouped by concern; the HTTP status the
platform returns alongside each is in the table.
| `code` | HTTP | Meaning |
| ------------------------------------------ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `limit_exceeded` | 402 | A usage/quota limit on a metered dimension is reached. |
| `rate_limited` | 429 | A per-window rate limit is reached. Retryable. |
| `credit_exhausted` | 402 | On a `block` plan, available funding cannot cover the request's evaluated economic maximum (owner: the monetary reservation). |
| `credit_state_unavailable` | 503 | A `block` plan's funding projection is absent or unreadable — fails closed before allocation. Retryable. |
| `enforcement_denied` | 402 | A consume-phase batch enforcement check denied the request. |
| `commercial_release_unprovable` | 503 | The per-business commercial-release bundle is absent, incomplete, hash-invalid, scope-mismatched, or the emergency budget is exhausted. Retryable. |
| `admission_descriptor_unavailable` | 503 | The route's admission descriptor is missing, corrupt, or from the wrong release / rating context. Retryable. |
| `admission_descriptor_no_admissible_tuple` | 503 | No conditional dimension tuple is consistent with the request. Retryable. |
| `invalid_admission_knob` | 422 | The client's output knob (`max_tokens` and aliases) is malformed or has conflicting aliases. Fix the request. |
| `admission_bound_exceeded` | 422 | The client's output knob exceeds the route's declared bound and cannot be safely rewritten. Lower the knob. |
| `limit_allocator_unavailable` | 503 | A limit check was transiently unavailable. Retryable. |
| `route_not_enabled` | 403 | The active plan does not grant the matched route identity. |
| `invalid_entitlement_shape` | 503 | The resolved plan access failed schema validation. Retryable. |
| `unsupported_constraint_schema` | 503 | A limit rule used an unsupported schema. Retryable. |
| `enforcement_error` | 500 | Enforcement hit an unexpected error. |
| `enforcement_dependency_unavailable` | 503 | An enforcement dependency was transiently unavailable. Retryable. |
| `concurrency_limit_exceeded` | 429 | The plan's concurrent-request cap is reached. Retryable. |
| `concurrency_context_unavailable` | 503 | Concurrency context couldn't be read. Retryable. |
| `concurrency_coordinator_unavailable` | 503 | The concurrency coordinator was unavailable. Retryable. |
| `key_expired` | 401 | The API key has expired. |
| `credential_revoked` | 401 | The credential was withdrawn (revoked, or its owning subscription, plan or business was removed). Not retryable; a new credential is required. |
| `credential_rotated` | 401 | The credential was superseded by a rotation. Re-read the stored secret and retry with the new one. |
| `credential_env_reset` | 401 | The preview environment was rebuilt from a new contract, which deletes its subscriptions and their keys. Re-subscribe, or bootstrap a new persona. |
| `permission_denied` | 403 | The member's resolved permissions do not satisfy the route requirement. |
| `permission_unresolved` | 403 | RBAC is enabled but the credential has no usable permission claim. |
| `geo_context_unavailable` | 503 | Geo context couldn't be resolved. Retryable. |
| `geo_blocked` | 403 | The request origin is in a blocked region. |
| `geo_not_allowed` | 403 | The request origin isn't in the allow-list. |
| `resource_count_limit_exceeded` | 402 | A counted-resource cap (e.g. `cron_jobs`) is reached. Core atomically authorizes the create and the gateway relays the denial. |
| `post_stream_overspend` | 402 | Previously reported streaming usage crossed a blocking quota; later requests remain locked out until reset. |
| `resolver_rate_limited` | 429 | An internal resolver was rate-limited. Retryable. |
| `resolver_unavailable` | 503 | An internal resolver was unavailable. Retryable. |
| `credential_resolver_miss_rate_limited` | 429 | Credential-resolver miss path was rate-limited. Retryable. |
| `request_too_large` | 413 | A per-request capacity/payload ceiling was exceeded; modify the request (`capacity` class, not retryable). |
| `provider_throttled` | 503 | A relayed upstream-provider throttle (`adaptive` class, `limitOrigin: provider`). Retryable. |
## Retryability is decision-specific
Use the public guards on the caught **error**, not a raw status or code.
`_fs.reaction` and `_fs.retrySafe` are authoritative when present, so a
structured decision can make a nominal 429/503 unsafe to replay. Only when no
structured envelope exists does the SDK fall back to the transient code/status
classification below.
```ts
import { isRetryable, isThrottled } from "@farthershore/farthershore-js/errors";
try {
await fs.route.get("/v1/cron-jobs");
} catch (err) {
if (!isReplaySafeOperation()) throw err;
if (isThrottled(err)) return backOffAndRetry(); // honor Retry-After
if (isRetryable(err)) return retryWithBackoff(); // transient fallback
throw err;
}
```
Fallback-transient codes are `limit_allocator_unavailable`, `rate_limited`, `invalid_entitlement_shape`,
`unsupported_constraint_schema`, `enforcement_dependency_unavailable`,
`concurrency_limit_exceeded`, `concurrency_context_unavailable`,
`concurrency_coordinator_unavailable`, `geo_context_unavailable`,
`resolver_rate_limited`, `resolver_unavailable`,
`credential_resolver_miss_rate_limited`, `provider_throttled`.
`limit_exceeded` and `credit_exhausted` are **not** retryable — the limit
won't clear by retrying. Surface an upgrade or funding affordance instead (see
the `limitCode` field below). `invalid_admission_knob` and
`admission_bound_exceeded` (422) mean the request itself must change.
Never automatically replay a mutation unless its idempotency contract proves
the same request cannot duplicate the effect.
## Upgrade affordance — `limitCode`
A limit deny also carries a separate `limitCode` field (an upgrade-affordance
value, **not** a wire code) telling the UI what kind of limit was hit. The fixed
values:
| `limitCode` | Hit |
| ----------------- | -------------------------------------------------------------------------------------------------- |
| `quota` | An included-usage / hard-cap quota. |
| `rate_limit` | A per-window rate limit. |
| `credit` | Funding exhaustion on a `block` plan (`credit_exhausted`). |
| `resource:` | A counted-resource cap, e.g. `resource:cron_jobs` (open family — match by the `resource:` prefix). |
## Runtime verification statuses
These come from the upstream's [`@farthershore/backend`](/reference/backend-sdk),
not the platform deny path — when `fs.verifyRequest()` / `fs.middleware()` rejects
a request the platform forwarded. Verification is **fail-closed**: every failure
maps to one status.
| HTTP | When |
| ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401` | Any verification failure — missing / malformed / bad-signature / stale / clock-skew / wrong-route / body-hash-mismatch / replayed-nonce / unknown-kid / jwks-unavailable. |
| `403` | Verified principal is the wrong subject or credential surface, or fails an application permission check (`member_subject_required`, `service_subject_required`, `surface_not_allowed`). |
| `413` | The request body exceeds `MAX_BODY_BYTES`. |
`FartherShoreError.code` carries the precise runtime reason (for example,
`invalid_token` or `jwks_unavailable`). `statusForCode(code)` supplies the
default mapping and explicitly handles `body_too_large` and
`surface_not_allowed`; subject and permission helpers construct their own 403
errors. There is no fail-open branch.
## Origin availability — `origin_unavailable` and `origin_timeout`
These two codes describe the hop between the gateway and **your** backend. They
are always the platform's own envelope — the gateway never relays your hosting
provider's error page (Railway's `Application not found`, a raw HTML 502), so a
caller can always tell "the origin is not there" from "your application said
no". Neither is ever billed to the customer, and any reservation the request
took is released.
| HTTP | `code` | When |
| ----- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `503` | `origin_unavailable` | The environment has no usable backend origin, or the origin could not be reached — connection refused, DNS/TLS failure, or a hosting-edge 5xx page. |
| `504` | `origin_timeout` | The origin did not produce response headers within the route's time-to-first-byte budget. |
Both carry `Retry-After` and an `_fs` envelope with
`{ "retrySafe": true, "reaction": "backoff_retry" }`:
```json
{
"error": "Origin unavailable",
"code": "origin_unavailable",
"_fs": { "retrySafe": true, "reaction": "backoff_retry" }
}
```
Your backend's OWN responses are never rewritten. A JSON body your application
returns — including a `404` or a `503` — is relayed verbatim; only a
signature-less hosting-edge error page is reclassified. A provider cap your
backend reports (for example `413 {"error":"too_many_pages"}`) keeps its body
too: the platform merges its `_fs` telemetry into your document rather than
replacing it.
## Other stable operational codes
Not every stable code is a member of the closed gateway-denial catalog.
`origin_unavailable` is a gateway routing failure: the selected environment has
no usable backend origin and returns 503 without falling back to production.
Backend verification and authorization also use stable runtime codes such as
`context_unverified`, `principal_required`, `member_subject_required`,
`service_subject_required`, and `surface_not_allowed`. Diagnose those at the
signature/principal/application boundary rather than adding them to
`FS_DENY_CODES`. A usage payload from an outdated `@farthershore/backend` that
lacks the served-identity block receives a permanent `410`/`422
unsupported_usage_schema` with `expected_schema_version` and
`received_schema_version` — upgrade the SDK; the event is never queued,
quarantined, or backfilled.
## HTTP status taxonomy
How the statuses map to categories across both surfaces:
| HTTP | Category | Typical codes |
| ----- | ------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `401` | Authentication | `key_expired`, `credential_revoked` / `credential_rotated` / `credential_env_reset`, runtime verification failures |
| `402` | Funding / quota | `credit_exhausted`, `limit_exceeded`, `post_stream_overspend`, `enforcement_denied`, resource create limits |
| `403` | Authorization | `route_not_enabled`, permission denies, geo denies, resource pre-request limits |
| `413` | Payload | `request_too_large` (oversized body / capacity) |
| `422` | Admission knob | `invalid_admission_knob`, `admission_bound_exceeded` (modify the request) |
| `429` | Rate / throughput | `rate_limited`, `concurrency_limit_exceeded` (customer wait-for-slot) |
| `503` | Origin availability | `origin_unavailable` (no usable / unreachable backend origin) |
| `504` | Origin availability | `origin_timeout` (no response headers within the route budget) |
| `500` | Runtime | `enforcement_error` |
| `503` | Transient runtime | `*_unavailable`, `*_rate_limited`, `commercial_release_unprovable` (retryable, fail-closed) |
## How to debug a denial
1. Read the `code` (not the message).
2. Call `isRetryable(err)` and require replay-safe operation semantics. Honor
the structured decision and `Retry-After` before retrying.
3. If it's a limit (`limit_exceeded` / `credit_exhausted` / `resource_count_limit_exceeded`), read `limitCode` and surface an upgrade or funding affordance.
4. If it's a 422 admission code, the client's output knob is malformed or above the route's declared bound — see [Monetary admission](/reference/monetary-admission).
5. If it's a 401 from your own upstream, it's a verification failure — check `FartherShoreError.code` and that `FS_RUNTIME_TOKEN` is current.
6. Confirm the subscriber's key, subscription, and [plan limits](/operate/limits).
---
# Operate and verify a business
Canonical URL: https://docs.farthershore.com/operations/overview
Operations change or observe platform state whose owner is outside the business program. Resolve the organization, business and environment before a write. Use the exact command help and structured output.
## Choose a workflow
| Need | Workflow |
| ------------------------------------------- | --------------------------------------------------------------------------------------- |
| Authenticate and select an organization | [Platform access](/operate/platform-access), [CLI authentication](/get-started/install) |
| Create a preview | [Environments](/operate/environments), [preview recipe](/cookbook/preview-environment) |
| Understand an accepted build | [Apply](/operate/apply), [Apply Timeline](/operate/apply-timeline) |
| Release or recover production | [Releases](/operate/releases) |
| Bind a concrete backend origin | [Backend overview](/backend/overview), [transports](/backend/transport-modes) |
| Change a hosted frontend variable or secret | [Variables](/frontend/variables) |
| Support a customer, role or persona | [Customer operations](/operate/customer-operations) |
| Deliver events to an integration | [Webhooks](/define/webhooks) |
| Trace usage, faults or agent activity | [Troubleshooting](/operate/observability-and-troubleshooting) |
| Configure notification delivery | [Notifications](/operate/notifications) |
The [complete command reference](/cli/overview) also includes organization membership, credentials, service accounts, agreements, tax settings, resource counts, dependents, knowledge and workflow control. These are discoverable capabilities even when they are not part of onboarding.
## Verify the transition
Inspect current state first. Review the requested impact, execute once, retain
the operation or workflow ID, and read back the narrow affected state. A
timeout is not evidence that a write never happened. Use the exact
`retry.kind` from `farthershore operations list --format json`; only
`same_key_replay` uses a caller-persisted key. A replay is historical, so always
run its named reconciliation read. See
[Retries and idempotency](/agents/retries-and-idempotency).
Apply acceptance is not frontend activation. Frontend activation is not backend health. A published commercial release is not proof that all existing subscribers moved. Choose verification that observes the requested outcome.
## Recover the owning layer
Correct repository-owned structure in Git. Change a wrong environment binding through the matching backend operation. Recover a bad hosted artifact through the documented frontend pointer workflow. Preserve evidence and escalate platform workflow or provider faults that the public operations cannot repair. [Troubleshooting](/operate/observability-and-troubleshooting) gives the evidence required for that handoff.
---
# Apply & deploy
Canonical URL: https://docs.farthershore.com/operate/apply
The managed repository owns the business contract: routes, plans, pricing,
meters, limits, policies, and surfaces under `business/`. FartherShore owns the
operational records produced when that contract is built and applied.
There is no imperative `apply` command. The normal path is:
```text
edit business/ -> farthershore build -> commit -> push
-> Build business -> Compile -> Accept -> Publish -> optional billing sync
```
## Before pushing
```bash
farthershore build --format json
```
This runs the repository build locally. It does not update a live environment.
Fix SDK, reference, validation, and determinism errors before committing.
## What Git triggers
- A pull request validates the proposed contract.
- A push to a branch already bound to a preview environment rebuilds that
environment.
- In branch-prefix mode, a push such as `env/payments` may create and build the
corresponding preview environment.
- A default-branch push validates production; when the compiler proves the
contract change is non-economic, it may also publish the runtime contract
immediately. Economic or indeterminate changes remain deferred.
- A published, non-draft, non-prerelease GitHub Release must carry an exact
immutable commit SHA. Its tag is checked against that SHA, then the
production build is pinned to the event SHA; branch targets and moved tags
fail closed.
An existing preview environment continues rebuilding from its bound branch even
when automatic preview creation is disabled. The trigger policy decides whether
new environments are created; it does not orphan existing ones.
## Apply phases
| Phase | Meaning |
| ------------ | ----------------------------------------------------------------------------- |
| Build | Execute the repository's Business program and produce the build artifact. |
| Compile | Validate and compile the artifact into platform state. |
| Accept | Record the accepted repository contract for this target. |
| Edge publish | Make the compiled routes and enforcement state available for the environment. |
| Billing | Reconcile economic state when the accepted change requires it. |
Each phase reports `pending`, `running`, `succeeded`, `failed`, or `skipped`.
Read them with `apply-timeline`; do not infer publication from a local build or
from the compile phase alone.
```bash
farthershore apply-timeline list acme --env all --format json
farthershore apply-timeline inspect acme --env production --format json
```
## Business apply and frontend build are separate
The Business program compiles the contract. A customized frontend has its own
build and release records. A contract-only push need not create a frontend
build, and a frontend rollback does not restore routes, plans, or pricing.
```bash
farthershore frontend status acme \
--ref "$(git rev-parse HEAD)" \
--wait \
--format json
```
There is intentionally no CLI frontend-deploy command. Preview deployment is
triggered by its branch push; production deployment is triggered by the GitHub
Release. For either Git event, `--ref` observes the latest known build for the
exact source revision, not a specific webhook delivery. The CLI observes status
and can reactivate a prior frontend release.
## Recover by failure boundary
### Build or compile failed
Nothing new was accepted. Fix the repository and push a new commit. Inspect the
failed entry rather than retrying an imperative platform write.
### Edge publish failed
The intended snapshot did not finish publishing. Keep the repository as the
source of truth, inspect the phase error, fix it if repository-owned, and push
again. If the error is platform infrastructure rather than business input,
capture the entry id, commit SHA, environment, and error code for support.
### Production change is bad after publication
Prefer a reviewed forward fix in `business/` followed by a new release. For an
urgent operational compensation, `business rollback` can enqueue a rollback
workflow against the prior publish workflow; it does not rewrite Git history or
change the repository contract. See [Production releases](/operate/releases#rollback-a-publish-workflow).
### Frontend artifact is bad
Reactivate a known frontend release without changing the Business program:
```bash
farthershore frontend status acme --format json
farthershore frontend rollback acme --release-id --format json
```
After any recovery, use the Apply Timeline to confirm which repository contract
is still accepted, then read the recovered surface itself (`business status`,
`frontend status`, or workflow detail). A successful enqueue is not proof of
convergence, and the source publish's timeline status is not the new rollback
workflow's status.
---
# Releases
Canonical URL: https://docs.farthershore.com/operate/releases
Production publication is Git-triggered and is never assembled from an
out-of-band contract edit. A provably non-economic default-branch change may
publish runtime contract state directly. A full versioned release—including
economic contract work plus customized frontend and repository-docs
artifacts—comes from an immutable tag in the managed repository.
`farthershore business publish` is the first-activation command for a draft
business. It previews the initial semantic version, creates the first GitHub
Release, and makes the business live. After that activation, a repo-backed
business is code-managed: repeating `business publish` returns
`MANAGED_BY_CODE`. Subsequent versioned releases are published in the managed
repository through GitHub, whose release webhook resolves the tag to its
immutable commit and queues the production build.
A **non-economic** default-branch push may apply runtime-safe contract work
without waiting for billing reconciliation. An **economic** change—a plan's
kind or recurring price, a pricing catalog, funding buckets, spend policy,
meter-route admission bounds, or another subscriber-money boundary—must remain
behind the reviewed GitHub Release. Use the Apply Timeline's semantic diff and
`farthershore commercial-release diff` instead of guessing from which files
changed. Every economic publish produces an immutable, content-addressed
[commercial release](/reference/commercial-releases) whose activation reaches
new subscriptions; existing subscriptions keep their pins.
## Prepare the exact commit
```bash
git switch main
git pull --ff-only origin main
farthershore build --format json
approved_sha="$(git rev-parse HEAD)"
git push origin main
farthershore apply-timeline inspect acme "$approved_sha" --env production --format json
```
Require the repository checks for the current SHA. A prior green commit is not
release evidence.
## Activate the first draft
Use this section only while `business status` is `DRAFT`. Preview the initial
release:
```bash
farthershore business publish acme --dry-run --format json
```
The preview returns the computed bump, first version, reasons, and any refusal.
Review that output with the repository diff. A first real release starts at
`v0.1.0`.
The platform refuses an unchanged release and refuses a breaking release unless
the caller explicitly accepts it. `--accept-breaking` is consent, not a way to
silence an unknown diff.
You can request the first bump or version when release policy requires it:
```bash
farthershore business publish acme --bump minor --dry-run --format json
farthershore business publish acme --version v2.0.0 --dry-run --format json
```
Run the final non-dry command only after the exact commit, version, and semantic
diff are approved:
```bash
farthershore business publish acme --format json --idempotency-key
```
The command creates the first published GitHub Release in the managed
repository and activates the draft business.
## Publish a later version
For an already-active repo-backed business, first push the reviewed commit and
require its repository checks. Inspect the default-push Apply Timeline entry:
a non-economic runtime change may already publish from that commit, while an
economic or indeterminate change remains deferred.
Create the next immutable semantic-version tag and published GitHub Release in
the managed repository. For an agent with an authenticated GitHub CLI, the
confirm-gated effect is:
```bash
git switch main
git pull --ff-only origin main
farthershore build --format json
approved_sha="$(git rev-parse HEAD)"
git push origin main
farthershore apply-timeline inspect acme "$approved_sha" --env production --format json
git tag -a v1.2.3 "$approved_sha" -m "v1.2.3"
git push origin v1.2.3
gh release create v1.2.3 --verify-tag --target "$approved_sha" --title "v1.2.3" --generate-notes
```
Choose the version from the reviewed semantic and subscriber impact; do not
copy `v1.2.3` blindly. Draft and prerelease GitHub releases do not trigger
production. The release webhook must carry the approved 40-character SHA and
the tag must still resolve to that SHA; do not target a branch such as `main`.
## Verify serving state
Release creation is only the handoff. Confirm the release apply:
```bash
farthershore apply-timeline inspect acme --env production --format json
farthershore business status acme --format json
farthershore frontend status acme --ref "$(git rev-parse HEAD)" --wait --format json
```
Require all of the following:
- the timeline entry has the expected `releaseTag` and `commitSha`;
- its publish phase settled successfully;
- `business status` reports the expected latest release and `live: true`;
- if frontend source changed, the expected frontend release settled too.
The business apply and frontend build are separate pipelines. There is no CLI
frontend-deploy verb: the GitHub Release triggers production; `frontend status`
observes the latest known build for the source revision in the current checkout.
It does not identify a specific webhook delivery; exact attempt proof requires a
build id where the trigger returned one.
## Recover a contract change
The durable fix is forward: revert or correct the Business program in Git,
validate it in preview, merge, and publish a new release. That restores the
desired contract while keeping Git and platform state aligned.
## Rollback a publish workflow
For urgent operational compensation, identify the prior **publish workflow
execution id** and enqueue rollback:
```bash
farthershore apply-timeline inspect acme --env production --format json
farthershore business rollback acme \
--reason "restore the prior serving snapshot" \
--idempotency-key rollback- \
--format json
```
This command does not move the Git tag, revert a commit, or change `business/`.
It starts an asynchronous rollback workflow using the snapshot captured by the
source publish workflow. On the commercial side a rollback appends a new,
higher release-log sequence that points at the older immutable release —
usage already admitted under the rolled-back release is still rated under it,
and no other business is affected. A cross-business or unknown workflow id is hidden as
not found; an already rolled-back or cancelled workflow returns
`WORKFLOW_NOT_ROLLBACKABLE`.
A `202`/`status: enqueued` response proves only that compensation was queued.
Record the returned `rollbackWorkflowExecutionId`. Where workflow-inspector
access is available, inspect that new execution directly:
```bash
farthershore workflows show --format json
```
Then read back `business status` and every affected serving, plan, customer, and
billing surface. The Apply Timeline still identifies the source publish; do not
misrepresent its old status as the rollback workflow's status. If no rollback
snapshot was captured, or compensation cannot converge every external surface,
complete the forward Git fix and retain the rollback evidence for support.
## Roll back only the frontend
If the Business contract is correct and only the hosted frontend artifact is
bad, reactivate a prior frontend release:
```bash
farthershore frontend status acme --format json
farthershore frontend rollback acme --release-id --format json
```
This does not change routes, plans, meters, billing, or the repository contract.
For production, it pins the frontend target. Later production builds still run
but do not autoactivate; after the forward fix produces a successful release,
activate that reviewed release id explicitly with `frontend rollback` and
verify the active id and pin state. There is no separate unpin command.
Preview rollback requires `--env ` and does not pin. The next
successful build for that preview environment autoactivates, so use preview
rollback only as temporary containment and verify the active release after each
subsequent build.
See [Apply and recover](/operate/apply) and
[Prepare a production release](/cookbook/release-production).
---
# Apply Timeline
Canonical URL: https://docs.farthershore.com/operate/apply-timeline
The **Apply Timeline** is the read-only history of repository work FartherShore
observed for a business. Use it to answer four questions: which commit ran,
what the compiler thought changed, which environment it targeted, and where the
apply stopped.
An entry is derived from platform build and deployment records. You do not
create or edit entries with the CLI; a pull request, push, or published GitHub
Release creates them.
## Find the entry
```bash
farthershore apply-timeline list acme --env all --format json
farthershore apply-timeline inspect acme 8f31a42 --env production --format json
```
`inspect` accepts an entry id, exact release tag, pull-request number, exact
branch name, or commit-SHA prefix. Matching prefers those forms in that order,
so a release tag remains unambiguous when several releases point at the same
commit.
The environment filter is `production`, `all`, or a preview environment name.
Use `all` when a commit may have run in both preview and production.
## Read the result
Every entry identifies its `source`, `branch`, `commitSha`, optional `prNumber`,
optional `releaseTag`, and optional `environmentId`. It also carries:
- `semanticDiff`: the structural diff, semantic changes, risk summary, and
lifecycle plan computed from the accepted Business program;
- `checks`: named projections of build, compile, accept, edge-publish, and
optional billing work;
- `apply`: phase status, timestamps, and errors when an apply workflow exists.
The top-level status is one of:
| Status | Operator meaning |
| ---------- | ----------------------------------------------------------------------------------------------- |
| `pending` | Recorded but not started. |
| `running` | At least one phase is still running. |
| `passed` | Validation passed; common for pull requests. It does not mean production is serving the commit. |
| `applied` | The apply published its target snapshot. |
| `deferred` | The contract was accepted but production publication awaits a release boundary. |
| `failed` | A check or phase failed; inspect its summary and error. |
| `stale` | Newer repository work superseded this entry. |
## The post-push loop
```bash
git push
farthershore apply-timeline inspect acme "$(git rev-parse HEAD)" --env all --format json
```
1. Match the returned `commitSha` and environment to the commit you pushed.
2. If it is `running`, poll the same selector; do not start a second apply.
3. If it is `failed`, fix the repository source and push a new commit.
4. If it is `deferred`, review and publish the production release.
5. If it is `applied`, confirm the serving state with `business status`.
```bash
farthershore business status acme --format json
```
GitHub checks are useful notification surfaces, but the Apply Timeline is the
platform-native record. A green validation entry and a live deployment are
different facts; require the expected commit, target environment, and final
publish phase.
See [Apply and recovery](/operate/apply) and
[Production releases](/operate/releases).
---
# Environments
Canonical URL: https://docs.farthershore.com/operate/environments
A business has production plus zero or more preview environments. A preview is
an active platform target bound to one Git branch. Its compiled contract,
runtime hostname, docs snapshot, frontend release, variables, backends, test
personas, usage, and customers are scoped by environment where the corresponding
surface supports it.
Those are **per-environment** operational records. The **environment branch**
is the sole source of that environment's Business SDK contract. Plans, pricing,
routes, permissions, meters, limits, policies, and frontend declarations are
compiled from that branch and are **never inherited from production**. A plan
that exists only on the default branch does not exist in the preview. Until the
environment has accepted its first branch contract, contract-backed operations
fail closed instead of borrowing the production snapshot.
Backend targets are the only fallback because origin URLs are operating state,
not Business SDK declarations. A preview resolves a declared backend's stable
slug to the production binding until a concrete environment override is bound.
This reuses where requests run; it does not copy any production contract state.
Routes retain that stable slug across environments. The gateway resolves it to
the inherited production backend or the concrete preview override at request
time; physical backend IDs and credentials never leak across environments.
The Business program remains repository-owned in every environment. Environment
rows, hostnames, bindings, credentials, and runtime state are platform-owned.
## Choose how previews are created
```bash
farthershore business preview-env set acme \
--trigger branch-prefix \
--branch-prefix env/ \
--format json
farthershore business preview-env set acme --trigger pull-request --format json
farthershore business preview-env set acme --trigger disabled --format json
```
- `branch-prefix` is the default. A push whose branch starts with the prefix
creates the environment named by the remaining suffix. `env/payments` becomes
`payments`; pushing the bare `env/` prefix creates nothing.
- `pull-request` creates previews only for same-repository pull requests into
the default branch. Fork PRs and PRs into another base are skipped. Closing
the PR tears its preview down.
- `disabled` stops automatic creation.
In every mode, pushes to a branch that already has an environment row rebuild
that environment. Deleting that tracked branch triggers teardown. Explicit CLI
creation also remains available in every mode.
## What a rebuild does to the environment's subscribers
An environment exists to match its branch exactly, so a push that **changes the
contract** rebuilds the environment from scratch: its plans and compiled plans
are replaced, and its subscriptions are deleted along with the API keys they
own. Test personas survive as users, but the key each one held does not.
Afterwards:
- `farthershore persona list --env ` reports the affected
personas with `revokedAt` set and `revokedReason: "env_reset"`.
- A request that still presents one of those keys is denied `401` with
`code: "credential_env_reset"` — distinct from `credential_revoked`, because
nothing was revoked for cause and the product is healthy. Retrying the same
key cannot succeed.
- Recover by subscribing again, or, for a test persona, by running
`farthershore persona bootstrap` with a fresh idempotency key.
A push that does **not** change the contract is a no-op for all of this. If the
commit compiles to the contract the environment has already accepted and
published — a comment, a README, a frontend-only change, or a re-push to retry
CI — the apply reports `No contract change` and
`ENVIRONMENT_CONTRACT_UNCHANGED`, nothing is rebuilt, and every subscription,
key and persona keeps working. A frontend-only commit still triggers its
frontend build.
A push whose build or compile **fails** performs no destructive work at all:
the environment keeps serving its last published release, keys included.
## Create explicitly
```bash
farthershore env create acme \
--name payments \
--branch env/payments \
--format json
```
Environment creation is convergent, not a replayed idempotency operation. On a
timeout or interrupted response, run `farthershore env list acme --format json`
and then repeat the same name-and-branch request if needed; the platform reads
current state and repairs the managed branch before exposing the environment.
Explicit creation accepts only the business's reserved preview namespace
(normally `env/`) and durably claims that exact branch before GitHub creates or
resolves it. A newly created environment reports `branchCreated`; a converged
existing environment omits it. A successful response always has an exact
managed branch head, and deleting that explicitly managed environment removes
its claimed branch. Webhook and PR environments retain their external source
branch ownership. If GitHub cannot create or resolve the branch, creation fails
safely: restore GitHub access and repeat the same request.
```bash
git fetch origin env/payments
git switch --track origin/env/payments
```
## Every push reflashes the environment
A push to a tracked environment branch does not patch the environment — it wipes
and recompiles it. Test personas, subscriber API keys, and customer identity rows
for that environment are destroyed and the contract is rebuilt from the new
commit.
Re-mint the persona after every push, and never carry a credential across one:
the API key that worked before the push is gone, not merely stale. Wait for the
apply to report `applied` before minting, or the mint races the recompile and
fails with `TEST_PERSONA_ENVIRONMENT_NOT_READY`.
If the branch already exists and the business has an accepted contract,
explicit creation may queue its initial environment build immediately.
## Find the environment and test it
```bash
farthershore env list acme --format json
farthershore apply-timeline list acme --env payments --format json
farthershore business show acme --env payments --format json
```
`env list` returns active preview rows with their exact `id`, `name`, `branch`,
`runtimeHostname`, `portalHostname`, and status. Use those returned values;
do not construct hostnames from the environment name.
`business show --env ` returns the **business-level** accepted spec — the
contract compiled from the default branch — not the environment's own accepted or
edge state. To see whether an environment has accepted its branch contract, read
its Apply Timeline entry; that is the authoritative per-environment view.
Wait until the branch's Apply Timeline entry reports `applied` and both
`Accept contract` and `Publish to edge` report `passed`. A successful preview
publish promotes its matching commercial release before subscriber credentials
are issued or repinned; do not mint a persona against a still-running apply.
For an API business, issue a persona only in a test environment and exercise
the preview runtime:
```bash
farthershore persona bootstrap acme --env payments --plan pro --format json --idempotency-key
farthershore persona list acme --env payments --format json
```
For a customized frontend, inspect the environment's frontend state:
```bash
farthershore frontend status acme --env \
--ref "$(git rev-parse HEAD)" --wait --format json
```
`frontend status` and `frontend rollback` accept an environment name or id.
Run the status command from the checkout at the commit pushed to that preview
branch.
The CLI resolves a name to the current environment id before it calls Core.
Not every command accepts `--env`; run the exact command's `--help` rather than
assuming a global environment flag exists.
Runtime tokens are scoped to backend rows, so create the environment's backend
binding before minting its token, and pass `--backend ` when the
environment has more than one backend. `backend list` and `backend tokens list`
are business-wide and accept no `--env`.
If a preview request returns `origin_unavailable`, the environment has no usable
upstream for that route. Inspect both the logical slug and its effective binding:
the preview uses its concrete override when present, otherwise the production
backend. It never falls through to a different preview. An environment-only
backend with no target still fails safely; do not weaken request verification.
## Production is different
The repository default branch targets production. An ordinary push validates
the contract, and a provably non-economic contract change may publish its
runtime state immediately. Economic or indeterminate contract changes wait for
a published GitHub Release. Production frontend and repository-docs artifacts
also build from the release, not from a plain default-branch push.
Production is not returned as a deletable `BusinessEnvironment` row, and `env
delete` is never a production rollback.
## Delete a preview
```bash
farthershore env delete acme payments --yes --format json
```
Deletion tries to remove edge state, any Git branch Core claimed for the
environment, environment docs, in-flight workflows, personas and customer
identity rows, compiled plans, environment-only permission inventory, backend
credentials, usage-meter state, and the Core environment row. It is deliberately
retryable.
`ENVIRONMENT_CLEANUP_FAILED` means some cleanup did not complete; the
environment row remains so you can run the same deletion again.
A live-provider environment with active subscriptions cannot be torn down.
Move or end those subscriptions first. Test environments do not have that live
subscription guard.
Deleting the environment is destructive operational work. It does not delete
the Business program from repository history.
See [Inspect an apply](/operate/apply-timeline),
[Production releases](/operate/releases),
[Test in a preview](/cookbook/preview-environment), and
[Infrastructure with OpenTofu](/backend/infrastructure-opentofu) for provisioning
a separate backend deployment per environment.
---
# Platform access and roles
Canonical URL: https://docs.farthershore.com/operate/platform-access
Farther Shore evaluates platform access from one canonical operation and
permission catalog. Dashboard requests and normal CLI requests reload the same
live organization membership and role. MakerTokens use the same operation
permissions, but store a frozen, organization-scoped subset for unattended
automation.
## Choose the principal deliberately
| Principal | Authority | Use it for |
| -------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
| Dashboard user | Current organization role | Interactive administration |
| `farthershore login` | The same current role as the approving user | Normal CLI work across the user's organizations and businesses |
| MakerToken | Frozen exact permissions plus `ALL` or selected-business scope | CI, headless agents, service automation, and shared machines |
Selecting an organization in the CLI changes command context; it does not
narrow the user's authority. Create a MakerToken under **Settings →
MakerTokens** when automation needs narrower or independently revocable access.
Pass it through `FARTHERSHORE_TOKEN` or stdin, never argv.
## Organization roles
Every organization starts with `owner`, `admin`, and `member`. They are ordinary
organization-local role rows with preconfigured permission sets: their names,
descriptions, and grants can be edited. You can also create custom roles. A
custom role key is immutable after creation; assignments and invitations refer
to that key.
The primary owner is tracked separately from the editable owner role. Transfer
ownership through the ownership flow before removing or reassigning the primary
owner. Editing an owner role never removes that safety boundary.
Use the Team page for visual editing, or inspect exact authority from the CLI:
```bash
farthershore organization role list --format json
farthershore organization role show --format json
farthershore organization role create incident-commander \
--name "Incident commander" \
--idempotency-key \
--permissions '["business:read","audit_log:read","business:rollback"]'
farthershore organization member-role \
--role incident-commander
```
Role mutations are checked against the actor's live exact grants inside the
same transaction as the write. A role administrator cannot add grants outside
their current ceiling. Built-in roles cannot be deleted; a custom role cannot
be deleted while a member or invitation references it.
## Filter and sort platform lists
Database-backed lists use the same server query contract in the dashboard and
CLI. Filtering, searching, and sorting happen before pagination; the CLI does
not re-sort a partial page locally.
```bash
farthershore business list \
--filter status=ACTIVE \
--search billing \
--sort updatedAt:desc
farthershore organization members \
--filter roleKey=incident-commander \
--sort email:asc
farthershore env list \
--search preview \
--sort name:asc
```
`--filter field=value` is exact and repeatable with AND semantics. `--search`
is case-insensitive on the endpoint's documented text fields. `--sort` accepts
one allowlisted field and `asc` or `desc`. Run the command with `--help` for its
closed field list; unsupported fields fail before a request is sent.
## MakerToken lifecycle
A MakerToken's exact permissions and business scope are a snapshot. Later role
edits do not silently widen it. The secret is shown once; store it in a secret
manager. Rotate it when changing the external consumer and revoke it when the
automation is retired or suspected compromised. Rotation and revocation do not
change a user's browser or normal CLI authority.
---
# Customer operations
Canonical URL: https://docs.farthershore.com/operate/customer-operations
Customer records, subscriptions, credentials, role assignments, proposals,
promo codes, and test personas are platform-owned operational state. Plans,
route grants, limits, and the raw product-permission vocabulary remain
repository-owned contract state under `business/`. Each subscribing
organization owns its product roles: it composes them from that raw vocabulary,
chooses its default, and assigns roles or direct grants to its members. The
Business SDK does not define customer roles.
Use structured output for every write and read the affected state back. A
successful request means the platform accepted that operation; it does not prove
that a later asynchronous workflow has converged.
## Find the exact customer
```bash
farthershore consumer list acme --format json
farthershore consumer list acme --env preview --format json
```
The response is capped at 500 customers. Use its `id` as the `subscriberId` in
subsequent commands. Do not substitute an email, external identity, owner
organization, or subscription id. The response also exposes status, current
plan, `onLatestPlan`, Managed RBAC roles, members, and each member's assigned or
stale role keys.
## Contain or remove access
Block is containment and revokes active API keys. There is currently no CLI
unblock command, so treat it as durable until the platform exposes an explicit
restoration path:
```bash
farthershore consumer block acme --yes --format json
farthershore consumer list acme --format json
```
Blocking suspends the customer and revokes active API keys. It is idempotent;
the response tells you whether the customer was already blocked and how many
keys were revoked. Read the customer list back and require `SUSPENDED`. This
proves control-plane state, not edge propagation: verify the compromised
credential is denied at the gateway before calling containment complete. A
downstream propagation failure can occur after the blocked response; retain
request and audit evidence and escalate if access remains possible.
Remove is destructive and has no restore command:
```bash
farthershore consumer remove acme --yes --format json
farthershore consumer list acme --format json
```
Confirm the business and subscriber id immediately before running it. Removal
tears down subscriptions, credentials, and related live state. Require explicit
approval, preserve any required audit evidence first, and verify that the id no
longer appears afterward when it was present in the bounded pre-write list.
Because `consumer list` is capped at 500, absence from that list alone is not
proof of removal; retain the structured removal response and audit evidence.
## Move subscriptions
Move one customer to the active head of its existing plan lineage:
```bash
farthershore consumer migrate-latest acme \
--idempotency-key --format json
farthershore consumer list acme --format json
```
The command does not choose an unrelated plan. It may report that the customer
is already at the head. Commercial-release cohort migration is deferred until
the post-launch migrate-to-latest workflow.
## Turn RBAC enforcement on for one customer
Managed-RBAC enforcement needs **two** flags: the product-wide one
(`farthershore business rbac enable`) and the per-subscriber one. The
subscribing organization owns the second flag and normally sets it at its
portal's **Settings → Team** page (`/settings/team`); these commands are the
builder-plane equivalent, over the same service layer:
```bash
farthershore consumer list acme --format json # find the subscriber id
farthershore consumer rbac enable acme \
--default-role reader --format json
farthershore consumer rbac disable acme --format json
```
`rbac.enabled` on the subscriber's `consumer list` row is the read-back. With
the product flag off every call answers `400 RBAC_NOT_ENABLED_BY_PRODUCT`; when
the customer organization has mandatory change control on, a direct
enable/disable answers `409 GOVERNED_BY_CHANGE_SET` and must go through
`proposal create` instead. Disabling is an access **expansion** — permission
resolution can return `['*']` — not a repair for one denied member.
## Manage a customer's roles
Support path over live customer state; the customer organization is still the
owner of its role definitions.
```bash
farthershore consumer rbac roles list acme --format json
farthershore consumer rbac roles create acme support \
--name "Support" --permissions "tickets:read,tickets:write" --format json
farthershore consumer rbac roles update acme support \
--permissions "tickets:read" --format json
farthershore consumer rbac roles delete acme support --yes
```
`--permissions` is set-replace: the list becomes the role's whole grant.
Permissions must come from the business's derived catalog
(`400 UNKNOWN_PERMISSION` otherwise), and the reserved key `owner` can never be
created, edited, or deleted (`409 OWNER_ROLE_IMMUTABLE`) — owners always hold
every permission. Deleting a role strips it from every member and credential
that holds it and clears it as the organization default, so read the role list
and `consumer list` members back afterward. Role edits republish live-bound key
claims asynchronously: retest EXISTING credentials once the change reaches the
edge, not only newly issued ones. When the customer must consent to the change,
use `proposal create` rather than these direct writes.
## Replace a member's business roles
This builder CLI command is a support override over live customer state, not
the normal ownership path. Subscriber owners/admins create roles, choose
defaults, and assign members through their customer access-control surface. Do
not use the support override to preconfigure a persona workspace or to make the
builder the author of customer roles.
First inspect the accepted role vocabulary and the current assignment:
```bash
farthershore business rbac acme --format json
farthershore consumer list acme --format json
```
Then replace the member's complete assignment:
```bash
farthershore consumer rbac assign acme \
--roles admin,analyst \
--format json
farthershore consumer rbac assign acme \
--roles "" \
--format json
```
This is set-replace, not additive. An empty string clears the assignment. Role
keys must already exist in the customer organization's accepted role set. Read
the returned membership first, then read `consumer list` back when the customer
is in its bounded result and compare `members[].businessRoleKeys` exactly; stale
keys grant nothing and should be investigated.
## Propose a governed customer change
An agent can create, simulate, and inspect a governed ChangeSet. Approval and
application are separate customer-organization decisions.
```bash
farthershore proposal create acme \
--intent "Replace the support role permissions" \
--operations '[{"type":"update_role_permissions","schemaVersion":1,"target":{"resourceType":"subscriber_business_role","resourceId":"support"},"payload":{"permissions":["tickets:read"]}}]' \
--idempotency-key proposal-support-v1 \
--format json
farthershore proposal preview acme --format json
farthershore proposal get acme --format json
farthershore proposal list acme --format json
```
Treat `proposal preview` as a write: it stores the simulation but does not apply
the operations. Read the ChangeSet after preview and report its risk,
simulation, approvals, and status. Do not represent a proposal or approval as
applied state; only the later status and customer state prove that.
## Manage checkout promo codes
Promo codes are live checkout state, not Business program declarations.
```bash
farthershore promo-code list acme --format json
farthershore promo-code create acme \
--code LAUNCH25 \
--kind percent_off \
--percent 25 \
--duration-months 3 \
--plan \
--idempotency-key \
--format json
farthershore promo-code list acme --format json
```
Kinds are `percent_off`, `amount_off`, and `free_months`. Use
`--amount-cents` for a fixed amount; `free_months` accepts no amount flag.
Omitting `--plan` on create targets all launch plans. A spec update rotates the
provider-side coupon, so `promo-code update` requires the complete kind,
duration, and amount specification rather than a partial economic edit.
```bash
farthershore promo-code archive acme --format json
farthershore promo-code reactivate acme --format json
farthershore promo-code list acme --format json
```
Always read the list back and verify status, applicability window, plan scope,
and redemption cap. Archiving prevents future use; it does not rewrite prior
redemptions.
## Exercise a preview environment with a test persona
Personas exist only for environments configured with the test-persona customer
authentication strategy. They are temporary users in a real subscriber-owned
workspace and therefore exercise the same account membership, product-role,
token-minting, and gateway authorization paths as human users.
```bash
# The first persona creates the temporary subscriber workspace and must own it.
farthershore persona bootstrap acme --env preview --plan starter --format json --idempotency-key
farthershore persona list acme --env preview --format json
# Sign the owner persona into the hosted portal. In that customer session,
# enable subscriber RBAC, compose roles from the live permission catalog,
# and choose a default.
farthershore persona login acme --env preview --format json
# Add another temporary user to the same subscriber. Omit --role to use the
# subscriber's default, or name existing subscriber-owned role keys explicitly.
farthershore persona bootstrap acme --env preview --plan starter \
--subscriber-id --account-role member \
--role viewer analyst \
--format json --idempotency-key
farthershore persona login acme --env preview --format json
farthershore persona rotate acme --env preview --format json --idempotency-key
farthershore persona delete acme --env preview --format json
```
Bootstrap and rotate return a test key once. Do not log or commit it. Browser
login opens the platform-owned `/persona-sign-in` path. Its 60-second,
single-use, host-bound handoff is fragment-only until that same-origin page
exchanges it for a server-owned HttpOnly cookie; the portal bundle never sees a
bearer, access key, or token-bearing URL. The first persona defaults to account `owner`;
personas joining its stable `subscriberId` default to account `member`. Account
roles govern workspace administration and are separate from subscriber-authored
product roles. Enabling RBAC creates no product roles and selects no default.
To exercise a frontend checkout locally as one of these personas — real
environment, hot reload, signed in through the same server-owned cookie — run
the live dev server (CLI 0.33.5+):
```bash
farthershore frontend dev --live --business acme --env preview --persona --format json
```
See
[Local live preview as a persona](/frontend/auth#local-live-preview-as-a-persona).
Prove a permission change with both personas: the member's constrained request
must receive the gateway's typed `permission_denied` while the owner's is
forwarded. Deleting a persona or its environment revokes its sessions and any
local preview lease it held.
New persona workspaces do not auto-enable the subscriber setting; that choice
belongs to the owner in the customer access-control surface.
Applying a new commit to the environment branch flashes its compiled contract
and invalidates the old environment subscriptions and plan ids. Persona
identities survive in a detached state: run `persona login` with the same id,
then choose a plan from the newly compiled branch contract. The portal must not
offer or recover a production plan, and an old bookmarked plan id must be
treated as unavailable rather than substituted. The old raw test key remains
invalid after the flash. Once the persona is reattached, use `persona rotate`
to mint a replacement gateway credential; the persona's product-role bindings
and explicit scope ceiling carry forward without widening.
Deletion revokes the persona credential and browser access before removing its
test-owned user and membership. If it was the last persona, the disposable
subscriber organization is removed too; otherwise ownership is transferred to
a remaining persona when necessary. Environment deletion performs the same
Core identity cleanup and also removes edge/runtime state, the hosted route, and
the managed environment branch. Verify persona operations with `persona list`
and with actual allowed and denied preview requests.
## Verification and recovery
- Re-run the narrowest read command after every mutation and compare exact ids,
environment, status, plan, roles, or promo-code fields.
- Use `farthershore audit-log business-list
--format json` when you need actor and decision evidence.
- If a customer mutation fails before a success response, inspect current state
before retrying; a lost response may hide a completed operation.
- Repeat idempotent containment or migration only after the read-back proves it
is still needed. Never repeat remove speculatively.
- If repository-owned plans, permissions, or limits are wrong, fix the Business
program and push a new commit. Do not compensate with unrelated live writes.
## Related
- [Limits and denials](/operate/limits)
- [Observability and troubleshooting](/operate/observability-and-troubleshooting)
---
# Observe and troubleshoot
Canonical URL: https://docs.farthershore.com/operate/observability-and-troubleshooting
Start with the narrowest failing scope and preserve the identifiers that join
the platform's signals: business id, environment id, commit SHA or release tag,
apply id, build id, workflow execution id, request id, and audit request id.
Do not change state until the evidence identifies which ownership boundary is
wrong.
- **Repository-owned:** routes, plans, pricing, meters, limits, policies,
logical backends, and surfaces under `business/`.
- **Platform-owned:** accepted versions, environments, releases, frontend
pointers, concrete backend origins, customers, credentials, workflows,
denials, usage, and audit records.
## Establish business and apply state
```bash
farthershore business show acme --format json
farthershore business status acme --format json
farthershore apply-timeline list acme --env all --format json
farthershore apply-timeline inspect acme \
--env production \
--format json
```
`business status` combines lifecycle state, latest release, latest deployment,
edge publication, and a derived `live` flag. An apply entry is the evidence for
one Git-triggered build/compile/accept/publish attempt. Select by exact id or
release tag when possible; a short SHA can match more than one attempt.
If the target is a preview, use its explicit environment name on every command.
Never compare a production result with an unlabelled preview result.
## Inspect the hosted frontend separately
```bash
farthershore frontend status acme --format json
farthershore frontend status acme --env --format json
farthershore frontend status acme --env \
--ref "$(git rev-parse HEAD)" \
--wait \
--timeout 600 \
--format json
```
Frontend status reports the active release, pin state, recent releases, recent
builds, and build errors. For a Git-triggered build, `--ref` observes the latest
known build for that immutable source revision; it does not identify a specific
webhook delivery or start a build. Exact attempt proof requires a build id where
the trigger returned one. A successful contract apply and a successful frontend
build are distinct facts.
## Inspect concrete backend state
```bash
farthershore backend list acme --format json
farthershore business routes acme --env --format json
```
`backend list` shows concrete backend rows, transport, environment, and status.
The Business program declares logical backends, but each environment needs its
own concrete direct origin or tunnel. There is no generic `backend health`
command: prove health with the backend status plus a signed request through the
gateway, then inspect analytics and the backend application's own logs.
If a direct backend exists in another environment, bind the target environment
explicitly rather than assuming the origin carries over:
```bash
farthershore backend bind acme \
--env \
--origin-url https://api.example.com \
--format json
```
## Inspect workflow progress
Apply Timeline is the business-scoped workflow signal available to normal
business operators. Where the current identity has internal workflow-inspector
access, deeper workflow evidence is available:
```bash
farthershore workflows ls --business --status FAILED --format json
farthershore workflows show --format json
```
The detail includes steps, events, and convergence. Do not assume every failed
workflow is replayable. `workflow-control replay` is a billing replay preview,
not a general workflow retry, and `workflow-control rollback-to-config` is only
a rollback preview. Use the purpose-built business rollback only for a prior
publish workflow and only after reviewing its safety boundary.
## Correlate usage and request behavior
```bash
farthershore usage summary acme --format json
farthershore analytics log acme --range 1h --domain usage --format json
farthershore analytics timeseries acme --range 24h --domain usage --format json
farthershore analytics latency acme --range 1h --format json
```
Analytics defaults to production. Pass `--env ` for a preview;
there is deliberately no implicit all-environment aggregate. Request latency is
end to end, including the business backend, so high latency alone does not
locate platform overhead.
For a rejected request, preserve its request id:
```bash
farthershore denial show acme --format json
```
The denial explanation identifies the enforced constraint and observed value.
Decide whether the remedy is a repository change, a correct resource-count or
usage report, or no change because the denial is expected. Do not raise a limit
merely to hide a reporting bug.
## Use the audit log for who and why
```bash
farthershore audit-log business-list \
--from 2026-08-08T00:00:00Z \
--format json
```
Filter further with `--action`, `--actor`, or `--decision`. Audit rows establish
the actor, action, decision, request, and time; they do not replace the current
business, apply, or frontend read-back.
## Recovery matrix
| Evidence | Likely boundary | Safe next action | Completion evidence |
| ----------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Local build fails | Repository contract | Fix `business/`; rebuild | Local build succeeds |
| Apply build/compile fails | Repository or build input | Fix and push a new commit | New apply passes for the exact SHA |
| Apply accepted, edge publish failed | Platform publication | Preserve apply id; retry only the documented operation or escalate | Edge phase succeeds and expected request serves |
| Frontend build failed | Frontend source/build variables | Fix and push; use `frontend status --ref "$(git rev-parse HEAD)" --wait` | Build succeeds and expected release is active |
| Wrong frontend is live | Platform frontend pointer | Confirm target, then `farthershore frontend rollback` | Status shows the reviewed release id and pin |
| Direct backend unreachable | Per-environment backend binding or backend app | Verify the exact environment origin, signing verification, and app logs | Signed gateway request succeeds |
| Tunnel not ready | Tunnel provisioning/runtime | Inspect backend status and tunnel process; do not replace it with an unrelated direct origin | Backend ready and gateway request succeeds |
| Request denied | Repo policy, plan state, or reported operational usage | Inspect `denial show`; fix the owning fact | Same intended request is allowed, or expected denial is documented |
| Usage missing | Backend reporting or ingestion | Compare gateway request, analytics log, meter dimensions, and usage summary | One correlated request appears with the intended dimensions |
| Publish workflow failed | Publish workflow or downstream provider | Inspect Apply Timeline/workflow detail; fix source or use the exact recovery command | Replacement apply converges |
## Rollback boundaries
- `farthershore frontend rollback acme --release-id ` changes only
the hosted frontend pointer and pins it. It does not roll back the Business
contract, plans, billing, or backend. Later builds do not autoactivate while
pinned; activate a reviewed successful forward-fix release id explicitly with
the same command and verify status. There is no separate unpin command.
- `farthershore business rollback acme --reason
"..."` starts an asynchronous rollback of one eligible prior publish workflow.
The returned workflow id proves enqueue, not convergence; inspect it and read
business status afterward.
- A repository revert plus a new GitHub Release is the normal forward recovery
when the repo-owned contract itself is wrong.
## When to stop and escalate
Stop mutating and preserve the evidence when:
- the same exact commit or request behaves differently in equivalent targets;
- a Git event has no Apply Timeline entry after the expected delivery window;
- an apply is accepted but publication or convergence repeatedly fails;
- the failure is in a platform-owned provider, queue, workflow, edge publish,
or retained state that the available CLI cannot safely repair;
- a destructive action would be needed without an exact target, current
read-back, approval, or documented restore path; or
- credentials or secrets may have appeared in output or logs.
Report the smallest reproducible scope, UTC timestamps, business/environment,
commit or release, apply/build/workflow/request ids, exact command, structured
error code, and what changed immediately beforehand. Redact secrets; never
rotate, revoke, delete, or roll back additional state merely to gather evidence.
## Related
- [Apply and recovery](/operate/apply)
- [Apply Timeline](/operate/apply-timeline)
- [Releases and rollbacks](/operate/releases)
- [Limits and denials](/operate/limits)
- [Customer operations](/operate/customer-operations)
---
# Notifications
Canonical URL: https://docs.farthershore.com/operate/notifications
A **notification** is one platform event projected onto email. The platform
already records everything that happens as an append-only ledger of
[events](./apply-timeline); notifications are a **derived view** over that ledger —
they surface the handful of events a person actually needs to know about, and
nothing else.
The event ledger and your domain data stay the source of truth; a notification is
a projection, so a missed email loses nothing but the reminder.
## Channels
Email is the notification channel. Notifications never touch your
[webhook endpoints](../define/webhooks): webhooks are a separate, machine-facing
delivery you configure per endpoint — the platform never re-delivers a webhook as
a notification.
## The curated allowlist — no notification fatigue
The platform does **not** email on everything. Exactly one curated list decides
which events send an email, to whom. Anything not on the list stays silent — by
construction, not by configuration. There is no "notify on all events" switch to
accidentally flip.
The list covers the events that matter operationally:
| Event | Who is notified |
| ----------------------- | --------------------------------- |
| Payment succeeded | The subscriber's members |
| Payment failed | The subscriber's members |
| Subscription canceled | The subscriber's members |
| Access request created | The subscriber's owners/admins |
| Access request resolved | The subscriber's owners/admins |
| Change set approved | Your organization's owners/admins |
| Change set applied | Your organization's owners/admins |
| Build failed | Your organization's members |
| Business published | Your organization's members |
Recipients are resolved from **live membership** at the moment the event
happens, then frozen — a member added during a delivery delay never receives an
event that predated them. There is no assumption of teams or roles: a solo
(personal) organization resolves to its single owner; an individual subscriber
resolves to its one member.
## Preferences — per-user opt-out
The smallest honest preference model: each person can **opt out** of email. There
is no per-event tuning (the allowlist is already curated) and no digest batching —
just a clean on/off, globally or per category.
- Email is on by default; a person opts out at the **master** level (all email) or
per **category** (e.g. billing, deployments).
- **Email always honors an opt-out.** If a member opts out, the platform never
emails them — even for a payment failure.
- Absence of a preference means opted-in — the platform only stores an override
when someone opts out.
**Builder plane:**
```text
GET /businesses/:id/notification-preferences
PATCH /businesses/:id/notification-preferences # { "master": true } → opt out of all email
```
**Portal plane:**
```text
GET /portal/businesses/:productId/me/notification-preferences
PATCH /portal/businesses/:productId/me/notification-preferences
```
The `PATCH` body is a partial patch — send only what you're changing; omitted
fields keep their current setting. `true` means "opted out." Opt out of a single
category with `{ "categories": { "billing": true } }`.
---
# Webhooks
Canonical URL: https://docs.farthershore.com/define/webhooks
Outbound webhooks deliver business events to your HTTPS receiver. Delivery is
at least once: verify the signature over the raw body, deduplicate on the
delivery id (`webhook-id`), return success quickly, and process asynchronously.
## Endpoints are platform-owned
Webhook endpoints are operated records — created, edited, rotated and deleted
through the dashboard's Developer tab, the CLI, or the core API. They are not
part of the business program: `fs.business()` has no `webhooks` option, and the
repository never holds a receiver URL or a signing secret. Where an event is
_produced_ is contract; where it is _delivered_ is operations.
```bash
farthershore webhook create \
--url https://hooks.example.com/farthershore \
--idempotency-key \
--events subscription.created,payment.failed
farthershore webhook list
farthershore webhook test --idempotency-key
farthershore webhook trigger --type payment.failed --idempotency-key
farthershore webhook deliveries
```
The signing secret from API creation is one-time material. Store it directly in
the receiver's secret store and never print or commit it. In human (non-JSON)
mode `webhook create` and `webhook rotate` print it as an `FS_WEBHOOK_SECRET=`
line followed by a three-line `createWebhookHandler` snippet, so the receiver
can be wired in one paste.
## Local development loop
`webhook listen` gives you the Stripe-CLI-style loop for a receiver running on
your machine: it opens a Cloudflare quick tunnel to the local URL, creates a
temporary endpoint on the tunnel, prints the endpoint id, the tunnel URL and
(with `--print-secret`) the `FS_WEBHOOK_SECRET=` line, then tails the deliveries
log until you press Ctrl-C — at which point the temporary endpoint is deleted
and the tunnel stops.
```bash
farthershore webhook listen \
--forward-to http://localhost:3000/webhooks/farthershore \
--print-secret --trigger payment.failed
```
Each delivery prints as `time type status responseStatus id`; add
`--format json` for one JSON object per line. `--events a,b` narrows the
subscription (default: the whole catalog) and `--trigger ` fires a
signed sample as soon as the tunnel is up. The tunnel needs `cloudflared`: the
CLI bundles it as an optional `@farthershore/cloudflared-` dependency
and falls back to a `cloudflared` on your PATH. A listener that is killed hard
cannot clean up; the leftover is recognisable by its `*.trycloudflare.com` URL
in `webhook list`, and `webhook delete --yes` removes it.
`webhook trigger --type ` works against any endpoint, not just a
listener's: Core signs and sends a realistic sample of that catalog event
(`data` for `payment.failed` carries an invoice id, amount, currency and
`card_declined`), and the deliveries log records it under that event type, so
every branch of your handler map can be exercised before a real subscription
exists. `webhook test` stays the plain `webhook.test` ping.
## Event catalog
Endpoints subscribe to seven event names:
`subscription.created`, `subscription.updated`, `subscription.canceled`,
`payment.succeeded`, `payment.failed`, `entitlement.changed`, and
`usage.threshold_reached`. Every name has a producer — an event exists on the
wire only when something emits it. A test send arrives as `webhook.test`; it is
not subscribable and every endpoint receives it when you ask for one.
## The delivery
Every delivery is a JSON envelope signed with the open
[Standard Webhooks](https://www.standardwebhooks.com) format, so you can verify
it with `@farthershore/backend` or with any Standard Webhooks library:
```http
POST https://hooks.example.com/farthershore
Content-Type: application/json
webhook-id: 6d3a… # the delivery id — stable across retries
webhook-timestamp: 1788000000 # unix seconds at send time
webhook-signature: v1,MdgW… # base64 HMAC-SHA256 over `${id}.${timestamp}.${body}`
x-fs-webhook-event: subscription.updated
{
"id": "6d3a…",
"type": "subscription.updated",
"createdAt": "2026-09-05T10:00:00.000Z",
"businessId": "biz_…",
"environmentId": null,
"data": { "subscriptionId": "sub_…", "reason": "plan_changed" }
}
```
`environmentId` is `null` for production events. `data` carries the
event-specific fields; the body is authoritative and the
`x-fs-webhook-event` header is a convenience copy of `type`.
## Verify the raw body
Compute HMAC-SHA256 with the secret's key bytes (the base64 after the `fswh_`
prefix) over `id.timestamp.body`, compare in constant time against every
`v1,` entry in the header, and reject timestamps more than 5 minutes from now
in either direction. Parse JSON only after verification.
```node
import { verifyWebhook } from "@farthershore/backend/webhooks";
const raw = await request.text();
const result = verifyWebhook({
body: raw,
headers: request.headers,
secrets: [process.env.FS_WEBHOOK_SECRET],
});
if (!result.ok) return new Response(result.reason, { status: 401 });
const event = JSON.parse(raw);
```
Persist the delivery id in the same transaction as the business effect so a
retry cannot apply it twice.
## Rotating the secret
`farthershore webhook rotate ` (or the dashboard action)
issues a new `fswh_` secret and returns it once. For 24 hours every delivery
carries two `v1,` signatures — the new secret first, then the retired one — so
you can roll the receiver at your own pace. Pass `secrets: [current, previous]`
to the SDK if you also want to keep the old one on your side during the roll.
## Delivery behavior
- A non-2xx response, timeout, dispatcher interruption, or redirect is a failed
attempt and may be retried (30 s, 5 min, 30 min backoff; `Retry-After` is
honoured on 429/503). Five consecutive failures disable the endpoint.
- The receiver can observe a duplicate even after returning 2xx if the
dispatcher lost the acknowledgement; deduplication on `webhook-id` is
mandatory.
- Return 2xx within 10 seconds and enqueue slow work locally.
- Inspect delivery attempts rather than inferring health from endpoint state.
- Treat preview and production events separately using `environmentId`.
Use `farthershore webhook test` after every receiver or secret change (and
`webhook trigger --type ` for each event the receiver handles), then
inspect `webhook deliveries` for response and timing evidence.
---
# Test in a preview environment
Canonical URL: https://docs.farthershore.com/cookbook/preview-environment
## Outcome
The current repository commit is live in an isolated test environment, with
the exact hostnames and apply result recorded before production is touched.
## Prerequisites
- A clean managed repository checkout with the intended preview change.
- An environment name and `env/*` branch that are not already used for another
purpose.
- Any preview-only backend origins or variables required by the change. With no
origin override, a declared backend slug resolves to its production binding;
all Business SDK contract state still comes only from the preview branch.
## Create the environment
```bash
farthershore env create acme \
--name pricing-preview \
--branch env/pricing-preview \
--format json
```
Environment creation has no side-effect-free preview. Resolve the name and
branch first. If the result is ambiguous, list environments and repeat the
same request; creation converges on current environment and branch state
rather than replaying a stale attempt result.
A successful create has created or resolved the exact managed branch; it never
returns a branchless environment. Explicit creation accepts only the reserved
preview namespace (normally `env/`) and claims that exact ref before GitHub is
called, so deletion later removes the associated managed branch even after a
create retry. A newly created result reports `branchCreated`; a converged
existing result omits it. Webhook and PR environments remain builder-managed
source branches. If GitHub cannot create or resolve the branch, the command
fails safely: restore GitHub access and repeat the same request.
```bash
git fetch origin env/pricing-preview
git switch --track origin/env/pricing-preview
farthershore build --format json
git push
```
## Verify the exact push
```bash
farthershore env list acme --format json
farthershore apply-timeline inspect acme "$(git rev-parse HEAD)" \
--env pricing-preview \
--format json
```
Use the returned runtime and portal hostnames rather than constructing them.
For an API business, create a test persona and exercise the returned runtime:
```bash
farthershore persona bootstrap acme --env pricing-preview --plan pro --format json --idempotency-key
```
A persona's key is scoped to the environment's current contract. Pushing a
**contract change** to the preview branch rebuilds the environment, which
deletes its subscriptions and the keys they own: the persona survives, but
`persona list` then reports it with `revokedAt` set and
`revokedReason: "env_reset"`, and the old key is denied `401`
`credential_env_reset`. Bootstrap a new persona after such a push. A push that
does not change the contract — a comment, a frontend-only edit, a re-push —
leaves the persona and its key working; the apply says
`ENVIRONMENT_CONTRACT_UNCHANGED`. See
[Environments](/operate/environments) for the full rule.
For a hosted frontend, wait by environment name or id and verify the active
release:
```bash
farthershore frontend status acme --env pricing-preview \
--ref "$(git rev-parse HEAD)" --wait --format json
```
Open the returned preview portal hostname, sign in as the persona, complete
managed onboarding, and require `/me` to report an active compiled plan before
the application becomes available. Then make one SDK route call and assert a
backend-produced response marker. A generic 404 or a gateway-generated
`Unknown project` response is not readiness, even though it is an HTTP response.
Public bootstrap for the preview must return the preview runtime hostname. If
the browser targets the production gateway, stop: the environment is not safe
to certify.
If the apply fails, fix the repository and push again. Do not mutate the
accepted contract through a platform write.
## Recovery and teardown
```bash
farthershore env delete acme pricing-preview --yes --format json
```
`env delete` also attempts docs, edge, and workflow cleanup, and removes the
branch when the explicit managed create claimed that exact reserved ref. If it returns
`ENVIRONMENT_CLEANUP_FAILED`, retry the same command; do not delete the
environment row by another route.
See [Preview environments](/operate/environments).
## Agent prompt
```text
Create or reuse one environment branch, build and push the exact current commit,
then verify its Apply Timeline entry and returned hostnames. Exercise it with a
test persona when applicable and delete it only after testing is complete.
```
---
# Release to production
Canonical URL: https://docs.farthershore.com/cookbook/release-production
## Outcome
One reviewed immutable commit is published by GitHub Release and both contract
and frontend convergence are proven for that exact release.
## Prerequisites
- The intended production commit is on current `main` and its checks are green.
- The semantic diff and customer impact have been reviewed.
- Explicit approval covers the exact SHA, version, and any breaking change.
## Prepare
```bash
git switch main
git pull --ff-only origin main
farthershore build --format json
farthershore apply-timeline inspect acme "$(git rev-parse HEAD)" \
--env production \
--format json
farthershore business status acme --format json
```
Review the exact SHA, semantic diff, customer impact, and intended semantic
version. Stop if the branch moved after review or if a release prerequisite is
unmet. For an economic change, add
`farthershore commercial-release diff previous-release.json candidate-release.json --format json`
to the evidence.
## Publish
If status is `DRAFT`, preview and explicitly approve the first activation, then
run `farthershore business publish acme --format json`. That is the only
repo-managed state where this command publishes.
If status is `ACTIVE`, `business publish` (including dry-run) returns
`MANAGED_BY_CODE`. After explicit approval, publish the reviewed version from
the managed repository instead:
```bash
approved_sha="$(git rev-parse HEAD)"
git tag -a v1.2.3 "$approved_sha" -m "v1.2.3"
git push origin v1.2.3
gh release create v1.2.3 --verify-tag --target "$approved_sha" --title "v1.2.3" --generate-notes
```
Replace the example version with the approved semantic version. The GitHub
Release webhook queues production. Release creation is a handoff, not proof of
convergence.
## Verify
```bash
farthershore apply-timeline inspect acme \
--env production \
--format json
farthershore business status acme --format json
farthershore frontend status acme --ref "$(git rev-parse HEAD)" --wait --format json
```
Require the expected tag and commit, an applied publish phase, and `live:true`.
Check frontend status only when a customized frontend artifact is part of the
release.
## Recover
The durable recovery is a reviewed repository fix and a new release. For urgent
compensation, use the bad publish's workflow execution id:
```bash
farthershore business rollback acme \
--reason "urgent production compensation" \
--idempotency-key rollback- \
--format json
```
Treat `status: enqueued` as pending work. Record the returned rollback workflow
id, inspect it when workflow-inspector access is available, and read every
affected serving and billing surface back. The source release's Apply Timeline
entry does not become the rollback workflow's status. This command does not
revert Git. See [Production releases](/operate/releases).
## Agent prompt
```text
Prepare release evidence for the exact current main commit, including local
build, Apply Timeline, intended semantic version, semantic diff, and customer
impact. Distinguish first DRAFT activation from later ACTIVE GitHub Releases.
Stop for approval, publish once, and prove the release and optional frontend
converged. Do not treat enqueue as completion.
```
---
# Diagnose billing and usage
Canonical URL: https://docs.farthershore.com/cookbook/diagnose-billing-usage
## Outcome
The first divergent boundary is identified with request, environment, plan,
meter, subscription, and period evidence before any corrective write.
## Prerequisites
- One affected business, environment, and time window.
- At least one request or decision id when the issue is request-specific.
- Read access to the accepted contract, analytics, and usage summary.
## Establish the accepted state
```bash
farthershore business status acme --format json
farthershore business routes acme --env production --format json
farthershore business contract acme --env production --format json
farthershore plan list acme --format json
```
Record the release, environment, route, meter, plan version, and billing period.
Do not compare production charges with preview traffic.
## Trace one request
```bash
farthershore analytics log acme --range 1h --domain usage --limit 100 --format json
farthershore analytics timeseries acme --range 24h --domain usage --format json
farthershore usage summary acme --format json
```
For one request id, compare:
1. The compiled route and its `meterRoutes` binding in the served release.
2. The gateway decision and response (its signed usage event carries the
served identity: subscription, commercial release, rating context).
3. The backend `report()` — meter, measure keys, dimension values.
4. The aggregated measure and quantity.
5. The rating context the event was admitted under, its RatedCharge, the
funding postings, and the subscriber's bill preview.
Common first divergences are an unmatched route, an unbound meter, a measure
or dimension key the release's schema rejects, a report retried outside its
original request context, the wrong environment, or a unit mismatch. Usage is
rated under the release it was admitted with — a catalog change after the fact
never explains an old charge.
Use historical replay only as a preview:
```bash
farthershore workflow-control replay acme \
--period-start 2026-08-01T00:00:00Z \
--period-end 2026-09-01T00:00:00Z \
--format json
```
The command does not charge or mutate provider state. Do not edit invoices or
replay real usage until the first divergence is understood. Fix contract or
backend code in preview; if the whole request chain is correct but settlement
is not, preserve request ids, decision ids, subscription id, period bounds, and
release for support.
See [Observe usage and billing](/operate/usage-billing-policy) and
[Ledger & settlement](/operate/ledger-and-settlement).
## Verify
Re-run one controlled request in the same environment and require matching
route, decision, reported dimensions, aggregated quantity, and plan version.
Do not compare preview traffic with production settlement.
## Recovery
Fix the first repository- or backend-owned divergence in preview and push a new
commit. A historical replay command is preview-only. If settlement remains
wrong after the request chain matches, stop and preserve ids and period bounds
for escalation.
## Agent prompt
```text
Trace one usage or billing mismatch from accepted route through gateway
decision, backend report, aggregation, and active plan settlement. Identify the
first divergence and report evidence; do not mutate billing state.
```
---
# Use agents across the platform
Canonical URL: https://docs.farthershore.com/agents/navigation
A coding agent authors and operates your application. The [Farther Shore Operator](/agents/platform-agent) runs within the platform against a bounded business view. They have different tools and responsibilities.
## Begin a coding task
Read the managed repository's AGENTS.md, installed package pins, [ownership boundaries](/agents/operation-classes), and the relevant collection from the [capability map](/get-started/capability-map). Inspect current target state before changing it.
Use the [operation catalog](/generated/cli/operation-catalog) to decide whether the task has a CLI command, an MCP tool, a repository authoring path or an explicit handoff. A missing MCP tool does not imply that the corresponding CLI capability is absent. A browser-only action requires the human surface; do not invent an internal endpoint.
## Traverse documentation from the CLI
Use documentation as a read-only filesystem: collections are root folders,
sidebar sections are directories, and pages are files. Listing a file returns
its headings. Discover paths instead of guessing them:
```bash
farthershore docs ls --format json
farthershore docs tree backend-sdk --format json
farthershore docs ls backend-sdk/connect-your-application --format json
farthershore docs read backend-sdk/connect-your-application/metering --format json
farthershore docs ls backend-sdk/connect-your-application/metering --format json
farthershore docs search "runtime token" --collection backend-sdk --format json
```
Pass a heading id returned by the last `ls` as `docs read --section `
to retrieve that heading and its complete subtree. Canonical slugs and official
docs URLs also work with `read`, so a link in a skill can be retrieved directly.
`docs collection ` returns all pages in a collection. Full content is not
truncated; search is bounded. JSON responses include provenance.
Docs require no login. Production is the default; `docs --stage ls` explicitly
selects stage, independently of `--api-url`. The published docs must include
the collection-aware machine artifacts. These commands read guidance; they do
not execute it or change platform state.
## Govern agent work
Use [proposals](/operate/customer-operations) to create and simulate a ChangeSet. Simulation stores evidence; it does not apply the requested changes. Approval and application are separate states.
The Operator's customer-facing actions require their own enabled gates and permissions. Read run receipts and Bulletin findings before acting. Acknowledging a Bulletin item records a handoff, not completion of its requested work.
## Discover automation and knowledge
Read [event-driven automation](/agents/automation) for the closed rule grammar,
action semantics, activation and recovery. Knowledge commands are paginated
read-only indexes: follow a returned cursor until exhausted and distinguish
platform guidance from business-scoped resources. Resource content informs a
task; it does not grant permission to execute a write.
The [automation commands](/generated/cli/automation), [knowledge commands](/generated/cli/knowledge), and [workflow-control commands](/generated/cli/workflow-control) expose their exact supported inputs. Read their help and the operation's permission and side-effect metadata. Do not assume every workflow has replay, cancellation or repair support.
MCP schemas are listed in the [tool reference](/generated/cli/mcp). Tools returning one-time secrets need protected storage even when their normal output is JSON. Contract changes remain repository work; automation cannot create a second contract authoring authority.
## Report completion with evidence
State what changed, which target received it, and what read-back or serving behavior proves the outcome. Report a pending asynchronous workflow as pending. Include the correlation identifiers needed to continue an interrupted operation without repeating its side effects.
---
# Event-driven automation
Canonical URL: https://docs.farthershore.com/agents/automation
Use an automation rule for deterministic reactions to platform events: record a
notification, emit a webhook event, propose a role change, or notify a permitted
audience. Use the Operator for analysis and findings; use repository code for
contract changes. A rule is data, not an arbitrary-code runtime or cron service.
## Define exactly what should match
A rule belongs to one business and can be environment-scoped. Its trigger names
one event from the closed catalog and optionally narrows subject type or id.
Zero to twenty conditions are ANDed. Conditions read allowed envelope fields or
a single-level `payload.`; nested traversal, JavaScript expressions, and
regular expressions are not supported. `path_prefix` tests whether any path in
an array starts with the literal prefix.
The [generated rule schemas](/generated/agents/automation) contain all event,
operator, field and action shapes. Treat names and bounds as exact. An unknown
event or extra body key is rejected, not silently ignored.
## Choose the effect deliberately
| Action | Observable effect | Important boundary |
| --------------------- | --------------------------------------- | -------------------------------------------------------------------------------- |
| `emit_webhook_event` | Delivery through the webhook channel | Event must be webhook-deliverable; normal signing and retries apply |
| `create_notification` | An `automation.notification` fact | Not a promise of an email or SMS |
| `propose_change_set` | Draft governed role-permission proposal | Proposer only; does not approve or apply it |
| `notify_audience` | Audience-delivery request | Only product-update or transactional bases; audience and consent are revalidated |
A rule has one to ten actions. Governance and native permissions still apply
when the rule fires; creating a rule does not bypass action authorization.
## Create disabled, inspect, then enable
Read current rules and the exact input before writing:
```bash
farthershore automation list --format json
farthershore automation create --help
farthershore operations list --format json
```
Prepare a body using the generated schema with `enabled: false`. Use the
command's dry-run and retain a stable idempotency key for the logical create.
After approval, create the rule, record its returned identity, and compare the
stored trigger, conditions, actions and environment with the intended scope.
Enable with an update only when its external effects are intended.
The create schema defaults `enabled` to true, so omitting it is not a safe
draft. For partial updates, omitted fields are retained; explicitly set
`enabled: false` to stop future matching.
## Verify execution and recover
Observe the triggering event and the resulting notification, webhook delivery,
proposal, or audience receipt. A successful rule-create response proves storage,
not that an event matched or every downstream effect completed. Correlate
identifiers and audit evidence before retrying an uncertain write.
Disable a mistaken rule to prevent future matches. Disabling or deleting it
does not retract delivered messages, remove created proposals, or undo earlier
effects. Do not repeatedly generate real events as a test against customers;
use the intended preview environment and a bounded audience.
---
# Farther Shore Agent
Canonical URL: https://docs.farthershore.com/agents/platform-agent
The Farther Shore Operator is a platform-run agent for a live business. It is
not the coding agent working in your repository. It observes a bounded business
view, produces evidence-backed findings, and can take only catalogued actions
that have been enabled for its role.
The Operator is off by default and enabled per business.
## What the Operator can do
On a run, the Operator may read product metadata, accepted plans and routes,
subscription and usage aggregates, revenue indicators, churn, and earlier
snapshots. It composes analysis, checks its own evidence, and posts findings,
warnings, or product-change requests to the Bulletin.
Most output is advisory. A requested route, plan, pricing, meter, or limit change
is a handoff to the coding agent because `business/` remains repository-owned.
Customer-facing effects use a closed action catalog. An action runs only when:
1. its capability exists in the catalog;
2. the business has enabled the action gate;
3. the Operator has the exact permission;
4. the payload and audience pass governance validation;
5. Core records the intent and idempotency identity before executing it.
Enabling the Operator does not enable customer messaging. Outward actions are
separate and default closed.
## Enable and inspect
```bash
farthershore agents list --format json
farthershore agents status --format json
farthershore agents enable --format json
farthershore bulletin list --format json
```
Use the run id from status to inspect a durable run:
```bash
farthershore agents runs-show --format json
```
A run records status, composed launches, token usage, trace summary, catalog
changes, and action receipts. Authorization decisions are audit events and
writes use idempotency keys.
## Review and hand off work
```bash
farthershore bulletin show --format json
farthershore bulletin ack --format json
farthershore bulletin resolve --format json
```
- A contract change request goes to the managed repository.
- A customer-operation suggestion goes through the matching CLI command and
its confirmation boundary.
- A surprising effect should be traced through the run receipt and audit log
before another write is attempted.
## Failure and recovery behavior
The dispatcher prevents overlapping runs. Lost or silent runs become stale.
Three consecutive failed or degraded runs set `needsAttention` and pause later
dispatches. Inspect the trace and action receipts before re-enabling the
Operator.
Disable immediately when no more runs or agent-work accrual are wanted:
```bash
farthershore agents disable --format json
farthershore agents status --format json
```
Disabling prevents new runs and further Operator work. It does not delete
Bulletin posts, prior run evidence, action receipts, or usage history.
## Verification checklist
- status shows the expected enabled/disabled state;
- no overlapping run is active;
- each surprising action has a matching receipt and audit decision;
- contract change requests are handed to the coding agent rather than applied
imperatively;
- Bulletin items are acknowledged or resolved only after their evidence is
reviewed.
See [Observe and troubleshoot](/operate/observability-and-troubleshooting),
[Customer operations](/operate/customer-operations), and
[Ownership boundaries](/agents/operation-classes).
---
# Your coding agent
Canonical URL: https://docs.farthershore.com/agents/overview
## Check the deployment prerequisite first
Farther Shore is the gateway, billing, and entitlement plane in front of an HTTP
service that the builder runs. Before creating a business, confirm the session
has (a) somewhere to run a long-lived HTTP service on a public HTTPS URL, (b) the
ability to set environment variables there for `FS_RUNTIME_TOKEN`, and (c) a way
to read that service's logs. If any is missing, stop and ask the human — or
provision it with OpenTofu when cloud credentials already exist and the human
approves. Going live requires a bound production origin for every declared
backend.
A coding agent operates Farther Shore through three sources, in this order:
1. the managed repository, including its `AGENTS.md`, for business-specific
contract and application code;
2. the installed `farthershore` CLI for live platform operations;
3. this documentation for product behavior and edge cases.
## Start every task with live context
```bash
curl -fsSL https://docs.farthershore.com/llms.txt
farthershore auth whoami --format json
farthershore operations list --format json
```
`llms.txt` is the machine-readable index of the current documentation. Follow
the exact page for the task instead of relying on a memorized command. The
operation catalog is generated from the same registry that powers the CLI; it
states whether an action is repository-authored, Git-triggered, available as a
command, or intentionally belongs to another principal.
## Authentication is deliberately simple
```bash
farthershore login --headless
farthershore auth organization list --format json
farthershore auth organization use
```
Normal login creates a user-bound CLI session. It follows the approving user's
live roles across every current and future organization and business; there are
no permission, organization, or business choices during login. Selecting an
organization changes command context, not authority.
Use a separately issued, organization-scoped MakerToken only when automation
must be narrower than the user. Pipe it from a secret provider with
`farthershore login --token-stdin`, or set `FARTHERSHORE_TOKEN` for one process.
Never place a credential in argv or logs.
## Create and enter the managed repository
```bash
CREATE_ATTEMPT=$(node -e 'console.log(crypto.randomUUID())')
REPO_URL=$(farthershore business create quillby \
--idempotency-key "$CREATE_ATTEMPT")
git clone "$REPO_URL"
cd quillby
```
Creation succeeds only after the managed GitHub repository exists. The command
prints its URL as the handoff. The repository contains tooling and instructions,
not a sample business. Read `AGENTS.md`, gather the product requirements, and
author the filename-agnostic `business/` program from scratch.
## Use the correct change path
| If the task changes… | Make the change through… |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| routes, plans, prices, meters, limits, resources, policies, RBAC, backend declarations, or customer surfaces | `business/` code, then build and push |
| frontend or backend application code | the managed repository, then its branch/release workflow |
| environments, live subscribers, runtime tokens, variables, API-managed webhook endpoints, metadata, agents, notifications, or rollback pointers | the CLI |
Read [Ownership boundaries](/agents/operation-classes) before acting when the
owner is ambiguous.
## The build and apply loop
```bash
farthershore build --format json
farthershore validate --format json
git add business/
git commit -m "define business behavior"
git push
farthershore apply-timeline list quillby --format json
```
The repository checks are named `farthershore/build` (Manifest IR build) and
`farthershore/apply` (compile, accept, publish to edge); a customized portal also
reports `farthershore/frontend`. `farthershore/validate` fires only on pull
requests — after a plain branch push, waiting for it is waiting for a check that
will never appear.
Stop on a failed local build. After a push, inspect the GitHub checks and Apply
Timeline. A timeout does not prove a write failed. Follow the operation
catalog's retry contract: only `same_key_replay` reuses a caller-persisted key,
and every replay must be followed by its reconciliation read. See
[Retries and idempotency](/agents/retries-and-idempotency).
## Choose the task guide
- [Quickstart](/get-started/quickstart) — create the first business.
- [Business program](/define/business-class) — author contract state.
- [Backend setup](/backend/overview) — connect server logic and verified identity.
- [Scaffold a backend](/backend/scaffold) — `farthershore create api --node`;
never hand-write the verification middleware.
- [Infrastructure with OpenTofu](/backend/infrastructure-opentofu) — one
deployment and one runtime token per environment.
- [Customer UI](/frontend/overview) — build the customer-facing app.
- [Environments](/operate/environments) — test safely before production.
- [Releases](/operate/releases) — understand Git pushes, Releases, and rollbacks.
- [CLI reference](/reference/cli) — operate current platform state.
- [Response codes](/reference/response-codes) — branch on stable errors.
## Agent operating rules
- Run commands with `--format json` and branch on exit status, `code`, and `op`,
never prose.
- Use `--help` for the installed command shape.
- Never claim business creation without the returned repository URL.
- Never report a push as live without reading its Apply Timeline and status.
- Treat production releases, economic changes, subscriber migrations, and
destructive live operations as confirmation-gated.
- Preserve platform error evidence before retrying or escalating.
- Never put a key on a preview or a convergent desired-state write.
- Scaffold the backend with `farthershore create api` rather than hand-writing
the raw-body capture or signature-verification middleware.
- Create the backend row before minting its runtime token, and pass `--backend
` when an environment has several.
- Treat every push to an environment branch as destroying that environment's
personas, subscriber keys, and customer rows; re-mint after each push.
- Never place a runtime token, subscriber API key, or persona session token in
chat, argv, logs, or source control.
---
# Ownership boundaries
Canonical URL: https://docs.farthershore.com/agents/operation-classes
Farther Shore has one source of truth for each fact. The dashboard and CLI may
display repository-owned state, but that does not make it editable there.
## The four execution paths
| Path | Owns | How it changes |
| ------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Business contract | routes, plans, prices, meters, limits, resources, grants, policies, RBAC, backend declarations, frontend integrations | edit any module under `business/`, build, and push |
| Application code | backend logic, frontend source, database schema, product-specific data | edit the managed repository and use its normal build/deploy workflow |
| Git automation | validating and applying pushed business code; production frontend builds; preview frontend builds | push the mapped branch or create the production GitHub Release |
| Platform operations | live state without a code representation | use `farthershore` CLI or the equivalent dashboard action |
The compiler discovers the whole `business/` folder and requires one
default-exported `fs.business()` result. No filename is part of the contract.
## Repository-owned contract state
These are code changes, even when the dashboard displays them:
- routes, route groups, features, and surfaces;
- plans (declared kind, recurring price, usage-pricing binding, funding
buckets, lifecycle, spend policy), pricing catalogs, and grants;
- measures, dimensions, meters, and meter-route bindings with their admission
bounds;
- rate, quota, concurrency, resource, and capacity limits;
- tenancy, RBAC, and backend/frontend integration declarations.
The loop is always edit → `farthershore build` → push → inspect checks and Apply
Timeline. There is no imperative contract writer.
## Platform-owned operating state
Typical CLI jobs include:
- business presentation metadata and origin settings;
- preview-environment lifecycle and organization context;
- backend instances, origin bindings, runtime tokens, and variables;
- frontend status and rollback pointers;
- subscriber inspection, blocking, removal, and accepted-version migration;
- usage, analytics, denials, audit logs, notifications, and workflow status;
- webhook endpoints (always platform-owned — never repository state), test
personas, operating agents, and Bulletin handoffs.
Use the live catalog rather than inferring a command:
```bash
farthershore operations list --format json
farthershore business --help
```
An operation without a command is not automatically missing. The catalog tells
you whether it is repository-authored, Git-triggered, a subscriber-portal or
human decision, a runtime SDK action, covered by another command, or a genuine
platform gap.
## Non-obvious examples
| Request | Owner | Correct path |
| ------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Declare a backend named `api` | Repository | add `fs.backend("api")` and attach routes |
| Bind that backend to a preview origin | Platform | create/read the environment-local backend row, then use `backend bind` |
| Deploy a production hosted frontend | Git | create the approved GitHub Release; there is no `frontend deploy` command |
| Roll back a hosted frontend | Platform | `farthershore frontend rollback … --release-id …` |
| Roll back a bad business publish | Platform | `farthershore business rollback …`, which starts a new forward workflow from a captured snapshot |
| Change a price | Repository | edit `fs.plan()`, then follow the economic release gate |
| Rebind existing commercial pins | Deferred | generalized recurring/non-current pricing rebind is unavailable; `pricing.current()` still follows activated catalogs forward |
| Store a customer record | Application | use the signed principal and a race-safe database upsert |
## Decision procedure
1. Can the desired fact be represented in `business/`? Edit the repository.
2. Is it application behavior or data? Edit the application, not the platform
contract.
3. Does the operation catalog say `git_triggered`? Push or release, then poll
status.
4. Does the operation have a CLI command? Run it and read the resource back.
5. Does it belong to a human or subscriber principal? Hand it off.
6. Only report a platform gap when the live catalog labels it
`not_implemented`.
See [Your coding agent](/agents/overview) for the complete loop and the live
[documentation index](https://docs.farthershore.com/llms.txt) for task-specific
details.
---
# Retries and idempotency
Canonical URL: https://docs.farthershore.com/agents/retries-and-idempotency
Idempotency is an **attempt identity**, not a freshness guarantee. A replay can
prove what one earlier request returned. It cannot prove that the resource is
still in that state.
Start with the installed CLI's machine-readable catalog:
```bash
farthershore operations list --format json
```
Read each operation's `retry.kind`, `retry.keyRequired`, `retry.enforcement`,
`retry.responseSemantics`, `retry.reconcile`, and `retry.rationale` before
automating it. The catalog also includes commands that are registered directly
instead of projected into MCP under `directCliOperations`; this includes remote
`farthershore validate --business` and subscriber service-account operations.
## The retry contracts
| `retry.kind` | Use an idempotency key? | Safe agent behavior |
| -------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `read_current` | No | Retry the read. Each successful response is a new observation. |
| `convergent_write` | No | After an ambiguous result, run `retry.reconcile` first. Repeat only if the intent is still current, then reread. This avoids overwriting a newer change. |
| `same_key_replay` | Yes, for the live request | Persist one key before the first dispatch. Reuse it only for that exact uncertain attempt, then reconcile. |
| `intrinsic_replay` | No | The stable target already names the one transition. Repeat the exact request and reconcile; Core rejects a changed payload. |
| `no_automatic_retry` | No | An ambiguous result may have caused an external effect. Stop and use `retry.reconcile` before deciding whether a new action is required. |
`retry.enforcement` states what actually supplies safety: a fresh read,
resource identity, Core's idempotency reservation, transactional secret replay,
or a server-derived transition identity. `retry.responseSemantics` makes the
freshness boundary explicit. In particular, `original_attempt_result` is
historical proof about one attempt, not current resource state.
`intrinsic_replay` is intentionally rare. Managed service-account approval is
one example: the approval ID and authenticated approver identify the transition,
so adding a random caller key would not make it safer.
For agent mutations, Core accepts `Idempotency-Key` only when the concrete
retry contract is `same_key_replay`. Sending one on a convergent write,
preview-only route, or `no_automatic_retry` operation returns
`400 IDEMPOTENCY_KEY_NOT_SUPPORTED`. Current-state reads expose no key in the
CLI/MCP contract and always execute as fresh reads; an incidental HTTP header
cannot turn a read into a cached response. These boundaries prevent a caller
from accidentally treating a desired-state write or observation as historical
replay state.
For a CLI-session or MakerToken principal, Core also rejects a live
`same_key_replay` request that omits the header with
`400 IDEMPOTENCY_KEY_REQUIRED`, before the operation handler runs. This
server-side requirement protects agents that call the HTTP API directly rather
than through the CLI or MCP. Shared human browser routes may remain unkeyed;
the browser flow is not an automatic agent retry contract.
## Execute a same-key operation
1. Resolve the organization, business, environment, and exact payload.
2. Run `--dry-run` without an idempotency key when preview is supported.
3. Generate and record a new opaque key in durable agent task state **before**
the first live dispatch.
4. Send the live request with `--idempotency-key `.
5. If the result is ambiguous, retry the byte-equivalent intent with the same
key. Do not generate another key.
6. Run the catalog's `retry.reconcile` read before reporting current state.
When Core served the completed attempt from its replay store, the success
envelope contains:
```json
{
"meta": {
"idempotency": {
"replayed": true
}
}
}
```
This metadata is outside `data`, so it cannot be mistaken for a resource field.
Its absence means Core did not mark that response as a replay; it still does
not make a write response a fresh read.
Example:
```bash
# Record this value in private task state before the live call.
CREATE_ATTEMPT=$(node -e 'console.log(crypto.randomUUID())')
farthershore business create quillby \
--idempotency-key "$CREATE_ATTEMPT" \
--format json
farthershore business show quillby --format json
```
Do not store attempt keys in the business repository. They are not credentials,
but they are operational correlation material and do not belong in product
source.
## What “the same attempt” means
A key is scoped to the authenticated organization and principal. Within that
scope it names one semantic request across every endpoint. The following must
remain identical on a retry:
- HTTP method and concrete path, including the target resource;
- environment selection;
- query values other than the preview control;
- canonical request body, including array order;
- any route-specific semantic target.
Reusing the key for a different intent returns
`422 IDEMPOTENCY_KEY_REUSED`; the second mutation does not run. A request still
executing returns `409 IDEMPOTENCY_KEY_IN_FLIGHT`; back off and retry the same
key. If an attempt has remained in progress long enough that Core can no longer
prove whether its handler committed, it returns
`409 IDEMPOTENCY_RESULT_INDETERMINATE` with `retryable: false`. Core retains the
attempt and does not execute it again. Run the reconciliation read; use a new
key only after current state proves that a new mutation is still required.
Keys are retained for 24 hours.
A `5xx` emitted after a keyed handler starts is also indeterminate: a server
error cannot prove that no database or provider effect committed. Core retains
the attempt instead of freeing the key for an automatic rerun. A `3xx`/`4xx`
response is stored as the terminal historical result of that attempt too,
because a handler may have partially applied before rejecting. Reconcile first;
if corrected input or a fresh action is required, give that new intent a new
key.
## Previews never consume a live attempt
Never send an idempotency key with `--dry-run`. The CLI and Core reject that
combination. Preview again from current state, then create a separate key for
the live operation. This prevents a preview response from being replayed as if
the write happened.
Only operations whose catalog entry has `retry.preview: true` expose preview.
Core returns `400 DRY_RUN_NOT_SUPPORTED` for any other mutating route instead
of silently running a live mutation or returning a false preview.
Some APIs expose a dedicated POST preview route and reuse the live operation's
authorization permission. The route-level OpenAPI
`x-farthershore-agent-retry` contract is authoritative: dedicated agreement
preview routes are fresh observations and reject `Idempotency-Key`, while the
separate confirmed agreement create or amend route requires one.
## One-time results
Token and credential mints use `same_key_replay` so an ambiguous first response
can recover the exact secret without minting another. The encrypted secret
recovery window is 15 minutes. After it expires, the completed attempt remains
tombstoned and returns `IDEMPOTENCY_RESULT_EXPIRED`; Core does not rerun the
mutation. Use the reconcile read to locate the created credential, then rotate
under a new persisted key if a new secret is actually required.
`auth context-token` is deliberately different. It mints a short-lived,
point-in-time authorization credential, so replay could return a token whose
expiry or embedded authorization snapshot is no longer useful. Its contract is
`no_automatic_retry`: after an ambiguous response, request a new token rather
than attaching an idempotency key or treating an older token as current state.
## Externally delivered effects
`webhook test` and `webhook trigger` send a real signed HTTP request to the
configured receiver. Persist a key before the first send so an uncertain retry
cannot deliver the same test twice:
```bash
WEBHOOK_ATTEMPT=$(node -e 'console.log(crypto.randomUUID())')
farthershore webhook trigger quillby \
--type payment.failed \
--idempotency-key "$WEBHOOK_ATTEMPT" \
--format json
farthershore webhook deliveries quillby --format json
```
`webhook listen` remains `no_automatic_retry` because it is a process-owning,
multi-step session (tunnel, temporary endpoint, optional send, tail, cleanup),
not one server mutation that response replay can make atomic.
## Desired-state examples
Backend binding is convergent and environment-specific, so it deliberately has
no replay key. After an ambiguous bind, inspect the current binding before
deciding whether to repeat it; do not blindly restore an older URL over a newer
human or agent change:
```bash
farthershore backend list quillby --format json
# Select the row whose environmentId matches the resolved preview environment.
# If it is not already correct and the original intent is still current:
farthershore backend bind quillby api \
--env preview \
--origin-url https://preview-api.example.com \
--format json
farthershore backend list quillby --format json
```
Usage-limit create is naturally identified by subscription, subject, and
quantity. An exact repeated create returns the current matching row; if that row
changed after the first request, the retry returns conflict instead of stale
success. Updates target a stable limit ID, but an absolute count can still be
newer than the ambiguous request. Both paths therefore run `limit list` before
any repeat and again after a write.
Never add a key merely because a request uses `POST`. Use the advertised retry
contract and the observed operation semantics.
---
# Enable the Farther Shore Agent
Canonical URL: https://docs.farthershore.com/cookbook/enable-platform-agent
## Outcome
The business's Operator is enabled, its state is observable, and you know how
to inspect or stop it. The first run happens on the platform-managed cadence;
enabling it does not synchronously force a run.
## Prerequisites
- A Farther Shore business
- The current `farthershore` CLI with an authenticated session
## Enable
```bash
BUSINESS=
farthershore agents enable "$BUSINESS" --format json
```
Enabling schedules the Operator. It does not force an immediate run.
## Verify
```bash
farthershore agents status "$BUSINESS" --format json
```
The status response should show `enabled: true`. A newly enabled Operator may
have no `lastRunAt` or recent runs yet. That is expected until its first cadence
window.
After a run appears, inspect its paper trail:
```bash
farthershore agents status "$BUSINESS" --format json
farthershore agents runs-show "$BUSINESS" --format json
```
Review these fields:
- Run status and any stable error code
- Composed launches and their outcomes
- Token and action usage
- Platform-action receipts and whether each was executed, denied, or failed
- Bulletin posts created from the run
## Expected result
The Operator periodically studies the business and writes useful output to the
Bulletin. It does not modify the repo or product contract. Customer-facing
actions remain unavailable unless their separate platform gate is enabled.
## Common failures
| Symptom | Meaning | Recovery |
| ----------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------- |
| No runs immediately after enable | Runs are scheduled, not forced by enable. | Wait for the cadence and check status again. |
| `needsAttention: true` | Repeated failed or degraded runs triggered quarantine. | Inspect recent runs, then re-enable to acknowledge and clear it. |
| An action receipt says `denied` | A config gate, payload, or exact permission check rejected it. | Treat the receipt as final; do not retry around governance. |
| The agent requests a product change | Product state is repo-owned. | Give the Bulletin request to the coding agent for a normal PR. |
## Stop and recover
Disable the Operator whenever you do not want new runs or further agent-work
billing:
```bash
farthershore agents disable "$BUSINESS" --format json
farthershore agents status "$BUSINESS" --format json
```
Disabling is reversible and preserves history. Re-enable later with the same
command used above. If a run is already executing, use status and its durable
run record to confirm the terminal outcome; do not delete receipts or audit
history.
## Agent prompt
```text
Inspect the Farther Shore Agent for business .
1. Run `farthershore agents status --format json`.
2. If it is disabled, report that fact and ask before enabling it.
3. If it is enabled, summarize the latest run status, usage, and errors.
4. Inspect the latest run with `farthershore agents runs-show`.
5. Separate Bulletin insights from executed platform-action receipts.
6. Never translate a product change request into an API mutation; make a repo PR.
7. If the Operator has `needsAttention`, explain the failed runs before re-enabling it.
```
## Next steps
- [Farther Shore Agent](/agents/platform-agent) — authority and safety model
- [Your coding agent](/agents/overview) — the separate product-building agent
- [Ownership boundaries](/agents/operation-classes) — where every change belongs
---
# Choose CLI and MCP operations
Canonical URL: https://docs.farthershore.com/cli/overview
The CLI is the broad public operating surface; its MCP server exposes a subset of the same registered operations. Authentication and authorization follow the saved credential, live platform role and selected target.
## Establish context
Documentation browsing needs no login: start with `farthershore docs ls`, expand
a collection with `farthershore docs tree backend-sdk`, and retrieve a file with
`farthershore docs read --format json`. See [documentation traversal](/agents/navigation)
for heading reads, search, and stage selection. Authentication below applies to
platform operations, not public docs retrieval.
Follow [installation and login](/get-started/install). Normal device login requires a human to allow the CLI to act as them. Selecting an organization changes routing context, not the user's authority. A separately issued MakerToken has its own restricted scope.
Inspect the installed version before copying a command:
```bash
farthershore --version
farthershore --help
farthershore operations list --format json
```
Use the [global options](/generated/cli/global) for context and output configuration. Prefer structured output for automation. Never place raw credentials in command arguments.
## Discover exact signatures
The sidebar's Complete reference group contains a page for every top-level command and every nested command, including local commands that are not platform operations. Generated help includes required arguments, flags, defaults and command-specific descriptions.
The [platform catalog](/generated/cli/operation-catalog) connects operation keys to ownership class, permission, CLI command, MCP tool and unavailable reason. Repository-authored and Git-triggered operations are documented boundaries. A catalog entry is not a promise that arbitrary REST access is supported.
## Connect MCP
Run the `farthershore-mcp` binary as a stdio MCP server in the agent host. The [MCP tool reference](/generated/cli/mcp) includes the exact input schema, side-effect annotation and one-time-secret behavior for each tool. Use the existing credential configuration; never put a credential in the MCP launch arguments.
## Execute and verify
Read the relevant product guide before a write. Confirm the business and environment. Use dry-run when the command supports it, retain idempotency keys for retries, and obey the documented confirmation boundary. Read back state after success; an accepted asynchronous operation still needs convergence evidence.
If help differs from this reference, check the installed version against the [catalog package versions](/capabilities.json). Do not guess a replacement flag or fall back to an undocumented endpoint.
---
# CLI reference
Canonical URL: https://docs.farthershore.com/reference/cli
The CLI is both a local business-program tool and the complete automation
surface for platform-owned state. Treat the installed CLI and its operation
catalog as canonical; documentation explains behavior, while `--help` supplies
the exact versioned flags.
## Authenticate
```bash
farthershore login
farthershore auth whoami --format json
farthershore auth organization list --format json
farthershore auth organization use
```
Device login opens the complete authorization request in your browser without
printing a code and waits for a human to approve it. `--headless` instead prints
the manual verification URL and user code without opening a browser. Normal
login is user-bound and has no permission or scope choices: the CLI acts as the
user, and Core reloads that user's live role on every request.
Normal login includes all current and future organization and business access.
Membership and role changes take effect on the next authenticated request.
`--organization ` selects command context for one invocation. It
never narrows the session's authority.
For deliberately restricted automation, pipe a pre-issued organization-scoped
MakerToken from its secret provider:
```bash
printf %s "$FARTHERSHORE_MAKER_TOKEN" | farthershore login --token-stdin
```
Or set `FARTHERSHORE_TOKEN` for a single process. Never place the secret in
argv. `farthershore logout` revokes a user CLI session and deletes the local
credential; a saved MakerToken is removed locally but remains governed by its
server-side token lifecycle.
## Discover before acting
```bash
farthershore operations list --format json
farthershore business --help
```
The operation catalog currently publishes each operation's class, side-effect
class, CLI command, MCP mapping, and either its exact permission/target kind or
an explicit handoff with null authority. Only `not_implemented` is a platform
gap; `repo_authored`, `git_triggered`, `subscriber_portal`, `human_decision`,
and `runtime_surface` are deliberate boundaries. For example,
`funding.top_up.purchase` is discoverable as a `subscriber_portal` handoff but
has no builder permission, CLI command, or MCP tool; use its documented public
Frontend SDK path in the [prepaid wallet cookbook](/cookbook/prepaid-credits#add-the-subscriber-refill-control).
Database-backed list commands that advertise the options share Core's list
query contract:
```bash
farthershore business list --filter status=ACTIVE --search billing --sort updatedAt:desc
farthershore organization role list --filter builtIn=false --sort name
farthershore organization members --filter roleKey=member --sort email
farthershore env list --search preview --sort name
```
Filtering and sorting happen on the server before pagination. `--filter` is
repeatable and exact; `--search` is case-insensitive; `--sort` defaults to
ascending when its direction is omitted. Each command's help prints its closed
field allowlist. See [Platform access and roles](/operate/platform-access).
## Create a managed business
```bash
CREATE_ATTEMPT=$(node -e 'console.log(crypto.randomUUID())')
REPO_URL=$(farthershore business create quillby \
--idempotency-key "$CREATE_ATTEMPT")
git clone "$REPO_URL"
```
This is the only creation path. Human-mode stdout is the repository URL and
success is impossible before the managed repository exists. Structured output
contains the business, `repoUrl`, and recovery metadata. On an ambiguous
timeout, retry the exact same intent with the persisted key, then run
`business show`; the replayed create result is not a fresh read.
The repository starts without a business shape. Author the filename-agnostic
`business/` program, then:
```bash
farthershore build --format json
farthershore validate --format json
git push
farthershore apply-timeline list quillby --format json
```
## Task map
| Job | Command groups |
| ---------------------------------------- | --------------------------------------------------------------------------------------------- |
| Business lifecycle and accepted contract | `business`, `apply-timeline`, `plan`, `env` |
| Customer and subscription operations | `consumer`, `proposal`, `persona`, `promo-code`, `resource-count` |
| Runtime application wiring | `backend`, `variables`, `webhook`, `frontend` |
| Observe and diagnose | `business status`, `usage`, `analytics`, `denial`, `dependents`, `audit-log`, `workflows` |
| Organization and personal context | `auth`, `organization`, `notifications` |
| Platform automation | `agents`, `bulletin`, `automation`, `knowledge`, `workflow-control` |
| Local repository tools | `link`, `unlink`, `create`, `import`, `build`, `validate`, `frontend dev`, `frontend preview` |
The top-level `tax-settings` group operates a business's Stripe Tax settings.
Use `farthershore --help` for the full current group list.
## Important command boundaries
### Business contract versus operations
There is no CLI writer for plans, prices, routes, meters, resources, limits,
policies, or surfaces. Read them with commands such as `business contract`,
`business routes`, and `plan list`; change them in `business/` and push.
Platform-owned metadata and live state use commands:
```bash
farthershore business update quillby --display-name "Quillby" --format json
farthershore business update quillby --unlist-from-org-page --format json
farthershore consumer list quillby --format json
```
### Git-triggered frontend deployment
The `frontend` group intentionally has no deploy subcommand. Preview frontend builds follow the
mapped preview branch; production frontend builds follow an approved GitHub
Release. The CLI inspects and recovers the result:
```bash
farthershore frontend status quillby --format json
farthershore frontend rollback quillby --release-id --dry-run --format json
```
### Backend declaration versus runtime binding
`fs.backend()` declares a logical backend in `business/`. CLI backend commands
operate an environment-local instance, origin, tunnel, and runtime tokens:
```bash
farthershore backend list quillby --format json
farthershore backend create quillby --name api --transport direct --origin-url https://api.example.com --format json --idempotency-key
farthershore backend bind quillby --origin-url https://preview-api.example.com --format json
farthershore backend tokens create quillby --backend --format json --idempotency-key
```
### Usage, analytics, audit, and notifications
```bash
farthershore usage summary quillby --format json
farthershore analytics timeseries quillby --format json
farthershore audit-log list --format json
farthershore notifications preferences quillby --format json
```
These reads do not modify contract state.
### Recovery commands and previews
`business rollback` starts a new publish workflow from a captured prior
snapshot and requires a caller-persisted attempt key. `frontend rollback` moves
the hosted release pointer and supports `--dry-run`. Neither rewrites Git
history. `workflow-control rollback-to-config` is preview-only: it computes the
configured rollback target but does not execute recovery, so it takes no key.
```bash
farthershore business rollback quillby --format json --idempotency-key
farthershore workflow-control rollback-to-config quillby --format json
```
Read [Releases](/operate/releases) before executing a production rollback.
## Output contract
Use `--format json` for the stable agent envelope:
```json
{
"schema_version": 1,
"ok": true,
"op": "business.status",
"data": {}
}
```
Failures set `ok: false` and include a stable `error.code`, message, and
optional hint. Branch on `code` and process exit status; never parse the English
message.
| Exit | Meaning |
| ---- | ---------------------------------------- |
| `0` | success |
| `1` | local, not-found, or generic API failure |
| `2` | invalid request |
| `3` | authentication or authorization failure |
| `4` | `MANAGED_BY_CODE` ownership boundary |
| `5` | retryable platform or in-flight failure |
## Safe writes
- Read `retry.kind` and `retry.reconcile` from
`farthershore operations list --format json`.
- For `same_key_replay`, persist one key before the first live dispatch and
reuse it only for that exact intent.
- Do not send a key for `read_current`, `convergent_write`,
`intrinsic_replay`, or `no_automatic_retry`.
- After an ambiguous `convergent_write`, run `retry.reconcile` before any
repeat. Repeat only if the original desired state is still authoritative;
then read again so an older request cannot overwrite a newer change.
- Prefer `--dry-run` where available, and never include an idempotency key in a
preview.
- Treat `--yes` as an explicit destructive confirmation boundary.
- Read the resource back after every replay or convergent write.
- Use the related job guide from
[the live documentation index](https://docs.farthershore.com/llms.txt) for
non-obvious environment, billing, and rollback behavior.
See [Retries and idempotency](/agents/retries-and-idempotency) for conflicts,
in-flight attempts, secret recovery windows, and exact semantic request scope.
---
# @farthershore/business exports
Canonical URL: https://docs.farthershore.com/generated/business-sdk/root
{/* Generated by generate-reference.ts. Do not edit. */}
Import from `@farthershore/business`. This reference is extracted from the published declaration surface for version **3.2.0**. Read the collection's guides for workflows, prerequisites and failure handling.
## farthershore-manifest-build
This package executable compiles locally; it does not apply or release a business. See [build output](/define/build-output) for generated artifacts and diagnostics.
~~~~text
Usage: farthershore-manifest-build [--entry business] [--out business-build.json] [--diagnostics-out manifest-diagnostics.json]
--entry defaults to the business/ folder (all modules imported; exactly one default fs.business() export is compiled).
Pass a folder to discover it, or a single file to load only that file.
~~~~
## AuthoredMoneyAmount
Public export `AuthoredMoneyAmount`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L230`.
~~~~text
export type AuthoredMoneyAmount = Readonly>;
~~~~
## backend
Public export `backend`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L66`.
~~~~text
export declare function backend(id: string, options?: FunctionalBackendOptions): BackendRef;
~~~~
## BackendDefinitionJson
Public export `BackendDefinitionJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L304`.
~~~~text
export type BackendDefinitionJson = {
name?: string;
slug?: string;
transport?: {
mode?: BackendTransportModeJson;
runner?: BackendRunnerJson;
};
/**
* Gateway->backend request signing. Emitted for every declared backend; the
* builder helper defaults omission to `{ required: true }` (HARD default).
*/
verification?: {
required?: boolean;
};
/** Meter allow-list. Omitted = all business meters allowed. */
meters?: string[];
/** Marks the default backend when a business declares more than one. */
default?: boolean;
};
~~~~
## BackendRef
Public export `BackendRef`.
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L42`.
~~~~text
export type BackendRef = DeclarationRef__2a230f417174<"backend">;
~~~~
## BackendRunnerJson
Public export `BackendRunnerJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L303`.
~~~~text
export type BackendRunnerJson = "embedded" | "sidecar";
~~~~
## BackendTransportModeJson
Public export `BackendTransportModeJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L302`.
~~~~text
export type BackendTransportModeJson = "direct" | "tunnel";
~~~~
## BoundRateRef
Public export `BoundRateRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L63`.
~~~~text
export type BoundRateRef = Readonly<{
kind: "bound_catalog_rate";
rate?: ExactRate;
tiers?: ExactCatalogTiers;
quote?: ExactQuoteBounds__e9cf817917c1;
measure: MeasureRef;
item?: ModelRef;
where: readonly DimensionValueRef[];
} & CommerceBrand__7b1b990b7c90<"bound_catalog_rate">>;
~~~~
## business
Public export `business`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L21`.
~~~~text
declare const business: BusinessFunction__7e4f3d5c2657;
~~~~
## BusinessSpecJson
Public export `BusinessSpecJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L395`.
~~~~text
export type BusinessSpecJson = {
business: BusinessBlockJson__a04690090fc8;
gateway?: {
authHeader?: string;
upstreamAuth?: {
type: "none" | "static_bearer";
token?: string;
};
};
metering?: {
meters?: MeterDefinitionJson[];
billOn4xx?: boolean;
};
/** BYO-Backend V1 — first-class backend declarations keyed by backend id.
* Emitted only when at least one backend is declared (so single-backend /
* no-backend products keep their pre-BYOB irHash). */
backend?: Record;
resources?: CountedResourceJson__90d0b7f16d08[];
/** Repo-owned custom permission subjects. Emitted (sorted by subject) only
* when at least one permission-carrying group is declared, so businesses
* without permission groups keep their pre-cutover irHash. */
permissions?: PermissionSubjectJson__d124b4a2259d[];
policies?: BusinessPoliciesJson__0d51efe66230;
customer_context?: BusinessCustomerContextJson__e5b9539124a5;
plans?: PlanSpecJson[];
/** Advanced/internal platform-schema blocks (usage, billing,
* environments, lifecycle, ephemeral, …) are validated by the platform
* schema but are not authorable through the functional SDK. */
[key: string]: unknown;
};
~~~~
## CacheProfile
Public export `CacheProfile`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L3`.
~~~~text
export type CacheProfile = "long" | "short" | "blocking";
~~~~
## canonicalIrJson
Public export `canonicalIrJson`.
Declaration source: `packages/business/dist/types/validate.d.ts#L68`.
~~~~text
export declare function canonicalIrJson(ir: unknown): string;
~~~~
## CatalogSelectorRef
Public export `CatalogSelectorRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L52`.
~~~~text
export type CatalogSelectorRef = ModelRef | DimensionValueRef;
~~~~
## CatalogTierInput
~~~~text
One authored tier bracket: `upTo` is the inclusive cumulative upper bound; `null` = open-ended final tier.
~~~~
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L12`.
~~~~text
export type CatalogTierInput = Readonly<{
upTo: number | null;
rate: UnboundRateRef;
}>;
~~~~
## commerceSnapshotOf
Public export `commerceSnapshotOf`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L22`.
~~~~text
export declare function commerceSnapshotOf(businessValue: CompiledBusiness__671c1daa22b7): CommerceAuthoringSnapshot__60159f2d0d70 | null;
~~~~
## dimension
Public export `dimension`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L4`.
~~~~text
export declare function dimension(key: string, options?: Readonly<{
values?: readonly string[];
}>): DimensionRef;
~~~~
## DimensionConditionRef
Public export `DimensionConditionRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L31`.
~~~~text
export type DimensionConditionRef = Readonly<{
kind: "dimension_condition";
dimension: DimensionRef;
value: string;
} & CommerceBrand__7b1b990b7c90<"dimension_condition">>;
~~~~
## DimensionRef
Public export `DimensionRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L36`.
~~~~text
export type DimensionRef = Readonly<{
kind: "dimension";
key: string;
value(value: string): DimensionValueRef;
is(value: string): DimensionConditionRef;
} & CommerceBrand__7b1b990b7c90<"dimension">>;
~~~~
## DimensionValueRef
Public export `DimensionValueRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L26`.
~~~~text
export type DimensionValueRef = Readonly<{
kind: "dimension_value";
dimension: DimensionRef;
value: string;
} & CommerceBrand__7b1b990b7c90<"dimension_value">>;
~~~~
## disclosure
Public export `disclosure`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L59`.
~~~~text
declare const disclosure: Readonly<{
opaque: DisclosureRef;
transparent: DisclosureRef;
}>;
~~~~
## DisclosureRef
Public export `DisclosureRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L118`.
~~~~text
export type DisclosureRef = Readonly<{
kind: "disclosure";
disclosure: "opaque" | "transparent";
} & CommerceBrand__7b1b990b7c90<"disclosure">>;
~~~~
## display
Public export `display`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L54`.
~~~~text
declare const display: Readonly<{
multiplier(options: Readonly<{
factor: number;
}>): MultiplierDisplayRef;
}>;
~~~~
## ExactCatalogTiers
Public export `ExactCatalogTiers`.
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L5`.
~~~~text
export type ExactCatalogTiers = Readonly<{
strategy: "graduated" | "volume_retroactive";
tiers: readonly Readonly<{
upTo: string | null;
rate: ExactRate;
}>[];
}>;
~~~~
## ExactRate
Public export `ExactRate`.
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L1`.
~~~~text
export type ExactRate = Readonly<{
num: string;
den: string;
}>;
~~~~
## exhaustion
Public export `exhaustion`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L67`.
~~~~text
declare const exhaustion: Readonly<{
block: ExhaustionRef;
overage(binding: PricingBindingRef): ExhaustionRef;
}>;
~~~~
## ExhaustionRef
Public export `ExhaustionRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L122`.
~~~~text
export type ExhaustionRef = Readonly<{
kind: "exhaustion";
behavior: "block" | "overage";
} & CommerceBrand__7b1b990b7c90<"exhaustion">>;
~~~~
## free
~~~~text
The zero recurring price. Only meaningful on a `plan.kind.free` plan,
where it is optional (a free plan needs no price control at all); every
other kind states its price with `money.usd(...)`.
~~~~
Declaration source: `packages/business/dist/types/value-model/money.d.ts#L31`.
~~~~text
export declare function free(): AuthoredPrice__fa4522a263d6;
~~~~
## frontendIntegration
Public export `frontendIntegration`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L67`.
~~~~text
export declare function frontendIntegration(id: string, options: FrontendIntegrationOptions): FrontendIntegrationRef;
~~~~
## FrontendIntegrationInjectionJson
Public export `FrontendIntegrationInjectionJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L159`.
~~~~text
export type FrontendIntegrationInjectionJson = {
secretRef: string;
location: "header";
name: string;
template: "{value}" | "Bearer {value}";
} | {
secretRef: string;
location: "query";
name: string;
};
~~~~
## FrontendIntegrationOperationJson
Public export `FrontendIntegrationOperationJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L152`.
~~~~text
export type FrontendIntegrationOperationJson = {
method: FrontendIntegrationMethodJson__404791a522ea;
path: string;
headers: string[];
query: string[];
body: FrontendIntegrationBodyJson__412664c4c97a;
};
~~~~
## FrontendIntegrationOptions
Public export `FrontendIntegrationOptions`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L43`.
~~~~text
export type FrontendIntegrationOptions = Omit & {
request: {
operations: ReadonlyArray & {
headers?: readonly string[];
query?: readonly string[];
body: FrontendIntegrationBodyOptions__69124a73fbef;
}>;
};
response: Omit & {
contentTypes: readonly string[];
jsonPointers: readonly string[];
responseHeaders?: readonly string[];
};
/** Integration calls are always platform-only and cannot carry customer economics. */
costs?: never;
reports?: never;
usagePolicy?: never;
creates?: never;
deletes?: never;
};
~~~~
## FrontendIntegrationRef
Public export `FrontendIntegrationRef`.
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L43`.
~~~~text
export type FrontendIntegrationRef = DeclarationRef__2a230f417174<"frontend_integration">;
~~~~
## FrontendIntegrationSpecJson
~~~~text
Secret-value-free mirror of contracts' FrontendIntegrationSpec.
~~~~
Declaration source: `packages/business/dist/types/ir-types.d.ts#L170`.
~~~~text
export type FrontendIntegrationSpecJson = {
id: string;
upstream: string;
request: {
operations: FrontendIntegrationOperationJson[];
};
injection: FrontendIntegrationInjectionJson;
response: {
kind: "json";
contentTypes: string[];
maxBytes: number;
jsonPointers: string[];
responseHeaders: string[];
};
};
~~~~
## FunctionalBackendOptions
Public export `FunctionalBackendOptions`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L12`.
~~~~text
export type FunctionalBackendOptions = Omit & {
meters?: MeterRef[];
};
~~~~
## FunctionalBusinessOptions
Public export `FunctionalBusinessOptions`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L8`.
~~~~text
export type FunctionalBusinessOptions = BusinessOptions__bbd5a4a3592b;
~~~~
## FunctionalGroupOptions
Public export `FunctionalGroupOptions`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L89`.
~~~~text
export type FunctionalGroupOptions = {
permission?: FunctionalGroupPermissionOptions;
};
~~~~
## FunctionalGroupPermissionOptions
~~~~text
Repo-owned raw permission vocabulary — the `permission` option of
`fs.group`. This declaration never creates a role, chooses a default role,
or assigns a user. Each subscribing organization composes these permissions
into its own CUSTOM roles at runtime.
Declaring it makes the group id a custom permission subject: member routes
are gated at the gateway by `:read|write`, and `verbs` are the EXTRA
grant-by-exact-name domain verbs the builder's own backend checks
(`read`/`write` are implied and may not be re-declared). Collisions with
the managed / platform permission vocabularies are rejected by the
platform compiler's validate pass.
~~~~
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L84`.
~~~~text
export type FunctionalGroupPermissionOptions = {
verbs?: readonly string[];
escalatory?: readonly string[];
description?: string;
};
~~~~
## FunctionalMeterOptions
Public export `FunctionalMeterOptions`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L15`.
~~~~text
export type FunctionalMeterOptions = Omit & {
pairsWith?: MeterRef;
};
~~~~
## FunctionalMeterRoutesOptions
Public export `FunctionalMeterRoutesOptions`.
Declaration source: `packages/business/dist/types/value-model/registry.d.ts#L28`.
~~~~text
export type FunctionalMeterRoutesOptions = Pick;
~~~~
## FunctionalRequestMeterOptions
Public export `FunctionalRequestMeterOptions`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L18`.
~~~~text
export type FunctionalRequestMeterOptions = Omit;
~~~~
## FunctionalResourceOptions
Public export `FunctionalResourceOptions`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L9`.
~~~~text
export type FunctionalResourceOptions = ResourceOptions__04a2cc1fd6ee & {
cap?: ResourceOptions__04a2cc1fd6ee["scope"];
};
~~~~
## FunctionalRouteLimitAuthoring
Public export `FunctionalRouteLimitAuthoring`.
Declaration source: `packages/business/dist/types/value-model/registry.d.ts#L26`.
~~~~text
export type FunctionalRouteLimitAuthoring = Extract | FunctionalRouteLimitInput;
~~~~
## FunctionalRouteLimitInput
Public export `FunctionalRouteLimitInput`.
Declaration source: `packages/business/dist/types/value-model/registry.d.ts#L23`.
~~~~text
export type FunctionalRouteLimitInput = Omit & {
dimension?: MeterRef;
};
~~~~
## FunctionalRouteOptions
Public export `FunctionalRouteOptions`.
Declaration source: `packages/business/dist/types/value-model/registry.d.ts#L7`.
~~~~text
export type FunctionalRouteOptions = Omit & {
action?: string;
/** Repo-owned custom permissions — require ONE of the route's permission
* group's declared verbs at the gateway for this operation, instead of the
* method-derived read/write default. Only valid on a route that is a
* member of a permission-carrying group declaring the verb. */
permission?: string;
backend?: BackendRef;
reports?: MeterRef | readonly MeterRef[];
costs?: MeterCost | readonly MeterCost[];
rateLimit?: FunctionalRouteLimitAuthoring;
quota?: FunctionalRouteLimitAuthoring;
creates?: ResourceRef;
deletes?: ResourceRef;
surfaces?: RouteOptions__8ba341feca83["surfaces"];
};
~~~~
## FundingBucketRef
Public export `FundingBucketRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L114`.
~~~~text
export type FundingBucketRef = Readonly<{
kind: "funding_bucket";
bucketKind: FundingBucketKind__eced6b195b95;
} & CommerceBrand__7b1b990b7c90<"funding_bucket">>;
~~~~
## group
Public export `group`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L92`.
~~~~text
export declare function group(id: string, members: readonly (RouteRef | GroupRef)[], options?: FunctionalGroupOptions): GroupRef;
~~~~
## GroupRef
Public export `GroupRef`.
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L36`.
~~~~text
export type GroupRef = DeclarationRef__2a230f417174<"group">;
~~~~
## hashIr
Public export `hashIr`.
Declaration source: `packages/business/dist/types/validate.d.ts#L67`.
~~~~text
export declare function hashIr(ir: unknown): string;
~~~~
## HttpMethod
Public export `HttpMethod`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L1`.
~~~~text
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
~~~~
## included
Public export `included`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L46`.
~~~~text
export declare function included(amount: AuthoredMoneyAmount, options?: Readonly<{
display?: MultiplierDisplayRef;
}>): FundingBucketRef;
~~~~
## LimitStrategy
Public export `LimitStrategy`.
Declaration source: `packages/business/dist/types/limit-strategy.d.ts#L1`.
~~~~text
export type LimitStrategy = "fixed_window" | "sliding_window" | "token_bucket";
~~~~
## ManifestBuilderError
~~~~text
Thrown on builder misuse (duplicate keys, unknown refs) at call time.
~~~~
Declaration source: `packages/business/dist/types/errors.d.ts#L19`.
~~~~text
export declare class ManifestBuilderError extends Error {
readonly code: string;
constructor(message: string, code?: string);
}
~~~~
## ManifestBuildResult
~~~~text
Platform compiler result: the validated envelope plus its canonical hash.
~~~~
Declaration source: `packages/business/dist/types/ir-types.d.ts#L434`.
~~~~text
export type ManifestBuildResult = {
ir: ManifestIrDocument;
irHash: string;
};
~~~~
## ManifestIrDocument
Public export `ManifestIrDocument`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L425`.
~~~~text
export type ManifestIrDocument = {
irVersion: 1;
sdkVersion: string;
business: BusinessSpecJson;
routes: RouteLayerJson[];
frontendIntegrations: FrontendIntegrationSpecJson[];
commerce: CommerceAuthoringSnapshot__60159f2d0d70;
};
~~~~
## ManifestIssue
~~~~text
One validation problem, in the platform's diagnostic envelope shape.
~~~~
Declaration source: `packages/business/dist/types/errors.d.ts#L2`.
~~~~text
export type ManifestIssue = {
code: string;
path: string;
message: string;
/** Builder source that owns the invalid business definition, when known. */
source?: string;
};
~~~~
## ManifestResourceGraphSnapshot
Public export `ManifestResourceGraphSnapshot`.
Declaration source: `packages/business/dist/types/resource-graph.d.ts#L11`.
~~~~text
export type ManifestResourceGraphSnapshot = {
readonly nodes: readonly Omit[];
};
~~~~
## ManifestResourceKind
Public export `ManifestResourceKind`.
Declaration source: `packages/business/dist/types/resource-graph.d.ts#L1`.
~~~~text
export type ManifestResourceKind = "business" | "meter" | "counted_resource" | "surface" | "route_layer" | "action" | "plan" | "backend" | "frontend_integration" | "permission_subject";
~~~~
## ManifestResourceUrn
Public export `ManifestResourceUrn`.
Declaration source: `packages/business/dist/types/resource-graph.d.ts#L2`.
~~~~text
export type ManifestResourceUrn = `urn:farthershore:business:${ManifestResourceKind}:${string}`;
~~~~
## ManifestValidationError
~~~~text
Thrown by the platform manifest compiler when the assembled manifest fails
schema validation. Plain-data `issues` so the error survives serialization
across the runner callback boundary.
~~~~
Declaration source: `packages/business/dist/types/errors.d.ts#L14`.
~~~~text
export declare class ManifestValidationError extends Error {
readonly issues: ManifestIssue[];
constructor(issues: ManifestIssue[]);
}
~~~~
## measure
Public export `measure`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L3`.
~~~~text
export declare function measure(key: string): MeasureRef;
~~~~
## MeasureMaximumRef
Public export `MeasureMaximumRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L21`.
~~~~text
export type MeasureMaximumRef = Readonly<{
kind: "measure_maximum";
measure: MeasureRef;
maximum: number;
} & CommerceBrand__7b1b990b7c90<"measure_maximum">>;
~~~~
## MeasureRef
Public export `MeasureRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L16`.
~~~~text
export type MeasureRef = Readonly<{
kind: "measure";
key: string;
atMost(maximum: number): MeasureMaximumRef;
} & CommerceBrand__7b1b990b7c90<"measure">>;
~~~~
## meter
Public export `meter`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L8`.
~~~~text
declare const meter: MeterFunction__02c80dd4d01e;
~~~~
## MeterCost
Public export `MeterCost`.
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L16`.
~~~~text
export type MeterCost = {
readonly kind: "meter_cost";
readonly meter: MeterRef;
readonly value: number;
};
~~~~
## MeterDefinitionJson
Public export `MeterDefinitionJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L4`.
~~~~text
export type MeterDefinitionJson = {
key: string;
display: string;
unit?: string;
routeDefault?: number;
enforcementType?: "exact_pre_request" | "estimated_then_settled" | "postpaid" | "strict_concurrency";
aggregation?: "SUM" | "COUNT" | "MAX" | "UNIQUE_COUNT" | "LATEST";
window?: "minute" | "hour" | "day" | "month" | "billing_period";
valueProperty?: string;
uniqueProperty?: string;
groupBy?: string[];
eventCode?: string;
/** Token category this meter measures, when it is a token meter. T5 covers
* the full token-accounting vocabulary: the input/output/total axes, the
* cache axes (`cached_read_input` / `cache_creation_input`), `context`, the
* output-budget axes (`max_output` / `estimated_output` / `actual_output`),
* and `streaming_output`. Mirrors `TOKEN_CATEGORIES` in contracts. */
tokenCategory?: "input" | "output" | "total" | "cached_read_input" | "cache_creation_input" | "context" | "max_output" | "estimated_output" | "actual_output" | "streaming_output";
/** How the gateway RESERVES units for this meter on an estimate-then-settle
* path: `reserve_max` (conservative worst-case) / `estimate` (heuristic) /
* `count_actual` (no reserve). T5 — authored per token category to its
* use-case: hard quota → `reserve_max` (optionally capped by
* `reserveCeiling`); soft billing → `estimate`; spend → `reserve_max`. */
reserveStrategy?: "reserve_max" | "estimate" | "count_actual";
/** T5 — optional configured reservation CEILING (meter's native unit) for a
* conservative `reserve_max` strategy that must not hold the full theoretical
* worst case. Positive integer. Omit ⇒ unbounded worst-case reserve. */
reserveCeiling?: number;
/** Key of a meter this one is paired with (e.g. an `input` token meter pairs
* with its `output` meter) so settlement / observability correlate the pair. */
pairsWith?: string;
};
~~~~
## MeterLimit
Public export `MeterLimit`.
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L11`.
~~~~text
export type MeterLimit = {
readonly kind: "meter_limit";
readonly meter: MeterRef;
readonly value: PlanLimitJson;
};
~~~~
## MeterOptions
~~~~text
`meter()` options. Every meter is structural (aggregation / window /
enforcement / unit — what routes report and limits ration). Declaring
`measures` (and optionally `dimensions`) additionally makes it a PRICEABLE
measurement meter that `pricing()` catalogs rate and
`meterRoutes(key, route, { reports })` binds.
~~~~
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L87`.
~~~~text
export type MeterOptions = FunctionalMeterOptions & Readonly<{
measures?: readonly MeasureRef[];
dimensions?: readonly DimensionRef[];
}>;
~~~~
## MeterRef
Public export `MeterRef`.
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L26`.
~~~~text
export type MeterRef = DeclarationRef__2a230f417174<"meter"> & {
fixed(value: number): MeterCost;
perSecond(value: number, options?: TemporalLimitOptions): MeterLimit;
perMinute(value: number, options?: TemporalLimitOptions): MeterLimit;
perHour(value: number, options?: TemporalLimitOptions): MeterLimit;
perDay(value: number, options?: TemporalLimitOptions): MeterLimit;
perWeek(value: number, options?: TemporalLimitOptions): MeterLimit;
perMonth(value: number, options?: TemporalLimitOptions): MeterLimit;
};
~~~~
## MeterRouteBindingOptions
~~~~text
`meterRoutes(key, routeRef, options)` — a concrete route's commerce
metering binding.
~~~~
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L163`.
~~~~text
export type MeterRouteBindingOptions = Readonly<{
/** Priceable meters (declared with `measures`) this route reports. */
reports: readonly MeterRef[];
maxOutputUnits?: MeasureMaximumRef;
/** FAR-909 — finite route_cap admission bounds for measures the client cannot declare. */
caps?: readonly MeasureMaximumRef[];
/**
* FAR-911 (H5) — the request-side clamp binding for `maxOutputUnits`: names
* the compiled admission knob `max_output_units` and the ONE gateway
* parser/mutator that reads and rewrites the request body's output-unit
* limit (`json_body_max_output_units_v1`: the JSON body's canonical
* output-unit field — for LLM chat/stream routes the `max_tokens` /
* `max_output_tokens` protocol aliases — parsed as a non-negative integer,
* clamped DOWN to the declared bound, and rewritten before upstream
* signing; a missing knob is set to the bound). Builders never name a raw
* JSON path: the route adapter owns protocol aliases. Compile-time only on
* this branch — the gateway consumer is FAR-905
* (`far905-requires-clamp-consumer.pending-gate.test.ts`); until it lands,
* a client that exceeds a declared bound is TERMINAL-QUARANTINED at
* ingestion (`ADMISSION_BOUND_BREACHED`, fail-closed, unbilled), never
* over-debited.
*/
maxOutputUnitsAdapter?: Readonly<{
knob: "max_output_units";
parser: "json_body_max_output_units_v1";
mutator: "json_body_max_output_units_v1";
}>;
/**
* FAR-911 (H3) — chunked top-up instead of a declared `maxOutputUnits`:
* `bound` is the cumulative per-operation ceiling (`out.atMost(65536)`),
* `chunkUnits` the initial reservation; the cumulative ceiling is funded
* before the first response byte. Exclusive with `maxOutputUnits`.
*/
chunkPolicy?: Readonly<{
bound: MeasureMaximumRef;
chunkUnits: number;
}>;
/**
* FAR-911 (H3) — post-stream measurement timing. `settlementMax` declares
* the finite per-measure settlement maxima prepaid/x402 plans require.
*/
postStream?: Readonly<{
settlementMax?: readonly MeasureMaximumRef[];
}>;
}>;
~~~~
## meterRoutes
~~~~text
THE meterRoutes verb — always `(key, target, options)` with an
author-supplied stable key. A concrete `route()` ref target is a COMMERCE
METERING BINDING (`reports` name priceable meters; admission bounds via
`maxOutputUnits` / `caps` / `chunkPolicy` / `postStream`). A wildcard path,
group ref, or array target is a STRUCTURAL OVERLAY on the matched routes
(`reports` / `costs` / `onStatusCodes` / `postStreamBilling`) — the
route-layer overlay under `key`.
~~~~
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L20`.
~~~~text
declare const meterRoutes: MeterRoutesFunction__799abcbb8f6e;
~~~~
## MeterRoutesOverlayTarget
~~~~text
Structural overlay targets: a wildcard path, a group ref, or an array of
paths / route refs / group refs. A single `route()` ref is a commerce
metering binding instead (see {@link MeterRouteBindingOptions}).
~~~~
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L224`.
~~~~text
export type MeterRoutesOverlayTarget = Exclude;
~~~~
## MeterRoutesTarget
Public export `MeterRoutesTarget`.
Declaration source: `packages/business/dist/types/value-model/registry.d.ts#L27`.
~~~~text
export type MeterRoutesTarget = string | RouteRef | GroupRef | readonly (string | RouteRef | GroupRef)[];
~~~~
## ModelRef
Public export `ModelRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L42`.
~~~~text
export type ModelRef = Readonly<{
kind: "model";
provider: ProviderRef;
key: string;
} & CommerceBrand__7b1b990b7c90<"model">>;
~~~~
## modifier
Public export `modifier`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L43`.
~~~~text
declare const modifier: Readonly<{
multiplier(numerator: number, denominator: number): ModifierBuilder__ae4a0e208869;
}>;
~~~~
## ModifierRef
Public export `ModifierRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L72`.
~~~~text
export type ModifierRef = Readonly<{
kind: "pricing_modifier";
multiplier: ExactRate;
when: DimensionConditionRef;
} & CommerceBrand__7b1b990b7c90<"pricing_modifier">>;
~~~~
## money
Public export `money`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L23`.
~~~~text
declare const money: MoneyFunction__dc62d2738f6d;
~~~~
## MultiplierDisplayRef
Public export `MultiplierDisplayRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L110`.
~~~~text
export type MultiplierDisplayRef = Readonly<{
kind: "multiplier_display";
factor: number;
} & CommerceBrand__7b1b990b7c90<"multiplier_display">>;
~~~~
## MutationClass
Public export `MutationClass`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L2`.
~~~~text
export type MutationClass = "runtime" | "contractual";
~~~~
## normalizeTemporalLimitOptions
~~~~text
Validate and presence-preservingly copy the temporal limit fields. A
resolved quota-length window rejects `token_bucket` up front (mirrors Core
`requireQuotaWindowStrategy`), including custom windows. `cell` carries the
surrounding limit's `capacity` + the dimension's `aggregation` so the
coherence rules Core enforces at publish also fail the local build.
~~~~
Declaration source: `packages/business/dist/types/limit-strategy.d.ts#L31`.
~~~~text
export declare function normalizeTemporalLimitOptions(options: TemporalLimitOptions, context: string, windowSeconds?: number, cell?: {
capacity?: number;
aggregation?: string;
}): TemporalLimitOptions;
~~~~
## plan
Public export `plan`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L10`.
~~~~text
declare const plan: PlanFunction__9b23957fc098;
~~~~
## PlanCapacityJson
~~~~text
A3 — per-request capacity ceiling authored on a plan. Mirrors the contracts
`planCapacitySchema` (ceilings + enforce/track + failMode + T9
`tokenCountSource` + T12 `scope`) WITHOUT the compiler-owned `kind` /
`provenance` discriminators. The core compiler turns it into a
`{ kind: "capacity", …, provenance: "plan" }` ConstraintSpec. At least one
ceiling must be present.
~~~~
Declaration source: `packages/business/dist/types/ir-types.d.ts#L52`.
~~~~text
export type PlanCapacityJson = {
maxInputTokens?: number;
maxOutputTokens?: number;
maxContextTokens?: number;
maxPayloadBytes?: number;
enforcement?: "enforce" | "track";
failMode?: "open" | "closed";
/** T9 — confidence input: `declared` (exact) vs `estimated` (tokenizer). */
tokenCountSource?: "declared" | "estimated";
/** T12 — FORWARD-COMPAT scope hint. */
scope?: LimitScopeJson__215e4eb2041c;
};
~~~~
## PlanKindJson
~~~~text
The DECLARED plan kind (`plan.kind.*`). Carried on the Manifest IR plan
and on the commerce manifest; surfaces read it, nothing infers it.
~~~~
Declaration source: `packages/business/dist/types/ir-types.d.ts#L66`.
~~~~text
export type PlanKindJson = "free" | "flat" | "usage" | "prepaid" | "hybrid" | "trial" | "custom";
~~~~
## PlanKindRef
Public export `PlanKindRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L131`.
~~~~text
export type PlanKindRef = Readonly<{
kind: "plan_kind";
name: Name;
} & CommerceBrand__7b1b990b7c90<`plan_kind:${Name}`>>;
~~~~
## PlanLimitJson
Public export `PlanLimitJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L75`.
~~~~text
export type PlanLimitJson = {
dimension: string;
window: PlanLimitWindowJson__34c3ba9c4506;
capacity: number;
enforcement?: "enforce" | "track";
/** A3 — optional WARN threshold as a fraction of `capacity` in [0, 1]. The
* core compiler threads it onto the emitted rate_limit / quota constraint's
* `warnAt`. Advisory only; never denies. Optional + additive. */
warnAt?: number;
strategy?: "fixed_window" | "sliding_window" | "token_bucket";
bucketCapacity?: number;
refillRatePerSecond?: number;
/** T12 — optional FORWARD-COMPAT scope hint. The core compiler threads it onto
* the emitted rate_limit / quota constraint's `scope`. v1 enforcement is
* unchanged. Optional + additive. */
scope?: LimitScopeJson__215e4eb2041c;
reset_trigger?: "billing_period" | "subscription_start";
};
~~~~
## PlanOptions
~~~~text
`plan()` options — the DECLARED `kind` (required), the five economics
controls, and the structural (entitlement) half: what the plan grants and
how it rations. Usage money is only ever `usagePricing` / `funding` /
`spendPolicy`; there is no per-meter rate on a plan.
~~~~
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L158`.
~~~~text
export type PlanOptions = PlanEconomicsControls__792c3ab4d885 & Readonly<{
kind: PlanKindRef;
}> & Readonly>;
~~~~
## PlanRef
Public export `PlanRef`.
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L37`.
~~~~text
export type PlanRef = DeclarationRef__2a230f417174<"plan">;
~~~~
## PlanSpecJson
Public export `PlanSpecJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L113`.
~~~~text
export type PlanSpecJson = {
key: string;
/** The DECLARED plan kind. Required: a plan is free / flat / usage / …
* because the builder said so, never because of its shape. Usage money
* never lives on the plan; it is commerce-manifest owned. */
kind: PlanKindJson;
description?: string;
details?: string[];
recurring_fee_cents?: number;
billing_interval?: "month" | "year";
trial_days?: number;
max_monthly_spend_cents?: number;
min_monthly_spend_cents?: number;
limits?: PlanLimitJson[];
/** Additive v2 direct grants using stable route ids. */
routeGrants?: string[];
/** Named managed-frontend integrations this plan permits. */
frontendIntegrationGrants?: string[];
resource_limits?: Record;
/** A3 — per-request capacity ceiling. Compiles to a `{ kind: "capacity" }`
* ConstraintSpec the gateway enforces (413 `request_too_large`). */
capacity?: PlanCapacityJson;
overageBehavior?: "block" | "allow_and_bill";
selfServeEnabled?: boolean;
archive?: PlanArchiveJson__452830f6517f;
/** Escape hatch: future fields ride through here. `variants` was removed and is rejected at validation. */
[key: string]: unknown;
};
~~~~
## prepaid
Public export `prepaid`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L49`.
~~~~text
export declare function prepaid(amount: AuthoredMoneyAmount, options?: Readonly<{
topUp?: boolean;
}>): FundingBucketRef;
~~~~
## pricing
Public export `pricing`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L9`.
~~~~text
export declare function pricing(key: string, options: PricingOptions): PricingRef;
~~~~
## PricingBindingRef
Public export `PricingBindingRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L97`.
~~~~text
export type PricingBindingRef = Readonly<{
kind: "pricing_binding";
binding: PricingBindingKind__ff438801f484;
pricing: PricingRef;
version?: number;
} & CommerceBrand__7b1b990b7c90<"pricing_binding">>;
~~~~
## PricingOptions
Public export `PricingOptions`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L92`.
~~~~text
export type PricingOptions = Readonly<{
/** A meter declared with `measures` (priceable). */
meter: MeterRef;
catalog: readonly PricingCatalogAuthoringEntry__8ad295725f7a[];
}>;
~~~~
## PricingRef
Public export `PricingRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L103`.
~~~~text
export type PricingRef = Readonly<{
kind: "pricing";
key: string;
current(): PricingBindingRef;
withContractTerms(): PricingBindingRef;
fixedVersion(version: number): PricingBindingRef;
} & CommerceBrand__7b1b990b7c90<"pricing">>;
~~~~
## promo
Public export `promo`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L52`.
~~~~text
export declare function promo(amount: AuthoredMoneyAmount): FundingBucketRef;
~~~~
## provider
Public export `provider`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L7`.
~~~~text
export declare function provider(key: string): ProviderRef;
~~~~
## ProviderRef
Public export `ProviderRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L47`.
~~~~text
export type ProviderRef = Readonly<{
kind: "provider";
key: string;
model(key: string): ModelRef;
} & CommerceBrand__7b1b990b7c90<"provider">>;
~~~~
## rail
~~~~text
FAR-911 — admission-rail constants for `spendPolicy.rail`.
~~~~
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L64`.
~~~~text
declare const rail: Readonly<{
x402: RailRef;
}>;
~~~~
## RailRef
~~~~text
FAR-911 — admission rail constant (`rail.x402`): every op is funded before it runs.
~~~~
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L127`.
~~~~text
export type RailRef = Readonly<{
kind: "admission_rail";
rail: "x402";
} & CommerceBrand__7b1b990b7c90<"admission_rail">>;
~~~~
## rate
Public export `rate`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L24`.
~~~~text
declare const rate: Readonly<{
perUnit(amount: AuthoredMoneyAmount): UnboundRateRef;
perMillion(amount: AuthoredMoneyAmount): UnboundRateRef;
per(unitCount: number, amount: AuthoredMoneyAmount): UnboundRateRef;
rational(numerator: number, denominator: number, amount: AuthoredMoneyAmount): UnboundRateRef;
/** FAR-909 — graduated tiers: each unit rated by the tier its cumulative window position falls in. */
graduated(tiers: readonly CatalogTierInput[]): UnboundRateRef;
/** FAR-909 — volume (retroactive) tiers: every unit rated at the tier the window total selects at close. */
volume(tiers: readonly CatalogTierInput[]): UnboundRateRef;
/**
* FAR-911 / M5 — bounded backend quote: the backend proposes a per-unit
* rate per report; Core clamps it into `[min, max]` (SDK-created flat
* rates, USD per unit) and dispute-flags out-of-range proposals.
*/
backendQuoted(bounds: Readonly<{
min: UnboundRateRef;
max: UnboundRateRef;
}>): UnboundRateRef;
}>;
~~~~
## referral
Public export `referral`.
Declaration source: `packages/business/dist/types/commerce/authoring.d.ts#L53`.
~~~~text
export declare function referral(amount: AuthoredMoneyAmount): FundingBucketRef;
~~~~
## requests
Public export `requests`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L64`.
~~~~text
export declare function requests(options?: FunctionalRequestMeterOptions): MeterRef;
~~~~
## resource
Public export `resource`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L65`.
~~~~text
export declare function resource(id: string, options?: FunctionalResourceOptions): ResourceRef;
~~~~
## resourceGraphOf
~~~~text
The declaration graph behind a compiled business — every node's URN, kind,
key, `dependsOn` edges, and declaration order.
This is the SDK's internal representation, exposed for inspection: tooling and
agents can reason about what a business declares and how its pieces reference
each other without re-parsing the Manifest IR (where branded refs have already
collapsed to plain strings, and `group()` / `meterRoutes()` have been erased
entirely).
Node payloads are NOT included — `snapshot()` strips `value`, so this is
structure only. The Manifest IR remains the wire format and the only thing
Core consumes.
Returns `null` for a value that is not an authentic `fs.business()` result.
For a deferred (folder-discovered) program the graph is captured when the
manifest materializes, so call `compileBusinessToManifest` first if you need
it before anything else has forced materialization.
~~~~
Declaration source: `packages/business/dist/types/business.d.ts#L133`.
~~~~text
export declare function resourceGraphOf(business: CompiledBusiness__671c1daa22b7): ManifestResourceGraphSnapshot | null;
~~~~
## ResourceLimit
Public export `ResourceLimit`.
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L21`.
~~~~text
export type ResourceLimit = {
readonly kind: "resource_limit";
readonly resource: ResourceRef;
readonly count: number;
};
~~~~
## ResourceRef
Public export `ResourceRef`.
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L38`.
~~~~text
export type ResourceRef = DeclarationRef__2a230f417174<"resource"> & {
/** Maximum number of this persistent resource a subscription may own. */
max(count: number): ResourceLimit;
};
~~~~
## route
Public export `route`.
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L68`.
~~~~text
export declare function route(path: string, operations: RouteOperations): RouteRef;
~~~~
## RouteDefinitionJson
Public export `RouteDefinitionJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L225`.
~~~~text
export type RouteDefinitionJson = {
match: {
method: HttpMethod;
path: string;
};
metering?: {
defaults?: Record;
reports?: string[];
onStatusCodes?: string | number[];
/** The authoritative billable units arrive through the attested
* post-stream reporter. The in-band gateway event remains reserve-only. */
postStreamBilling?: boolean;
};
/** Explicit billing semantics belong to the route, not the metering block;
* contracts reparses route layers strictly enough to strip unknown nested
* keys, which would silently turn authored free operations into inferred
* billable metered operations. */
usagePolicy?: RouteUsagePolicyJson__bd930ebf5713;
unmetered?: boolean;
inheritDefaultMeters?: boolean;
action?: string;
/** Repo-owned custom permissions — stamped by lowering when this route is a
* member of a permission-carrying `fs.group`. The gateway gates the route
* on `:read|write`; metering/grant identity (`action` /
* canonical method+path) is untouched. Never authored directly on a route. */
permissionSubject?: string;
/** Repo-owned custom permissions — gateway-required verb override for this
* operation (`:` instead of the
* method-derived read/write). Present only with `permissionSubject`. */
permissionVerb?: string;
/** BYO-Backend V1 — route→backend binding id. Omitted = the sole / default
* backend (single-backend businesses stay zero-config). */
backend?: string;
/** Canonical per-route auth/timeout/retry policy (FAR-680). Omitted when no
* policy intent was authored — keeps the IR (and irHash) byte-identical. */
policy?: RoutePolicyJson;
/** FAR-684 — canonical per-route rate/quota overrides, normalized from
* `rateLimit` / `quota` sugar. Omitted when the route authored none — keeps
* the IR (and irHash) byte-identical. */
limits?: RouteLimitJson__077809c9f639[];
};
~~~~
## RouteLayerJson
Public export `RouteLayerJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L340`.
~~~~text
export type RouteLayerJson = {
description?: string;
mutation_class?: MutationClass;
cacheProfile?: CacheProfile;
routes: RouteDefinitionJson[];
runtime?: {
rollout_key?: string;
required_flags?: string[];
};
actions?: ActionSpecJson__e01b08e9d184[];
/** Layer-level default backend binding; exact routes may override it. */
backend?: string;
};
~~~~
## RouteOperations
Public export `RouteOperations`.
Declaration source: `packages/business/dist/types/value-model/registry.d.ts#L30`.
~~~~text
export type RouteOperations = Partial>;
~~~~
## RoutePolicyJson
Public export `RoutePolicyJson`.
Declaration source: `packages/business/dist/types/ir-types.d.ts#L206`.
~~~~text
export type RoutePolicyJson = {
authMode?: "public" | "required";
timeoutMs?: number;
idleTimeoutMs?: number;
retry?: RouteRetryPolicyJson;
/** Consumer-principal subject requirement the gateway enforces (403
* `member_subject_required` / `service_subject_required`). OMITTED when the
* route imposes no requirement ("any") — absence-vs-presence is the only
* irHash signal, so a route with no subject requirement stays byte-identical. */
subject?: "member" | "service";
/** CALLABILITY axis — allowlist of credential surfaces the gateway admits.
* OMITTED = all admitted (the implicit "hybrid" state). Emitted as a
* non-empty, deduped, canonically-sorted array so the irHash is stable. */
surfaces?: RouteSurface__b5c8a54b503c[];
/** VISIBILITY axis — true excludes the route from generated docs + grantable
* API-key scopes, and makes a callability deny return 404 (not 403). OMITTED
* when the route is visible, so a listed route stays byte-identical. */
hidden?: boolean;
};
~~~~
## RouteRef
Public export `RouteRef`.
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L35`.
~~~~text
export type RouteRef = DeclarationRef__2a230f417174<"route">;
~~~~
## RouteRetryOnJson
~~~~text
Retry conditions the canonical retry policy fires on (FAR-680).
~~~~
Declaration source: `packages/business/dist/types/ir-types.d.ts#L186`.
~~~~text
export type RouteRetryOnJson = "network" | "5xx";
~~~~
## RouteRetryPolicyJson
~~~~text
CANONICAL retry policy — the explicit IR shape SDK sugar normalizes into
and FAR-689 consumes. Never hand-authored; `retry: true` expands to it.
~~~~
Declaration source: `packages/business/dist/types/ir-types.d.ts#L189`.
~~~~text
export type RouteRetryPolicyJson = {
maxAttempts: number;
retryOn: RouteRetryOnJson[];
backoff: {
base: number;
max: number;
jitter: number;
};
budgetRatio: number;
};
~~~~
## scope
Public export `scope`.
Declaration source: `packages/business/dist/types/value-model/constants.d.ts#L5`.
~~~~text
declare const scope: Readonly<{
subscription: "subscription";
subject: "subject";
}>;
~~~~
## SDK_VERSION
Public export `SDK_VERSION`.
Declaration source: `packages/business/dist/types/version.d.ts#L1`.
~~~~text
declare const SDK_VERSION: string;
~~~~
## surfaces
Public export `surfaces`.
Declaration source: `packages/business/dist/types/value-model/constants.d.ts#L1`.
~~~~text
declare const surfaces: Readonly<{
ui: "ui";
api: "api";
}>;
~~~~
## TemporalLimitOptions
Public export `TemporalLimitOptions`.
Declaration source: `packages/business/dist/types/limit-strategy.d.ts#L2`.
~~~~text
export type TemporalLimitOptions = {
/** Temporal accounting strategy. Omitted preserves fixed-window IR. */
strategy?: LimitStrategy;
/** Token-bucket burst capacity. Valid only with `token_bucket`. */
bucketCapacity?: number;
/** Token-bucket steady-state refill rate in units per second. */
refillRatePerSecond?: number;
};
~~~~
## UnboundRateRef
Public export `UnboundRateRef`.
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L53`.
~~~~text
export type UnboundRateRef = Readonly<{
kind: "catalog_rate";
/** Flat per-unit rate; absent when `tiers` is present. */
rate?: ExactRate;
/** Tiered pricing model (FAR-909); absent when `rate` is present. */
tiers?: ExactCatalogTiers;
/** Bounded backend-quoted rule (FAR-911); absent when `rate`/`tiers` is present. */
quote?: ExactQuoteBounds__e9cf817917c1;
for(measure: MeasureRef, ...selectors: readonly CatalogSelectorRef[]): BoundRateRef;
} & CommerceBrand__7b1b990b7c90<"catalog_rate">>;
~~~~
## validateManifestIr
~~~~text
Validate a candidate envelope against the platform schemas. On success
the returned `ir` is the JSON-normalized ORIGINAL candidate — NOT the
zod-parsed value. The platform treats the business spec as a raw document
because the YAML path never zod-transformed it before storing/compiling.
Validation proves the document is acceptable; the bytes stay the author's.
~~~~
Declaration source: `packages/business/dist/types/validate.d.ts#L18`.
~~~~text
export declare function validateManifestIr(candidate: unknown): ValidationResult;
~~~~
## validatePlanRateLimitCompleteness
~~~~text
BUILD-completeness check (distinct from the structural `validateManifestIr`
above): emit an `error` issue for every plan missing a `limits[]` rule, using
the shared `isCompletePlanRateLimit` predicate so the LOCAL build matches the
PUBLISH gate exactly (`isCompletePlanRateLimit`, apps/core
`services/plan-rate-limit.ts`). A limitless plan thus fails at author/build
time instead of passing locally and only failing at push. The predicate is
the single source of truth in `@farthershore/contracts/plans`, so local ==
publish by construction.
It is run by the build entry (`farthershore build` → src/bin.ts) AFTER a
clean `compileBusinessToManifest`, NOT folded into the platform compiler's
structural validation: the compiler's direct callers (and the golden
IR-equivalence fixtures) compile minimal, intentionally-limitless products
to assert IR byte-identity, and must stay free of the completeness gate.
The hint names the plan + shows the minimal structured `limits` shape.
~~~~
Declaration source: `packages/business/dist/types/validate.d.ts#L37`.
~~~~text
export declare function validatePlanRateLimitCompleteness(ir: ManifestIrDocument): ManifestIssue[];
~~~~
## ValidationResult
Public export `ValidationResult`.
Declaration source: `packages/business/dist/types/validate.d.ts#L3`.
~~~~text
export type ValidationResult = {
ok: true;
ir: ManifestIrDocument;
irHash: string;
} | {
ok: false;
issues: ManifestIssue[];
};
~~~~
## Supporting declarations
These declarations explain types reachable from the public exports above. They are source evidence, not supported package imports. Suffixed names are documentation identifiers that preserve distinct lexical bindings. Standard-library and third-party types (for example Promise, React and Zod) remain external boundaries; their implementation declarations are not expanded here.
### ActionSpecJson__e01b08e9d184
Declaration source: `packages/business/dist/types/ir-types.d.ts#L323`.
~~~~text
export type ActionSpecJson__e01b08e9d184 = {
id: string;
title?: string;
kind: "query" | "mutation";
actorType?: string;
subject?: {
type: string;
from: "header" | "path_param";
name: string;
};
inputSchemaRef?: string;
audit?: "none" | "metadata" | "full";
resource?: {
resource: string;
effect: "create" | "delete";
};
};
~~~~
### AUTHENTIC_PRICE__9ac77f4b80b2
Declaration source: `packages/business/dist/types/value-model/money.d.ts#L3`.
~~~~text
declare const AUTHENTIC_PRICE__9ac77f4b80b2: unique symbol;
~~~~
### AuthoredPrice__fa4522a263d6
Declaration source: `packages/business/dist/types/value-model/money.d.ts#L4`.
~~~~text
export type AuthoredPrice__fa4522a263d6 = Readonly;
~~~~
### BackendOptions__63ffee6039bc
Declaration source: `packages/business/dist/types/backend.d.ts#L8`.
~~~~text
export type BackendOptions__63ffee6039bc = {
/** Human-friendly label (defaults to the id). */
name?: string;
/** Stable slug (defaults to the id). */
slug?: string;
transport?: BackendTransportOptions__6fbea01c7324;
/**
* Per-request gateway->backend signature verification. The JWKS + signer
* keystone is proven, so this is the HARD default: OMITTING it compiles to
* `verification: { required: true }` — the gateway signs every request, the
* backend fails closed, and `ctx.principal` is guaranteed. Set
* `verification: { required: false }` to opt a backend out (escape hatch).
*/
verification?: {
required?: boolean;
};
/** Meter allow-list. Omitted = all business meters allowed. */
meters?: Array;
/** Marks this as the default backend when a business declares more than one. */
default?: boolean;
};
~~~~
### BackendRef__376a47b9b8d4
Declaration source: `packages/business/dist/types/business.d.ts#L110`.
~~~~text
export type BackendRef__376a47b9b8d4 = {
readonly kind: "backend";
readonly key: string;
};
~~~~
### BackendTransportOptions__6fbea01c7324
Declaration source: `packages/business/dist/types/backend.d.ts#L4`.
~~~~text
export type BackendTransportOptions__6fbea01c7324 = {
mode?: BackendTransportModeJson;
runner?: BackendRunnerJson;
};
~~~~
### BusinessBlockJson__a04690090fc8
Declaration source: `packages/business/dist/types/ir-types.d.ts#L383`.
~~~~text
export type BusinessBlockJson__a04690090fc8 = {
visibility?: "public" | "private";
};
~~~~
### BusinessChangeApprovalRiskJson__e8db37f1738a
Declaration source: `packages/business/dist/types/ir-types.d.ts#L363`.
~~~~text
export type BusinessChangeApprovalRiskJson__e8db37f1738a = "safe" | "non_blocking" | "economic_risk" | "blocking";
~~~~
### BusinessCleanupPolicyModeJson__41a5ae49826e
Declaration source: `packages/business/dist/types/ir-types.d.ts#L362`.
~~~~text
export type BusinessCleanupPolicyModeJson__41a5ae49826e = "report" | "pull_request";
~~~~
### BusinessCustomerContextJson__e5b9539124a5
Declaration source: `packages/business/dist/types/ir-types.d.ts#L375`.
~~~~text
export type BusinessCustomerContextJson__e5b9539124a5 = {
context_tokens?: {
enabled?: boolean;
};
portal_auth?: {
strategy: CustomerPortalAuthStrategyJson__c8cec76b5a5a;
};
};
~~~~
### BusinessFunction__7e4f3d5c2657
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L229`.
~~~~text
export type BusinessFunction__7e4f3d5c2657 = (options?: FunctionalBusinessOptions) => CompiledBusiness__671c1daa22b7;
~~~~
### BusinessOptions__bbd5a4a3592b
Declaration source: `packages/business/dist/types/business.d.ts#L35`.
~~~~text
export type BusinessOptions__bbd5a4a3592b = {
visibility?: "public" | "private";
/** API-key header the gateway reads (default x-api-key). */
authHeader?: string;
upstreamAuth?: {
type: "none" | "static_bearer";
token?: string;
};
billOn4xx?: boolean;
/** Business operator policies, e.g. zero-traffic cleanup report/PR mode. */
operatorPolicies?: BusinessPoliciesJson__0d51efe66230;
/** Customer-context controls emitted as business.customer_context. */
customerContext?: {
contextTokens?: BusinessCustomerContextJson__e5b9539124a5["context_tokens"];
customerAuth?: BusinessCustomerContextJson__e5b9539124a5["portal_auth"];
};
billing?: {
applyLimitUpgradesInstantly?: boolean;
/** D4 — how plan changes cascade to EXISTING subscribers. Absent =
* platform default: auto-advance (economic changes reach existing
* subscribers at each one's next renewal — `period_end`; price
* decreases apply immediately). `immediate` on price
* increases / entitlement reductions requires the matching
* `allowImmediate*` consent flag (schema-enforced). Shape mirrors
* `subscriberChangePolicySchema` in @farthershore/contracts. */
subscriberChangePolicy?: SubscriberChangePolicyOptions__b422925d18da;
};
};
~~~~
### BusinessPoliciesJson__0d51efe66230
Declaration source: `packages/business/dist/types/ir-types.d.ts#L364`.
~~~~text
export type BusinessPoliciesJson__0d51efe66230 = {
cleanup?: {
enabled?: boolean;
mode?: BusinessCleanupPolicyModeJson__41a5ae49826e;
};
change_approval?: {
auto_merge_max_risk?: "none" | "safe" | "non_blocking";
require_human_for?: BusinessChangeApprovalRiskJson__e8db37f1738a[];
};
};
~~~~
### CommerceAuthoringSnapshot__60159f2d0d70
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L92`.
~~~~text
export type CommerceAuthoringSnapshot__60159f2d0d70 = Readonly<{
schemaVersion: 1;
measures: readonly Readonly<{
key: string;
}>[];
dimensions: readonly Readonly<{
key: string;
values?: readonly string[];
}>[];
providers: readonly Readonly<{
key: string;
models: readonly string[];
}>[];
meters: readonly Readonly<{
key: string;
measures: readonly string[];
dimensions: readonly string[];
}>[];
pricingPolicies: readonly LoweredPricingPolicy__51372c7aeb1f[];
plans: readonly LoweredPlan__acc0ad17968f[];
meterRouteBindings: readonly Readonly<{
key: string;
/** Served route keys — ONE per declared operation of the bound `route()`
* (`servedRouteKey`: the gateway route id, e.g. `POST /v1/chat`),
* sorted. Never a bare path. */
routeKeys: readonly string[];
reports: readonly string[];
caps?: readonly Readonly<{
measurementKey: string;
maximum: number;
}>[];
maxOutputUnits?: Readonly<{
measurementKey: string;
maximum: number;
adapter?: Readonly<{
knob: "max_output_units";
parser: "json_body_max_output_units_v1";
mutator: "json_body_max_output_units_v1";
}>;
}>;
}>[];
}>;
~~~~
### CommerceBrand__7b1b990b7c90
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L8`.
~~~~text
type CommerceBrand__7b1b990b7c90 = {
readonly [commerceBrand__afc79e100d58]: Name;
};
~~~~
### commerceBrand__afc79e100d58
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L7`.
~~~~text
declare const commerceBrand__afc79e100d58: unique symbol;
~~~~
### CompiledBusiness__671c1daa22b7
Declaration source: `packages/business/dist/types/business.d.ts#L7`.
~~~~text
export type CompiledBusiness__671c1daa22b7 = Readonly<{
readonly kind: "business";
}>;
~~~~
### CountedResourceCountSourceJson__7b01580d882e
Declaration source: `packages/business/dist/types/ir-types.d.ts#L354`.
~~~~text
export type CountedResourceCountSourceJson__7b01580d882e = "reported" | "action_inferred";
~~~~
### CountedResourceJson__90d0b7f16d08
Declaration source: `packages/business/dist/types/ir-types.d.ts#L355`.
~~~~text
export type CountedResourceJson__90d0b7f16d08 = {
name: string;
display?: string;
scope?: CountedResourceScopeJson__5c4e2863f653;
subjectType?: string;
countSource?: CountedResourceCountSourceJson__7b01580d882e;
};
~~~~
### CountedResourceScopeJson__5c4e2863f653
Declaration source: `packages/business/dist/types/ir-types.d.ts#L353`.
~~~~text
export type CountedResourceScopeJson__5c4e2863f653 = "subscription" | "subject";
~~~~
### Currency__c07b80881979
Declaration source: `packages/business/dist/types/value-model/money.d.ts#L2`.
~~~~text
export type Currency__c07b80881979 = "usd";
~~~~
### CustomerPortalAuthStrategyJson__c8cec76b5a5a
Declaration source: `packages/business/dist/types/ir-types.d.ts#L374`.
~~~~text
export type CustomerPortalAuthStrategyJson__c8cec76b5a5a = "clerk" | "test-personas";
~~~~
### DeclarationRef__2a230f417174
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L5`.
~~~~text
export type DeclarationRef__2a230f417174 = {
readonly kind: K;
readonly id: string;
readonly generation: number;
readonly [refBrand__3cba6d1ed1ab]: true;
};
~~~~
### DurationInput__9fbd3d189b84
Declaration source: `packages/business/dist/types/route-policy.d.ts#L4`.
~~~~text
export type DurationInput__9fbd3d189b84 = string | number;
~~~~
### ExactQuoteBounds__e9cf817917c1
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L13`.
~~~~text
export type ExactQuoteBounds__e9cf817917c1 = Readonly<{
kind: "backend_quoted";
min: ExactRate;
max: ExactRate;
}>;
~~~~
### FrontendIntegrationBodyJson__412664c4c97a
Declaration source: `packages/business/dist/types/ir-types.d.ts#L142`.
~~~~text
export type FrontendIntegrationBodyJson__412664c4c97a = {
kind: "none";
} | {
kind: "json";
maxBytes: number;
} | {
kind: "text";
maxBytes: number;
contentTypes: string[];
};
~~~~
### FrontendIntegrationBodyOptions__69124a73fbef
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L36`.
~~~~text
type FrontendIntegrationBodyOptions__69124a73fbef = Extract | (Omit, "contentTypes"> & {
contentTypes: readonly string[];
});
~~~~
### FrontendIntegrationMethodJson__404791a522ea
Declaration source: `packages/business/dist/types/ir-types.d.ts#L141`.
~~~~text
export type FrontendIntegrationMethodJson__404791a522ea = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
~~~~
### FundingBucketKind__eced6b195b95
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L19`.
~~~~text
export type FundingBucketKind__eced6b195b95 = "included" | "prepaid" | "promo" | "referral";
~~~~
### KeyRef__d164043d89c3
Declaration source: `packages/business/dist/types/refs.d.ts#L1`.
~~~~text
export type KeyRef__d164043d89c3 = string | {
key: string;
};
~~~~
### LimitScopeJson__215e4eb2041c
Declaration source: `packages/business/dist/types/ir-types.d.ts#L40`.
~~~~text
export type LimitScopeJson__215e4eb2041c = {
endpoint?: string;
apiKey?: string;
workspace?: string;
region?: string;
};
~~~~
### LoweredCatalogCondition__91bc16beb904
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L21`.
~~~~text
export type LoweredCatalogCondition__91bc16beb904 = Readonly<{
dimensionKey: string;
value: string;
}>;
~~~~
### LoweredCatalogItem__4eb2f217a7cb
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L25`.
~~~~text
export type LoweredCatalogItem__4eb2f217a7cb = Readonly<{
provider: string;
model: string;
}>;
~~~~
### LoweredFundingBucket__ee3504094266
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L53`.
~~~~text
export type LoweredFundingBucket__ee3504094266 = Readonly<{
kind: FundingBucketKind__eced6b195b95;
currency: "usd";
amountMinor: number;
topUp?: boolean;
display?: Readonly<{
kind: "multiplier";
factor: number;
baseMinor: number;
}>;
}>;
~~~~
### LoweredPlan__acc0ad17968f
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L64`.
~~~~text
export type LoweredPlan__acc0ad17968f = Readonly<{
key: string;
kind: PlanKindName__d0fd9bcc38b8;
price?: Readonly<{
currency: "usd";
interval: "month" | "year";
recurringFeeMinor: number;
}>;
usagePricing?: Readonly<{
kind: PricingBindingKind__ff438801f484;
pricingPolicyKey: string;
version?: number;
}>;
funding?: Readonly<{
buckets: readonly LoweredFundingBucket__ee3504094266[];
}>;
lifecycle?: Readonly<{
trialDays: number;
}>;
spendPolicy?: Readonly<{
disclosure?: "opaque" | "transparent";
rail?: "x402";
onExhaustion: Readonly<{
behavior: "block" | "overage";
pricingPolicyKey?: string;
}>;
}>;
}>;
~~~~
### LoweredPricingCatalogEntry__1d1b855a7ba8
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L29`.
~~~~text
export type LoweredPricingCatalogEntry__1d1b855a7ba8 = Readonly<{
key: string;
item?: LoweredCatalogItem__4eb2f217a7cb;
measurementKey: string;
where?: readonly LoweredCatalogCondition__91bc16beb904[];
rate?: ExactRate;
tiers?: ExactCatalogTiers;
quote?: ExactQuoteBounds__e9cf817917c1;
}>;
~~~~
### LoweredPricingModifier__1c95de5a5c16
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L38`.
~~~~text
export type LoweredPricingModifier__1c95de5a5c16 = Readonly<{
multiplier: ExactRate;
when: Readonly<{
dimensionKey: string;
operator: "eq";
value: string;
}>;
}>;
~~~~
### LoweredPricingPolicy__51372c7aeb1f
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L46`.
~~~~text
export type LoweredPricingPolicy__51372c7aeb1f = Readonly<{
key: string;
meterKey: string;
currency: "usd";
catalog: readonly LoweredPricingCatalogEntry__1d1b855a7ba8[];
modifiers: readonly LoweredPricingModifier__1c95de5a5c16[];
}>;
~~~~
### ManifestResourceNode__c5816bda01cb
Declaration source: `packages/business/dist/types/resource-graph.d.ts#L3`.
~~~~text
export type ManifestResourceNode__c5816bda01cb = {
readonly urn: ManifestResourceUrn;
readonly kind: ManifestResourceKind;
readonly key: string;
readonly value: T;
readonly dependsOn: readonly ManifestResourceUrn[];
readonly declarationOrder: number;
};
~~~~
### MeterCost__c48c806cf197
Declaration source: `packages/business/dist/types/route-metering.d.ts#L5`.
~~~~text
export type MeterCost__c48c806cf197 = {
readonly kind: "meter_cost";
readonly meter: string;
readonly value: number;
};
~~~~
### MeterFunction__02c80dd4d01e
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L220`.
~~~~text
export type MeterFunction__02c80dd4d01e = (id: string, options?: MeterOptions) => MeterRef;
~~~~
### MeterOptions__b9495f4ba34f
Declaration source: `packages/business/dist/types/business.d.ts#L63`.
~~~~text
export type MeterOptions__b9495f4ba34f = Omit & {
display?: string;
};
~~~~
### MeterRef__af57686b0ce1
Declaration source: `packages/business/dist/types/route-metering.d.ts#L10`.
~~~~text
export type MeterRef__af57686b0ce1 = {
readonly kind: "meter";
readonly key: string;
/** Fixed gateway-known usage for route/business defaults. */
fixed(value: number): MeterCost__c48c806cf197;
};
~~~~
### MeterRoutesFunction__799abcbb8f6e
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L225`.
~~~~text
export type MeterRoutesFunction__799abcbb8f6e = {
(key: string, target: RouteRef, options: MeterRouteBindingOptions): void;
(key: string, target: MeterRoutesOverlayTarget | GroupRef, options: FunctionalMeterRoutesOptions): void;
};
~~~~
### ModifierBuilder__ae4a0e208869
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L77`.
~~~~text
export type ModifierBuilder__ae4a0e208869 = Readonly<{
when(condition: DimensionConditionRef): ModifierRef;
} & CommerceBrand__7b1b990b7c90<"modifier_builder">>;
~~~~
### MoneyAmount__51964157493a
Declaration source: `packages/business/dist/types/value-model/money.d.ts#L11`.
~~~~text
export type MoneyAmount__51964157493a = Readonly<{
currency: Currency__c07b80881979;
minor: number;
monthly(): MoneyPrice__4792013e3b32;
yearly(): MoneyPrice__4792013e3b32;
}>;
~~~~
### MoneyFunction__dc62d2738f6d
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L231`.
~~~~text
export type MoneyFunction__dc62d2738f6d = Readonly<{
usd(major: number): AuthoredMoneyAmount;
}>;
~~~~
### MoneyPrice__4792013e3b32
Declaration source: `packages/business/dist/types/value-model/money.d.ts#L7`.
~~~~text
export type MoneyPrice__4792013e3b32 = Readonly;
~~~~
### PermissionSubjectJson__d124b4a2259d
Declaration source: `packages/business/dist/types/ir-types.d.ts#L389`.
~~~~text
export type PermissionSubjectJson__d124b4a2259d = {
subject: string;
verbs: string[];
escalatory?: string[];
description?: string;
};
~~~~
### PlanArchiveJson__452830f6517f
Declaration source: `packages/business/dist/types/ir-types.d.ts#L108`.
~~~~text
export type PlanArchiveJson__452830f6517f = {
at?: string;
transitionTo?: string;
strategy?: "auto" | "explicit" | "block";
};
~~~~
### PlanEconomicsControls__792c3ab4d885
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L135`.
~~~~text
type PlanEconomicsControls__792c3ab4d885 = Readonly<{
/** `money.usd(n).monthly()` / `.yearly()`; `free()` only on `kind: free`. */
price?: MoneyPrice__4792013e3b32 | AuthoredPrice__fa4522a263d6;
usagePricing?: PricingBindingRef;
funding?: {
buckets: readonly FundingBucketRef[];
};
lifecycle?: {
trialDays: number;
};
spendPolicy?: {
disclosure?: DisclosureRef;
/** FAR-911 — `rail.x402`: compile under the prepaid admission matrix rows. */
rail?: RailRef;
onExhaustion: ExhaustionRef;
};
}>;
~~~~
### PlanFunction__9b23957fc098
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L208`.
~~~~text
export type PlanFunction__9b23957fc098 = {
(id: string, options: PlanOptions): PlanRef;
readonly kind: Readonly<{
free: PlanKindRef<"free">;
flat: PlanKindRef<"flat">;
usage: PlanKindRef<"usage">;
prepaid: PlanKindRef<"prepaid">;
hybrid: PlanKindRef<"hybrid">;
trial: PlanKindRef<"trial">;
custom: PlanKindRef<"custom">;
}>;
};
~~~~
### PlanKindName__d0fd9bcc38b8
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L20`.
~~~~text
export type PlanKindName__d0fd9bcc38b8 = "free" | "flat" | "usage" | "prepaid" | "hybrid" | "trial" | "custom";
~~~~
### PlanLimitWindowJson__34c3ba9c4506
Declaration source: `packages/business/dist/types/ir-types.d.ts#L67`.
~~~~text
export type PlanLimitWindowJson__34c3ba9c4506 = {
type: "named";
name: "second" | "minute" | "hour" | "day" | "week" | "month";
} | {
type: "custom";
seconds: number;
label?: string;
};
~~~~
### PlanOptions__0e8a3d72b1c6
Declaration source: `packages/business/dist/types/business.d.ts#L75`.
~~~~text
export type PlanOptions__0e8a3d72b1c6 = {
/** The DECLARED plan kind (`plan.kind.*`); never inferred from shape. */
kind: PlanKindJson;
name: string;
description?: string;
details?: string[];
/** The normalized recurring price spec. Omit for a 0-fee plan. */
price?: PriceSpec__1288e85e83b9;
trialDays?: number;
maxMonthlySpendCents?: number;
minMonthlySpendCents?: number;
limits?: PlanLimitJson[];
/** Direct grants using stable route ids. */
routeGrants?: string[];
/** Named managed-frontend integrations this plan permits. */
frontendIntegrationGrants?: string[];
resourceLimits?: Record;
/** A3 — per-request capacity ceiling (normalized PlanCapacityJson). */
capacity?: PlanCapacityJson;
overageBehavior?: "block" | "allow_and_bill";
selfServeEnabled?: boolean;
archive?: {
at?: string;
transitionTo?: string;
strategy?: "auto" | "explicit" | "block";
};
};
~~~~
### PriceSpec__1288e85e83b9
Declaration source: `packages/business/dist/types/price.d.ts#L9`.
~~~~text
export type PriceSpec__1288e85e83b9 = {
recurring_fee_cents: number;
billing_interval: "month" | "year";
};
~~~~
### PricingBindingKind__ff438801f484
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L18`.
~~~~text
export type PricingBindingKind__ff438801f484 = "current" | "current_with_contract_terms" | "fixed_version";
~~~~
### PricingCatalogAuthoringEntry__8ad295725f7a
Declaration source: `packages/business/dist/types/commerce/types.d.ts#L91`.
~~~~text
export type PricingCatalogAuthoringEntry__8ad295725f7a = UnboundRateRef | BoundRateRef | ModifierRef;
~~~~
### RawRetryPolicyInput__d96fd6958073
Declaration source: `packages/business/dist/types/route-policy.d.ts#L7`.
~~~~text
export type RawRetryPolicyInput__d96fd6958073 = RouteRetryPolicyJson;
~~~~
### refBrand__3cba6d1ed1ab
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L4`.
~~~~text
declare const refBrand__3cba6d1ed1ab: unique symbol;
~~~~
### RefKind__6d76109c9bb1
Declaration source: `packages/business/dist/types/value-model/refs.d.ts#L3`.
~~~~text
export type RefKind__6d76109c9bb1 = "meter" | "route" | "group" | "plan" | "resource" | "backend" | "frontend_integration";
~~~~
### RequestMeterOptions__7ec5d6047951
Declaration source: `packages/business/dist/types/business.d.ts#L66`.
~~~~text
export type RequestMeterOptions__7ec5d6047951 = Partial;
~~~~
### ResourceOptions__04a2cc1fd6ee
Declaration source: `packages/business/dist/types/business.d.ts#L74`.
~~~~text
export type ResourceOptions__04a2cc1fd6ee = Omit;
~~~~
### RouteAuthoredUsagePolicyJson__622a0aa384fd
Declaration source: `packages/business/dist/types/ir-types.d.ts#L273`.
~~~~text
export type RouteAuthoredUsagePolicyJson__622a0aa384fd = {
preset: RouteUsagePolicyPresetJson__0df8c3d02afc;
providerCostTracked?: RouteProviderCostTrackedJson__69509ac0044b;
chargeableOutcomes?: RouteChargeableOutcomesJson__ba3a3aead9e9;
};
~~~~
### RouteChargeableOutcomesJson__ba3a3aead9e9
Declaration source: `packages/business/dist/types/ir-types.d.ts#L267`.
~~~~text
export type RouteChargeableOutcomesJson__ba3a3aead9e9 = "success_only" | "success_and_partial" | "attempted" | "trusted_actual_usage_only";
~~~~
### RouteLimitAuthoring__ae39586db789
Declaration source: `packages/business/dist/types/route-metering.d.ts#L34`.
~~~~text
export type RouteLimitAuthoring__ae39586db789 = string | RouteLimitInput__250d9325480a;
~~~~
### RouteLimitInput__250d9325480a
Declaration source: `packages/business/dist/types/route-metering.d.ts#L22`.
~~~~text
export type RouteLimitInput__250d9325480a = TemporalLimitOptions & {
/** Capacity in the dimension's native unit. */
limit: number;
/** Window the cap resets on. */
per: RouteLimitWindowName__a0705d660e11;
/** Metered dimension. Defaults to `requests` (native per-call count). */
dimension?: string;
/** Record without denying past capacity (default: false → enforce). */
track?: boolean;
};
~~~~
### RouteLimitJson__077809c9f639
Declaration source: `packages/business/dist/types/ir-types.d.ts#L99`.
~~~~text
export type RouteLimitJson__077809c9f639 = {
dimension: string;
window: PlanLimitWindowJson__34c3ba9c4506;
capacity: number;
enforcement?: "enforce" | "track";
strategy?: "fixed_window" | "sliding_window" | "token_bucket";
bucketCapacity?: number;
refillRatePerSecond?: number;
};
~~~~
### RouteLimitWindowName__a0705d660e11
Declaration source: `packages/business/dist/types/route-metering.d.ts#L19`.
~~~~text
export type RouteLimitWindowName__a0705d660e11 = "second" | "minute" | "hour" | "day" | "week" | "month";
~~~~
### RouteMeteringOptions__00c1790b3d64
Declaration source: `packages/business/dist/types/route-metering.d.ts#L52`.
~~~~text
export type RouteMeteringOptions__00c1790b3d64 = {
/**
* Dynamic meter keys the upstream may report with @farthershore/backend.
*/
reports?: string | MeterRef__af57686b0ce1 | Array;
/** Fixed gateway-known route costs. */
costs?: MeterCost__c48c806cf197 | Array;
/** Override the default successful-response range. */
onStatusCodes?: string | number[];
/** Usage-policy preset authoring. The core compiler expands this to compiled UsagePolicy. */
usagePolicy?: RouteAuthoredUsagePolicyJson__622a0aa384fd | RouteUsagePolicyJson__bd930ebf5713;
/** Declare that billable units normally arrive after the response stream.
* Fixed costs are admitted before the call; actual reports settle later. */
postStreamBilling?: boolean;
/** FAR-684 — short-window velocity cap for THIS route, e.g.
* `rateLimit: "10/min"`. Normalized to a route-scoped `rate_limit`
* ConstraintSpec the gateway enforces (429) only on this route. */
rateLimit?: RouteLimitAuthoring__ae39586db789;
/** FAR-684 — longer-period budget for THIS route, e.g. `quota: "10000/month"`.
* Normalized to a route-scoped `quota` ConstraintSpec the gateway enforces
* (402) only on this route. */
quota?: RouteLimitAuthoring__ae39586db789;
/** FAR-684 — ADVANCED escape hatch: canonical route-scoped limits used
* verbatim (the codegen round-trip path). Wins over `rateLimit`/`quota`. */
limits?: RouteLimitJson__077809c9f639[];
};
~~~~
### RouteOperationOptions__902951868956
Declaration source: `packages/business/dist/types/value-model/registry.d.ts#L29`.
~~~~text
export type RouteOperationOptions__902951868956 = FunctionalRouteOptions;
~~~~
### RouteOptions__8ba341feca83
Declaration source: `packages/business/dist/types/business.d.ts#L67`.
~~~~text
export type RouteOptions__8ba341feca83 = RouteMeteringOptions__00c1790b3d64 & RoutePolicyOptions__ed332c838a9c & {
action?: string;
/** BYO-Backend V1 — bind this route to a declared backend. Omitted = the
* business's sole / default backend (single-backend products stay
* zero-config). */
backend?: string | BackendRef__376a47b9b8d4;
};
~~~~
### RoutePolicyOptions__ed332c838a9c
Declaration source: `packages/business/dist/types/route-policy.d.ts#L12`.
~~~~text
export type RoutePolicyOptions__ed332c838a9c = {
/** Sugar for `policy.authMode: "public"`. Omitted/`false` = required. */
public?: boolean;
/** Overall request timeout: `"30s"` | `30_000` (ms) → `policy.timeoutMs`. */
timeout?: DurationInput__9fbd3d189b84;
/** Idle/TTFB timeout: `"5s"` | `5_000` (ms) → `policy.idleTimeoutMs`. */
idleTimeout?: DurationInput__9fbd3d189b84;
/**
* `true` → the platform-canonical safe retry default; a full object → the
* advanced escape hatch (used verbatim); `false`/omitted → no retry.
*/
retry?: boolean | RawRetryPolicyInput__d96fd6958073;
/**
* Consumer-principal subject requirement (D4). `requireMember` forces a
* MEMBER subject (a session or a personal key) → `policy.subject: "member"`;
* `requireService` forces a SERVICE subject (an org-owned service-account
* key) → `policy.subject: "service"`. Omit both = no requirement ("any").
* Mutually exclusive, and incompatible with `public` (a public route carries
* no credential, so it can't require a subject).
*/
requireMember?: true;
requireService?: true;
/**
* CALLABILITY — the allowlist of credential surfaces the gateway admits, e.g.
* `surfaces: ["api"]` (key-only) or `["ui"]` (portal-only).
* Omitted = all admitted ("hybrid" — the common case). Authored order is
* irrelevant (deduped + canonically sorted at compile). Refused on a `public`
* route (no credential to classify). A route NOT admitting `api` is
* automatically absent from the generated API reference and un-grantable to
* API keys — a key can't call it, so listing it would mislead.
*/
surfaces?: RouteSurface__b5c8a54b503c[];
/**
* VISIBILITY — `true` also 404s a wrong-surface caller (instead of an
* informative 403) and force-hides an OTHERWISE-api-callable route from docs +
* scopes (a private beta). Redundant on a non-api route (already hidden by its
* surfaces). Allowed on a `public` route (visibility isn't a credential
* concern). Omitted = visible.
*/
hidden?: boolean;
/**
* ADVANCED escape hatch — the raw canonical policy object, merged UNDER the
* sugar fields above (sugar wins on conflict). Prefer the one-liner intent
* forms; reach for this only when you need a knob the sugar doesn't expose.
*/
policy?: RoutePolicyJson;
};
~~~~
### RouteProviderCostTrackedJson__69509ac0044b
Declaration source: `packages/business/dist/types/ir-types.d.ts#L268`.
~~~~text
export type RouteProviderCostTrackedJson__69509ac0044b = boolean | {
providerCostTracked?: boolean;
provider?: string;
model?: string;
};
~~~~
### RouteSurface__b5c8a54b503c
Declaration source: `packages/business/dist/types/ir-types.d.ts#L205`.
~~~~text
export type RouteSurface__b5c8a54b503c = "ui" | "api" | "agent" | "webhook" | "cli";
~~~~
### RouteTrafficClassJson__0fcb4c4bf86d
Declaration source: `packages/business/dist/types/ir-types.d.ts#L278`.
~~~~text
export type RouteTrafficClassJson__0fcb4c4bf86d = "customer_operation" | "control_plane" | "admin_internal" | "background_job" | "webhook" | "healthcheck" | "unclassified";
~~~~
### RouteUsagePolicyJson__bd930ebf5713
Declaration source: `packages/business/dist/types/ir-types.d.ts#L281`.
~~~~text
export type RouteUsagePolicyJson__bd930ebf5713 = {
trafficClass: RouteTrafficClassJson__0fcb4c4bf86d;
policySource: RouteUsagePolicySourceJson__956c2e9dd016;
limitParticipation: {
profile: RouteUsagePolicyLimitProfileJson__5436ffa8006e;
pools?: string[];
scopes?: Array>;
metrics?: string[];
};
customerBillingPolicy: {
customerBillable: boolean;
meterKey?: string;
chargeableOutcomes: RouteChargeableOutcomesJson__ba3a3aead9e9;
invoiceBehavior?: string;
};
providerCostPolicy: {
providerCostTracked: boolean;
declaredProviderHint?: string;
declaredModelHint?: string;
};
};
~~~~
### RouteUsagePolicyLimitProfileJson__5436ffa8006e
Declaration source: `packages/business/dist/types/ir-types.d.ts#L280`.
~~~~text
export type RouteUsagePolicyLimitProfileJson__5436ffa8006e = "customer_usage" | "business_capacity" | "control_plane" | "admin_internal" | "platform_abuse_only" | "healthcheck" | "none";
~~~~
### RouteUsagePolicyPresetJson__0df8c3d02afc
Declaration source: `packages/business/dist/types/ir-types.d.ts#L266`.
~~~~text
export type RouteUsagePolicyPresetJson__0df8c3d02afc = "billable_operation" | "free_operation" | "control_plane" | "admin_internal" | "webhook" | "background_job" | "healthcheck";
~~~~
### RouteUsagePolicySourceJson__956c2e9dd016
Declaration source: `packages/business/dist/types/ir-types.d.ts#L279`.
~~~~text
export type RouteUsagePolicySourceJson__956c2e9dd016 = "declared" | "inherited" | "defaulted" | "inferred";
~~~~
### StructuralPlanOptions__f7824ff3bdd0
Declaration source: `packages/business/dist/types/value-model/declarations.d.ts#L26`.
~~~~text
export type StructuralPlanOptions__f7824ff3bdd0 = Pick & {
kind: PlanKindJson;
name?: string;
price?: AuthoredPrice__fa4522a263d6;
grants?: readonly (RouteRef | GroupRef | FrontendIntegrationRef)[];
limits?: readonly (MeterLimit | ResourceLimit)[];
archive?: Omit & {
transitionTo?: PlanRef;
};
};
~~~~
### SubscriberChangeActionOption__99f1a181d6ed
Declaration source: `packages/business/dist/types/business.d.ts#L15`.
~~~~text
export type SubscriberChangeActionOption__99f1a181d6ed = "immediate" | "period_end";
~~~~
### SubscriberChangePolicyOptions__b422925d18da
Declaration source: `packages/business/dist/types/business.d.ts#L19`.
~~~~text
export type SubscriberChangePolicyOptions__b422925d18da = {
default?: SubscriberChangeActionOption__99f1a181d6ed;
when?: {
price_increase?: SubscriberChangeActionOption__99f1a181d6ed;
price_decrease?: SubscriberChangeActionOption__99f1a181d6ed;
route_grant_added?: SubscriberChangeActionOption__99f1a181d6ed;
route_grant_removed?: SubscriberChangeActionOption__99f1a181d6ed;
limit_increased?: SubscriberChangeActionOption__99f1a181d6ed;
limit_reduced?: SubscriberChangeActionOption__99f1a181d6ed;
credit_increased?: SubscriberChangeActionOption__99f1a181d6ed;
credit_reduced?: SubscriberChangeActionOption__99f1a181d6ed;
rating_changed?: SubscriberChangeActionOption__99f1a181d6ed;
};
allowImmediatePriceIncrease?: boolean;
allowImmediateEntitlementReduction?: boolean;
};
~~~~
---
# @farthershore/business/codegen exports
Canonical URL: https://docs.farthershore.com/generated/business-sdk/codegen
{/* Generated by generate-reference.ts. Do not edit. */}
Import from `@farthershore/business/codegen`. This reference is extracted from the published declaration surface for version **3.2.0**. Read the collection's guides for workflows, prerequisites and failure handling.
## irToValueModelSource
~~~~text
Generate the canonical functional Business SDK program for a value-model IR.
Every declaration is assigned one stable identifier and all relationships
reuse those branded refs; no stringly-typed cross-reference is emitted.
~~~~
Declaration source: `packages/business/dist/types/codegen/index.d.ts#L7`.
~~~~text
export declare function irToValueModelSource(ir: ManifestIrDocument__3d04599c0d50): string;
~~~~
## Supporting declarations
These declarations explain types reachable from the public exports above. They are source evidence, not supported package imports. Suffixed names are documentation identifiers that preserve distinct lexical bindings. Standard-library and third-party types (for example Promise, React and Zod) remain external boundaries; their implementation declarations are not expanded here.
### ActionSpecJson__e01b08e9d184
Declaration source: `packages/business/dist/types/ir-types.d.ts#L323`.
~~~~text
export type ActionSpecJson__e01b08e9d184 = {
id: string;
title?: string;
kind: "query" | "mutation";
actorType?: string;
subject?: {
type: string;
from: "header" | "path_param";
name: string;
};
inputSchemaRef?: string;
audit?: "none" | "metadata" | "full";
resource?: {
resource: string;
effect: "create" | "delete";
};
};
~~~~
### BackendDefinitionJson__10a9a390d24d
Declaration source: `packages/business/dist/types/ir-types.d.ts#L304`.
~~~~text
export type BackendDefinitionJson__10a9a390d24d = {
name?: string;
slug?: string;
transport?: {
mode?: BackendTransportModeJson__ce5a0a6d1c03;
runner?: BackendRunnerJson__0e11128e42f5;
};
/**
* Gateway->backend request signing. Emitted for every declared backend; the
* builder helper defaults omission to `{ required: true }` (HARD default).
*/
verification?: {
required?: boolean;
};
/** Meter allow-list. Omitted = all business meters allowed. */
meters?: string[];
/** Marks the default backend when a business declares more than one. */
default?: boolean;
};
~~~~
### BackendRunnerJson__0e11128e42f5
Declaration source: `packages/business/dist/types/ir-types.d.ts#L303`.
~~~~text
export type BackendRunnerJson__0e11128e42f5 = "embedded" | "sidecar";
~~~~
### BackendTransportModeJson__ce5a0a6d1c03
Declaration source: `packages/business/dist/types/ir-types.d.ts#L302`.
~~~~text
export type BackendTransportModeJson__ce5a0a6d1c03 = "direct" | "tunnel";
~~~~
### BusinessBlockJson__a04690090fc8
Declaration source: `packages/business/dist/types/ir-types.d.ts#L383`.
~~~~text
export type BusinessBlockJson__a04690090fc8 = {
visibility?: "public" | "private";
};
~~~~
### BusinessChangeApprovalRiskJson__e8db37f1738a
Declaration source: `packages/business/dist/types/ir-types.d.ts#L363`.
~~~~text
export type BusinessChangeApprovalRiskJson__e8db37f1738a = "safe" | "non_blocking" | "economic_risk" | "blocking";
~~~~
### BusinessCleanupPolicyModeJson__41a5ae49826e
Declaration source: `packages/business/dist/types/ir-types.d.ts#L362`.
~~~~text
export type BusinessCleanupPolicyModeJson__41a5ae49826e = "report" | "pull_request";
~~~~
### BusinessCustomerContextJson__e5b9539124a5
Declaration source: `packages/business/dist/types/ir-types.d.ts#L375`.
~~~~text
export type BusinessCustomerContextJson__e5b9539124a5 = {
context_tokens?: {
enabled?: boolean;
};
portal_auth?: {
strategy: CustomerPortalAuthStrategyJson__c8cec76b5a5a;
};
};
~~~~
### BusinessPoliciesJson__0d51efe66230
Declaration source: `packages/business/dist/types/ir-types.d.ts#L364`.
~~~~text
export type BusinessPoliciesJson__0d51efe66230 = {
cleanup?: {
enabled?: boolean;
mode?: BusinessCleanupPolicyModeJson__41a5ae49826e;
};
change_approval?: {
auto_merge_max_risk?: "none" | "safe" | "non_blocking";
require_human_for?: BusinessChangeApprovalRiskJson__e8db37f1738a[];
};
};
~~~~
### BusinessSpecJson__7ce46a3ce9fb
Declaration source: `packages/business/dist/types/ir-types.d.ts#L395`.
~~~~text
export type BusinessSpecJson__7ce46a3ce9fb = {
business: BusinessBlockJson__a04690090fc8;
gateway?: {
authHeader?: string;
upstreamAuth?: {
type: "none" | "static_bearer";
token?: string;
};
};
metering?: {
meters?: MeterDefinitionJson__a744b09a0a2a[];
billOn4xx?: boolean;
};
/** BYO-Backend V1 — first-class backend declarations keyed by backend id.
* Emitted only when at least one backend is declared (so single-backend /
* no-backend products keep their pre-BYOB irHash). */
backend?: Record;
resources?: CountedResourceJson__90d0b7f16d08[];
/** Repo-owned custom permission subjects. Emitted (sorted by subject) only
* when at least one permission-carrying group is declared, so businesses
* without permission groups keep their pre-cutover irHash. */
permissions?: PermissionSubjectJson__d124b4a2259d[];
policies?: BusinessPoliciesJson__0d51efe66230;
customer_context?: BusinessCustomerContextJson__e5b9539124a5;
plans?: PlanSpecJson__47392a63c977[];
/** Advanced/internal platform-schema blocks (usage, billing,
* environments, lifecycle, ephemeral, …) are validated by the platform
* schema but are not authorable through the functional SDK. */
[key: string]: unknown;
};
~~~~
### CacheProfile__876a201699a6
Declaration source: `packages/business/dist/types/ir-types.d.ts#L3`.
~~~~text
export type CacheProfile__876a201699a6 = "long" | "short" | "blocking";
~~~~
### CommerceAuthoringSnapshot__60159f2d0d70
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L92`.
~~~~text
export type CommerceAuthoringSnapshot__60159f2d0d70 = Readonly<{
schemaVersion: 1;
measures: readonly Readonly<{
key: string;
}>[];
dimensions: readonly Readonly<{
key: string;
values?: readonly string[];
}>[];
providers: readonly Readonly<{
key: string;
models: readonly string[];
}>[];
meters: readonly Readonly<{
key: string;
measures: readonly string[];
dimensions: readonly string[];
}>[];
pricingPolicies: readonly LoweredPricingPolicy__51372c7aeb1f[];
plans: readonly LoweredPlan__acc0ad17968f[];
meterRouteBindings: readonly Readonly<{
key: string;
/** Served route keys — ONE per declared operation of the bound `route()`
* (`servedRouteKey`: the gateway route id, e.g. `POST /v1/chat`),
* sorted. Never a bare path. */
routeKeys: readonly string[];
reports: readonly string[];
caps?: readonly Readonly<{
measurementKey: string;
maximum: number;
}>[];
maxOutputUnits?: Readonly<{
measurementKey: string;
maximum: number;
adapter?: Readonly<{
knob: "max_output_units";
parser: "json_body_max_output_units_v1";
mutator: "json_body_max_output_units_v1";
}>;
}>;
}>[];
}>;
~~~~
### CountedResourceCountSourceJson__7b01580d882e
Declaration source: `packages/business/dist/types/ir-types.d.ts#L354`.
~~~~text
export type CountedResourceCountSourceJson__7b01580d882e = "reported" | "action_inferred";
~~~~
### CountedResourceJson__90d0b7f16d08
Declaration source: `packages/business/dist/types/ir-types.d.ts#L355`.
~~~~text
export type CountedResourceJson__90d0b7f16d08 = {
name: string;
display?: string;
scope?: CountedResourceScopeJson__5c4e2863f653;
subjectType?: string;
countSource?: CountedResourceCountSourceJson__7b01580d882e;
};
~~~~
### CountedResourceScopeJson__5c4e2863f653
Declaration source: `packages/business/dist/types/ir-types.d.ts#L353`.
~~~~text
export type CountedResourceScopeJson__5c4e2863f653 = "subscription" | "subject";
~~~~
### CustomerPortalAuthStrategyJson__c8cec76b5a5a
Declaration source: `packages/business/dist/types/ir-types.d.ts#L374`.
~~~~text
export type CustomerPortalAuthStrategyJson__c8cec76b5a5a = "clerk" | "test-personas";
~~~~
### ExactCatalogTiers__367557c966f6
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L5`.
~~~~text
export type ExactCatalogTiers__367557c966f6 = Readonly<{
strategy: "graduated" | "volume_retroactive";
tiers: readonly Readonly<{
upTo: string | null;
rate: ExactRate__d20ab6d469eb;
}>[];
}>;
~~~~
### ExactQuoteBounds__e9cf817917c1
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L13`.
~~~~text
export type ExactQuoteBounds__e9cf817917c1 = Readonly<{
kind: "backend_quoted";
min: ExactRate__d20ab6d469eb;
max: ExactRate__d20ab6d469eb;
}>;
~~~~
### ExactRate__d20ab6d469eb
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L1`.
~~~~text
export type ExactRate__d20ab6d469eb = Readonly<{
num: string;
den: string;
}>;
~~~~
### FrontendIntegrationBodyJson__412664c4c97a
Declaration source: `packages/business/dist/types/ir-types.d.ts#L142`.
~~~~text
export type FrontendIntegrationBodyJson__412664c4c97a = {
kind: "none";
} | {
kind: "json";
maxBytes: number;
} | {
kind: "text";
maxBytes: number;
contentTypes: string[];
};
~~~~
### FrontendIntegrationInjectionJson__0aad8e29552d
Declaration source: `packages/business/dist/types/ir-types.d.ts#L159`.
~~~~text
export type FrontendIntegrationInjectionJson__0aad8e29552d = {
secretRef: string;
location: "header";
name: string;
template: "{value}" | "Bearer {value}";
} | {
secretRef: string;
location: "query";
name: string;
};
~~~~
### FrontendIntegrationMethodJson__404791a522ea
Declaration source: `packages/business/dist/types/ir-types.d.ts#L141`.
~~~~text
export type FrontendIntegrationMethodJson__404791a522ea = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
~~~~
### FrontendIntegrationOperationJson__e8605b7d664b
Declaration source: `packages/business/dist/types/ir-types.d.ts#L152`.
~~~~text
export type FrontendIntegrationOperationJson__e8605b7d664b = {
method: FrontendIntegrationMethodJson__404791a522ea;
path: string;
headers: string[];
query: string[];
body: FrontendIntegrationBodyJson__412664c4c97a;
};
~~~~
### FrontendIntegrationSpecJson__46198141209a
Declaration source: `packages/business/dist/types/ir-types.d.ts#L170`.
~~~~text
export type FrontendIntegrationSpecJson__46198141209a = {
id: string;
upstream: string;
request: {
operations: FrontendIntegrationOperationJson__e8605b7d664b[];
};
injection: FrontendIntegrationInjectionJson__0aad8e29552d;
response: {
kind: "json";
contentTypes: string[];
maxBytes: number;
jsonPointers: string[];
responseHeaders: string[];
};
};
~~~~
### FundingBucketKind__eced6b195b95
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L19`.
~~~~text
export type FundingBucketKind__eced6b195b95 = "included" | "prepaid" | "promo" | "referral";
~~~~
### HttpMethod__7ba25e3979be
Declaration source: `packages/business/dist/types/ir-types.d.ts#L1`.
~~~~text
export type HttpMethod__7ba25e3979be = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
~~~~
### LimitScopeJson__215e4eb2041c
Declaration source: `packages/business/dist/types/ir-types.d.ts#L40`.
~~~~text
export type LimitScopeJson__215e4eb2041c = {
endpoint?: string;
apiKey?: string;
workspace?: string;
region?: string;
};
~~~~
### LoweredCatalogCondition__91bc16beb904
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L21`.
~~~~text
export type LoweredCatalogCondition__91bc16beb904 = Readonly<{
dimensionKey: string;
value: string;
}>;
~~~~
### LoweredCatalogItem__4eb2f217a7cb
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L25`.
~~~~text
export type LoweredCatalogItem__4eb2f217a7cb = Readonly<{
provider: string;
model: string;
}>;
~~~~
### LoweredFundingBucket__ee3504094266
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L53`.
~~~~text
export type LoweredFundingBucket__ee3504094266 = Readonly<{
kind: FundingBucketKind__eced6b195b95;
currency: "usd";
amountMinor: number;
topUp?: boolean;
display?: Readonly<{
kind: "multiplier";
factor: number;
baseMinor: number;
}>;
}>;
~~~~
### LoweredPlan__acc0ad17968f
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L64`.
~~~~text
export type LoweredPlan__acc0ad17968f = Readonly<{
key: string;
kind: PlanKindName__d0fd9bcc38b8;
price?: Readonly<{
currency: "usd";
interval: "month" | "year";
recurringFeeMinor: number;
}>;
usagePricing?: Readonly<{
kind: PricingBindingKind__ff438801f484;
pricingPolicyKey: string;
version?: number;
}>;
funding?: Readonly<{
buckets: readonly LoweredFundingBucket__ee3504094266[];
}>;
lifecycle?: Readonly<{
trialDays: number;
}>;
spendPolicy?: Readonly<{
disclosure?: "opaque" | "transparent";
rail?: "x402";
onExhaustion: Readonly<{
behavior: "block" | "overage";
pricingPolicyKey?: string;
}>;
}>;
}>;
~~~~
### LoweredPricingCatalogEntry__1d1b855a7ba8
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L29`.
~~~~text
export type LoweredPricingCatalogEntry__1d1b855a7ba8 = Readonly<{
key: string;
item?: LoweredCatalogItem__4eb2f217a7cb;
measurementKey: string;
where?: readonly LoweredCatalogCondition__91bc16beb904[];
rate?: ExactRate__d20ab6d469eb;
tiers?: ExactCatalogTiers__367557c966f6;
quote?: ExactQuoteBounds__e9cf817917c1;
}>;
~~~~
### LoweredPricingModifier__1c95de5a5c16
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L38`.
~~~~text
export type LoweredPricingModifier__1c95de5a5c16 = Readonly<{
multiplier: ExactRate__d20ab6d469eb;
when: Readonly<{
dimensionKey: string;
operator: "eq";
value: string;
}>;
}>;
~~~~
### LoweredPricingPolicy__51372c7aeb1f
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L46`.
~~~~text
export type LoweredPricingPolicy__51372c7aeb1f = Readonly<{
key: string;
meterKey: string;
currency: "usd";
catalog: readonly LoweredPricingCatalogEntry__1d1b855a7ba8[];
modifiers: readonly LoweredPricingModifier__1c95de5a5c16[];
}>;
~~~~
### ManifestIrDocument__3d04599c0d50
Declaration source: `packages/business/dist/types/ir-types.d.ts#L425`.
~~~~text
export type ManifestIrDocument__3d04599c0d50 = {
irVersion: 1;
sdkVersion: string;
business: BusinessSpecJson__7ce46a3ce9fb;
routes: RouteLayerJson__1ae1dfa1e3a3[];
frontendIntegrations: FrontendIntegrationSpecJson__46198141209a[];
commerce: CommerceAuthoringSnapshot__60159f2d0d70;
};
~~~~
### MeterDefinitionJson__a744b09a0a2a
Declaration source: `packages/business/dist/types/ir-types.d.ts#L4`.
~~~~text
export type MeterDefinitionJson__a744b09a0a2a = {
key: string;
display: string;
unit?: string;
routeDefault?: number;
enforcementType?: "exact_pre_request" | "estimated_then_settled" | "postpaid" | "strict_concurrency";
aggregation?: "SUM" | "COUNT" | "MAX" | "UNIQUE_COUNT" | "LATEST";
window?: "minute" | "hour" | "day" | "month" | "billing_period";
valueProperty?: string;
uniqueProperty?: string;
groupBy?: string[];
eventCode?: string;
/** Token category this meter measures, when it is a token meter. T5 covers
* the full token-accounting vocabulary: the input/output/total axes, the
* cache axes (`cached_read_input` / `cache_creation_input`), `context`, the
* output-budget axes (`max_output` / `estimated_output` / `actual_output`),
* and `streaming_output`. Mirrors `TOKEN_CATEGORIES` in contracts. */
tokenCategory?: "input" | "output" | "total" | "cached_read_input" | "cache_creation_input" | "context" | "max_output" | "estimated_output" | "actual_output" | "streaming_output";
/** How the gateway RESERVES units for this meter on an estimate-then-settle
* path: `reserve_max` (conservative worst-case) / `estimate` (heuristic) /
* `count_actual` (no reserve). T5 — authored per token category to its
* use-case: hard quota → `reserve_max` (optionally capped by
* `reserveCeiling`); soft billing → `estimate`; spend → `reserve_max`. */
reserveStrategy?: "reserve_max" | "estimate" | "count_actual";
/** T5 — optional configured reservation CEILING (meter's native unit) for a
* conservative `reserve_max` strategy that must not hold the full theoretical
* worst case. Positive integer. Omit ⇒ unbounded worst-case reserve. */
reserveCeiling?: number;
/** Key of a meter this one is paired with (e.g. an `input` token meter pairs
* with its `output` meter) so settlement / observability correlate the pair. */
pairsWith?: string;
};
~~~~
### MutationClass__7be649da1488
Declaration source: `packages/business/dist/types/ir-types.d.ts#L2`.
~~~~text
export type MutationClass__7be649da1488 = "runtime" | "contractual";
~~~~
### PermissionSubjectJson__d124b4a2259d
Declaration source: `packages/business/dist/types/ir-types.d.ts#L389`.
~~~~text
export type PermissionSubjectJson__d124b4a2259d = {
subject: string;
verbs: string[];
escalatory?: string[];
description?: string;
};
~~~~
### PlanArchiveJson__452830f6517f
Declaration source: `packages/business/dist/types/ir-types.d.ts#L108`.
~~~~text
export type PlanArchiveJson__452830f6517f = {
at?: string;
transitionTo?: string;
strategy?: "auto" | "explicit" | "block";
};
~~~~
### PlanCapacityJson__cc200bb824bb
Declaration source: `packages/business/dist/types/ir-types.d.ts#L52`.
~~~~text
export type PlanCapacityJson__cc200bb824bb = {
maxInputTokens?: number;
maxOutputTokens?: number;
maxContextTokens?: number;
maxPayloadBytes?: number;
enforcement?: "enforce" | "track";
failMode?: "open" | "closed";
/** T9 — confidence input: `declared` (exact) vs `estimated` (tokenizer). */
tokenCountSource?: "declared" | "estimated";
/** T12 — FORWARD-COMPAT scope hint. */
scope?: LimitScopeJson__215e4eb2041c;
};
~~~~
### PlanKindJson__a2fffc829df1
Declaration source: `packages/business/dist/types/ir-types.d.ts#L66`.
~~~~text
export type PlanKindJson__a2fffc829df1 = "free" | "flat" | "usage" | "prepaid" | "hybrid" | "trial" | "custom";
~~~~
### PlanKindName__d0fd9bcc38b8
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L20`.
~~~~text
export type PlanKindName__d0fd9bcc38b8 = "free" | "flat" | "usage" | "prepaid" | "hybrid" | "trial" | "custom";
~~~~
### PlanLimitJson__7fa99c13f45f
Declaration source: `packages/business/dist/types/ir-types.d.ts#L75`.
~~~~text
export type PlanLimitJson__7fa99c13f45f = {
dimension: string;
window: PlanLimitWindowJson__34c3ba9c4506;
capacity: number;
enforcement?: "enforce" | "track";
/** A3 — optional WARN threshold as a fraction of `capacity` in [0, 1]. The
* core compiler threads it onto the emitted rate_limit / quota constraint's
* `warnAt`. Advisory only; never denies. Optional + additive. */
warnAt?: number;
strategy?: "fixed_window" | "sliding_window" | "token_bucket";
bucketCapacity?: number;
refillRatePerSecond?: number;
/** T12 — optional FORWARD-COMPAT scope hint. The core compiler threads it onto
* the emitted rate_limit / quota constraint's `scope`. v1 enforcement is
* unchanged. Optional + additive. */
scope?: LimitScopeJson__215e4eb2041c;
reset_trigger?: "billing_period" | "subscription_start";
};
~~~~
### PlanLimitWindowJson__34c3ba9c4506
Declaration source: `packages/business/dist/types/ir-types.d.ts#L67`.
~~~~text
export type PlanLimitWindowJson__34c3ba9c4506 = {
type: "named";
name: "second" | "minute" | "hour" | "day" | "week" | "month";
} | {
type: "custom";
seconds: number;
label?: string;
};
~~~~
### PlanSpecJson__47392a63c977
Declaration source: `packages/business/dist/types/ir-types.d.ts#L113`.
~~~~text
export type PlanSpecJson__47392a63c977 = {
key: string;
/** The DECLARED plan kind. Required: a plan is free / flat / usage / …
* because the builder said so, never because of its shape. Usage money
* never lives on the plan; it is commerce-manifest owned. */
kind: PlanKindJson__a2fffc829df1;
description?: string;
details?: string[];
recurring_fee_cents?: number;
billing_interval?: "month" | "year";
trial_days?: number;
max_monthly_spend_cents?: number;
min_monthly_spend_cents?: number;
limits?: PlanLimitJson__7fa99c13f45f[];
/** Additive v2 direct grants using stable route ids. */
routeGrants?: string[];
/** Named managed-frontend integrations this plan permits. */
frontendIntegrationGrants?: string[];
resource_limits?: Record;
/** A3 — per-request capacity ceiling. Compiles to a `{ kind: "capacity" }`
* ConstraintSpec the gateway enforces (413 `request_too_large`). */
capacity?: PlanCapacityJson__cc200bb824bb;
overageBehavior?: "block" | "allow_and_bill";
selfServeEnabled?: boolean;
archive?: PlanArchiveJson__452830f6517f;
/** Escape hatch: future fields ride through here. `variants` was removed and is rejected at validation. */
[key: string]: unknown;
};
~~~~
### PricingBindingKind__ff438801f484
Declaration source: `packages/business/dist/types/commerce/lowered-types.d.ts#L18`.
~~~~text
export type PricingBindingKind__ff438801f484 = "current" | "current_with_contract_terms" | "fixed_version";
~~~~
### RouteChargeableOutcomesJson__ba3a3aead9e9
Declaration source: `packages/business/dist/types/ir-types.d.ts#L267`.
~~~~text
export type RouteChargeableOutcomesJson__ba3a3aead9e9 = "success_only" | "success_and_partial" | "attempted" | "trusted_actual_usage_only";
~~~~
### RouteDefinitionJson__16a975e76419
Declaration source: `packages/business/dist/types/ir-types.d.ts#L225`.
~~~~text
export type RouteDefinitionJson__16a975e76419 = {
match: {
method: HttpMethod__7ba25e3979be;
path: string;
};
metering?: {
defaults?: Record;
reports?: string[];
onStatusCodes?: string | number[];
/** The authoritative billable units arrive through the attested
* post-stream reporter. The in-band gateway event remains reserve-only. */
postStreamBilling?: boolean;
};
/** Explicit billing semantics belong to the route, not the metering block;
* contracts reparses route layers strictly enough to strip unknown nested
* keys, which would silently turn authored free operations into inferred
* billable metered operations. */
usagePolicy?: RouteUsagePolicyJson__bd930ebf5713;
unmetered?: boolean;
inheritDefaultMeters?: boolean;
action?: string;
/** Repo-owned custom permissions — stamped by lowering when this route is a
* member of a permission-carrying `fs.group`. The gateway gates the route
* on `