Farther ShoreDocs
Go to Farther Shore
What is FartherShore
Install the CLI
Quickstart
Core concepts
The @Product class
Meters & resources
Features & routes
Capabilities & entitlements
Plans & pricing
The Manifest IR
Bring your own backend
The @Backend decoratorBinding routes to backendsOperating backends from the CLIWhat to read next
Transport modes
Metering & verification
Runtime tokens
Frontend SDK
Root & data components
Auth & sessions
Entitlement gates
Connect Stripe
Subscriptions & usage
Plan changes & grandfathering
Billing strategies
Apply & deploy
Environments
Migrations
Docs versions & archive
Operate with an agent
Operation classes
MCP server
End-to-end via CLI/MCP
CLI reference
@farthershore/product
@farthershore/backend
@farthershore/farthershore-js
Environment variables
Response & deny codes
Add a metered capability
Gate a feature
Change a price
Prepaid credits
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Connect your backend/Bring your own backend

Bring your own backend

Front your existing API with the @Backend decorator — the gateway wraps the service you already run.

PreviousThe Manifest IRNextTransport modes

Farther Shore does not host your code. You keep running the API you already have; the gateway sits in front of it, authenticates each subscriber, enforces plans, meters usage, and forwards the allowed requests to your origin. A backend is how you tell the platform where that origin is and how to reach it.

You declare backends in TypeScript, inside the same @Product class that defines your plans and routes. There is no separate config file and no YAML — the decorated class is the single source of truth, compiled to the Manifest IR the platform applies.

The @Backend decorator

@Backend(id, options) registers one origin. The id is a lowercase slug ([a-z0-9][a-z0-9_-]*) you reference from routes. With a single backend you do not even need to bind routes to it — it is the implicit default.

import { Product, Backend, Feature, Plan, Requests } from "@farthershore/product";

@Product({
  name: "croncloud",
  origin: "https://api.example.com",
  displayName: "CronCloud",
  description: "Managed cron jobs",
})
class CronCloud {
  @Requests()
  requests!: unknown;

  // One backend, declared once. Default transport is `direct`.
  @Backend("core", {
    transport: { mode: "direct" },
    originUrl: "https://api.example.com",
  })
  core!: unknown;

  @Feature("cron-jobs", {
    plans: ["starter"],
    routes: {
      "GET /v1/cron-jobs": {},
      "POST /v1/cron-jobs": {},
      "DELETE /v1/cron-jobs/{id}": {},
    },
  })
  cronJobs!: unknown;

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

@Backend options

OptionTypeMeaning
transport{ mode?: "direct" | "tunnel"; runner?: "embedded" | "sidecar" }How the gateway reaches the origin. See transport modes.
originUrlstringThe origin URL the gateway forwards to (direct mode).
originHostnamestringOrigin hostname when you do not want a full URL.
verification{ required?: boolean }Require per-request Ed25519 signing on this backend. See metering & verification.
metersstring[]Allow-list of meter keys this backend may report. Omit to allow all product meters.
defaultbooleanMarks the default backend when a product declares more than one.
name / slugstringHuman label / stable slug (both default to the id).

Binding routes to backends

One backend is the default for every route. When a product declares more than one, each route resolves to a backend in this order:

  1. The route's own { backend } entry, else
  2. the @Feature({ backend }) default, else
  3. the single backend marked default: true.
@Product({ name: "croncloud", origin: "https://api.example.com" })
class CronCloud {
  @Requests()
  requests!: unknown;

  @Backend("api", { default: true, originUrl: "https://api.example.com" })
  api!: unknown;

  @Backend("search", { originUrl: "https://search.example.com" })
  search!: unknown;

  @Feature("cron-jobs", {
    plans: ["starter"],
    routes: {
      "GET /v1/cron-jobs": {},                  // → "api" (default)
      "GET /v1/search": { backend: "search" },  // → "search"
    },
  })
  cronJobs!: unknown;

  @Plan("starter", { name: "Starter" })
  starter!: unknown;
}

If a product declares two or more backends with no single default: true and a route has no explicit binding, the build fails with AMBIGUOUS_DEFAULT_BACKEND. Mark exactly one backend default: true or bind the route. Binding a route to an undeclared id fails with UNKNOWN_BACKEND_IN_ROUTE. These are build-time errors — you see them before anything reaches the platform.

Operating backends from the CLI

Backends and their origins also exist as platform records you can create and inspect without editing the manifest — useful for previews and for scoping a runtime token to a specific origin.

# List a product's backends (id, name, transport, status)
farthershore backend list <productId>

# Create a backend record (default transport: direct)
farthershore backend create <productId> \
  --name prod-origin --origin-url https://api.example.com --default

# Delete a backend (also revokes its runtime tokens)
farthershore backend delete <productId> <backendId> --yes

The product's contractual definition — which backends exist, their transport, and route bindings — is managed as code. The canonical way to change it is to edit the @Backend declarations in your product repo and push. The CLI records above are for operating origins and minting tokens, not for rewriting the contract.

What to read next

  • Transport modes — direct vs Cloudflare tunnel, and when to use each.
  • Metering & verification — verify gateway requests and report usage with @farthershore/backend.
  • Runtime tokens — provision and rotate the FS_RUNTIME_TOKEN your backend reads.
  • @farthershore/backend reference — the full SDK export surface.