TerraDart Type-safe IaC for Dart.
Typed outputs, straight into your Flutter app.
terradart apply
Write your cloud as a Dart class — Google Cloud, AWS, Cloudflare, Appwrite. The compiler checks every reference between resources, one command provisions it, and the values your app needs arrive as typed Dart.
dart pub global activate terradart_cli How it works
From a Dart class to a running app
The Stack declares the infrastructure and the values the app reads. One command provisions it and writes those values for the client build. Rename an output and the app stops compiling — not in production.
-
1Write a Stack
lib/app_stack.dart final class AppStack extends Stack { AppStack({required String projectId}) : super( providers: [GoogleProvider(project: projectId)], appExports: AppExports('lib/generated/app_stack.app.dart'), ) { final api = add(GoogleCloudRunV2Service( 'api', name: .literal('app-api'), location: .literal('asia-northeast1'), template: CloudRunV2ServiceTemplate( containers: [ .new(image: .literal('us-docker.pkg.dev/my-app/web/api')), ], ), )); addOutput('api_url', api.uri); addDartDefineOutput(); } } -
2Apply it
terradart apply
- synth runs bin/infra.dart
- plan + apply provisions the Stack
- outputs .terradart/dart_defines.json
Uses the engine on your PATH, or downloads a pinned, checksum-verified one.
-
3Read it in Flutter
lib/api_client.dart import 'generated/app_stack.app.dart'; const outputs = AppStackOutputs.fromDartDefine(); Uri endpoint(String path) => Uri.parse(outputs.apiUrl).resolve(path);flutter run \
--dart-define-from-file=.terradart/dart_defines.json
Outputs in client apps The terradart command How TerraDart works
Providers
One API, whichever cloud you run on
Every package wraps its provider's full catalog with the same rules: an argument that names another resource
takes its ref, fixed values are enums, and exactly-one-of choices are sealed types. One Stack can mix
them.
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, region: 'us-central1')],
appExports: AppExports('lib/generated/orders_stack.app.dart'),
) {
// …
final topic = add(
GooglePubsubTopic(
'orders',
name: .literal('orders-prod'),
messageRetentionDuration: .literal(
const Duration(days: 7).toTfDurationString(),
),
),
);
add(
GooglePubsubSubscription(
'orders_push',
name: .literal('orders-push'),
topic: topic.ref,
ackDeadlineSeconds: .literal(60),
delivery: .pushConfig(
.new(pushEndpoint: .literal('https://app.example.com/push')),
),
),
);
// …
addConstant('ordersTopicName', .ref(topic.name));
addOutput('orders_topic_id', topic.id);
}
}import 'package:terradart_aws/lambda.dart';
import 'package:terradart_aws/provider.dart';
final class AwsLambdaStack extends Stack {
AwsLambdaStack()
: super(
providers: [
AwsProvider(
region: Platform.environment['AWS_REGION'] ?? 'us-east-1',
defaultTags: const {'app': 'terradart-lambda-quickstart'},
),
],
appExports: AppExports('lib/generated/aws_lambda_stack.app.dart'),
) {
// …
final fn = AwsLambdaFunction(
'hello',
functionName: .literal(_functionName),
role: role.ref,
runtime: .providedAl2023,
handler: .literal('bootstrap'),
architectures: [.x8664],
code: .filename(.literal('../build/bootstrap.zip')),
memorySize: .literal(128),
timeout: .literal(10),
loggingConfig: LambdaFunctionLoggingConfig(
logFormat: .text,
logGroup: logs.ref,
),
);
// …
final url = add(
AwsLambdaFunctionUrl(
'hello',
functionName: fn.ref,
authorizationType: .none,
),
);
addOutput(
'function_url',
url.functionUrl,
description: 'Public URL of the function.',
);
addDartDefineOutput();
}
}import 'package:terradart_cloudflare/dns.dart';
import 'package:terradart_cloudflare/provider.dart';
import 'package:terradart_cloudflare/zone.dart';
final class CloudflareDnsStack extends Stack {
CloudflareDnsStack() : super(providers: [const CloudflareProvider()]) {
final zone = CloudflareZone(
'main',
name: .literal('terradart-demo.example'),
account: ZoneAccount(id: .literal('terradart-demo-account')),
);
add(zone);
add(
CloudflareDnsRecord(
'api',
zoneId: zone.ref,
name: .literal('api.terradart-demo.example'),
type: .cname,
ttl: .literal(1),
content: .content(.literal('ghs.googlehosted.com')),
proxied: .literal(true),
),
);
}
}import 'package:terradart_appwrite/auth.dart';
import 'package:terradart_appwrite/provider.dart';
import 'package:terradart_appwrite/storage.dart';
import 'package:terradart_appwrite/tablesdb.dart';
final class AppwriteDemoStack extends Stack {
AppwriteDemoStack()
: super(
providers: [
const AppwriteProvider(
endpoint: 'https://cloud.appwrite.io/v1',
organizationId: 'terradart-demo-org',
projectId: 'terradart-demo',
),
],
) {
// …
final team = add(AppwriteAuthTeam('editors', name: .literal('editors')));
// …
final bucket = add(
AppwriteStorageBucket(
'uploads',
name: .literal('uploads'),
fileSecurity: .literal(true),
maximumFileSize: .literal(10485760),
permissions: .literal([
.read(.any),
.write(.team(team.ref, role: 'owner')),
]),
),
);
// …
final db = add(AppwriteTablesdb('main', name: .literal('main')));
final table = add(
AppwriteTablesdbTable(
'users',
databaseId: db.ref,
name: .literal('users'),
permissions: .literal([
.read(.users()),
.create(.users(verified: true)),
]),
),
);
// …
}
}Type-safe
The compiler reviews your infrastructure
Mistakes that usually surface halfway through a deploy show up as red squiggles while you type.
-
topic: orders.refReferences, not strings
An argument that names another resource takes that resource's ref. Pass the wrong kind and it does not compile.
-
ingress: .allEnums for fixed values
Every fixed value set is an enum, written with Dart 3.10 dot shorthands and completed by your IDE.
-
delivery: .pushConfig(...)Sealed choices
When a block takes exactly one of several shapes, it is a sealed type. Pick a variant; the compiler rules out the rest.
-
for (final env in Env.values)Just Dart
Loops, functions, enums for environments, packages and tests. A Stack is a final class, not a template language.
Already have Terraform?
Bring it over one resource at a time
terradart migrate turns an existing tree into a Dart package. Size it first, migrate it, and keep
shipping while you move the rest.
- One Stack per module
- Each directory becomes a Dart class; local modules get a typed wrapper.
- Nothing is dropped
- Blocks no factory covers yet stay beside the generated JSON, verbatim, with the reason.
- No changes
- Resource addresses are kept, so the plan against your existing state is empty.
- Environments merge
- --merge-envs folds dev and prod into one Stack and an Env enum.
$ terradart migrate --report --dir infra
$ terradart migrate --dir infra --out infra_dart
$ cd infra_dart && terradart planNo changes. Your infrastructure matches the configuration.
Start with one Stack.
Install the command, add a provider package, and apply.
dart pub global activate terradart_cli