Terraform provider
Preview The official Terraform provider lets you
declare your managed.dev fleet as code. It’s generated from the same
OpenAPI 3.1 spec as the SDKs and the
mf CLI, so its resources and arguments track the
API exactly. You get the usual Terraform workflow —
plan, apply, drift detection, state — over sites, environments, teams, and the
scoped keys that drive your other automation.
resources
Section titled “resources”The provider models the resource graph as Terraform resources. The runtime and scopes that the API treats as first-class are declared right in HCL.
| Resource | Manages | Key arguments |
|---|---|---|
forge_site |
A site/app | name, project_id, runtime, repository |
forge_environment |
An environment under a site | site_id, name, branch, type (production/staging/preview) |
forge_team |
A team | name, members |
forge_api_key |
A scoped key for other automation | team_id, scopes, ttl, resource_constraint |
A runtime of static advertises a different
capability set than wordpress, so the
provider validates that an argument you set is actually supported by the runtime
before it ever calls the API.
a worked example
Section titled “a worked example”This configuration provisions a WordPress site, a preview environment wired to a
branch, and a least-privilege service key for a deploy bot —
all in one apply.
terraform { required_providers { forge = { source = "managed-dev/forge" version = "~> 0.1" } }}
provider "forge" { # Reads FORGE_TOKEN from the environment by default. # Use an mfs_ service token here, never a personal key. version = "2026-06-23"}
resource "forge_site" "store" { name = "acme-store" project_id = "proj_01J6ABCD" runtime = "wordpress" repository = "git@github.com:acme/store.git"}
resource "forge_environment" "checkout_preview" { site_id = forge_site.store.id name = "feature-checkout" branch = "feature/checkout" type = "preview"}
# A scoped key for the deploy bot — the Deploy preset, expanded.resource "forge_api_key" "deploy_bot" { team_id = "team_01J5WXYZ" scopes = [ "sites:read", "deployments:write", "environments:write", "domains:read", "jobs:read", "observability:read", ] ttl = "90d" resource_constraint = forge_site.store.id}
output "preview_url" { value = forge_environment.checkout_preview.preview_url}propose and validate (dry-run)
Section titled “propose and validate (dry-run)”Beyond terraform plan, the provider supports a server-side propose / validate
step (DigitalOcean app-spec style): it submits the desired configuration to the API,
which returns the set of jobs it would run and any
capability or
scope conflicts — without changing anything. This catches a bad
runtime/argument combination or an insufficiently-scoped key before apply, not
in the middle of it.
terraform planmf propose validate --from-plan tfplanjobs in CI: ETag long-poll, not SSE
Section titled “jobs in CI: ETag long-poll, not SSE”CI runners and the Terraform provider are hostile to long-lived
SSE streams — connections get cut, proxies buffer, and
there’s no terminal to tail. So when an apply triggers an async job, the provider
waits with ETag long-poll instead: it polls GET /v1/jobs/{id} with
If-None-Match, getting a cheap 304 Not Modified until the job’s state changes,
then reads the new body. This is the same job, just consumed over a transport that
survives CI. See async jobs for the full model and the
other two consumption paths.