---
title: "State and native cloud"
description: "Immutable OpenTofu state history, locking, authenticated downloads, and migration through the native cloud contract."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.stackship.run/llms.txt
> Use this file to discover all available pages before exploring further.

# State and native cloud

StackShip implements OpenTofu's native cloud/TFE-compatible state contract.
Customers configure a `cloud` block and authenticate with `tofu login`; they
do not configure the generic HTTP backend.

## Discovery

`https://stackship.run/.well-known/terraform.json` advertises:

- OAuth login through `https://api.stackship.run/api/auth`;
- the compatibility API at `https://api.stackship.run/api/v2/`; and
- the [module registry](/registry) protocol (`modules.v1`) at
  `https://api.stackship.run/registry/modules/v1/`.

`stackship.run` remains the preferred cloud hostname. For existing
configurations that use `api.stackship.run`, the API publishes the same
discovery contract and redirects OpenTofu's API-hosted run links to the
canonical dashboard route.

The OpenTofu login callback range is inclusive: StackShip registers
`http://localhost:10000/login` through
`http://localhost:10010/login`. Login uses authorization code plus PKCE S256;
no client secret is embedded in OpenTofu.

Configure:

```hcl
terraform {
  cloud {
hostname     = "stackship.run"
organization = "example"

workspaces {
  name = "network-foundation"
}
  }
}
```

Then authenticate:

```bash
tofu login stackship.run
```

Automation can use a separately issued, scoped service-account token through
`TF_TOKEN_stackship_run`. Managed execution uses that same
hostname-derived environment variable for a different credential: StackShip
injects a separate opaque `rat_` attempt credential derived with
`RUNNER_RUN_TOKEN_HMAC_KEY`. The customer's interactive or service-account
credential is never passed into the sandbox.

The managed credential is accepted only for the exact organization, project,
workspace, run, attempt, phase, sandbox, and current workspace fence resolved
by the API. It fails closed when that authority is terminal, revoked, expired,
replaced, or stale. It cannot create another run, perform state recovery, or
authenticate to MCP.

The workspace dashboard exposes this surface as **CLI access** in the
workspace section navigation, at `/workspaces/{workspace_id}/cloud`.
StackShip first verifies the active workspace, project, organization slug,
and `credential:write`; members without that permission see a quiet
read-only notice instead of token controls. The page renders the exact
native cloud block and can issue a workspace/project-scoped token for state
migration or remote plan and apply. The service-account name and description
sit behind an advanced disclosure with sensible defaults, the token scope
line shows the project and workspace names with their identifiers as
secondary detail, and the cloud block, `tofu login`, and automation command
blocks each carry a copy control. Token plaintext is displayed once; after
the operator acknowledges saving it, the page clears the plaintext, shows a
transient "Token disclosure cleared" confirmation, and returns to the
create-token form so the next token uses a fresh idempotent request.

The block uses the workspace's unique `cloud_name`; the human-readable
dashboard name is not sent to OpenTofu. The state-migration token includes
`state:outputs:read` so ordinary `tofu output` works after publication while
sensitive output values still require `state:read`.

## State versions

Every committed state version records:

- an immutable public id;
- workspace and organization;
- lineage and serial;
- plaintext SHA-256 checksum;
- plaintext byte size;
- resource count;
- creator and creation time; and
- an encrypted R2 object reference.

PlanetScale stores metadata, not state JSON. R2 object keys use opaque
identities rather than customer names.

Workspace state history uses opaque forward pagination. Read
`meta.page.next_cursor` from a list response and send a non-null value as the
next request's `cursor`. State creation time and public state id form the
stable descending key, including when multiple versions share a timestamp.
Bare timestamps and delimiter cursors are invalid and return
`400 STATE_VERSION_LIST_CURSOR_INVALID`.

OpenTofu publishes managed state directly through the Cloud/TFE contract. The
run token can read only its pinned predecessor and can create a successor only
under the exact active managed-run reservation, attempt, sandbox, lock, and
fence. Raw state streams through the Worker into its final encrypted R2
identity; finalization verifies its byte count, checksum, lineage, serial,
predecessor, outputs, and fence before advancing the current pointer.

The runner does not download predecessor state, upload resulting state or
outputs, run `tofu output`, or promote a second object copy. After a successful
command it requests only bounded publication metadata. StackShip accepts that
metadata when both the published upload and state-version row carry the exact
managed-run principal and object identities, then binds the result digest to
the state checksum. If the commit response is lost after lock release, an exact
callback replay uses the persisted resulting-state pointer; it cannot present a
different digest, state version, attempt, sandbox, predecessor, lock, or fence.
An emergency-state artifact exists only when OpenTofu leaves
`errored.tfstate` after the Cloud backend cannot persist a failed or
interrupted write, or when the runner safely recovers OpenTofu's last-resort
state JSON from guarded human-view stderr or the JSON-view stdout emergency
envelope. That fallback is spooled, classified, and extracted by bounded disk
streams under the same exact `100_000_000`-byte limit; raw state bytes never
enter StackShip logs or Worker memory. The runner never captures a root
`terraform.tfstate` as an ordinary resulting-state copy.

Every emergency candidate is untrusted sandbox evidence. Providers,
provisioners, `local-exec`, and OpenTofu share the sandbox trust domain, so
StackShip does not describe the file or guarded output as OpenTofu-attested or
verified. Its digest and byte count identify the exact candidate shown to the
operator. It never advances the current pointer automatically and can enter
state history only through the explicit recovery flow with a reason,
attestation, provider reconciliation, and predecessor, lineage, fence, and
successor validation.

OpenTofu lineage values use the canonical lowercase, hyphenated 16-byte hex
shape but are not required to carry an RFC UUID version or variant. StackShip
preserves and validates that OpenTofu shape instead of rejecting valid random
lineages as malformed UUIDs.

## Locking and concurrency

Local cloud clients and managed runs use the same workspace writer
reservation. A lock has an owner and a monotonically advancing fence. Stale
owners cannot publish after the fence changes.

Managed-run access is phase-specific:

| Attempt | State read | State lock | State publication |
| --- | --- | --- | --- |
| Managed plan | Current state | Acquired and released for the OpenTofu command | Never |
| Speculative plan | Run-pinned state version only | Never | Never |
| Managed apply | Current predecessor | Bound to the active run reservation and attempt | Only with the expected predecessor and current fence |

Workspace run-capacity saturation receives an immediate JSON:API
`409 state_conflict` with guidance to finish or cancel an active run; this
stable contention is not reported as a retryable service outage. When the
configured capacity is zero, an administrator must increase it before a run
can start. A separate writer-reservation conflict reports that another state
operation owns the workspace. Organization-wide capacity exhaustion remains
a temporary `503` because it can clear independently of the requested
workspace.

The apply credential has no `state:recover` authority. Publication still
requires the active managed writer reservation, an owned operation lock, and
the exact attempt and fence; possession of the bearer value alone is
insufficient.

State upload happens outside the database transaction. A pending version
becomes current only after the encrypted object, size, checksum, lineage,
serial, current predecessor, and lock fence are verified. A canceled,
truncated, over-limit, or failed upload leaves the current pointer unchanged.
Each upload capability is atomically claimed before the API pulls its body or
touches R2. Concurrent or replayed callers therefore cannot overwrite or
delete the winning upload. A capability that is expired, already claimed, or
otherwise unresolved fails the product-admission check with the same
generic retryable denial used for unavailable product scope. A request that
loses the claim after admission receives `404`; in both cases StackShip does
not read the caller's body.

Compatibility responses that can carry upload capabilities use
`Cache-Control: private, no-store`. JSON:API errors retain the compatibility
media type even when the successful form of the endpoint is a raw upload or
download. Structured rejection logs record the matched route pattern, such as
`/capabilities/state-uploads/:capability`, and never the literal
capability-bearing request path.

Finalization is replay-safe. When concurrent readers finalize the same
verified upload, the committed state version and its output handles win;
newly allocated candidate handles are not treated as conflicting state
evidence.

OpenTofu may publish apply checkpoints about every 20 seconds. StackShip
accepts an exact contiguous managed-run chain of at most 1,024 versions,
covering roughly 720 checkpoints in the four-hour runner authority window plus
operational headroom. `TF_STATE_PERSIST_INTERVAL` is reserved and cannot be
overridden by workspace variables. The chain lookup is indexed by the exact
organization, workspace, lock, fence, serial, creation time, and public id.

Discarded uploads retain durable cleanup state. Raw, JSON, and outputs objects
are deleted only after the database row is discarded, and
`cleanup_completed_at` is recorded only after all three deletions succeed.
Failures remain queryable for retry. Scheduled artifact maintenance processes a
bounded batch of incomplete discarded uploads before ordinary R2 orphan
reconciliation, so cleanup does not depend on the original client retrying.
Each upload is isolated: a failed deletion is counted and retried later without
blocking cleanup of the remaining batch.

## Customer access

Authorized customers can:

- inspect immutable state-version metadata in the workspace dashboard
  without receiving raw state;
- download a selected version through the restricted native v1 API when
  their administrative role grants both raw-state permissions;
- use `tofu state pull` and `tofu state push` through `/api/v2`; and
- migrate with ordinary OpenTofu initialization and state commands.

The dashboard's state history is a dense table: serial, creation time,
resource count, size, truncated checksum with the full digest as a tooltip,
and a Current badge on the active version. Selecting **Download** on a row
opens one shared download panel bound to that version; it presents the
restricted-download steps in order — verify identity, record an operator
reason, then download.

The dashboard does not currently provide state diff, rollback, editing,
retention, or resource-search controls, and the state history says so in
one line near its header — rolling back means pulling and pushing state
through the native cloud contract. The run recovery panel does provide a
bounded comparison of current and emergency-state identities; it is not a
general rollback control.

Raw state download is a restricted-artifact operation. The v1 route accepts
only `POST`, requires both `state:read` and
`artifact:read_sensitive`, a typed operator reason of at most 500
characters, and step-up reauthentication no older than 15
minutes. Ordinary organization members receive `state:outputs:read`, not
`state:read`, and the dashboard does not render raw-state controls for them.

StackShip commits `state.downloaded` with the exact operator reason before
releasing bytes, then streams authenticated decryption through the API and
dashboard Worker. The reason is sent in a bounded form body, not the URL.
The dashboard's same-origin referrer policy keeps the form mutation's
`Origin` verifiable without disclosing a referrer to another origin.
Every non-empty query string is rejected before state storage or audit access.
If the audit commit fails, StackShip cancels the decrypted storage stream and
releases no state bytes. StackShip never returns a plaintext R2 URL.

The service-account `/api/v2` download path is a separate, non-browser
OpenTofu trust path, but it keeps the same fail-closed storage guarantees.
Before auditing or releasing bytes, StackShip requires the decrypted object's
plaintext byte count and SHA-256 digest to match the immutable state-version
row. A mismatch or audit-write failure cancels the decrypted stream.

## Reading state from other workspaces

By default no workspace can read another workspace's state. Cross-workspace
reads are ordinary RBAC assignments: a workspace is a subject that assumes a
system role on another workspace, on every workspace in a project, or on the
whole organization. Two roles exist:

- **Outputs consumer** (`state-outputs-consumer`) — read the producer's
  current non-sensitive output values. This is what the `tfe_outputs` data
  source needs.
- **State consumer** (`state-consumer`) — additionally download the
  producer's current raw state, including sensitive output values. This is
  what the `terraform_remote_state` data source needs, and it discloses every
  attribute of every resource in the state file — prefer the outputs role
  unless raw state is genuinely required.

Assignments are managed on the workspace's **Access** tab (or the
`/v1/workspaces/{workspace_id}/access` API) and require the organization
admin role's `state:share` permission. Subjects can be a specific workspace,
all workspaces in a project (membership is evaluated live — a workspace that
moves projects gains or loses coverage immediately), or all workspaces in
the organization.

During a remote plan or apply, the injected run credential acts as its
workspace and can therefore exercise these assignments — no extra tokens in
variables. A consumer always reads the producer's **current** state version:
historical versions, pending uploads, locking, publication, and variables
stay invisible, and unauthorized reads return `404` exactly like nonexistent
resources. The assumed role never exceeds what the consuming credential
itself may do, and every shared raw-state download is audited with the
consuming workspace recorded.

The compatibility surface reports `global-remote-state: true` on a workspace
when an organization-wide state-consumer assignment covers it; the TFC
`remote-state-consumers` relationship endpoints are not implemented.

## Interrupted apply recovery

An uncertain or interrupted apply retains its writer reservation until an
authorized recovery transaction advances the fence. The dashboard action
requires `run:recover`, `state:recover`, recent step-up, a typed reason, and an
explicit fence confirmation. StackShip verifies the submitted current pointer
under the lineage lock. An unchanged predecessor can be retained or advanced
by an exact emergency successor. A moved pointer can be retained only when it
is the exact published partial state of the interrupted run, with matching run,
attempt, sandbox, predecessor, lock, fence, and object identities. Every other
moved pointer fails with `STATE_PREDECESSOR_CHANGED`. The interrupted apply
authorization and exact managed-run lock and reservation must remain active,
and both recovery preparation and commit recheck those facts under the lineage
lock. Durable uploaded or claimed-pending successors are finalized first. An
expired pending upload is reclaimed automatically only when both raw and JSON
claim timestamps are null.

An unresolved claimed upload is never discarded because of age or status
alone. The dashboard shows the exact upload id and claim timestamp and warns
that supersession may lose in-flight state. Submission first attempts to
finalize the exact durable artifact. If it remains unavailable, recovery stays
blocked until an operator confirms that exact pair. Commit rechecks it while
holding the upload and lineage locks, advances the fence, and discards only
that row in the same transaction. The audit event records the exact superseded
id and claim timestamp.

A prepared recovery is leased to its operator for exactly 15 minutes according
to PostgreSQL's clock. Run review shows the authoritative expiry and a bounded
remaining-time value. A takeover before expiry is rejected; after expiry it
still requires both recovery permissions, recent step-up, unchanged evidence,
and explicit takeover confirmation.

Downloading the separate emergency-state artifact is not the recovery
mutation. It requires `artifact:read_sensitive` and `state:recover`; holding
`run:recover` without `state:recover` does not grant access to state bytes.

Emergency state is decrypted and parsed as a bounded stream, then copied
through envelope encryption without buffering the state body. The commit
accepts it only as the exact lineage and serial successor, records a new
immutable state version under the recovery fence, and never overwrites a newer
version. A verified-current-state resolution advances the same fence without
changing the state pointer. Both paths revoke the old runner authority and
leave the next ordinary action as a new plan.

Idempotent response-loss replay is storage-independent once recovery is
completed. A prepared emergency replay verifies every byte of its persisted
state and required outputs through EOF before commit. Corrupt persisted
metadata or content fails closed without reading the emergency source. A
source copy is resumed only when the persisted target is genuinely absent.

Customer OpenTofu cloud credentials remain a separate non-browser trust path.
A service-account token must be explicitly scoped to `state:read`; the
`/api/v2` compatibility flow is not authorized by a dashboard session or by
the member role. Managed-run credentials are separate internal principals and
do not inherit that customer's permissions.

## Streaming limit

State plaintext is limited to exactly `100_000_000` bytes. A known larger body
is rejected before ingestion. An unknown-length stream is rejected as soon as
its incremental counter observes byte `100_000_001`.

Uploads flow through bounded encryption and digest calculation to R2.
Downloads flow from R2 through bounded authenticated decryption to the client.
Neither path concatenates, clones, or parses the complete state in Worker
memory.

## Sensitive outputs

The native compatibility surface distinguishes ordinary and sensitive state
outputs. Access is authorized at the endpoint; a sensitive value is not made
safe merely because its metadata is visible.

Treat every raw state download as sensitive. Store it outside the repository,
limit access, and delete temporary copies after migration or investigation.

## Migration

Local-to-cloud migration is covered by a real OpenTofu integration test that
creates local state, adds the StackShip cloud block, initializes interactively,
and proves that lineage, serial, and outputs arrive through the native
contract.

See [migrate state](/start/migrate) for the operator sequence.

## Related pages

- [Access tokens](/admin/tokens)
- [Migrate state](/start/migrate)
- [Architecture](/architecture)

Source: https://docs.stackship.run/workspaces/state/index.mdx
