
HashiCorp is sunsetting Terraform CDK. Terraform CDK let you write infrastructure in TypeScript instead of HCL - you'd define AWS resources in TypeScript, run cdktf synth to generate Terraform JSON, then terraform apply to provision everything. Writing infrastructure in a real programming language with type safety and IDE support instead of learning HCL syntax.
At Encore, we approach this differently. While Encore isn't a replacement for Terraform CDK, we think developers who appreciated CDK's approach might find our perspective interesting. Instead of writing infrastructure declarations that generate configs, you write application code with infrastructure primitives. Encore parses the application code to understand what infrastructure is needed, then provisions it directly through cloud provider APIs in your AWS or GCP account (or generates a Docker image for self-hosting). The infrastructure declarations are part of your application code, not separate from it.
Creating a database with Terraform CDK meant writing infrastructure configuration for the database instance, subnet groups, security groups, and all the wiring between them. Here's what that looked like:
import { Construct } from "constructs";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { DbInstance } from "@cdktf/provider-aws/lib/db-instance";
import { DbSubnetGroup } from "@cdktf/provider-aws/lib/db-subnet-group";
import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group";
const subnetGroup = new DbSubnetGroup(this, "db-subnet", {
name: "my-db-subnet",
subnetIds: ["subnet-12345", "subnet-67890"],
});
const securityGroup = new SecurityGroup(this, "db-sg", {
name: "my-db-sg",
vpcId: "vpc-12345",
ingress: [{
fromPort: 5432,
toPort: 5432,
protocol: "tcp",
cidrBlocks: ["10.0.0.0/16"],
}],
});
const db = new DbInstance(this, "postgres", {
identifier: "my-postgres",
engine: "postgres",
engineVersion: "14.7",
instanceClass: "db.t3.micro",
allocatedStorage: 20,
dbName: "myapp",
username: "admin",
password: process.env.DB_PASSWORD,
dbSubnetGroupName: subnetGroup.name,
vpcSecurityGroupIds: [securityGroup.id],
skipFinalSnapshot: true,
});
The application code that used this database was written separately:
import { Pool } from 'pg';
const pool = new Pool({
host: process.env.DB_HOST,
database: 'myapp',
user: 'admin',
password: process.env.DB_PASSWORD,
});
const result = await pool.query('SELECT * FROM users');
Two files, two different purposes. The infrastructure file declared AWS resources. The application file used those resources through environment variables. Running cdktf synth generated Terraform JSON, then terraform apply provisioned everything, then you'd deploy your application code separately.
With Encore, application code includes infrastructure declarations. The database declaration and its usage are in the same file:
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("myapp", {
migrations: "./migrations",
});
const result = await db.query`SELECT * FROM users`;
The SQLDatabase declaration tells Encore this application needs a PostgreSQL database. When you run the app locally with encore run, Encore starts a local PostgreSQL instance. When you deploy to AWS, Encore provisions RDS with backups, high availability, security groups, and least-privilege IAM policies configured automatically. The application code stays the same - locally it connects to the local database, in production it connects to RDS.
Local development environment with database running
Here's a user management API. With Terraform CDK, this would require separate infrastructure code for Lambda functions (or ECS tasks), API Gateway configuration, methods, integrations, IAM roles, security groups, and the database setup - easily several hundred lines of infrastructure declarations, plus the application code itself, plus state management setup.
With Encore:
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("users", {
migrations: "./migrations",
});
interface User {
id: number;
email: string;
name: string;
}
export const createUser = api(
{ method: "POST", path: "/users", expose: true },
async ({ email, name }: { email: string; name: string }): Promise<User> => {
await db.exec`
INSERT INTO users (email, name)
VALUES (${email}, ${name})
`;
return await db.queryRow<User>`
SELECT id, email, name FROM users WHERE email = ${email}
`;
}
);
export const getUser = api(
{ method: "GET", path: "/users/:email", expose: true },
async ({ email }: { email: string }): Promise<User | undefined> => {
return await db.queryRow<User>`
SELECT id, email, name FROM users WHERE email = ${email}
`;
}
);
Running encore run starts a local API server with a PostgreSQL database. The API is available at http://localhost:4000 and there's a development dashboard at http://localhost:9400 showing your services, APIs, and database schema. Deploying to AWS provisions ECS tasks for running the services, an Application Load Balancer for routing requests, RDS for the database, TLS certificates, and IAM roles with appropriate permissions.
API Explorer with automatic documentation and testing interface
Database migrations are standard SQL files. Here's migrations/1_create_users.up.sql:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
We run these migrations automatically when the application starts, both locally and in production.
Terraform CDK required configuring SNS topics, SQS queues, subscriptions, IAM policies, and then connecting them in your application code - usually 40+ lines of infrastructure configuration plus separate publisher and consumer code.
With Encore's Pub/Sub:
import { Topic, Subscription } from "encore.dev/pubsub";
interface Order {
orderId: number;
userId: string;
total: number;
}
const orders = new Topic<Order>("orders", {
deliveryGuarantee: "at-least-once",
});
// Publishing events
await orders.publish({ orderId: 123, userId: "user_123", total: 99.99 });
// Subscribing to events
const _ = new Subscription(orders, "process-orders", {
handler: async (order) => {
await db.exec`
INSERT INTO processed_orders (order_id, user_id, total)
VALUES (${order.orderId}, ${order.userId}, ${order.total})
`;
},
});
The same pattern applies - the Topic declaration works locally with an in-memory pub/sub system and provisions SNS topics and SQS queues in AWS. Publishers and subscribers are type-safe through TypeScript.
The pattern extends to object storage and other primitives:
import { Bucket } from "encore.dev/storage/objects";
const uploads = new Bucket("user-uploads", {
versioned: false,
});
// Upload a file
await uploads.upload("document.pdf", fileBuffer, {
contentType: "application/pdf",
});
// Download a file
const file = await uploads.download("document.pdf");
Locally, this uses the filesystem. In AWS, it provisions S3 buckets with appropriate permissions and lifecycle policies.
Encore supports more infrastructure primitives - secrets management, cron jobs, streaming APIs - all following the same pattern. See the full list of primitives in the docs.
Terraform CDK still used Terraform underneath, which meant managing state files. State was typically stored in S3 with DynamoDB for locking, and you'd occasionally hit state drift issues where your actual infrastructure didn't match what Terraform thought it provisioned. Team coordination required careful state locking, and migrating state between backends was manual work.
We don't use state files in Encore. Instead, we analyze your application code to understand what infrastructure exists and what needs to be provisioned. When you deploy, we compare what's declared in your code to what's actually running in your cloud account and provision any differences. Infrastructure always matches code because the code is the source of truth - no separate state to drift out of sync, no locking mechanisms to configure, no state backend to manage.
Infrastructure changes require approval before provisioning
Terraform CDK provided type safety for infrastructure declarations - TypeScript would catch errors if you misconfigured an RDS instance. But when application code connected to that infrastructure, type safety stopped. Connection strings came from environment variables, database queries were strings, API responses were any.
With Encore, type safety extends throughout the stack. Database queries are type-checked:
interface User {
id: number;
email: string;
name: string;
}
const user = await db.queryRow<User>`
SELECT id, email, name FROM users WHERE email = ${email}
`;
// user is User | undefined, fully type-safe
API endpoints are type-safe:
export const getUser = api(
{ method: "GET", path: "/users/:email" },
async ({ email }: { email: string }): Promise<User | undefined> => {
return await db.queryRow<User>`SELECT * FROM users WHERE email = ${email}`;
}
);
Service-to-service calls are type-safe when you import other services from ~encore/clients. The TypeScript compiler catches type mismatches, your IDE provides autocomplete for API parameters and database columns, and runtime errors from type mismatches are caught at compile time.
Terraform CDK wasn't the best experience locally. Local development meant setting up Docker containers manually, writing docker-compose files, and configuring environment variables differently for local vs production. The local environment approximated production but never quite matched.
We designed Encore to run the same code locally and in production. Running encore run starts your application with local infrastructure - PostgreSQL databases, pub/sub messaging, object storage buckets - whatever your code declares. The local development dashboard shows your services, API documentation, database schema, distributed tracing, and logs. When you deploy, the same code runs with AWS resources instead of local ones, maintaining 1:1 parity between environments.
Automatic service flow visualization and architecture diagram
If you have existing Terraform managing infrastructure, Encore can run alongside it. Use Terraform for your existing resources, Encore for new services. Encore services are just TypeScript or Go code - they can connect to existing databases, Redis instances, external APIs, or any other infrastructure through standard connection strings and environment variables.
Install the Encore CLI:
# macOS
brew install encoredev/tap/encore
# Linux
curl -L https://encore.dev/install.sh | bash
# Windows
iwr https://encore.dev/install.ps1 | iex
Create an application:
encore app create my-app --example=ts/hello-world
cd my-app
Run it locally:
encore run
The application runs at http://localhost:4000 with a development dashboard at http://localhost:9400. The dashboard shows your API endpoints, lets you test them, displays distributed tracing for requests, and includes a database inspector for viewing your schema and data.
When you're ready to deploy, connect your AWS account through Encore Cloud or self-host the deployment infrastructure. We handle provisioning everything your application declares.
Terraform CDK and Encore share a core belief: infrastructure definitions belong in real programming languages, not domain-specific configuration languages. If you chose Terraform CDK because you wanted type safety, IDE autocomplete, and familiar programming constructs for infrastructure, we built Encore with the same philosophy.
Where we differ is in execution. Terraform CDK generated Terraform configs from your TypeScript, which meant managing state files, running synthesis steps, and deploying infrastructure separately from application code. We took the idea further: infrastructure declarations are part of the application code itself. When you write new SQLDatabase() in Encore, that's both application code (using a database) and infrastructure declaration (needs a database). We analyze your code and provision what it declares, directly in your AWS or GCP account.
If you were using Terraform CDK to provision databases, APIs, pub/sub, and similar resources, Encore can handle that workflow. You can't just swap CDK code for Encore code 1:1, but if you're starting fresh or migrating gradually, the TypeScript knowledge transfers directly - same language, same type safety, same IDE tooling. The main difference is that infrastructure declarations live in your application code rather than separate configuration files.
To sum it up: Encore provisions standard AWS and GCP resources directly in your account - VPCs, ECS clusters, RDS databases, load balancers, IAM roles. Since everything runs in your infrastructure, there's minimal lock-in. You can see and manage resources through your cloud console, costs appear in your existing billing, and your ops team keeps full access and control. Companies like Groupon use this in production, running it in their existing AWS organization alongside their current setup.
Learn more at encore.dev/docs or try the Quick Start guide.


