Encore.rs
An infrastructure SDK for Rust
Encore.rs is an infrastructure SDK for Rust. You declare the infrastructure your application needs, such as databases, Pub/Sub topics, object storage buckets, Redis databases, cron jobs and secrets, as statics in your Rust code, and use them through their methods. Encore reads the declarations from your source, runs matching infrastructure locally with encore run, and provisions the real resources when you deploy.
The SDK is the encore-dev crate, imported as encore. It links Encore's Rust runtime directly: there is no FFI layer and no code generation.
Encore.rs covers infrastructure only. It has no API framework: you serve HTTP with the web framework you already use, such as axum or actix-web, and call the SDK from your handlers.
Encore.rs is in early access. APIs may still change, and some features are not yet available. The sections below note where behavior is incomplete.
What you get with Encore.rs
- Infrastructure as Rust: declare databases, Pub/Sub topics and subscriptions, object storage buckets, Redis databases, cron jobs, secrets and configuration as
staticvalues. Encore works out from your code which service uses what, and grants exactly that access. - Local infrastructure, automatically:
encore runstarts Postgres, a Pub/Sub broker, an object storage emulator and a Redis server for your app, applies your migrations, and builds and runs each service with Cargo. - Isolated tests: under
encore test, every test gets its own databases, buckets, Redis databases and topics. See Testing. - Observability: database, Pub/Sub, object storage and Redis calls are traced, and your own spans join them through OpenTelemetry. See Tracing, Logging and Metrics.
- Your own web framework: Encore.rs doesn't own your
mainor your router.
The infrastructure primitives
Each primitive is a module-level static, built with a const constructor. Options are chained onto the constructor.
use encore::storage::postgres::Database;
static DB: Database = Database::new("orders").migrations("./migrations");
use encore::pubsub::Topic;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct OrderPlaced {
order_id: String,
}
static ORDERS: Topic<OrderPlaced> = Topic::new("order-placed");
use encore::objects::Bucket;
static INVOICES: Bucket = Bucket::new("invoices");
use encore::storage::redis;
static SESSIONS: redis::Database = redis::Database::new("sessions");
use encore::cron::{Job, Schedule};
static SEND_INVOICES: Job =
Job::new("send-invoices", "/internal/send-invoices", Schedule::Every("2h"))
.title("Send pending invoices");
use encore::secrets::Secret;
static STRIPE_KEY: Secret = Secret::new("StripeKey");
Encore reads these declarations statically, so a few rules apply to all of them:
- A declaration is a
staticat module level, not a local variable or a value built in a function. - Names and options are string literals (or other literals) written at the declaration.
- A declared resource is used by calling its methods. To hand it to other code, create a handle, such as a
DatabaseReffromDB.read(). - Every use must be inside a service, so Encore knows which service to grant it to.
Encore reports a broken rule when it parses the app, on encore run, encore test or encore build, even where the Rust compiler accepts the code.
Bring your own web framework
A Rust service is an ordinary binary. It listens on the address Encore gives it in the ENCORE_LISTEN_ADDR environment variable (PORT carries the same port), and calls the SDK from its handlers. With axum:
src/main.rsuse axum::{extract::Path, http::StatusCode, routing::post, Router};
use encore::storage::postgres::Database;
static DB: Database = Database::new("users").migrations("./migrations");
async fn add_user(Path(name): Path<String>) -> StatusCode {
match DB.execute("INSERT INTO users (name) VALUES ($1)", &[&name]).await {
Ok(_) => StatusCode::CREATED,
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
#[tokio::main]
async fn main() {
// Fail at startup, not on the first request, if the runtime isn't configured.
encore::initialize();
let app = Router::new().route("/users/:name", post(add_user));
let addr = std::env::var("ENCORE_LISTEN_ADDR").unwrap_or_else(|_| {
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
format!("0.0.0.0:{port}")
});
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(encore::shutdown::signal())
.await
.unwrap();
std::process::exit(encore::shutdown::drained().await.code());
}
The last lines hand shutdown to Encore: the server stops on SIGTERM or SIGINT and drains its requests, then Encore drains Pub/Sub handlers and flushes telemetry. See Graceful shutdown.
App layout
An Encore app is a directory with an encore.app file. Each service is a directory below it with an encore.service.json file, and for Rust, a Cargo package:
/my-app
├── encore.app
└── users // the users service
├── encore.service.json
├── Cargo.toml
├── migrations
│ └── 1_create_users.up.sql
└── src
└── main.rs
encore app new my-app --template rust creates an app with one Rust service at its root, and encore app init makes an existing directory an app.
The service file names the service and sets "lang": "rust":
users/encore.service.json{
"name": "users",
"lang": "rust",
"api": [
{ "type": "http", "prefix": "/", "name": "users", "expose": true }
]
}
The api entry tells Encore that the service serves HTTP itself under /, and expose routes requests from the API gateway to it. See Services for every field, and for apps with several services or a Cargo workspace.
The package depends on encore-dev, and on Tokio and your web framework:
users/Cargo.toml[package]
name = "users"
version = "0.1.0"
edition = "2024"
[dependencies]
encore-dev = { path = "/path/to/encore-dev" }
axum = "0.7"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"] }
During early access, encore-dev is not published on crates.io. Add it as a path or git dependency pointing at the SDK source you were given. Keep the library name encore: the SDK's macros, such as #[encore::test], refer to the crate by that name.
Cargo features
The crate compiles only the infrastructure providers you need, since their clients account for most of its build time. None are on by default:
You don't enable the provider features yourself. encore run and encore test build with local, and encore build with the clouds it builds for, using the name your Cargo.toml gives the dependency. A service with a custom run or test command has to pass --features encore-dev/local itself. Postgres, Redis, secrets and config need no feature.
The runtime
The SDK starts its runtime on first use, from the configuration Encore passes to the process. If that fails, for example because the binary was started with cargo run instead of encore run, the process prints encore: failed to initialize the SDK runtime and exits.
Three functions control it:
encore::initialize()starts the runtime right away, so a misconfigured service fails at startup. It also starts the Pub/Sub subscriptions declared with#[encore::pubsub::register]. Call it at the top ofmain.encore::run_blocking()starts the registered subscriptions, blocks until a shutdown signal, drains, and exits the process. It is for a service that serves no HTTP of its own, such as a Pub/Sub worker. Call it from a synchronousmain, not from inside a Tokio runtime.encore::tokio_handle()returns a handle to the Tokio runtime the SDK's core runs its background work on, for spawning tasks onto it.
The SDK's methods are ordinary async functions. Your application brings its own Tokio runtime, typically with #[tokio::main], and awaits them there.
A worker service with no HTTP server:
use encore::pubsub::{Message, Subscription, Topic};
static ORDERS: Topic<String> = Topic::new("order-placed");
#[encore::pubsub::register]
static FULFIL: Subscription<String> = Subscription::new(&ORDERS, "fulfil", fulfil);
async fn fulfil(msg: Message<String>) -> Result<(), encore::Error> {
println!("fulfilling {}", msg.data);
Ok(())
}
fn main() {
encore::run_blocking();
}
Running and testing
encore runbuilds each service withcargo build, runs it withcargo run, provisions the infrastructure it declares, and rebuilds and restarts a service when its code changes.encore testrunscargo testin each service, with infrastructure of its own for every test. See Testing.encore buildbuilds the services for deployment, with the cloud providers compiled in. See Build Docker Images for self-hosting.
See the CLI reference for every command.
The Encore.rs docs
Primitives
- Services: the service file, how Cargo packages map to services, and service URLs.
- Databases: Postgres databases, migrations and queries.
- Object Storage: buckets and objects.
- Pub/Sub: topics, subscriptions and ordered delivery.
- Cron Jobs: requests Encore makes of a service on a schedule.
- Redis: Redis databases, transactions and scripts.
- Secrets: service, shared and global secrets.
Develop
- Configuration: typed config values in files beside your code.
- Metadata: the app's environment, build and deployment.
- Testing: isolated infrastructure per test, and fixtures.
- Graceful shutdown: draining requests and messages on shutdown.
Observability