Bring your own backend
Front your existing API with the @Backend decorator — the gateway wraps the service you already run.
Front your existing API with the @Backend decorator — the gateway wraps the service you already run.
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.
@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| Option | Type | Meaning |
|---|---|---|
transport | { mode?: "direct" | "tunnel"; runner?: "embedded" | "sidecar" } | How the gateway reaches the origin. See transport modes. |
originUrl | string | The origin URL the gateway forwards to (direct mode). |
originHostname | string | Origin hostname when you do not want a full URL. |
verification | { required?: boolean } | Require per-request Ed25519 signing on this backend. See metering & verification. |
meters | string[] | Allow-list of meter keys this backend may report. Omit to allow all product meters. |
default | boolean | Marks the default backend when a product declares more than one. |
name / slug | string | Human label / stable slug (both default to the id). |
One backend is the default for every route. When a product declares more than one, each route resolves to a backend in this order:
{ backend } entry, else@Feature({ backend }) default, elsedefault: 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.
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.
@farthershore/backend.FS_RUNTIME_TOKEN your backend reads.@farthershore/backend reference — the full SDK export surface.