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
Transport modes
DirectTunnelProvisioning a tunnel backendChoosingRelated
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/Transport modes

Transport modes

Direct calls vs a Cloudflare tunnel — how the gateway reaches your origin, and when to use each.

PreviousBring your own backendNextMetering & verification

A backend's transport decides how the gateway gets a request to your origin. There are two modes. direct is the default: the gateway calls your origin URL over public HTTPS. tunnel is for backends with no public ingress: your service opens an outbound-only Cloudflare tunnel and the gateway reaches it through that.

Both modes are equally secure on the request path — every forwarded request is signed and verified the same way. Transport only changes the network path, not the trust model.

// Direct (default): the gateway calls originUrl over public HTTPS.
@Backend("core", {
  transport: { mode: "direct" },
  originUrl: "https://api.example.com",
})
core!: unknown;

// Tunnel: no public origin URL; the runner dials out to Cloudflare.
@Backend("core", {
  transport: { mode: "tunnel", runner: "embedded" },
})
core!: unknown;

Direct

Use direct when your API is already reachable on a public HTTPS URL. This is the default and needs no extra process — declare the origin and you are done.

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

  @Backend("core", {
    transport: { mode: "direct" },
    originUrl: "https://api.example.com",
  })
  core!: unknown;

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

  @Plan("starter", {
    name: "Starter",
    limits: {
      requests: { rate: 600, interval: "minute", enforcement: "enforce" },
    },
  })
  starter!: unknown;
}

In direct mode your backend returns usage by signing it into response headers with withUsage() — the gateway settles and strips them before the subscriber sees the response. There is no inbound connection from you to the platform on the request path.

Tunnel

Use tunnel when your origin has no public ingress — it sits behind a firewall, on a private network, or in a container with no inbound ports. Your service runs cloudflared, which dials out to Cloudflare and holds the connection open; the gateway routes requests back down that tunnel. Nothing inbound is ever exposed.

The tunnel has a runner — who supervises the cloudflared process:

RunnerWho runs cloudflaredWhen to use
embeddedThe @farthershore/backend SDK, as a child process of your app.Single-process deploys; simplest DX.
sidecarYou — a separate container/process you operate.Kubernetes, Compose, or when you already manage cloudflared.
@Backend("core", {
  transport: { mode: "tunnel", runner: "embedded" },
})
core!: unknown;

Embedded runner

With runner: "embedded", the SDK spawns and supervises cloudflared for you using a tunnel token it fetches at bootstrap. Call fs.start() once during boot; for every non-tunnel transport (and for sidecar) it is a safe no-op.

import { fartherShore } from "@farthershore/backend";

const fs = fartherShore.initFromEnv(); // reads FS_RUNTIME_TOKEN

// Launches the embedded cloudflared supervisor IF this backend is
// tunnel + embedded. No-op for direct / sidecar.
await fs.start();

// ... run your server ...

process.on("SIGTERM", () => void fs.shutdown()); // tears the tunnel down first

By default the embedded runner is fail-open: if cloudflared cannot start, your app keeps running rather than crashing. Opt into crash-on-failure when the tunnel is load-bearing:

const fs = fartherShore.initFromEnv({
  tunnel: { failClosed: true },
});

Other tunnel options on initFromEnv (all advanced opt-ins): enabled: false to skip the embedded runner entirely, binaryPath to point at a specific cloudflared, and logger to capture its redacted output.

Request verification is always fail-closed — a tunnel that cannot start is a different axis from a request that cannot be verified. Even with failClosed unset, unverifiable requests are still rejected. See metering & verification.

Sidecar runner

With runner: "sidecar", you own the cloudflared process — typically a second container next to your app. The SDK does not spawn anything; fs.start() is a no-op. You still call initFromEnv() and verify requests exactly the same way; only the tunnel process is yours to run.

Provisioning a tunnel backend

Tunnel backends need a runtime token whose capabilities include tunnel. Create the backend with the tunnel transport, then mint the token:

# Declare the backend record as a tunnel origin
farthershore backend create <productId> \
  --name private-origin --transport tunnel --runner embedded --default

# Mint a token that includes the tunnel capability
farthershore backend tokens create <productId> \
  --backend <backendId> --capabilities gateway_verification,metering,health,tunnel

The mint command prints FS_RUNTIME_TOKEN once — set it in your backend's environment. See runtime tokens for the full lifecycle.

Choosing

  • Public HTTPS API? Use direct. Nothing to run.
  • No public ingress, single process? Use tunnel + embedded and call fs.start().
  • No public ingress, you already run cloudflared (k8s, Compose)? Use tunnel + sidecar.

Related

  • Bring your own backend — declare origins with @Backend.
  • Runtime tokens — mint a token with the tunnel capability.
  • Metering & verification — verify and report on every transport.
  • @farthershore/backend reference — the full SDK export surface.