Skip to content

Why TerraDart

If you read the landing page first: TerraDart keeps Terraform as the execution layer while you author infrastructure in Dart. This page goes deeper on motivation, the type system, and where TerraDart sits next to other IaC tools.

If your team ships Flutter on the client and Dart on the server — on Cloud Run, Lambda, ECS, an Appwrite backend, or any other host — there is usually one layer left where Dart’s tooling does not reach: infrastructure definitions — often maintained in HCL, a Terraform-compatible pipeline, or cloud consoles. TerraDart closes that gap without asking you to give up Terraform itself.

LayerAlready Dart?
Mobile UI (Flutter)✅
Backend handlers (Cloud Run / Lambda / Shelf)✅
Application business logic✅
Infrastructure definitions❌ — HCL, Terraform-compatible tooling, or console

That bottom row is what people mean by the last non-Dart layer: everything above it shares dart analyze, refactors, and IDE navigation; infrastructure often does not.

A boundary held together by string literals

Section titled “A boundary held together by string literals”

Topic names typed in HCL or the console and again in your function handlers. Renames do not reach subscribers via dart fix. IAM member strings drift with no compiler visibility. Infra values flowing into app code are often the place Dart’s type system never sees.

TerraDart pulls infrastructure into the same authoring surface as the rest of your code, and hands its values back: addConstant and addOutput become a generated Dart file the app imports, so a topic name or a service URL has one source. dart analyze catches resource-name typos. Refactoring a Stack class is the same operation as refactoring any other Dart class. The same final / sealed discipline that already protects your domain model can protect your network topology.

TerraDart does not replace Terraform. The terradart command runs plan and apply with the tofu or terraform you already have (or a pinned OpenTofu it downloads), but it does not own your state file, and it does not introduce a new provider or a new lock format.

What it does is take Terraform’s authoring model — provider blocks, resource blocks, references, lifecycle hooks — and express it as Dart code that generates the same *.tf.json Terraform would have accepted from HCL. Planning, applying and your remote state backend stay exactly where they are.

This is intentionally narrower than Pulumi, which has its own runtime, its own state model, and its own resource graph. With TerraDart the boundary is sharp: Dart is the authoring layer, Terraform is the execution layer. If you migrate off TerraDart tomorrow, your existing state and your existing modules are unchanged.

CDKTF used to occupy this same “code-first authoring of Terraform” niche, but only for TypeScript / Python / Java / Go — never Dart. CDKTF was archived in December 2025, leaving a Dart-shaped hole that TerraDart now fills.

Three concrete patterns where the Dart type system catches what HCL cannot:

Terraform schemas often have fields that accept one of a small, fixed set of strings — INGRESS_TRAFFIC_ALL vs INGRESS_TRAFFIC_INTERNAL_ONLY, for example. TerraDart emits these as Dart enums:

add(GoogleCloudRunV2Service(
'api',
name: .literal('api'),
location: .literal('us-central1'),
ingress: .all, // .al → compile error
template: CloudRunV2ServiceTemplate(containers: [
.new(image: .literal('us-docker.pkg.dev/cloudrun/container/hello')),
]),
));

Sealed classes for exactly-one-of nested blocks

Section titled “Sealed classes for exactly-one-of nested blocks”

Some Terraform blocks accept exactly one variant from a set — for instance, a BigQuery dataset’s access block has 8 mutually exclusive variants (user_by_email, group_by_email, special_group, domain, iam_member, view, dataset, routine). TerraDart emits these as a sealed class hierarchy:

final reader = add(GoogleServiceAccount('reader', accountId: .literal('reader')));
final runSa = add(GoogleServiceAccount('run', accountId: .literal('run')));
add(GoogleBigqueryDataset(
'events',
datasetId: .literal('events'),
access: [
.userByEmail(userByEmail: reader.email, role: .literal('OWNER')),
.iamMember(iamMember: runSa.principal, role: .literal('READER')),
],
));

The compiler enforces that exactly one variant is constructed per entry. Each variant is a factory constructor on the sealed type (BigqueryDatasetAccess.userByEmail), so a Dart 3.10 dot shorthand picks it and IDE completion lists the choices. A sealed argument is named for what its members are alternatives of, like a protobuf oneof: AwsLambdaFunction(code: .filename(...)), CloudflareDnsRecord(content: .content(...)).

Stack, Resource, and Data are declared abstract base class. Your own subclasses must use a class modifier:

final class AppInfraStack extends Stack {
AppInfraStack() : super(providers: [GoogleProvider(project: 'my-project')]);
}

This prevents two foot-guns: forgetting to extend (using implements would skip the base-class state that synth needs), and accidentally subclassing an internal class that was not meant to be extended further. Both are compile errors.

TerraDart fits teams already centered on Dart, already running Terraform (or a compatible pipeline) or managing infrastructure in cloud consoles, where the seam between infra and app code hurts.

TerraDartHCLCDKTFPulumi
Dart authoringYesNoNo (TypeScript, Python, Java, Go)Community runtime only
Type-safe handoff to your appYes, compile-timeNo (terraform output + parse)No DartNo typed Dart export
Runs on OpenTofu / TerraformYesYesYesDifferent model
Curated Dart factoriesYesNoNoNo
Project statusAlphaMatureArchived 2025Active

HCL has provider schema types; CDKTF bindings are typed in TypeScript and other languages. TerraDart’s difference is Dart-native authoring without replacing your Terraform execution pipeline.

HCL is the native, mature option. If your team is comfortable maintaining HCL alongside your Dart application code, there is no urgency to switch. TerraDart’s value proposition only matters once the cost of context-switching between Dart and HCL — and the absence of refactor / typecheck across that boundary — starts to bite.

CDKTF would have been the obvious model to follow, except it was archived by HashiCorp in December 2025 and never targeted Dart in any case. TerraDart is the Dart-shaped slot CDKTF did not reach.

Pulumi is a different model entirely: its own runtime, its own state, and its own resource graph. If your team is already invested in Pulumi and wants to author in Dart, consider Pulumi Dart (kingwill101/pulumi-dart), an active community language runtime for Pulumi. TerraDart is for teams who already use Terraform and want type-safe Dart authoring without replacing their Terraform state or execution pipeline.

TerraDart makes sense when you are already centered on Dart (Flutter plus a Dart backend, on whichever cloud), already using Terraform (or heading there), and the gap between those two — IaC stuck outside your Dart repo — is the part that hurts.

Each provider package wraps the whole catalog of its Terraform provider at its pinned release: Google Cloud in terradart_google, with the beta-only types in terradart_google_beta; terradart_aws; terradart_cloudflare; and terradart_appwrite.

Each provider has its own page — Google Cloud, AWS, Cloudflare, Appwrite — and every provider’s factory list with example pointers is on Coverage; see also status and How TerraDart works — Provider packages. Runnable stacks live in examples and the cookbook. Upgrading across minors? Read Upgrading first.

  • Not a Terraform replacement — state and apply stay in Terraform.
  • Not a multi-cloud abstraction layer — wrappers faithfully mirror provider schemas rather than imposing cross-cloud abstractions.
  • Not a constructs framework in the pre-1.0 cycle.
  • Not a module system — addModule(ModuleCall(...)) calls an existing Terraform module by its source, and HCL files beside the generated *.tf.json feed the same apply; TerraDart does not turn modules into Dart.