Getting started
This guide matches the README quickstart for the 0.35.x line. TerraDart is alpha — breaking changes land only on minor bumps; see Status & versioning for the change policy and the path to beta.
Choose your path
Section titled “Choose your path”Each of these is one path you can paste, from installing the terradart command to an apply. The rest of this page builds the same pieces by hand, one at a time.
Prerequisites
Section titled “Prerequisites”- Dart SDK ≥ 3.10
- Nothing else to install for plan and apply: the
terradartcommand downloads a checksum-verified OpenTofu on first use, or runs thetofuorterraformalready on yourPATH. - Appwrite only: Terraform on your
PATH. Theappwrite/appwriteprovider is published to the Terraform registry only, so OpenTofu cannot install it, and an Appwrite Stack runs on Terraform (--engine terraform, orterradart: engine: terraforminpubspec.yaml). - Credentials for your provider at apply time — Google Cloud Application Default Credentials (
gcloud auth application-default login), the AWS SDK credential chain,CLOUDFLARE_API_TOKEN, orAPPWRITE_API_KEY. Synth needs none, and none are written into the output.
1. Create a project
Section titled “1. Create a project”Install the terradart command once, and let it write the project:
dart pub global activate terradart_cliterradart init --provider google --env dev,prd --gcp-project dev=acme-dev,prd=acme-prd --backend localcd infraterradart plan --env devterradart init writes infra/ (or the directory you name), runs dart pub get in it, and prints what to run next. The project plans as it is:
| File | Holds |
|---|---|
pubspec.yaml | terradart_core and the provider packages, at the command’s own version; terradart: engine: terraform with Appwrite |
lib/env.dart | the Env enum: each environment and its values — project or account ID, region, state bucket |
lib/stack.dart | a Stack with the provider, the backend, one resource and one output |
bin/infra.dart | the entry point, which hands the environments to runEnvironments |
.gitignore | tf-out/ and .terradart/ |
README.md, AGENTS.md | the next steps, and the commands and Agent Skill a coding agent works with |
A value you did not give is a placeholder marked TODO in lib/env.dart.
In a terminal, terradart init asks for what the flags leave out — the providers, the environment names, each environment’s project, region or account ID, whether a state bucket exists — and ends by printing the terradart init ... command that gives the same answers as flags. Asked whether a bucket for the state already exists, no keeps the state in a local file (move it later with terradart state migrate); yes takes its name and the backend of your provider. Without a terminal it never asks and never chooses for you: --provider, --env and --backend (or --state-bucket) are required, and a run without them prints the command to edit; --defaults accepts dev,prd and local state. --dry-run lists the files without writing them, and an existing file is never overwritten without --force.
| Flag | Takes |
|---|---|
--provider | google, aws, cloudflare, appwrite, comma-separated |
--env | any lowerCamelCase names, comma-separated |
--gcp-project, --aws-region, --aws-account, --cloudflare-account, --appwrite-endpoint, --appwrite-project | <env>=<value> pairs, or one value for every environment |
--state-bucket | a bucket that already exists, one name or <env>=<name> pairs: GCS for google, S3 for aws, R2 for cloudflare |
--backend | local (a file under tf-out/), gcs, s3 or r2 |
--defaults | --env dev,prd --backend local |
--[no-]flutter | wiring to the Flutter app in the current directory |
Run inside a Flutter app, it also generates the reader the app imports into the app’s lib/generated/, and declares the define file the app builds with:
terradart initcd infra && terradart apply --env dev && cd ..flutter run --dart-define-from-file=infra/.terradart/dart_defines.dev.jsonA directory that already holds *.tf files, at any depth, gets no scaffold: terradart init names where it found them and points at terradart migrate instead, and in a terminal offers to run its report.
The rest of this guide builds the same pieces by hand, one at a time.
2. Add dependencies
Section titled “2. Add dependencies”terradart_core plus the package for each provider your Stack uses (all on pub.dev). Every provider barrel re-exports terradart_core, so the Stack itself imports only the barrels:
name: my_appenvironment: sdk: ^3.10.0dependencies: terradart_core: ^0.35.0 terradart_google: ^0.35.0 # terradart_google_beta: ^0.35.0 # beta-only types (Firebase, ...)name: my_appenvironment: sdk: ^3.10.0dependencies: terradart_core: ^0.35.0 terradart_aws: ^0.35.0name: my_appenvironment: sdk: ^3.10.0dependencies: terradart_core: ^0.35.0 terradart_cloudflare: ^0.35.0name: my_appenvironment: sdk: ^3.10.0dependencies: terradart_core: ^0.35.0 terradart_appwrite: ^0.35.0terradart: engine: terraform # Appwrite currently needs Terraform on PATHdart pub getdart pub global activate terradart_cliThe second line installs the terradart command, which synthesizes the Stack and runs plan or apply for you. To pin it per project instead, dart pub add --dev terradart_cli and run it as dart run terradart_cli:terradart.
Each provider package wraps the whole catalog of its Terraform provider (Google’s beta-only types live in terradart_google_beta), so any resource the provider documents has a factory. One Stack can mix packages.
2. Define a Stack
Section titled “2. Define a Stack”A Stack is one Terraform root module. add registers a resource; addConstant and addOutput declare what your app reads back — a constant at synth time, an output after apply — and appExports names the Dart file they are generated into:
import 'package:terradart_google/provider.dart';import 'package:terradart_google/pubsub.dart';
final class OrdersStack extends Stack { OrdersStack({required String projectId}) : super( providers: [GoogleProvider(project: projectId)], appExports: AppExports('lib/generated/orders_stack.app.dart'), ) { final topic = add(GooglePubsubTopic( 'orders', name: .literal('orders-prod'), )); addConstant('ordersTopicName', .ref(topic.name)); addOutput('orders_topic_id', topic.id); }}import 'package:terradart_aws/provider.dart';import 'package:terradart_aws/sqs.dart';
final class QueueStack extends Stack { QueueStack() : super( providers: [const AwsProvider(region: 'us-east-1')], appExports: AppExports('lib/generated/queue_stack.app.dart'), ) { final queue = add(AwsSqsQueue( 'orders', name: .literal('orders-prod'), )); addConstant('ordersQueueName', .ref(queue.name)); addOutput('orders_queue_url', queue.url); }}More on AWS — Lambda, ECS Express Mode, S3 + CloudFront: AWS.
import 'package:terradart_cloudflare/dns.dart';import 'package:terradart_cloudflare/provider.dart';import 'package:terradart_cloudflare/zone.dart';
final class EdgeStack extends Stack { EdgeStack({required String accountId}) : super( providers: [const CloudflareProvider()], appExports: AppExports('lib/generated/edge_stack.app.dart'), ) { final zone = add(CloudflareZone( 'main', name: .literal('example.com'), account: ZoneAccount(id: .literal(accountId)), )); final api = add(CloudflareDnsRecord( 'api', zoneId: zone.ref, // only a CloudflareZone fits here name: .literal('api.example.com'), type: .cname, ttl: .literal(1), content: .content(.literal('ghs.googlehosted.com')), proxied: .literal(true), )); addConstant('apiHost', .ref(api.name)); addOutput('zone_id', zone.id); }}Runnable: examples/cloudflare_dns_quickstart.
import 'package:terradart_appwrite/provider.dart';import 'package:terradart_appwrite/storage.dart';
final class UploadsStack extends Stack { UploadsStack() : super( providers: [ const AppwriteProvider( endpoint: 'https://cloud.appwrite.io/v1', projectId: 'my-project', ), ], appExports: AppExports('lib/generated/uploads_stack.app.dart'), ) { final uploads = add(AppwriteStorageBucket( 'uploads', name: .literal('uploads'), fileSecurity: .literal(true), )); addOutput('uploads_bucket_id', uploads.id); }}Runnable: examples/appwrite_quickstart, which covers every Appwrite factory.
.literal(...) is a value known now; ... is one Terraform resolves during apply. An argument that names another resource takes that resource’s ref (zoneId: zone.ref) and does not compile with the wrong kind. Both are Dart 3.10 dot shorthands, like the enum in .cname — every argument is typed, so the type name can be left out. A block inside another block or inside a sealed choice is built the same way, with .new(...): delivery: .pushConfig(.new(pushEndpoint: ...)). Keep the class name on a resource’s own arguments (template: CloudRunV2ServiceTemplate(...)), where it tells a reader which block starts.
Writing arguments
Section titled “Writing arguments”Pick the form by where the value comes from. The argument’s type tells you which forms it takes, and a dot shorthand (.literal, .new, .providedAl2023) names the constructor of that type.
| The value is | Write | Example |
|---|---|---|
| known when you synth | .literal(...) | functionName: .literal('hello') |
another resource of this Stack (a RefTo<R> input) | its ref | role: role.ref |
| one attribute of another block | its getter | assumeRolePolicy: trust.json |
a resource outside this Stack (a RefTo<R> input) | .literal(id) | zoneId: .literal('023e105f4ecef8ad9ca31a8372d0c353') |
| one of a fixed set (an enum) | the member | runtime: .providedAl2023 |
| one of several exclusive arguments (a sealed type) | the variant | code: .filename(.literal('build/fn.zip')) |
| a nested block | its helper class; .new(...) inside another block or a variant | environment: LambdaFunctionEnvironment(...) |
| a Terraform variable | the handle variable<T>() returns | memorySize: memory |
a variable in an enum or RefTo<R> input | .arg(handle) | runtime: .arg(runtimeName) |
a secret (a Sensitive<T> input) | a sensitive variable, never a literal | value: .value(dbPassword) |
| a reference inside a literal list or map | the getter’s .interpolation | {'ROLE_ARN': role.arn.interpolation} |
| anything else Terraform evaluates | .expression(...) | .expression(r'${file("trust.json")}') |
final memory = variable<num>('memory_mb');final runtimeName = variable<String>('runtime');final dbPassword = variable<String>('db_password', sensitive: true);
final trust = add( DataAwsIamPolicyDocument( 'trust', statement: [ DataIamPolicyDocumentStatement( actions: .literal(['sts:AssumeRole']), principals: [ .new( type: .literal('Service'), identifiers: .literal(['lambda.amazonaws.com']), ), ], ), ], ),);final role = add(AwsIamRole('hello', assumeRolePolicy: trust.json));add( AwsLambdaFunction( 'hello', functionName: .literal('hello'), role: role.ref, runtime: .arg(runtimeName), code: .filename(.literal('build/fn.zip')), memorySize: memory, environment: LambdaFunctionEnvironment( variables: .literal({'ROLE_ARN': role.arn.interpolation}), ), ),);add( AwsSsmParameter( 'db_password', name: .literal('/hello/db_password'), type: .securestring, value: .value(dbPassword), ),);More on each row: Writing arguments.
3. Synth Terraform JSON
Section titled “3. Synth Terraform JSON”Synthesis is plain Dart: bin/infra.dart, the project’s entry point, constructs the Stack and hands it to runStack, which writes it. The rest of this guide follows the Google Cloud Stack; for the others, construct QueueStack(), EdgeStack(accountId: ...) or UploadsStack() the same way.
import 'package:my_app/orders_stack.dart';import 'package:terradart_core/terradart_core.dart';
Future<void> main(List<String> args) => runStack(args, () => OrdersStack(projectId: 'YOUR-PROJECT-ID'));terradart synthThis runs bin/infra.dart, which writes tf-out/main.tf.json (with the orders_topic_id output) and lib/generated/orders_stack.app.dart (with the ordersTopicName constant and the OrdersStackOutputs reader). dart run bin/infra.dart does the same without the command.
4. Plan and apply
Section titled “4. Plan and apply”terradart planterradart applyEach command synthesizes first, then runs init and plan or apply in tf-out/ with the tofu or terraform on your PATH, or a checksum-verified OpenTofu it downloads when there is neither. A Stack that uses Appwrite runs on the terraform on your PATH, and stops before init when there is none or --engine tofu asks for OpenTofu. For several environments, declare them as a Dart enum and hand it to runEnvironments in bin/infra.dart; terradart apply --env prod then runs one of them. See Environments and The terradart command.
On a new Google Cloud project every API is off, so the first apply fails with a 403 until the Stack turns on the ones it uses with enableApis([.pubsub]) — see Enabling APIs. The project terradart init writes does it already.
The output is ordinary Terraform JSON, so your existing remote state backend and modules stay unchanged — TerraDart only replaces HCL/JSON authoring.
5. Read it from your app
Section titled “5. Read it from your app”App code imports the generated file instead of repeating strings:
import 'dart:io';
import 'generated/orders_stack.app.dart';
bool acceptsTopic(String eventTopic) => eventTopic == OrdersStackConstants.ordersTopicName;
/// Known only after apply, so it comes from the generated outputs reader.String ordersTopicId() => OrdersStackOutputs.fromEnvironment(Platform.environment).ordersTopicId;A deployed service reads outputs from its environment (ORDERS_TOPIC_ID) — the Stack passes them with outputEnvironment() — and a script can read the define file terradart outputs writes. A Flutter or web client is built with them instead. Add addDartDefineOutput() to the Stack, and terradart apply writes the define file the build compiles in:
terradart applyflutter build web --dart-define-from-file=.terradart/dart_defines.jsonWith environments, each one gets its own file: terradart apply --env prod writes .terradart/dart_defines.prod.json. The client reads each value with its type through the same OrdersStackOutputs reader; see Outputs in client apps.
Rename orders-prod in the Stack and the subscriber follows on the next synth — there is no second copy of the string to update. Rename or remove the constant and dart analyze fails. See How TerraDart works — the app boundary and the runnable pubsub quickstart (lib/subscriber_stub.dart).
Next steps
Section titled “Next steps”- Why TerraDart — motivation and comparisons
- Outputs in client apps — build a Flutter or web client with the values of an apply
- Environments — dev, staging and prod as a Dart enum, one define file each
- How TerraDart works — synth, plan and apply, and the values your app reads back
- Coverage — every factory of every provider package, its barrel, and runnable examples
- Providers: Google Cloud (with google-beta and Firebase), AWS, Cloudflare, Appwrite
- Migrating from HCL — bring an existing Terraform tree over
- Upgrading — read before every minor bump
- Status & versioning — alpha vs beta vs 1.0
- Examples and cookbook for fuller stacks