Scaffold a backend service
Create a Node backend, verify gateway requests, deploy it, and bind it to a business environment.
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.
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 <dir> 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:
- unsigned health route;
- raw-body capture;
fs.middleware()signature verification;- JSON parsing;
- 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.
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.
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:
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.
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
farthershore backend create my-business \
--name "Application API" \
--slug api \
--transport direct \
--origin-url https://your-service.example.com \
--idempotency-key <persisted-backend-create-attempt-key> \
--default
Previews inherit this production backend automatically. Use --env <name-or-id> 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 <backend-id> so the
token resolves to the intended row.
For a deployment that can serve this business in every environment:
farthershore backend tokens create my-business --format json --idempotency-key <persisted-backend-tokens-create-attempt-key>
Copy the one-time token into FS_RUNTIME_TOKEN, restart or redeploy the service,
and inspect the result:
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.
farthershore backend list my-business --format json
farthershore backend bind my-business \
--env production \
--origin-url https://your-production-service.example.com \
--format json