Terraform state backend
Orunbase serves Terraform's native backend "http" protocol, so a tenant's Terraform state can live on the platform instead of in an S3 bucket plus OIDC role in the tenant's own AWS account. There is nothing to provision: if a repo is linked and CI can push objects, it can store Terraform state at:
https://api.orunbase.com/v1/organizations/{orgId}/projects/{projectId}/state/tfstate/{component}/{environment}
One state address exists per (org, project, component, environment); a single row holds the state document and its lock atomically, so a lock can never drift from the state it protects.
Methods
| Method | Behavior |
|---|---|
GET | Returns the raw state document (200, content-type: application/json). 404 when no state has been written — Terraform treats that as a fresh state. |
POST ?ID=<lockId> | Writes the state document, honoring the held lock: a write carrying the wrong (or no) lock id while a lock is held answers 423 with the holder's lock-info document. An empty body or non-JSON body is 422 validation_failed. |
DELETE | Deletes the state document (200). |
LOCK | Acquires the lock. The request body is Terraform's lock-info document; a conflicting holder answers 423 with that holder's lock-info. |
UNLOCK | Releases the lock. The body is {"ID": "…"}; releasing a lock held by someone else answers 423. |
LOCK and UNLOCK are only valid on tfstate paths — anywhere else on the API they are 405.
The {component} and {environment} segments must match ^[a-z0-9][a-z0-9._-]{0,127}$ (case-insensitive); anything else is rejected with 422.
The wire shape is Terraform's, not the platform's
These routes speak raw Terraform JSON, not the standard { "data": …, "meta": … } envelope: the GET body is the state document itself, and lock conflicts answer 423 with the holder's lock-info document so Terraform can print the lock id and owner directly. This is a documented exception to the error envelope — Terraform parses these bodies natively and knows nothing about platform envelopes.
Authentication
Terraform's HTTP backend sends Basic auth. The API edge normalizes it: the Basic password is promoted to the request's Bearer token, and the username is ignored (orun by convention). So the backend password is any credential the state plane accepts — in CI, the short-lived run token.
The orun runner does this wiring for you: it exports the TF_HTTP_* variables per job, including TF_HTTP_PASSWORD set to the job's run token, so a Terraform component needs no credential configuration at all.
Configure the backend
terraform {
backend "http" {
address = "https://api.orunbase.com/v1/organizations/org_1f6a3c9e/projects/prj_2d3e4f5a/state/tfstate/network/prod"
lock_address = "https://api.orunbase.com/v1/organizations/org_1f6a3c9e/projects/prj_2d3e4f5a/state/tfstate/network/prod"
unlock_address = "https://api.orunbase.com/v1/organizations/org_1f6a3c9e/projects/prj_2d3e4f5a/state/tfstate/network/prod"
lock_method = "LOCK"
unlock_method = "UNLOCK"
username = "orun"
# password arrives via TF_HTTP_PASSWORD — never commit it
}
}
Authorization
The routes carry the state plane's normal posture, deny-by-default:
| Operation | Requirement |
|---|---|
GET | state.object.read |
POST / DELETE / LOCK / UNLOCK | state.object.write, plus the CI repo allow-list (the repo's workspace link), plus an active project |
Workflow actors minted via OIDC exchange hold these within their token-bound workspace + project — see CI workflow grants. CI cannot write Terraform state into an archived project.
State documents contain resolved values — that is exactly why they never lived in the repo. Treat the state address like any other secret-adjacent surface: don't log response bodies, and don't widen read access beyond what state.object.read already grants.