A batteries-included web framework for Rust: declare a model once, and get an ORM, auto-migrations, an auto-admin, multi-tenancy, and a REST API out of it.
Runs on axum and tokio. Handlers are plain axum handlers and everything Rustango adds is a tower layer or an axum::Router, so any axum extractor, middleware or crate from that ecosystem drops straight in.
One #[derive(Model)] is the whole contract β from it Rustango emits typed queries, migration diffs, admin screens, serializers, and CRUD endpoints. A tri-dialect ORM, first-class auth, and every standard middleware ship in the box: all opt-out via cargo features, and all working on Postgres, MySQL, and SQLite from the same source.
π Docs: rustango.com Β· in-repo guides Β· API reference
π Also in: Deutsch Β· EspaΓ±ol Β· FranΓ§ais β every published guide, not a subset.
π³ Cookbook: cookbook_blog/COOKBOOK.md β a runnable, test-backed recipe for every feature below.
[dependencies]
# Postgres (default)
rustango = "0.58"
# SQLite β file-backed or in-memory
rustango = { version = "0.58", default-features = false, features = ["sqlite", "tenancy", "admin", "manage"] }
# MySQL 8+
rustango = { version = "0.58", default-features = false, features = ["mysql", "tenancy", "admin", "manage"] }Every capability is a cargo feature you can turn off. Renaming the dep works too β #[derive(Model)] resolves the crate root via proc-macro-crate, so orm = { package = "rustango", version = "0.58" } needs no extra wiring.
Moving between versions? Rustango is 0.x, so a minor bump is allowed to break things and several have. UPGRADING.md has the per-version notes and a checklist β including the two that bite regardless of version: a session secret that can stop a booting app, and a generated system migration that has to reach production.
use std::sync::Arc;
use axum::{routing::get, Extension, Json, Router};
use rustango::core::Model as _;
use rustango::server::AppBuilder;
use rustango::sql::{Auto, FetcherPool, Pool};
use rustango::Model;
#[derive(Model, Debug, Clone, serde::Serialize)]
#[rustango(table = "demo_user")]
pub struct User {
#[rustango(primary_key)] pub id: Auto<i64>,
#[rustango(max_length = 80)] pub name: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
AppBuilder::from_env().await? // reads DATABASE_URL
.bootstrap(&[User::SCHEMA]).await? // CREATE TABLE IF NOT EXISTS
.api(Router::new().route("/users", get(list)))
.serve("0.0.0.0:8080").await
}
async fn list(Extension(pool): Extension<Arc<Pool>>) -> Json<Vec<User>> {
Json(User::objects().fetch(&pool).await.unwrap())
}# Backend selection lives in your Cargo.toml β see Install above.
# `cargo run --features sqlite` would name a feature of *your* crate, not rustango's.
DATABASE_URL='sqlite:./var/app.db?mode=rwc' cargo runThe same code boots on Postgres with DATABASE_URL=postgres://β¦ or MySQL with DATABASE_URL=mysql://β¦ β no changes. Every SQLite connection turns on sensible defaults automatically (PRAGMA foreign_keys = ON, journal_mode = WAL for file-backed DBs, busy_timeout = 5s).
- One ORM, three backends. Models, queries, migrations, relations, and aggregates emit correct SQL for Postgres, MySQL 8+, and SQLite from the same code.
- Batteries included. Auth (sessions + JWT + OAuth2/OIDC + HMAC + API keys + TOTP), an auto-admin, multi-tenancy, caching, background jobs, email, file storage, signals, i18n, an MCP server, and OpenAPI β not add-ons, in the box.
- Declare it, don't wire it. A project scaffolder (
cargo rustango new),make:*generators, amanageCLI,#[derive(Model)]/#[derive(ViewSet)]/#[derive(Serializer)], and admin config blocks β an app is described in attributes, not assembled by hand. - Opt-out, not opt-in. Everything is a cargo feature. A JSON-only API binary compiles out the admin, templates, and tenancy entirely.
- Quick start
- The ORM
- Migrations
- Auto-admin
- APIs β ViewSets, Serializers, JWT, OpenAPI
- HTML views & forms
- Multi-tenancy
- Authentication & permissions
- Security middleware
- Caching, email, storage, jobs
- Signals, i18n, MCP
- The
manageCLI - Configuration
- Testing
- Features
- Documentation
cargo install cargo-rustango
cargo rustango new myblog # default: ORM + admin
cargo rustango new myapi --template api # JSON-only, no admin
cargo rustango new shop --template tenant # multi-tenancy + operator consolecd myblog
cp .env.example .env # edit DATABASE_URL
docker compose up -d postgres # starts Postgres only
cargo run -- makemigrations # generate migrations from your models
cargo run -- migrate # apply pending migrations
cargo run # http://localhost:8080For autoreload during development: cargo watch -x run (or bacon run).
Add an app and a model:
cargo run -- startapp blog # scaffolds src/blog/ with a starter model + admin block + smoke testuse rustango::{Auto, Model};
use chrono::{DateTime, Utc};
#[derive(Model, Clone)]
#[rustango(
table = "posts",
display = "title",
admin(list_display = "id, title, published_at", search_fields = "title, body"),
audit(track = "title, body"),
index("published_at, author_id"),
)]
pub struct Post {
#[rustango(primary_key)]
pub id: Auto<i64>,
#[rustango(max_length = 200)]
pub title: String,
pub body: String,
pub author_id: i64,
#[rustango(auto_now_add)]
pub published_at: Auto<DateTime<Utc>>,
}cargo run -- makemigrations # generates migration JSON from the model diff
cargo run -- migrate # applies it
cargo run -- make:viewset PostViewSet --model Post
cargo run -- make:serializer PostSerializer --model PostFull walkthrough: getting started Β· scaffolding.
#[derive(Model)] registers a struct in a global inventory and emits typed query, save, and FromRow code. The query builder is lazy and chainable (.filter(), .exclude(), .order_by(), .annotate(), .select_related(), .prefetch_related()) β nothing hits the database until you .fetch() β and the same code runs on all three backends through the Pool enum.
// Filter, order, paginate
let recent = Post::objects()
.filter("published_at__lt", Utc::now())
.exclude("status", "draft")
.order_by(&[("published_at", true)]) // true = DESC
.limit(20)
.fetch(&pool).await?;
// Aggregate (scalar): .values(&[]) β one row, no GROUP BY
let stats = Post::objects()
.values(&[])
.annotate("total", AggregateExpr::Count(None))
.annotate("avg_views", AggregateExpr::Avg("view_count"))
.compile()?;Supported: every field type (ints, floats, String, bool, DateTime/Date, Uuid, Json, Decimal, plus PG-only Array/Range/HStore/Vector/Geometry), nullable Option<T>, Auto<T> primary keys, ForeignKey<T> / one-to-one / many-to-many, generic FKs + composite-key FKs (ContentTypes), soft-delete, unique_together / index_together, container-level default scopes, subquery/EXISTS filters, bulk insert/update, transactions, and raw SQL escape hatches. EXPLAIN works on any queryset.
π ORM guide Β· models Β· runnable ORM recipes
makemigrations diffs your models against the last migration snapshot and emits JSON operations; migrate applies pending ones, and downgrade rolls back. Schema changes (create/alter/drop tables, columns, indexes, constraints, composite FKs) are auto-detected; data migrations are hand-authored with sql + reverse_sql. embed_migrations!("migrations") bakes them into the binary.
cargo run -- makemigrations
cargo run -- migrate
cargo run -- downgrade # roll back the last migrationπ Adopt an existing schema with manage inspectdb β it emits #[derive(Model)] source for every table.
An admin(...) block on a model gives you a full CRUD admin β list_display, list_filter, search_fields, date_hierarchy, fieldsets, ordering, pagination, and bulk actions β with zero hand-written views:
Also included: a token-driven theme system with dark mode and per-tenant branding (logo/colors via the pluggable Storage trait β S3/R2/B2/MinIO/local), inline child editing (register_admin_inline!), a per-write audit trail with JSON diffs, a users/roles/permissions RBAC surface, a self-serve change-password page, and session invalidation on password rotation.
π Admin guide
#[derive(ViewSet)] gives you full REST CRUD β list (page or cursor pagination), retrieve, create (one row or a whole array in a single POST), update, partial update, destroy (soft when the model opts in) β with per-action permission gates:
#[derive(ViewSet)]
#[viewset(
model = Post,
fields = "id, title, body, author_id, published_at",
filter_fields = "author_id, status",
search_fields = "title, body",
ordering = "-published_at",
page_size = 20,
permissions(list = "post.view", create = "post.add", update = "post.change", destroy = "post.delete"),
)]
pub struct PostViewSet;
let app = Router::new().merge(PostViewSet::router("/api/posts", pool.clone()));#[derive(Serializer)] is a declarative JSON faΓ§ade over a model (read-only / write-only / renamed / computed method fields, per-field validate, nested FK serialization, and many collections). JWT ships a full lifecycle (issue with custom claims, verify without a DB hit, refresh, re-check permissions, revoke/blacklist). OpenAPI 3.1 auto-derives from your serializers + viewsets, and responses follow JSON:API + RFC 7807 Problem Details. The HTTP QUERY method (RFC 10008) is supported for body-carrying reads.
π ViewSets Β· serializers Β· JWT Β· OpenAPI Β· QUERY method
Class-based views (ListView, DetailView, CreateView, UpdateView, DeleteView) render Tera templates with pagination, filters, bulk actions, FK-display, and business-validation hooks. ModelForm-style forms parse and validate against a model (auto-skipping DB-populated fields), aggregate per-field errors, and emit an insert query. Every view router with a POST route checks the CSRF token.
π HTML views
Cli::tenancy().run() boots an operator console, a per-tenant admin, and host-based request dispatch on any backend. Tenants resolve from subdomain, header, path, or port via a resolver chain; each Org picks its isolation strategy:
| Mode | Backends | What it does | When |
|---|---|---|---|
database (default) |
PG, MySQL, SQLite | A dedicated database (or SQLite file) per tenant, one cached pool each. | Enterprise B2B, compliance, sharding β anything on MySQL/SQLite. |
schema |
Postgres only | All tenants share one DB, one per PG schema, one shared pool with SET search_path per request. |
High-N SaaS on PG (500+ small tenants) where connection counts bite. |
Database-mode is the default and works identically everywhere; schema-mode is a Postgres-only pool optimization. Set schema on MySQL/SQLite and the framework returns a clear error pointing you back to database-mode.
π Runnable walkthrough: cookbook Ch. 5 β Multi-tenancy
Pluggable auth backends (model / API-key / JWT β first to recognize the credential wins), argon2id password hashing, typed permission helpers (codename-based, superuser bypass), sessions, TOTP/2FA, API keys, and signed URLs (magic links / time-bounded file downloads).
π passwords Β· sessions Β· backends Β· API keys Β· decorators Β· flows
One hardened middleware chain: request IDs, access logging, rate limiting (in-process or distributed via cache), CORS presets, security-header presets + custom/staged CSP, CSP report endpoint, IP allow/block, CSRF, and per-account lockout. manage check --deploy runs an automated pre-ship audit.
π Security guide Β· middleware catalog
- Caching β in-memory / Redis backends,
get_or_setmemoization, and per-view response caching (CachePageLayer). caching - Email β a renderer +
Mailable+ job-backed delivery pipeline with pluggable backends. email - Storage & media β pluggable
Storage(S3/R2/B2/MinIO/local),Mediarows, presigned uploads, collections, and tags. files - Background jobs β in-memory or DB queue (
FOR UPDATE SKIP LOCKEDon PG/MySQL 8+, transaction-boundedUPDATE β¦ RETURNINGon SQLite) plus scheduled tasks. jobs
- Signals β model lifecycle (
pre_save/post_save/pre_delete/post_delete) and request lifecycle (request_started/request_finished/got_request_exception). - i18n β
Translatoris agettext-style translation API: per-locale catalogs, base-language fallback,{name}placeholders, CLDR pluralization, plus a DB-override layer and live admin translation editor. i18n - MCP server β the
mcpfeature turns an app into a Model Context Protocol server: AI agents authenticate as tenant-scoped identities and call your framework-exposed tools over JSON-RPC 2.0. mcp
Your app's binary doubles as its admin CLI β cargo run -- <cmd>. Migrations (makemigrations / migrate / inspectdb), scaffolders (startapp / make:viewset / make:serializer), system commands (check / check --deploy / dbshell), and β with the tenancy feature β operator/tenant/superuser provisioning and recovery verbs.
π manage reference
Layered config: a <env>_settings.toml pipeline (default.toml β <env>_settings.toml β RUSTANGO__* environment variables), typed sections, compile-time feature reflection, and a deploy audit. Everything has a sensible default; override only what you need.
A TestClient drives the router as a tower service (no socket), a RequestFactory builds requests, and fixtures seed data. The cookbook's ~150 tests run against live Postgres, MySQL, and SQLite.
π testing
Always on: the ORM, the query builder, the SQL layer and migrations. Everything else is a cargo feature.
default = ["postgres", "batteries"] turns on most of the list below.
The ones it does not, opt in by name: mysql, sqlite, tenancy,
csrf, sso, admin-sso, passkey, cache-redis, cache-page,
email-smtp, mcp, testkit and test_utils.
Backends β pick one; every framework surface works the same on all three.
| Feature | |
|---|---|
postgres |
PostgreSQL, TLS included. The default. |
mysql |
MySQL 8.0+. |
sqlite |
SQLite 3.35+, with WAL and foreign keys on. |
Data & storage
| Feature | |
|---|---|
casts |
Typed field conversions. |
media |
Media library with collections and tags. |
storage |
File storage abstraction. |
storage-s3 |
S3-compatible backend. |
uploads |
Multipart upload handling. |
signed_url |
Expiring signed URLs. |
Web
| Feature | |
|---|---|
runserver |
The development server. |
manage |
cargo run -- <verb> CLI dispatcher. |
template_views |
Generic CRUD view handlers. |
forms |
Forms framework with multi-error validation. |
sessions |
Server-side sessions. |
compression |
Response compression. |
sse |
Server-sent events. |
websocket |
WebSocket support. |
http-client |
Outbound HTTP client. |
APIs
| Feature | |
|---|---|
serializer |
Typed JSON serializers. |
openapi |
OpenAPI schemas generated from serializers. |
jwt |
JWT with refresh, blacklist and custom claims. |
api_keys |
API key authentication. |
hmac-auth |
HMAC request signing. |
oauth2 |
OAuth2 provider. |
webhook |
Webhook registration. |
webhook-delivery |
Delivery with retries. |
Admin
| Feature | |
|---|---|
admin |
The auto-admin site. |
admin-sso |
OIDC single sign-on for admins. |
Auth & security
| Feature | |
|---|---|
auth_flows |
Login, logout, password reset. |
passwords |
Password hashing and validators. |
totp |
TOTP two-factor authentication. |
passkey |
WebAuthn / passkeys. |
sso |
OIDC single sign-on for app users. |
csrf |
CSRF middleware for form POSTs. |
csp-nonce |
Per-response CSP nonces. |
secrets |
Secret management and rotation. |
Operations
| Feature | |
|---|---|
cache |
Cache framework. |
cache-redis |
Redis backend. |
cache-page |
Whole-page response caching. |
jobs |
In-process background job queue. |
jobs-postgres |
Database-backed queue, surviving restarts. |
scheduler |
Fixed-interval tasks. |
signals |
Model lifecycle signals. |
email |
Email framework. |
email-smtp |
SMTP transport. |
notifications |
User notifications. |
config |
Layered settings and deploy audit. |
Multi-tenancy
| Feature | |
|---|---|
tenancy |
Tenant registry, per-tenant databases, operator console. Schema mode is PostgreSQL only. |
Tooling
| Feature | |
|---|---|
mcp |
Model Context Protocol server for AI agents. |
testkit |
Schema builders and model factories for tests. |
test_utils |
Test-only constructors for downstream crates. |
Internationalisation, signals, content types, permissions and the audit
log need no feature flag. cargo rustango new --help prints the opt-in
list as the scaffolder sees it.
- Guides & tutorials: https://rustango.com
- Runnable cookbook:
cookbook_blog/COOKBOOK.mdβ a test-backed recipe for every feature, on all three backends. - In-repo guides (
docs/): getting started Β· models Β· ORM Β· migrations & CLI Β· admin Β· viewsets Β· serializers Β· auth Β· security Β· middleware Β· caching Β· email Β· files Β· jobs Β· i18n Β· MCP Β· testing Β· glossary - API reference: https://docs.rs/rustango
- Changelog:
CHANGELOG.md
Git hooks (fmt + secret/debris scan on pre-commit; cargo check --all-features + clippy + lib tests on pre-push) install with bin/install-hooks.sh (sets git config core.hooksPath .githooks). Please run cargo fmt and cargo clippy --all-targets before opening a PR.
Licensed under either of
- Apache License, Version 2.0 (
LICENSE-APACHEor http://www.apache.org/licenses/LICENSE-2.0) - MIT license (
LICENSE-MITor http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
