Skip to main content

Guide

Encore vs Nitric vs Shuttle (2026)

Compare Encore, Nitric, and Shuttle for cloud-native backend development. Infrastructure-from-code, auto-provisioning, and which framework fits your backend.

·PkgPulse Team·
0

TL;DR: Encore is the TypeScript/Go backend framework that infers infrastructure from your code — define API endpoints, databases, and pub/sub with type-safe primitives, and Encore provisions everything automatically. Nitric is the cloud-agnostic framework — declare what cloud resources you need (APIs, queues, storage, schedules), write handlers in any language, and deploy to AWS/GCP/Azure without lock-in. Shuttle is the Rust backend framework with built-in infrastructure — annotate your Rust functions with macros, and Shuttle provisions databases, secrets, and static assets on deploy. In 2026: Encore for TypeScript/Go with automatic infrastructure, Nitric for polyglot cloud-agnostic backends, Shuttle for Rust backends with zero DevOps.

Key Takeaways

  • Encore: TypeScript + Go. Infrastructure-from-code — Encore analyzes your code and provisions databases, pub/sub, caches, cron jobs. Local dev dashboard, distributed tracing. Best for teams wanting zero-config infrastructure with strong typing
  • Nitric: Any language (TS, Python, Go, Java, .NET, Dart). Declare resources → write handlers → deploy anywhere. Pulumi-based provisioning, cloud-agnostic. Best for teams wanting cloud portability and language flexibility
  • Shuttle: Rust-only. Macro-based infrastructure — #[shuttle_runtime::main] with resource annotations. Built-in Postgres, secrets, static files. Best for Rust developers wanting the simplest cloud deployment

Encore — Infrastructure from Code

Encore analyzes your TypeScript or Go code and automatically provisions the infrastructure — APIs, databases, pub/sub, caches, and cron jobs.

API Endpoints

// encore.app — project configuration
// { "id": "my-app" }

// backend/users/users.ts — API service
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";

// Define the database — Encore provisions it automatically
const db = new SQLDatabase("users", {
  migrations: "./migrations",
});

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

interface CreateUserParams {
  name: string;
  email: string;
}

// Public API endpoint — Encore generates routes, docs, and client
export const create = api(
  { method: "POST", path: "/users", expose: true },
  async (params: CreateUserParams): Promise<User> => {
    const row = await db.queryRow`
      INSERT INTO users (name, email, created_at)
      VALUES (${params.name}, ${params.email}, NOW())
      RETURNING id, name, email, created_at
    `;

    return {
      id: row.id,
      name: row.name,
      email: row.email,
      createdAt: row.created_at,
    };
  }
);

export const get = api(
  { method: "GET", path: "/users/:id", expose: true },
  async ({ id }: { id: number }): Promise<User> => {
    return db.queryRow`
      SELECT id, name, email, created_at FROM users WHERE id = ${id}
    `;
  }
);

// Auth middleware — Encore handles auth at the framework level
export const list = api(
  { method: "GET", path: "/users", expose: true, auth: true },
  async (): Promise<{ users: User[] }> => {
    const rows = await db.query`SELECT * FROM users ORDER BY created_at DESC`;
    return { users: rows };
  }
);

Pub/Sub and Background Jobs

// backend/notifications/notifications.ts
import { Topic, Subscription } from "encore.dev/pubsub";
import { CronJob } from "encore.dev/cron";

// Define a topic — Encore provisions the message queue
interface UserCreatedEvent {
  userId: number;
  email: string;
  name: string;
}

export const userCreated = new Topic<UserCreatedEvent>("user-created", {
  deliveryGuarantee: "at-least-once",
});

// Subscribe to events — Encore wires up the subscription
const _ = new Subscription(userCreated, "send-welcome-email", {
  handler: async (event: UserCreatedEvent) => {
    await sendWelcomeEmail(event.email, event.name);
    console.log(`Welcome email sent to ${event.email}`);
  },
  retryPolicy: { maxRetries: 3 },
});

// Publish from another service
// In users service:
// await userCreated.publish({ userId: user.id, email: user.email, name: user.name });

// Cron job — Encore schedules it automatically
const dailyDigest = new CronJob("daily-digest", {
  title: "Send daily digest emails",
  schedule: "0 9 * * *", // 9 AM daily
  endpoint: sendDailyDigest,
});

export const sendDailyDigest = api(
  { method: "POST", path: "/internal/daily-digest" },
  async () => {
    const users = await getActiveUsers();
    for (const user of users) {
      await sendDigestEmail(user);
    }
  }
);

Caching and Secrets

// backend/products/products.ts
import { CacheCluster, CacheKeyspace } from "encore.dev/storage/cache";
import { secret } from "encore.dev/config";

// Cache — Encore provisions Redis automatically
const cluster = new CacheCluster("products-cache");
const productCache = new CacheKeyspace<Product>(cluster, {
  keyPattern: "product/:id",
  defaultExpiry: { hours: 1 },
});

export const getProduct = api(
  { method: "GET", path: "/products/:id", expose: true },
  async ({ id }: { id: string }): Promise<Product> => {
    // Try cache first
    const cached = await productCache.get(id);
    if (cached) return cached;

    // Fetch from DB
    const product = await db.queryRow`SELECT * FROM products WHERE id = ${id}`;
    await productCache.set(id, product);
    return product;
  }
);

// Secrets — Encore manages them securely
const stripeKey = secret("StripeSecretKey");
// Set via: encore secret set --type prod StripeSecretKey

Local Development

# Install Encore CLI
curl -L https://encore.dev/install.sh | bash

# Create a new project
encore app create my-app --example=ts/hello-world

# Run locally — Encore starts all services, databases, caches
encore run

# Local development dashboard — auto-generated API docs,
# distributed tracing, database explorer
# Available at http://localhost:9400

# Deploy to Encore Cloud
encore deploy --env=staging

Nitric — Cloud-Agnostic Backend Framework

Nitric lets you declare cloud resources and write handlers in any language — then deploy to AWS, GCP, or Azure without lock-in.

API and Resources

// services/users.ts
import { api, topic, kv, bucket, schedule } from "@nitric/sdk";

// Declare resources — Nitric provisions them on deploy
const usersTopic = topic("user-events").allow("publish");
const usersKv = kv("users").allow("get", "set", "delete");
const avatarsBucket = bucket("avatars").allow("read", "write");

// API endpoints
const usersApi = api("users");

usersApi.post("/users", async (ctx) => {
  const { name, email } = ctx.req.json();

  const userId = crypto.randomUUID();
  await usersKv.set(userId, { id: userId, name, email, createdAt: new Date() });

  // Publish event
  await usersTopic.publish({
    type: "user.created",
    userId,
    email,
    name,
  });

  ctx.res.json({ id: userId, name, email });
});

usersApi.get("/users/:id", async (ctx) => {
  const { id } = ctx.req.params;
  const user = await usersKv.get(id);

  if (!user) {
    ctx.res.status = 404;
    ctx.res.json({ error: "User not found" });
    return;
  }

  ctx.res.json(user);
});

// File upload
usersApi.post("/users/:id/avatar", async (ctx) => {
  const { id } = ctx.req.params;
  const body = ctx.req.data;

  await avatarsBucket.file(`${id}/avatar.png`).write(body);

  ctx.res.json({ message: "Avatar uploaded" });
});

Event Handling and Schedules

// services/notifications.ts
import { topic, schedule } from "@nitric/sdk";

// Subscribe to events
const userEvents = topic("user-events").allow("subscribe");

userEvents.subscribe(async (ctx) => {
  const event = ctx.req.json();

  switch (event.type) {
    case "user.created":
      await sendWelcomeEmail(event.email, event.name);
      break;
    case "user.deleted":
      await cleanupUserData(event.userId);
      break;
  }
});

// Scheduled tasks
schedule("daily-digest").every("1 day", async (ctx) => {
  const users = await getActiveUsers();
  for (const user of users) {
    await sendDigestEmail(user);
  }
});

schedule("cleanup").cron("0 2 * * *", async (ctx) => {
  // Run at 2 AM daily
  await cleanupExpiredSessions();
});

Middleware and Auth

// services/middleware.ts
import { api, oidcRule } from "@nitric/sdk";

// OIDC authentication
const secureApi = api("secure", {
  security: {
    oidc: oidcRule({
      name: "auth0",
      issuer: "https://acme.auth0.com/",
      audiences: ["https://api.acme.com"],
    }),
  },
});

secureApi.get("/profile", async (ctx) => {
  // ctx.req.context contains the verified JWT claims
  const userId = ctx.req.context.identity.sub;

  const profile = await getProfile(userId);
  ctx.res.json(profile);
});

// Custom middleware
const loggingApi = api("logged", {
  middleware: [
    async (ctx, next) => {
      const start = Date.now();
      await next(ctx);
      console.log(`${ctx.req.method} ${ctx.req.path} - ${Date.now() - start}ms`);
    },
    async (ctx, next) => {
      ctx.res.headers["X-Request-ID"] = crypto.randomUUID();
      await next(ctx);
    },
  ],
});

Deployment Configuration

# nitric.yaml — project configuration
name: my-app
services:
  - match: services/*.ts
    start: npx ts-node $SERVICE_PATH

# nitric-aws.yaml — AWS provider configuration
provider: nitric/aws@latest
region: us-east-1

config:
  lambda:
    memory: 512
    timeout: 30
  api:
    domains:
      users: api.acme.com

# Deploy
# nitric up -s nitric-aws.yaml

# nitric-gcp.yaml — same app, different cloud
# provider: nitric/gcp@latest
# region: us-central1

Shuttle — Rust Backend Framework

Shuttle gives Rust developers the simplest deployment experience — annotate functions with macros and Shuttle provisions everything.

API with Axum

// src/main.rs
use axum::{
    extract::{Path, State},
    routing::{get, post},
    Json, Router,
};
use serde::{Deserialize, Serialize};
use shuttle_axum::ShuttleAxum;
use shuttle_shared_db::Postgres;
use sqlx::PgPool;

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    email: String,
}

#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

async fn create_user(
    State(pool): State<PgPool>,
    Json(params): Json<CreateUser>,
) -> Json<User> {
    let user = sqlx::query_as!(
        User,
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email",
        params.name,
        params.email
    )
    .fetch_one(&pool)
    .await
    .unwrap();

    Json(user)
}

async fn get_user(
    State(pool): State<PgPool>,
    Path(id): Path<i32>,
) -> Json<User> {
    let user = sqlx::query_as!(
        User,
        "SELECT id, name, email FROM users WHERE id = $1",
        id
    )
    .fetch_one(&pool)
    .await
    .unwrap();

    Json(user)
}

async fn list_users(State(pool): State<PgPool>) -> Json<Vec<User>> {
    let users = sqlx::query_as!(User, "SELECT id, name, email FROM users")
        .fetch_all(&pool)
        .await
        .unwrap();

    Json(users)
}

// Shuttle macro provisions Postgres automatically
#[shuttle_runtime::main]
async fn main(
    #[shuttle_shared_db::Postgres] pool: PgPool,
) -> ShuttleAxum {
    // Run migrations
    sqlx::migrate!("./migrations")
        .run(&pool)
        .await
        .unwrap();

    let router = Router::new()
        .route("/users", post(create_user).get(list_users))
        .route("/users/:id", get(get_user))
        .with_state(pool);

    Ok(router.into())
}

Secrets and Static Assets

use shuttle_runtime::SecretStore;
use shuttle_static_folder::StaticFolder;
use std::path::PathBuf;

#[shuttle_runtime::main]
async fn main(
    #[shuttle_shared_db::Postgres] pool: PgPool,
    #[shuttle_runtime::Secrets] secrets: SecretStore,
    #[shuttle_static_folder::StaticFolder] static_folder: PathBuf,
) -> ShuttleAxum {
    // Access secrets (set via `shuttle secrets set`)
    let stripe_key = secrets.get("STRIPE_SECRET_KEY").unwrap();
    let jwt_secret = secrets.get("JWT_SECRET").unwrap();

    let router = Router::new()
        .route("/api/users", post(create_user))
        .nest_service("/static", ServeDir::new(static_folder))
        .with_state(AppState { pool, stripe_key, jwt_secret });

    Ok(router.into())
}

Deployment

# Install Shuttle CLI
cargo install cargo-shuttle

# Initialize a project
cargo shuttle init --name my-app --template axum

# Run locally (provisions local Postgres)
cargo shuttle run

# Deploy to Shuttle Cloud
cargo shuttle deploy

# Set secrets
cargo shuttle secrets set STRIPE_SECRET_KEY sk_live_...

# View logs
cargo shuttle logs

# View deployment status
cargo shuttle status

Feature Comparison

FeatureEncoreNitricShuttle
LanguagesTypeScript, GoTS, Python, Go, Java, .NET, DartRust
ApproachInfrastructure-from-codeResource declarationMacro annotations
Cloud ProvidersEncore Cloud, AWS, GCPAWS, GCP, AzureShuttle Cloud
Database✅ (auto-provisioned Postgres)KV store, SQL (via providers)✅ (Postgres, Turso)
Pub/Sub✅ (Topic/Subscription)✅ (topic/subscribe)❌ (external)
Caching✅ (auto-provisioned Redis)❌ (external)❌ (external)
Object Storage✅ (Buckets)✅ (bucket API)✅ (static folder)
Cron/Schedules✅ (CronJob)✅ (schedule)✅ (shuttle-cron)
Secrets✅ (managed)✅ (environment)✅ (SecretStore)
Auth✅ (built-in)✅ (OIDC rules)DIY
API Docs✅ (auto-generated)
Tracing✅ (distributed tracing)❌ (external)❌ (external)
Local Dev✅ (full local stack)✅ (nitric start)✅ (cargo shuttle run)
Dev Dashboard✅ (rich UI)Basic CLIBasic CLI
Cloud AgnosticEncore Cloud + AWS/GCP✅ (AWS/GCP/Azure)Shuttle Cloud only
Self-Host✅ (generate Terraform)✅ (Pulumi-based)
LicenseBSL → Apache 2.0Apache 2.0Apache 2.0
MaturityGrowingGrowingGrowing
Best ForTS/Go auto-infraPolyglot, cloud-portableRust simplicity

When to Use Each

Choose Encore if:

  • You want infrastructure automatically provisioned from your code (no YAML)
  • TypeScript or Go is your backend language
  • Auto-generated API documentation and distributed tracing are valuable
  • A rich local development dashboard accelerates your workflow
  • You want to deploy to Encore Cloud or self-host to AWS/GCP

Choose Nitric if:

  • Cloud portability (AWS ↔ GCP ↔ Azure) without code changes is important
  • Your team uses multiple languages and needs a unified resource model
  • You want Pulumi-based provisioning for infrastructure customization
  • Avoiding vendor lock-in is a priority
  • You need APIs, queues, storage, and schedules in a single abstraction

Choose Shuttle if:

  • You're building a Rust backend and want the simplest deployment path
  • Zero DevOps — cargo shuttle deploy provisions everything
  • Postgres and secrets management without configuration is appealing
  • You're prototyping or building side projects in Rust
  • The Rust ecosystem (Axum, Actix, Rocket) is your framework of choice

Methodology

Feature comparison based on Encore (TypeScript/Go), Nitric, and Shuttle documentation as of March 2026. Encore evaluated on infrastructure-from-code model and local dev experience. Nitric evaluated on cloud portability and resource abstraction. Shuttle evaluated on Rust integration and deployment simplicity. Code examples use official APIs and CLI tools.

Production Maturity and Operational Confidence

The infrastructure-from-code approach these frameworks offer is compelling, but production confidence requires understanding how mature the underlying provisioning engines are. Encore generates standard AWS and GCP infrastructure — CloudFormation on AWS, Deployment Manager on GCP — meaning the underlying resources are first-class cloud constructs that can be inspected and managed with standard tools if Encore itself has issues. This is a meaningful safety net: if Encore's CLI became unavailable tomorrow, your infrastructure would continue running and could be managed directly. Nitric generates Pulumi stacks under the hood, so the provisioned infrastructure is a standard Pulumi state file that can be managed with the Pulumi CLI independently of Nitric. Shuttle is the highest lock-in option — your backend runs on Shuttle's proprietary infrastructure, and migrating away requires extracting your business logic from Shuttle's macro system and re-deploying to a different provider. The trade-off is that Shuttle's simplicity is unmatched for Rust backends; the lock-in risk is acceptable for projects where operational simplicity outweighs portability.

Security and Secrets Management

Each framework handles secrets and environment configuration differently, with important security implications. Encore's secret() primitive fetches secrets from a managed store that's separate from the code and infrastructure configuration — secrets are never written to the Encore config file or committed to the repository. Nitric's secrets management delegates to the cloud provider's native secrets service (AWS Secrets Manager, GCP Secret Manager) with a unified API, giving you cloud-native secrets management without cloud-specific SDK calls. Shuttle's SecretStore reads from a Secrets.toml file set via cargo shuttle secrets set — secrets are managed via CLI commands and stored on Shuttle's platform, not in your repository. All three correctly avoid hardcoding secrets in code or infrastructure config. For compliance-heavy environments requiring audit trails for secret access, ArangoDB, AWS Secrets Manager, and GCP Secret Manager provide access logs that Shuttle's platform-level secrets do not expose.

TypeScript and Language Ecosystem Fit

Encore's TypeScript support is its primary strength for web development teams — the infrastructure primitives (api, SQLDatabase, Topic, CacheCluster) feel like TypeScript types because they are, generating IDE autocomplete and type errors for misconfigured infrastructure. The constraint is that Encore's TypeScript backend is opinionated about code structure: services are directories, API handlers are exported functions with specific signatures, and the framework analyzes your code at compile time to generate infrastructure. This works seamlessly for new projects but can require refactoring existing code to fit Encore's conventions during adoption. Nitric's TypeScript SDK is flexible — it's a declarative resource declaration layer that doesn't constrain how you structure your handler code. Shuttle's Rust macro system is elegant for Rust developers but entirely inaccessible to JavaScript or Python developers. Teams choosing based on language should: Encore or Nitric for TypeScript/JavaScript backends, Nitric for Python or Go backends, Shuttle exclusively for Rust.

Local Development Experience

The local development story is a key differentiator in daily productivity. Encore's encore run command starts all services defined in your project, provisions a local PostgreSQL database, runs migrations, and opens a development dashboard at http://localhost:9400 — showing request traces, API documentation, and database explorer in a rich UI. This dashboard reduces the need for separate tools like Postman or TablePlus during development. Nitric's nitric start command starts a local server that emulates cloud resources — APIs run locally, while topics and key-value stores use in-memory implementations. The emulation is faithful enough for most development but doesn't run the exact same infrastructure as production. Shuttle's cargo shuttle run provisions an actual local PostgreSQL database via Docker and wires it to your application — what you run locally matches production closely because both use the same Shuttle runtime. For team onboarding, Encore's development dashboard provides the best documentation and discoverability of a project's structure.

Cloud Portability and Vendor Lock-in Risk

Cloud portability is increasingly important as cloud provider pricing and feature sets evolve. Nitric is the strongest choice for cloud portability — the same Nitric application code deploys to AWS, GCP, or Azure by changing a single configuration file. This abstraction is possible because Nitric limits its resource primitives to the common denominator available across providers: APIs, topics, key-value stores, object storage, and schedules. If you need a provider-specific service (Aurora Serverless, Cloud Spanner, or Azure Service Bus specifically), Nitric's abstraction breaks down and you need to call the provider SDK directly. Encore supports deployment to Encore Cloud, AWS, and GCP, but the infrastructure code is Encore-specific and doesn't translate to other frameworks without refactoring. Shuttle is the most locked-in, deploying exclusively to Shuttle's own infrastructure. Teams with multi-cloud strategies or active cloud migration plans should weight Nitric's portability heavily.

See also: Best Serverless Frameworks for Node.js 2026, Express vs Fastify, and Cloudflare Workers vs Vercel Edge vs AWS Lambda 2026

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.