Start from an existing Flutter app
Your Flutter app needs values that exist only after an apply: a topic ID, an API URL, a bucket. Here they reach the app typed, through a reader class TerraDart generates and the --dart-define-from-file flag Flutter already has. Dart and Flutter are all you install; the terradart command downloads a checksum-verified OpenTofu the first time it needs an engine.
The steps use Google Cloud, with two projects: one for dev, one for prd.
The layout
Section titled “The layout”The infrastructure is a Dart package of its own, infra/, inside the app:
my_app/├── pubspec.yaml the Flutter app├── lib/│ ├── main.dart│ └── generated/infra.g.dart written by terradart synth: the typed reader└── infra/ ├── pubspec.yaml terradart_core and the provider packages ├── lib/env.dart dev and prd, and their values ├── lib/stack.dart the Stack ├── bin/infra.dart the entry point the terradart command runs └── .terradart/ dart_defines.<env>.json, written by terradart applyIt is a package of its own because it runs on the Dart VM (dart run, through the terradart command), where the app needs the Flutter SDK, and so the TerraDart packages never enter the app’s dependencies or its binary. The two meet at one generated file: the Stack writes lib/generated/infra.g.dart into the app, and the app imports it.
1. Install the command
Section titled “1. Install the command”dart pub global activate terradart_cliterradart --helpterradart lands in ~/.pub-cache/bin. If the second line is not found, add that directory to your PATH, as dart pub global activate says.
2. Create infra/
Section titled “2. Create infra/”From the app’s directory, the one with its pubspec.yaml:
terradart init --provider google --env dev,prd --gcp-project dev=my-app-dev,prd=my-app-prd --backend localUse your own project IDs. terradart init sees the Flutter app (a pubspec.yaml that depends on flutter) and wires infra/ to it:
Created my_app_infra in infra (google; environments dev, prd; local state; wired to the Flutter app): pubspec.yaml lib/env.dart lib/stack.dart bin/infra.dart .gitignore README.md AGENTS.mdDefaults: --flutter.Synth writes lib/generated/infra.g.dart for the app to read the Stack's outputs.> dart pub get...
Next: cd infra terradart apply --env dev cd .. flutter run --dart-define-from-file=infra/.terradart/dart_defines.dev.jsonIn a terminal, terradart init without flags asks for each of them instead, and asks before it wires the app. --no-flutter leaves the app alone. Every flag is in The terradart command.
3. Read the Stack
Section titled “3. Read the Stack”/// The environments of the Stack: `terradart plan --env <name>`/// picks one. Replace each placeholder marked TODO.enum Env { dev(projectId: 'my-app-dev', region: 'us-central1'), prd(projectId: 'my-app-prd', region: 'us-central1');
const Env({required this.projectId, required this.region});
/// The Google Cloud project the Stack deploys to. final String projectId;
/// The default Google Cloud region. final String region;}Two lines connect the Stack to the app. appExports writes the reader into the app’s lib/generated/, and addDartDefineOutput() declares the define file — every non-sensitive output, as the strings --dart-define-from-file reads:
import 'package:terradart_google/project.dart';import 'package:terradart_google/provider.dart';import 'package:terradart_google/pubsub.dart';import 'package:terradart_time/terradart_time.dart';
import 'env.dart';
/// The infrastructure of one environment. Replace the example resources/// with your own.final class MyAppInfraStack extends Stack { MyAppInfraStack({required Env env}) : super( providers: [ GoogleProvider(project: env.projectId, region: env.region), const TimeProvider(), ], backend: const LocalBackend(), appExports: AppExports('../lib/generated/infra.g.dart'), ) { // Enables the Pub/Sub API on the project and waits for it to propagate. final apis = enableApis([.pubsub]); final topic = add( GooglePubsubTopic( 'events', name: .literal('${env.name}-events'), dependsOn: apis, ), ); addOutput('events_topic_id', topic.id);
// The outputs above, as the file `terradart apply --env <name>` writes // for `flutter run --dart-define-from-file`. addDartDefineOutput(); }}Each addOutput becomes a typed getter of MyAppInfraStackOutputs; the events_topic_id output is eventsTopicId. Add the resources your app uses — a Cloud Run service, a bucket, Firebase — and an output for each value the app reads. Every resource of the provider has a factory, listed in Coverage.
infra/bin/infra.dart gives runEnvironments the first environment as defaultEnv, so a command without --env runs against dev — see Start from an empty directory. The commands below name the environment anyway, as a build script should.
4. Synth and validate
Section titled “4. Synth and validate”cd infraterradart synthterradart validate --env devterradart synth writes tf-out/dev/ and tf-out/prd/, and the reader, ../lib/generated/infra.g.dart. terradart validate checks the configuration without credentials or state; the first time, it downloads OpenTofu into your user cache:
env: dev (--env)Using OpenTofu 1.13.1 (no tofu or terraform on PATH; managed OpenTofu 1.13.1)> tofu init -backend=false -input=false (in tf-out/dev)...> tofu validate (in tf-out/dev)Success! The configuration is valid.5. Plan and apply
Section titled “5. Plan and apply”Sign in as an account that can create resources in the project, then plan and apply dev:
gcloud auth application-default loginterradart plan --env devterradart apply --env devThe apply shows the plan and asks before it changes anything. Then it writes the outputs to the define file and prints the lines that build with it:
Wrote 1 dart-defines to .terradart/dart_defines.dev.json flutter run --dart-define-from-file=.terradart/dart_defines.dev.json flutter build <target> --dart-define-from-file=.terradart/dart_defines.dev.jsonThose paths are relative to infra/; from the app, the file is infra/.terradart/dart_defines.dev.json.
6. Read the outputs in the app
Section titled “6. Read the outputs in the app”The reader’s fromDartDefine constructor reads the values compiled into the app, so it is a constant:
import 'package:flutter/material.dart';
import 'generated/infra.g.dart';
const infra = MyAppInfraStackOutputs.fromDartDefine();
void main() => runApp( MaterialApp( home: Scaffold(body: Center(child: Text('Events: ${infra.eventsTopicId}'))), ),);Run it with the define file of dev:
cd ..flutter run --dart-define-from-file=infra/.terradart/dart_defines.dev.jsonA build without the file fails at the first getter it reads, with a StateError that names the variable and the output. Rename the output in the Stack and the getter changes on the next synth, so flutter analyze finds every place the app read it.
7. Production, and builds in CI
Section titled “7. Production, and builds in CI”Apply prd the same way, and build the release with its file:
cd infraterradart apply --env prdcd ..flutter build web --dart-define-from-file=infra/.terradart/dart_defines.prd.jsonA build job should read the applied state, not change it. terradart outputs synthesizes — which also writes the reader — then writes the define file from the state alone, with no plan and no apply:
cd infraterradart outputs --env prdcd ..flutter build apk --dart-define-from-file=infra/.terradart/dart_defines.prd.jsonFor that job to see the state, it has to live in a bucket rather than in infra/tf-out/ on your machine: create one, set the Stack’s backend: to GcsBackend(bucket: 'my-app-tfstate', prefix: 'my_app_infra/${env.name}'), and run terradart state migrate --env dev and terradart state migrate --env prd once, from infra/. Each asks before it copies the state.
Only public values belong in a client: anything compiled into an app can be read by whoever has it. The define file leaves sensitive outputs out, and secrets stay on a server — see Outputs in client apps.
- Outputs in client apps — one define file per client, the
dartcommand, servers and scripts - Environments — each environment’s own project, state and define file
- Appwrite and Google Cloud with Firebase — backends for a Flutter app
- Let an AI agent do it — the
AGENTS.mdinfra/came with, and the Agent Skill