Infrastructure with OpenTofu
Provision the backend host, its secrets, and one deployment per environment with tofu, then bind each origin to a Farther Shore environment.
Farther Shore is the gateway, billing, and entitlement plane in front of an HTTP service that you run. It does not host that service. Creating it with a host's CLI is the fastest way to a first working preview; describing it with OpenTofu is what makes a second environment, a rebuild, and a handover reproducible.
Use this page once the product is real enough to need more than one environment. For the first hour, Scaffold a backend is the shorter path.
What OpenTofu owns, and what it does not
| Thing | Owner |
|---|---|
| The host project, service, deployment, and public hostname | OpenTofu |
FS_RUNTIME_TOKEN and other process secrets in the host | OpenTofu (as a sensitive variable it does not mint) |
Plans, pricing, routes, meters, limits, fs.backend() | The business/ program in the managed repository |
| The Farther Shore environment row and its origin binding | The farthershore CLI |
OpenTofu never authors contract state, and the CLI never provisions hosting. Runtime tokens are minted by the CLI and delivered by OpenTofu.
Map platform environments to infrastructure environments
Keep the mapping one-to-one and name both sides identically.
| Farther Shore | Git branch | Infrastructure |
|---|---|---|
preview environment preview | env/preview | workspace preview |
| production | default branch | workspace production |
Each side stays independent: the contract comes from the branch, the origin
comes from the infrastructure, and farthershore backend create --env /
farthershore backend bind --env is the single join between them.
Give every environment its own deployment and its own FS_RUNTIME_TOKEN. A
token minted with --env preview cannot bootstrap production, which is the
point: a compromised preview deployment cannot serve live customers.
Keep state where a second machine can read it
Shared infrastructure must not use local state. Configure a remote backend with
locking before the first tofu apply, and keep one state file per environment —
either separate workspaces or separate backend keys.
terraform {
required_version = ">= 1.10"
backend "s3" {
bucket = "acme-tofu-state"
key = "farthershore/api.tfstate"
region = "us-east-1"
# Native S3 conditional-write locking. On older OpenTofu, lock with
# `dynamodb_table` instead; both mechanisms remain supported.
use_lockfile = true
}
}
State contains secret values in plaintext. Encrypt the bucket, restrict who can
read it, and never commit a .tfstate file. OpenTofu's built-in state
encryption is worth
enabling on top of the backend's own encryption.
A concrete example: Railway
The community Railway provider models a project, its environments, one service per environment, that service's domain, and its variables. Substitute your own provider if you deploy elsewhere — the shape below is what matters, not the vendor.
terraform {
required_providers {
railway = {
source = "terraform-community-providers/railway"
version = "~> 0.5"
}
}
}
# RAILWAY_TOKEN comes from the environment; never write it into a .tf file.
provider "railway" {}
variable "environment_name" {
type = string
description = "preview or production; matches the Farther Shore environment"
}
variable "fs_runtime_token" {
type = string
sensitive = true
description = "Minted by farthershore backend tokens create for THIS environment"
}
resource "railway_project" "api" {
name = "acme-api"
private = true
}
resource "railway_environment" "this" {
name = var.environment_name
project_id = railway_project.api.id
}
resource "railway_service" "api" {
name = "api"
project_id = railway_project.api.id
source_repo = "acme/acme-api"
source_repo_branch = var.environment_name == "production" ? "main" : "env/preview"
root_directory = "/api"
}
resource "railway_service_domain" "api" {
subdomain = "acme-api-${var.environment_name}"
environment_id = railway_environment.this.id
service_id = railway_service.api.id
}
resource "railway_variable" "runtime_token" {
name = "FS_RUNTIME_TOKEN"
value = var.fs_runtime_token
environment_id = railway_environment.this.id
service_id = railway_service.api.id
}
output "origin_url" {
value = "https://${railway_service_domain.api.domain}"
}
Pass the secret from your own secret store rather than a variable file:
tofu workspace select preview
TF_VAR_fs_runtime_token="$(read-from-your-secret-store)" \
tofu apply -var environment_name=preview
If you use a different host, the four resources to look for are the same: a project or account scope, one service per environment, a public HTTPS hostname, and a secret variable bound to that service and environment.
Order of operations
The runtime token is scoped to a backend row, so the row must exist first.
-
Declare the logical backend in
business/and push the branch, so the environment has an accepted contract:tsconst api = fs.backend("api", { transport: { mode: "direct" }, default: true, }); -
Provision the hosting for that environment and read its hostname:
bashtofu workspace select preview tofu apply -var environment_name=preview tofu output -raw origin_url -
Register the origin for that environment:
bashfarthershore backend create acme \ --env preview \ --name api --slug api \ --transport direct \ --origin-url "$(tofu output -raw origin_url)" \ --default \ --idempotency-key <persisted-backend-create-attempt-key> \ --format json -
Mint the environment's token after that row exists, then deliver it:
bashfarthershore backend tokens create acme \ --env preview \ --idempotency-key <persisted-backend-tokens-create-attempt-key> \ --format jsonPass
--backend <backend-id>as well when the environment has more than one backend, so the token resolves to the intended row. Store the one-time value in your secret store and re-apply so the host receives it. -
Repeat for production with
--env productionomitted (production is the default target) and a production-scoped token.
Steps 3 and 4 cannot be inverted, and they cannot be moved into OpenTofu: the token is a one-time secret returned by a CLI write, not a declarable resource.
What to re-run when something changes
| Change | Re-run |
|---|---|
Plans, pricing, routes, meters, limits — anything in business/ | git push only. No tofu run. |
Backend application code in api/ | Your host's deploy (often automatic from the branch). |
A new fs.backend() slug | tofu apply, then backend create + tokens create. |
| A new environment | farthershore env create, then tofu apply in a new workspace, then backend create + tokens create. |
| Hostname, region, replica count, resource sizing | tofu plan then tofu apply, then backend bind --env <name> --origin-url <new-url> if the hostname moved. |
| Token rotation | backend tokens create, update the secret, re-apply, verify, then revoke the predecessor. |
Always read tofu plan before tofu apply. A plan that proposes replacing the
service or its domain will change the origin URL, which requires a matching
farthershore backend bind or the gateway will return origin_unavailable.
Verify
tofu output -raw origin_urlreturns an HTTPS URL that serves/healthzunauthenticated.farthershore backend list acme --format jsonshows the row for each environment with a concrete target. This command is business-wide and takes no--env; read the environment off each returned row.- A signed gateway request reaches the backend, and a direct call to the origin
without a platform signature fails with
missing_signature. - Publishing production succeeds. If it fails with
BACKEND_TARGET_REQUIRED, a declared backend has no production binding — see Production releases.
Next steps
- Runtime tokens — scope, delivery, and rotation.
- Preview environments — branches, applies, and teardown.
- Scaffold a backend — the single-service starting point.