<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ujeenet/rustango/main/docs/rustango_dark.png">
<img src="https://raw.githubusercontent.com/ujeenet/rustango/main/docs/rustango_light.png" alt="Rustango β the batteries-included web framework for Rust" width="640">
</picture>
</p>
# Rustango
**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](https://github.com/tokio-rs/axum) and [tokio](https://tokio.rs).** 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](https://rustango.com) Β· [in-repo guides](docs/) Β· [API reference](https://docs.rs/rustango)
π **Also in:** [Deutsch](docs/de/) Β· [EspaΓ±ol](docs/es/) Β· [FranΓ§ais](docs/fr/) β every published guide, not a subset.
π³ **Cookbook:** [`cookbook_blog/COOKBOOK.md`](https://github.com/ujeenet/rustango/blob/main/crates/rustango/examples/cookbook_blog/COOKBOOK.md) β a runnable, test-backed recipe for every feature below.
---
## Install
```toml
[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](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.
## An app on SQLite in 30 lines
```rust
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())
}
```
```sh
# 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 run
```
The **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`).
## Why Rustango
- **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, a `manage` CLI, `#[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.
---
## Table of contents
- [Quick start](#quick-start)
- [The ORM](#the-orm)
- [Migrations](#migrations)
- [Auto-admin](#auto-admin)
- [APIs β ViewSets, Serializers, JWT, OpenAPI](#apis--viewsets-serializers-jwt-openapi)
- [HTML views & forms](#html-views--forms)
- [Multi-tenancy](#multi-tenancy)
- [Authentication & permissions](#authentication--permissions)
- [Security middleware](#security-middleware)
- [Caching, email, storage, jobs](#caching-email-storage-jobs)
- [Signals, i18n, MCP](#signals-i18n-mcp)
- [The `manage` CLI](#the-manage-cli)
- [Configuration](#configuration)
- [Testing](#testing)
- [Features](#features)
- [Documentation](#documentation)
---
## Quick start
```bash
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 console
```
```bash
cd 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:8080
```
For autoreload during development: `cargo watch -x run` (or [`bacon`](https://github.com/dtolnay/bacon) `run`).
Add an app and a model:
```bash
cargo run -- startapp blog # scaffolds src/blog/ with a starter model + admin block + smoke test
```
```rust
use 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>>,
}
```
```bash
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 Post
```
Full walkthrough: [getting started](docs/getting-started.md) Β· [scaffolding](docs/scaffolding.md).
---
## The ORM
`#[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.
```rust
// 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](docs/orm.md) Β· [models](docs/models.md) Β· [runnable ORM recipes](https://github.com/ujeenet/rustango/blob/main/crates/rustango/examples/cookbook_blog/COOKBOOK.md)
## Migrations
`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.
```bash
cargo run -- makemigrations
cargo run -- migrate
cargo run -- downgrade # roll back the last migration
```
π [Adopt an existing schema](docs/manage.md) with `manage inspectdb` β it emits `#[derive(Model)]` source for every table.
## Auto-admin
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:
<p align="center">
<img src="https://raw.githubusercontent.com/ujeenet/rustango/main/docs/img/admin.png" alt="The auto-admin: a Posts list with filter facets, search, bulk actions, and pagination β all from one admin(...) block" width="760">
</p>
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](docs/admin.md)
## APIs β ViewSets, Serializers, JWT, OpenAPI
`#[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:
```rust
#[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](docs/viewsets.md) Β· [serializers](docs/serializers.md) Β· [JWT](docs/auth-jwt-api.md) Β· [OpenAPI](docs/openapi.md) Β· [QUERY method](docs/query-method.md)
## HTML views & forms
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](docs/html-views.md)
## Multi-tenancy
`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](https://github.com/ujeenet/rustango/blob/main/crates/rustango/examples/cookbook_blog/COOKBOOK.md#chapter-5--multi-tenancy)
## Authentication & permissions
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](docs/auth-passwords.md) Β· [sessions](docs/auth-sessions.md) Β· [backends](docs/auth-backends.md) Β· [API keys](docs/auth-api-keys.md) Β· [decorators](docs/auth-decorators.md) Β· [flows](docs/auth-flows.md)
## Security middleware
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](docs/security.md) Β· [middleware catalog](docs/middleware.md)
## Caching, email, storage, jobs
- **Caching** β in-memory / Redis backends, `get_or_set` memoization, and per-view response caching (`CachePageLayer`). [caching](docs/caching.md)
- **Email** β a renderer + `Mailable` + job-backed delivery pipeline with pluggable backends. [email](docs/email.md)
- **Storage & media** β pluggable `Storage` (S3/R2/B2/MinIO/local), `Media` rows, presigned uploads, collections, and tags. [files](docs/files.md)
- **Background jobs** β in-memory or DB queue (`FOR UPDATE SKIP LOCKED` on PG/MySQL 8+, transaction-bounded `UPDATE β¦ RETURNING` on SQLite) plus scheduled tasks. [jobs](docs/jobs.md)
## Signals, i18n, MCP
- **Signals** β model lifecycle (`pre_save` / `post_save` / `pre_delete` / `post_delete`) and request lifecycle (`request_started` / `request_finished` / `got_request_exception`).
- **i18n** β `Translator` is a `gettext`-style translation API: per-locale catalogs, base-language fallback, `{name}` placeholders, CLDR pluralization, plus a DB-override layer and live admin translation editor. [i18n](docs/i18n.md)
- **MCP server** β the `mcp` feature 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](docs/mcp.md)
## The `manage` CLI
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](docs/manage.md)
## Configuration
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.
## Testing
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](docs/testing.md)
---
## Features
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.
---
## Documentation
- **Guides & tutorials**: <https://rustango.com>
- **Runnable cookbook**: [`cookbook_blog/COOKBOOK.md`](https://github.com/ujeenet/rustango/blob/main/crates/rustango/examples/cookbook_blog/COOKBOOK.md) β a test-backed recipe for every feature, on all three backends.
- **In-repo guides** ([`docs/`](docs/)): [getting started](docs/getting-started.md) Β· [models](docs/models.md) Β· [ORM](docs/orm.md) Β· [migrations & CLI](docs/manage.md) Β· [admin](docs/admin.md) Β· [viewsets](docs/viewsets.md) Β· [serializers](docs/serializers.md) Β· [auth](docs/auth-flows.md) Β· [security](docs/security.md) Β· [middleware](docs/middleware.md) Β· [caching](docs/caching.md) Β· [email](docs/email.md) Β· [files](docs/files.md) Β· [jobs](docs/jobs.md) Β· [i18n](docs/i18n.md) Β· [MCP](docs/mcp.md) Β· [testing](docs/testing.md) Β· [glossary](docs/glossary.md)
- **API reference**: <https://docs.rs/rustango>
- **Changelog**: [`CHANGELOG.md`](CHANGELOG.md)
## Contributing
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.
## License
Licensed under either of
- Apache License, Version 2.0 ([`LICENSE-APACHE`](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([`LICENSE-MIT`](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
at your option.
### Contribution
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.