# 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