Skip to main content

Encore vs Nitric vs Shuttle: Cloud-Native Backend Frameworks Compared (2026)

·PkgPulse Team

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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.