@farthershore/backend
v0.21.2
Published
Farther Shore backend SDK for builder upstreams: signed response usage, fail-closed gateway request verification, first-class webhook consumption, health, and lifecycle from FS_RUNTIME_TOKEN
Downloads
307
Readme
@farthershore/backend
The runtime SDK for your own backend. When you run a software business on Farther
Shore with a bring-your-own-backend, the platform's edge gateway sits in front of
your service. This package lets your backend trust the gateway (verify that
each request really came from it) and report usage back for metering and
billing — from a single token, FS_RUNTIME_TOKEN.
Install one package, set one environment variable, and you get fail-closed gateway-to-upstream request verification, response-bound usage reporting, and graceful lifecycle (health + shutdown). Everything else — your business, backend, and environment ids, the verification keys, and the metering endpoint — is fetched automatically from the token at startup.
Status:
0.21.2. Pre-1.0: minor releases may include breaking changes, so pin this package to an exact version (or a patch-only range) and upgrade deliberately.
Install
npm install @farthershore/backendRequires Node 22+. The Express adapter has an optional express peer dependency
(v4 or v5); the core verification primitive is framework-neutral.
Quick start (any Fetch-compatible handler)
import { fartherShore } from "@farthershore/backend";
const fs = fartherShore.initFromEnv(); // derives everything from FS_RUNTIME_TOKEN
export async function POST(request: Request) {
const url = new URL(request.url);
const body = new Uint8Array(await request.clone().arrayBuffer());
// Fail-closed: throws a FartherShoreError if the request is not a genuine,
// unmodified request signed by the gateway.
const ctx = await fs.verifyRequest({
method: request.method,
path: url.pathname,
query: url.search,
headers: request.headers,
body,
});
const result = await runWorkflow(await request.json());
// ONE reporting verb. No identity argument (the verified context carries the
// served identity) and no transport argument (the SDK picks one).
await ctx.report({
meter: "model_usage",
values: { tokens_used: result.tokensUsed },
dims: { model: result.model },
});
return Response.json(result);
}Quick start (Express)
import { fartherShore } from "@farthershore/backend";
const fs = fartherShore.initFromEnv();
app.use(fs.middleware()); // fail-closed verify -> req.fartherShore
app.post("/v1/runs", async (req, res) => {
const result = await runWorkflow(req.body);
await req.fartherShore.report({
meter: "model_usage",
values: { tokens_used: result.tokensUsed },
});
res.json(result);
});
await fs.ready(app); // bootstrap, reconcile routes, report this replica ready
app.listen(3000);
process.on("SIGTERM", () => void fs.shutdown());What initFromEnv() derives
You configure exactly one thing: FS_RUNTIME_TOKEN (mint it for your backend
with the Farther Shore CLI or dashboard). Everything else — business / backend /
environment ids, the JWKS url used to verify signatures, the metering endpoint
and credential, and verification settings — is fetched from the platform at
startup and cached in memory. The token is validated eagerly, so a
missing or malformed token fails fast.
You can override the core URL via FS_CORE_URL (or pass options to
initFromEnv()), but in normal use no other configuration is needed.
Request verification (fail-closed, always)
fs.middleware() (Express) and the framework-neutral
fs.verifyRequest({ method, path, query, headers, body }) recompute a canonical
signing string from the actual request and verify the gateway's Ed25519 signature
against a JWKS-resolved public key. The plaintext X-FS-* headers are
untrusted — identity comes only from a signature whose claims match the real
request, so a forged or replayed request cannot impersonate the gateway.
Every failure (missing / malformed / bad-signature / stale / clock-skew /
wrong-route / body-hash-mismatch / replayed-nonce / unknown-key /
keys-unavailable) throws a typed FartherShoreError that maps to HTTP 401
(413 for oversized bodies). There is no fail-open path.
Replay protection (nothing to configure)
A signed request is one-time-use, and two things enforce that:
- A time window, always on. The gateway signs a timestamp into the request; anything older than ~305s (replay window + clock skew) is rejected outright. This holds for every deployment shape and needs nothing from you.
- A seen-id list. Inside that window, the SDK remembers each
X-Fs-Request-Idit has accepted and rejects a second sighting. The default list is in-memory and per-process.
That default is deliberate: running more than one replica means each has its own list, so a captured request could be replayed once per replica within the ~305s window — and closing that would mean asking you to provision and operate a distributed cache. We would rather keep your setup to one environment variable and let the time window bound the exposure.
If you do want one-time-use enforced across replicas, inject a shared store:
initFromEnv({ nonceStore: myStore }); // checkAndRemember(id) => boolean | Promise<boolean>Make it TTL-bound to the signature validity window. If that store goes down,
requests fail closed — it never degrades to "not a replay".
fs.replayProtection() reports which mode is active ("shared" |
"single-instance") if you want it in your boot logs.
Authorization — the permission grammar & in-handler checks
The edge permission constraint is the route-level security boundary: the
gateway resolves the acting user's effective permissions at token mint and
carries them in the signed X-Fs-Context claim. These SDK helpers exist for
finer-grained, in-handler checks the route layer can't express (field- or
record-level gating).
The grammar
A permission is a plain string, checked with three rungs:
*— the global wildcard. Grants every key (org OWNER, RBAC disabled, personal orgs — the gateway stamps an explicit["*"]).<subject>:*— the subject wildcard, e.g.widgets:*grantswidgets:read,widgets:write, and any otherwidgets:<verb>. (A literal*subject never takes this rung — only the bare*grant is global.)- exact keys — e.g.
widgets:write. Custom permission strings work: any<subject>:<verb>you invent is checked verbatim; there is no fixed verb vocabulary at this layer.
Route-shaped keys follow routePermission(subject, method) — <subject>:read
for safe verbs (GET / HEAD / OPTIONS, any casing) and <subject>:write for
everything else — the SAME helper the platform uses to derive a route's
required permission, exported here so you never re-spell the suffix.
Fail-closed at the carrier: an absent permission set
(ctx.permissions === undefined) always denies — absence never means
grant-all, even on a fully verified request. [] (authenticated, no grants)
also denies. A route you don't want gated simply doesn't call a check.
Checking permissions
// Namespace form — dev (`rt.authz`, traced) and prod (`fs.authz`) match:
app.post(
"/v1/widgets",
fs.middleware(),
fs.handler((ctx, req, res) => {
fs.authz.requirePermission(ctx, "widgets:write"); // throws 403 permission_denied
// or: if (fs.authz.hasPermission(ctx, "widgets:publish")) { ... }
res.json({ ok: true });
}),
);
// Declarative form — the handler options overload runs the same fail-closed
// check BEFORE your callback:
app.post(
"/v1/widgets",
fs.middleware(),
fs.handler({ permission: "widgets:write" }, (ctx, req, res) => {
res.json({ ok: true });
}),
);A failed check responds 403 { "error": "permission_denied" } (a thrown
FartherShorePermissionError is mapped by fs.handler). The standalone
hasPermission / requirePermission / permissionSatisfies /
routePermission exports are available for non-Express frameworks. In the dev
runtime, use rt.authz.* — the same shape, with every decision recorded into
the per-request trace (see templates/3-simulated-authz.test.ts).
Usage reporting — one verb
ctx.report({ meter, values, dims?, quote? }) on the verified context is the
ONLY reporting surface. Backends report measurements, never money: values
are observed facts (tokens, jobs, rows), dims name the catalog tuple they were
produced under, and the platform owns what they cost.
await req.fartherShore.report({
meter: "model_usage",
values: { input_tokens: 1200, output_tokens: 850 },
dims: { model: "acme-4", cache_status: "hit" },
});No identity ceremony. The subscription and served release ride the signed
context the gateway already sent, so there is no subscriptionId to forget —
the "unbilled because the handler omitted an id" failure mode is unreachable.
Hand the same FartherShoreContext to a background job and it keeps reporting
against that same served identity:
import type { FartherShoreContext } from "@farthershore/backend";
export async function runJob(job, fartherShore: FartherShoreContext) {
const result = await perform(job);
await fartherShore.report({
meter: "jobs",
values: { jobs: 1 },
dims: { queue: job.queue },
});
return result.output;
}Transport is an implementation detail. Reported before the response is sent,
the measurement rides signed x-fs-metering response headers — no extra network
call; the gateway verifies, settles, and strips them before the subscriber sees
the response. Reported after res.end() (a stream) or from a background job, it
goes over the attested post-stream channel with the same served identity. The
builder never picks; report() resolves { ok, transport } if you want to know.
Multiple meters after the response is sent → ONE batched call. A served
request owns exactly ONE post-stream callback identity, so sequential awaited
single-meter calls after the response cannot all be delivered — the first call
flushes the callback and every later call resolves { ok: false }. Pass an
ARRAY to report several meters atomically through that single callback:
await fartherShore.report([
{ meter: "model_usage", values: { output_tokens: 512 } },
{ meter: "jobs", values: { jobs: 1 } },
]);All entries of a batch share one quote (supplying two different quotes throws)
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.
Before the response is sent this constraint does not exist — sequential in-band
reports accumulate into the same signed response payload automatically.
Malformed input (a bad meter/measure/dimension key, a negative or non-finite
value, a malformed quote) throws — a dropped measurement is unbilled
revenue. Delivery failures resolve { ok: false, reason } instead of rejecting,
so a metering hiccup never breaks your endpoint. Calling report() on a context
that did not come from the runtime (the bare verifyRequest() primitive) throws
an error naming the fix.
The meter keys you report must match meters declared in your business; the gateway validates them against the served release's measurement-emission schema. Request-count style limits are enforced by the gateway and need no backend code.
Quotes: the one bounded money channel
quote is the sole exception to "never money": a proposed rate input for a
pricing policy that declared the backendQuoted rule with repo-authored
{min,max} bounds (dynamic upstream resale, bespoke jobs).
await fartherShore.report({
meter: "jobs",
values: { jobs: 1 },
quote: { currency: "usd", amountNanos: "250000000" }, // $0.25
});Core clamps it to the declared bounds and flags an out-of-range proposal for dispute; contract modifiers and funding still apply on top, and the ledger only ever records core-rated charges. The SDK does not validate the bounds (it cannot know them) — it rejects only structurally malformed quotes.
Non-JS backends
The wire recipe is language-neutral: any backend can stamp the same signed
headers with a stdlib HMAC. See
docs/response-metering-wire.md, or use
computeMeteringHeaders() directly from a non-Express JS host.
Consuming platform webhooks
Endpoints are created in the dashboard or CLI (farthershore webhook create);
the SDK consumes what they deliver. @farthershore/backend/webhooks is
standalone — a receiver needs only its fswh_ signing secret, not a runtime
token.
import { createWebhookHandler } from "@farthershore/backend/webhooks";
const webhooks = createWebhookHandler({
secret: process.env.FS_WEBHOOK_SECRET!,
on: {
"subscription.created": async (event) => {
await provision(event.data.subscriptionId, event.businessId);
},
"payment.failed": async (event) => {
await flagAccount(event.data.subscriptionId);
},
},
});
// Express — mount after a RAW body parser so the signature can be checked:
app.post(
"/webhooks/farthershore",
express.raw({ type: "*/*" }),
webhooks.express(),
);
// Fetch-style runtimes (Next.js route handlers, Hono, Workers):
export const POST = webhooks.fetch;What the handler does for you, in order: verifies the
Standard Webhooks signature over the raw
body (webhook-id.webhook-timestamp.body, HMAC-SHA256, any v1, entry —
so a platform-side rotation's dual signature just works, and you can pass
secrets: [current, previous] while you roll your own copy); rejects
timestamps outside ±5 minutes; deduplicates on webhook-id (a retry after a
lost 2xx is acknowledged without re-running your code; pass a shared
nonceStore on multi-instance receivers); parses the typed envelope
{ id, type, createdAt, businessId, environmentId, data }; acknowledges
unknown event types with 2xx (onUnknown to log them) so a newer platform
never causes a 500 storm; and turns a thrown handler into a 500 so the
platform retries (30 s / 5 min / 30 min) — the delivery id is released so
that retry runs your handler again.
verifyWebhook({ body, headers, secrets }) is the bare primitive if you want
to wire routing yourself, and signWebhookForTesting() from
@farthershore/backend/testing produces a platform-identical signed delivery
for your receiver tests.
Lifecycle
await fs.ready(app)bootstraps, reconciles the registered route surface, and sends the replacement replica'sreadyheartbeat. Call it after registering routes and before accepting traffic.fs.health()returns the current local health report (token present, bootstrap loaded, verification + metering status).fs.shutdown()flushes any buffered metering and sends astoppingheartbeat. Call it onSIGTERM/SIGINTfor graceful shutdown.
Local development & testing — the mode ladder
You do not need the platform to test a backend that runs behind it. Pick the lowest tier that answers your question — each is a superset of the one below.
Tier 0 — off (needs NOTHING from the SDK). Unit-test your business logic
directly. The gateway sits in front of you in production; your pure handlers
don't import Farther Shore to be tested. There is no SDK step at this tier — see
templates/1-unit.test.ts.
Tier 1 — passthrough. Run your real app over HTTP with fs.middleware()
mounted but verification OFF for local development only: the middleware passes
requests through without attaching a context, so your routes run normally.
Never use this mode in a deployed environment. Activate locally with
FS_DEV_MODE=passthrough. See
templates/2-passthrough-http.test.ts.
Tier 2 — simulated. A real runtime wired to an in-process gateway with
fail-closed verification ON and SIGNED personas driving requests. Assert the
fail-closed boundary (a persona without a permission gets 403) and that usage is
metered. Activate with FS_DEV_MODE=simulated, or construct explicitly:
import { createDevRuntime, definePersona } from "@farthershore/backend/testing";
const rt = createDevRuntime({
mode: "simulated",
personas: {
creator: definePersona({
name: "creator",
permissions: ["widgets:create"],
}),
},
});
// Sign a request as a persona and drive your app (supertest, fetch, or inject):
const headers = await rt.asPersona("creator").headers({ path: "/v1/widgets" });
// rt.usage.byMeter() → assert reported usage
// rt.trace.forRequest(id) → why a request verified / was denied@farthershore/backend/testing gives you signed personas (owner / admin /
member / anonymous, plus your own), an in-process gateway fixture, and
assertable usage + per-request trace side channels. It is dev/test tooling
only — it throws when NODE_ENV=production, and FS_DEV_MODE is never a
required-at-boot variable. When FS_DEV_MODE is set, initFromEnv()
self-constructs the simulator (ephemeral keys, a loud banner, JSONL usage/trace
logs under .farthershore/, and a mode-600 .farthershore/dev-keys.json so a
separate test-runner process can sign against a running service via
personaClientFromKeysFile).
See templates/3-simulated-authz.test.ts for the full fail-closed + usage flow.
Key exports
| Export | Purpose |
| ------------------------------------ | ----------------------------------------------------- |
| fartherShore.initFromEnv() | Create the runtime instance from FS_RUNTIME_TOKEN. |
| fs.middleware() | Express fail-closed verify → req.fartherShore. |
| fs.verifyRequest({...}) | Framework-neutral request verification. |
| fs.handler({ permission? }, cb) | Verified-principal handler (+ declarative gate). |
| fs.authz.requirePermission(ctx, k) | In-handler authz (fail-closed; also hasPermission). |
| routePermission(subject, method) | Route-derived permission key (:read/:write). |
| ctx.report({meter, values, …}) | THE reporting verb (SDK picks the transport). |
| computeMeteringHeaders() | Metering headers as a plain map — never throws. |
| fs.health() / fs.shutdown() | Health report and graceful shutdown. |
| FartherShoreError, MeteringError | Typed errors. |
| @farthershore/backend/webhooks | createWebhookHandler / verifyWebhook (receivers). |
| @farthershore/backend/testing | Dev-mode + persona test harness (dev/test only). |
A subpath export, @farthershore/backend/express, exposes the Express adapter
types directly if you prefer to wire the middleware yourself.
Metering transports
One verb, two transports — ctx.report() chooses; you never do:
- In-band (signed
x-fs-meteringresponse headers) while the response is still open: the attested, request-bound settlement channel. The gateway verifies the HMAC and settles the reported units against the request's lease in the same lifecycle, then strips the headers. Wire recipe (any language):docs/response-metering-wire.md. - Post-stream (the attested
POST /v1/metering/eventscallback) once the response is on the wire, or from a background job holding the context. It is HMAC-attested and carries the same served identity, writes the sole billable row for the reported units, and never mutates real-time enforcement windows — so units unknown at admission cannot be hard-enforced.
Learn more
- Platform documentation: https://docs.farthershore.com
- Provisioning a backend and minting a runtime token is done through the Farther Shore CLI or dashboard.
