Type-safe IaC for Dart.
Cloud infrastructure as real Dart code β typed, refactor-safe, drop-in for
terraform apply.
Alpha β no SemVer until v1.0.0, but breaking changes land only on minor bumps. Pin ^0.27.x, read MIGRATING.md before minor bumps, and see status on terradart.dev.
See terradart.dev for documentation, guides, and API reference.
| Package | Description | Pub |
|---|---|---|
terradart_core |
Core runtime β Stack, Resource, Provider, Data, TfArg, and synth behavior. |
|
terradart_google |
Curated factory wrappers for Google Cloud resources (hashicorp/google). |
|
terradart_google_beta |
Curated factory wrappers for beta-only Google Cloud resources (hashicorp/google-beta). |
|
terradart_appwrite |
Curated factory wrappers for Appwrite resources (appwrite/appwrite). |
|
terradart_cloudflare |
Curated factory wrappers for Cloudflare edge infrastructure (cloudflare/cloudflare). |
|
terradart_agent |
MCP server (terradart-mcp) exposing the curated factory catalog to AI agents. |
(unlisted) |
terradart_codegen |
Maintainer generation tooling and CLI (terradart wrap). |
|
terradart_hcl |
Pure Dart HCL / *.tf.json front-end and Terraform module model β the input side of terradart-migrate. |
(unlisted) |
terradart_migrate |
HCL β Dart migrator (terradart-migrate): migration manifests, emitter, leftover sidecar and the CLI that turns a Terraform source tree into a Stack per directory. brew install nozomi-koborinai/tap/terradart-migrate. |
(unlisted) |
# pubspec.yaml
dependencies:
terradart_core: ^0.27.x
terradart_google: ^0.27.x
# Optional: terradart_cloudflare / terradart_appwrite / terradart_google_beta// docs:pitch:start
// infra/lib/app_infra.dart
// A Stack is one Terraform root module of GCP resources, written in Dart.
import 'package:terradart_core/terradart_core.dart';
import 'package:terradart_google/cloud_run.dart';
import 'package:terradart_google/cloud_sql.dart';
import 'package:terradart_google/iam.dart';
import 'package:terradart_google/provider.dart';
final class AppInfraStack extends Stack {
AppInfraStack({required String projectId})
: super(providers: [
GoogleProvider(project: projectId, region: 'asia-northeast1'),
]) {
add(GoogleSqlDatabaseInstance(
localName: 'app_sql',
name: TfArg.literal('app-sql'),
databaseVersion: TfArg.literal(DatabaseVersion.postgres15),
region: TfArg.literal('asia-northeast1'),
settings: SqlDatabaseInstanceSettings(
tier: TfArg.literal('db-f1-micro'),
),
));
final runSa = add(GoogleServiceAccount(
localName: 'run_sa',
accountId: TfArg.literal('app-run-sa'),
));
add(GoogleProjectIamMember(
localName: 'run_sa_sql_client',
project: TfArg.literal(projectId),
role: TfArg.literal('roles/cloudsql.client'),
member: TfArg.ref(runSa.iamMember),
));
add(GoogleCloudRunV2Service(
localName: 'app',
name: TfArg.literal('app'),
location: TfArg.literal('asia-northeast1'),
template: CloudRunV2ServiceTemplate(
serviceAccount: TfArg.ref(runSa.email),
containers: [
CloudRunV2ServiceServiceContainer(
name: TfArg.literal('app'),
image: TfArg.literal('gcr.io/cloudrun/hello'),
ports: CloudRunV2ServiceContainerPort(
containerPort: TfArg.literal(8080),
),
env: [
CloudRunV2ServiceEnvVar(
name: TfArg.literal('DATABASE_URL'),
source: CloudRunV2ServiceEnvVarFromLiteral(
TfArg.literal(
'postgresql://app-client@${projectId}.iam@localhost:5432/app',
),
),
),
],
),
CloudRunV2ServiceServiceContainer(
name: TfArg.literal('cloud-sql-proxy'),
image: TfArg.literal(
'gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.18.1',
),
args: TfArg.literal([
'--port=5432',
'--auto-iam-authn',
'${projectId}:asia-northeast1:app-sql',
]),
),
],
),
));
}
}
// docs:pitch:enddart pub get
dart run bin/infra.dart # synth β tf-out/
cd tf-out && terraform init && terraform applyTfArg.literal(...) wraps known values at synth time, while TfArg.ref(...) (e.g. runSa.iamMember or runSa.email) passes typed references between resources that Terraform resolves during plan/apply. TfArg.variable('name') reads a Terraform input variable declared with addVariable, and TfArg.expression(r'${...}') passes a raw Terraform expression through verbatim β accepted on sensitive fields, checked for undeclared variables at synth time.
Per-service imports (cloud_run.dart, cloud_sql.dart, β¦) keep IDE completion scoped; the legacy package:terradart_google/terradart_google.dart barrel re-export remains supported.
Runnable end-to-end example: examples/pubsub_quickstart/. Looking for edge infrastructure? See the Cloudflare DNS quickstart.
the boundary = the place where infrastructure values (topic IDs, queue names, secret refs, IAM members) flow into runtime Dart code. Today that boundary is held together by string literals on both sides:
- A Pub/Sub topic name is hand-typed in HCL and again as a string literal in a Cloud Function.
- A renamed secret silently breaks runtime resolution because the reference is a string.
- IAM binding members drift between modules with no compiler visibility.
TerraDart makes this boundary a first-class artifact. When synth runs (stack.writeTo(...)), literal-resolvable exports are emitted as typed Dart constants in <stack>.app.dart that your app/function code imports directly β while computed exports become standard Terraform outputs. dart analyze catches drift the moment it happens.
// infra/lib/orders_stack.dart
import 'package:terradart_core/terradart_core.dart';
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)]) {
final orders = GooglePubsubTopic(
localName: 'orders',
name: TfArg.literal('orders-prod'),
messageRetentionDuration: TfArg.literal('604800s'),
);
add(orders);
addExport('ORDERS_TOPIC', ResourceIdExport(orders.nameRef));
setAppExportsOutputPath('lib/generated/orders_stack.app.dart');
}
}// functions/lib/orders_handler.dart (regenerated on synth)
import 'package:my_app_infra/generated/orders_stack.app.dart';
Future<void> handle(PubsubEvent event) async {
if (event.topic == OrdersStackExports.ORDERS_TOPIC) {
// ... process event
}
}Rename orders-prod in the Stack and the handler will not compile until the reference is fixed.
GoogleStorageBucket(
localName: 'assets',
name: TfArg.literal('my-app-assets-prod'),
storageClass: TfArg.literal(BucketStorageClass.standard), // not 'STANDARD'
);
// BucketStorageClass.standerd β typo: compile errorThe .terraformValue getter convention encodes BucketStorageClass.standard as "STANDARD" at synth time. ArgumentError (not silent wrong JSON) on missing convention.
GoogleCloudRunV2Service(
template: Template(
containers: [ServiceContainer(
image: TfArg.literal('gcr.io/cloudrun/hello'),
env: [
EnvVar(name: 'LOG_LEVEL',
source: EnvVarFromLiteral(TfArg.literal('info'))),
EnvVar(name: 'DB_PASSWORD',
source: EnvVarFromSecret(secret: TfArg.literal('db-pwd'))),
],
)],
),
);EnvVarSource is a sealed type β the compiler keeps env.value and env.value_source.secret_key_ref mutually exclusive. Same pattern for BigQuery's 8-variant Access, Cloud Storage's BucketObjectContent, Cloud Run's VolumeSource.
Stack subclasses are regular Dart classes. Loops, conditionals, env config, dependency injection β all work the way they already work. There is no synth CLI; you call stack.writeTo('tf-out') from your own bin/infra.dart (or stack.synth() for an in-memory SynthResult without writing to disk).
Alpha. terradart-mcp is an MCP server that exposes the curated factory catalog β and the migrator β to coding agents (Claude Code, Cursor, Claude Desktop). Six tools β list_barrels, list_resources, get_resource_schema, get_quickstart, check_coverage, and migrate_module β help agents author correct Dart without guessing factory names, and translate an existing Terraform module into a Stack. Every one of them answers from the text you pass and the catalog compiled into the binary: it does not run Terraform, write files, or touch GCP.
brew install nozomi-koborinai/tap/terradart-mcp
# or download from GitHub releases β see packages/terradart_agent/README.mdDocs: terradart.dev/docs/agent/
Alpha. terradart-migrate turns an existing Terraform source tree into a TerraDart package: one Stack per module directory, a tf-out/ tree mirroring the source, and a leftover sidecar (terradart_leftover.tf and friends) beside each main.tf.json holding, verbatim and with a reason each, every block the curated factories do not cover yet. Resource addresses are preserved, so terraform plan against the existing state reports No changes β migrate one resource at a time, no big-bang rewrite. It reads .tf / .tf.json only: no Terraform run, no state access, nothing written into the source tree.
brew install nozomi-koborinai/tap/terradart-migrate
terradart-migrate --dir infra --out infra_dart
cd infra_dart && dart pub get && dart run bin/infra.dart # then: terraform init && terraform plan in tf-out/<root>Docs: terradart.dev/docs/migrate-from-hcl/
terradart_googleships 1337 curated resource factories + 461 data sources (1798 catalog entries) across per-service barrels (compute,pubsub,cloud_run,bigquery, β¦). The GAhashicorp/googlecatalog is filled.terradart_google_betaships the beta-onlyhashicorp/google-betacatalog (128 resource factories, schema pin tracking the weekly GA bump).terradart_appwriteships the filledappwrite/appwritecatalog at2.0.0-beta.1(38 resource factories + 24 data sources).terradart_cloudflareships the filledcloudflare/cloudflarecatalog at5.23.0(257 resource factories + 446 data sources). Nested plugin-framework objects are typed Dart helpers.
Explore ready-to-run examples in examples/:
- Foundational & IAM: Pub/Sub, Cloud Tasks, Secret Manager, IAM
- Compute & Networking: Compute & Firewall, GKE, Cloud DNS
- Data & Storage: Cloud Storage, BigQuery, Cloud Bigtable, KMS
- Application Platform: Cloud Run v2, Cloud Monitoring, Workflows, Eventarc
- AI & Agents: Vertex AI, Agentic Applications
- Multi-provider & Edge: Cloudflare DNS, Appwrite
See the full factory table on terradart.dev/docs/coverage/.
| TerraDart | HCL | CDKTF | Pulumi | |
|---|---|---|---|---|
| Dart authoring | β | β | β (TS / Py / Java / Go) | |
| Type-safe handoff to your app | β (compile-time) | β (terraform output + parse) |
β (no Dart) | β (no typed Dart export) |
Drop-in for terraform apply |
β
(emits *.tf.json) |
β (native) | β | β (different state engine) |
| Execution engine | Plain terraform |
Plain terraform |
Plain terraform |
Pulumi engine + state backend |
| Project status | Alpha | Mature | Archived Dec 2025 | Active |
Already using Pulumi? If your team already runs on Pulumi and wants to write stacks in Dart, check out Pulumi Dart (kingwill101/pulumi-dart), an active community language runtime and provider SDK ecosystem for Pulumi. TerraDart is designed specifically for teams using Terraform who want type-safe Dart authoring without replacing their existing Terraform state or execution pipeline.
- Not a Terraform replacement. TerraDart synthesizes JSON;
terraform plan / applyruns as before. State stays where you already keep it. - Not a multi-cloud abstraction layer. Curated wrappers faithfully mirror provider schemas rather than imposing cross-cloud abstractions.
- Not a constructs framework. Composite abstractions are out of scope for the pre-1.0 cycle.
- Not module-block support. Compose Terraform modules in HCL alongside TerraDart-generated
*.tf.jsonβ both feed the sameterraform apply.
Alpha, pre-1.0 (0.27.x). No SemVer until v1.0.0, but breaking changes land only on minor bumps, always documented in MIGRATING.md; pin ^0.27.x and take patches freely. Beta needs external validation β see the path to beta. Expectations: terradart.dev/docs/status/.
See CONTRIBUTING.md. For security issues, use the GitHub private security advisory flow.
"Terraform" is a registered trademark of HashiCorp, Inc.
Dartβ’ and the related logo are trademarks of Google LLC. We are not endorsed by or affiliated with Google LLC.
TerraDart is an independent open-source project and is not affiliated with, endorsed by, or sponsored by HashiCorp or Google.
Apache-2.0. See LICENSE.
The framing draws on prior work in CDKTF (archived Dec 2025), AWS CDK, and Pulumi.