Published on

Encore.ts — Infrastructure From Code, or How to Never Write Terraform Again

Authors

Introduction

Infrastructure-as-code tools like Terraform require learning HCL and managing state files. Encore.ts takes the opposite approach: declare infrastructure directly in your application code. The framework infers databases, APIs, services, and deployment requirements from TypeScript decorators. This guide covers building and deploying with Encore.

What Encore.TS Does

Encore automatically manages:

  • API endpoints via @api decorator
  • SQL databases via @encore.db with auto-migrations
  • Message queues (pubsub) via @encore.pubsub
  • Scheduled tasks via @encore.cron
  • Secrets via Encore.meta.environment
  • Service-to-service calls with automatic type safety

Your code is the source of truth. Encore generates CloudFormation, Terraform, or Kubernetes manifests.

Declaring HTTP Endpoints

The @api decorator exposes TypeScript functions as REST endpoints:

import { api } from 'encore.dev/api'

interface User {
  id: number
  name: string
  email: string
}

export const getUser = api(
  { method: 'GET', path: '/users/:id', expose: true },
  async (req: { id: number }): Promise<User> => {
    return {
      id: req.id,
      name: 'Alice',
      email: 'alice@example.com',
    }
  }
)

export const createUser = api(
  { method: 'POST', path: '/users', expose: true },
  async (req: { name: string; email: string }): Promise<User> => {
    return {
      id: 1,
      ...req,
    }
  }
)

Encore infers the OpenAPI schema from parameter and return types. You get auto-generated docs, client SDKs, and type safety.

TypeScript Auto-Migrations with @encore.db

Declare databases without SQL:

import { db } from 'encore.dev/db'

interface User {
  id: number
  name: string
  email: string
  createdAt: Date
}

export const usersDB = db.createClient('users', {
  migration: `
    CREATE TABLE users (
      id SERIAL PRIMARY KEY,
      name TEXT NOT NULL,
      email TEXT UNIQUE NOT NULL,
      created_at TIMESTAMP DEFAULT NOW()
    );
  `,
})

export const createUser = api(
  { method: 'POST', path: '/users' },
  async (req: { name: string; email: string }): Promise<User> => {
    const result = await usersDB.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
      [req.name, req.email]
    )
    return result.rows[0]
  }
)

export const getUser = api(
  { method: 'GET', path: '/users/:id' },
  async (req: { id: number }): Promise<User> => {
    const result = await usersDB.query(
      'SELECT * FROM users WHERE id = $1',
      [req.id]
    )
    return result.rows[0]
  }
)

Encore runs migrations on encore run and encore deploy. No separate migration tools needed.

PubSub for Message Queues

Declare message queues as first-class primitives:

import { pubsub } from 'encore.dev/pubsub'

interface UserCreatedEvent {
  userId: number
  email: string
}

export const userCreated = pubsub.newTopic('user-created', {
  deliveryGuarantee: 'at-least-once',
})

export const createUser = api(
  { method: 'POST', path: '/users' },
  async (req: { name: string; email: string }) => {
    // Create user...
    const userId = 1
    const email = req.email

    // Publish event
    await userCreated.publish({
      userId,
      email,
    })

    return { userId, email }
  }
)

// Subscriber
export const onUserCreated = userCreated.subscribe(
  'send-welcome-email',
  async (msg: UserCreatedEvent) => {
    console.log(`Sending welcome email to ${msg.email}`)
    // Send email via Mailgun, SendGrid, etc.
  }
)

Subscribers are automatically scaled. Encore handles delivery and retries.

Cron Jobs

Schedule tasks with simple decorators:

import { cron } from 'encore.dev/cron'

export const dailyDigest = cron.newJob('daily-digest', {
  title: 'Send daily digest',
  every: '24h',
  retryPolicy: {
    maxAttempts: 3,
    backoffFactor: 2,
  },
})

export const handler = dailyDigest(async () => {
  console.log('Sending daily digest...')
  // Fetch users, aggregate data, send emails
})

Cron schedules are declared in code. No separate cron orchestration needed.

Secrets Management

Store secrets without .env files:

import { meta } from 'encore.dev'

export const sendEmail = api(
  { method: 'POST', path: '/send-email' },
  async (req: { to: string; subject: string; body: string }) => {
    const apiKey = meta.environment.MAILGUN_API_KEY
    // Use apiKey to call Mailgun...
    return { success: true }
  }
)

Secrets are stored in Encore''s vault. Access them via meta.environment. No env files in version control.

Service-to-Service Calls with Type Safety

Call other Encore services with full type safety:

// users/users.ts
export const getUser = api(
  { method: 'GET', path: '/users/:id' },
  async (req: { id: number }): Promise<User> => {
    // ...
  }
)

// posts/posts.ts
import * as users from '../users'

export const getUserPosts = api(
  { method: 'GET', path: '/posts/user/:userId' },
  async (req: { userId: number }) => {
    // Call users service with full type inference
    const user = await users.getUser({ id: req.userId })
    const posts = await db.query(
      'SELECT * FROM posts WHERE user_id = $1',
      [user.id]
    )
    return posts
  }
)

Encore generates service definitions automatically. No HTTP URLs, no manual type definitions.

Local Development with encore run

Test locally before deploying:

encore run
# Output:
# Listening on http://localhost:4000
# Dashboard at http://localhost:4000/encore/dashboard

The dashboard shows:

  • Request/response tracing
  • Database queries
  • Service dependencies
  • Performance metrics

Test with curl:

curl -X POST http://localhost:4000/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"Alice","email":"alice@example.com"}'

Deploying to AWS, GCP, Azure

Encore generates infrastructure:

# Connect to cloud provider
encore auth login

# Deploy to production
encore deploy production

# Output:
# Deploying to AWS...
# ✓ API Gateway configured
# ✓ RDS PostgreSQL provisioned
# ✓ Lambda functions deployed
# ✓ SQS queues created
# ✓ CloudWatch alarms configured

Encore provisions:

  • AWS: API Gateway, Lambda, RDS, SQS, CloudWatch
  • GCP: Cloud Run, Cloud SQL, Pub/Sub, Cloud Trace
  • Azure: App Service, Azure SQL, Service Bus

No Terraform. No CloudFormation. Encore handles it.

Tracing and Metrics

Built-in observability:

import { telemetry } from 'encore.dev/telemetry'

export const createUser = api(
  { method: 'POST', path: '/users' },
  async (req: { name: string; email: string }) => {
    telemetry.recordMetric('users.created', 1)

    const user = {
      id: Math.random(),
      ...req,
    }

    telemetry.recordSpan('user.validation', () => {
      // Validation logic
    })

    return user
  }
)

Traces are exported to Datadog, Honeycomb, or Otel collectors automatically.

Environment Configuration

Manage different environments (dev, staging, prod):

// encore.app.ts
export default new AppConfig({
  name: 'my-api',
  environments: {
    dev: {
      location: 'us-west-1',
      replicas: 1,
    },
    staging: {
      location: 'us-west-1',
      replicas: 2,
    },
    prod: {
      location: 'us-west-1',
      replicas: 3,
      autoScaling: {
        minReplicas: 3,
        maxReplicas: 10,
        targetCPUUtilization: 70,
      },
    },
  },
})

encore deploy uses the environment-specific config.

When Encore Makes Sense

Use Encore if:

  • You''re building REST APIs
  • Your infrastructure is standard (PostgreSQL, APIs, queues)
  • You want to avoid Terraform/CloudFormation
  • Your team is TypeScript-focused

Avoid Encore if:

  • You have exotic infrastructure (Cassandra, Kubernetes)
  • You need fine-grained infrastructure control
  • You have existing Terraform code
  • Your organization requires multi-cloud flexibility

Encore is opinionated. It trades flexibility for simplicity.

Encore vs Serverless Framework

AspectEncoreServerless Framework
LanguageTypeScript onlyAny language (via plugins)
InfrastructureDeclarativeMostly declarative
MigrationEasy from ExpressDifficult from Express
Cloud providers3 (AWS, GCP, Azure)6+ (AWS, GCP, Azure, Heroku, etc.)
DXExcellentGood
MaturityGrowingMature

Encore is newer but has superior developer experience. Serverless Framework is more flexible.

Checklist

  • Install Encore CLI
  • Create project with encore create
  • Define API endpoints with @api
  • Set up PostgreSQL database with @encore.db
  • Add PubSub topics and subscribers
  • Schedule cron jobs
  • Test locally with encore run
  • Set up secrets in Encore''s vault
  • Configure environments
  • Deploy to production with encore deploy
  • Monitor tracing and metrics

Conclusion

Encore.ts bridges the gap between application code and infrastructure. By declaring infrastructure in TypeScript, you eliminate context-switching to YAML or HCL. Migrations, scaling, and deployments are one command. It won''t replace Terraform for complex multi-cloud setups, but for typical REST APIs, Encore is faster, simpler, and more maintainable. For new projects, it should be your default.