<p align="center">
<img src="https://raw.githubusercontent.com/ujeenet/rustango/main/crates/rustango/src/tenancy/static/rustango.png" alt="Rustango" width="480">
</p>
# Rustango
**A Django-shaped, batteries-included web framework for Rust.**
Rustango gives you the productivity of Django or Laravel with the speed and type-safety of Rust: a tri-dialect ORM, auto-migrations, an auto-generated admin, multi-tenancy, first-class auth, and every standard middleware β all shipped, all opt-out via cargo features, and all working on **Postgres, MySQL, and SQLite** out of the box.
π **Docs:** [rustango.im-fullstack.dev](https://rustango.im-fullstack.dev) Β· [in-repo guides](docs/) Β· [API reference](https://docs.rs/rustango)
π³ **Cookbook:** [`cookbook_blog/COOKBOOK.md`](crates/rustango/examples/cookbook_blog/COOKBOOK.md) β a runnable, test-backed recipe for every feature below.
---
## Install
```toml
[dependencies]
# Postgres (default)
rustango = "0.44"
# SQLite β file-backed or in-memory
rustango = { version = "0.44", default-features = false, features = ["sqlite", "tenancy", "admin", "manage"] }
# MySQL 8+
rustango = { version = "0.44", 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.44" }` needs no extra wiring.
## 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(&pool).await.unwrap())
}
```
```sh
DATABASE_URL='sqlite:./var/app.db?mode=rwc' cargo run --features sqlite,runserver
```
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.
- **Django ergonomics.** A project scaffolder (`cargo rustango new`), `make:*` generators, `manage` CLI, `#[derive(Model)]` / `#[derive(ViewSet)]` / `#[derive(Serializer)]`, and admin config blocks that feel familiar coming from Django, DRF, or Laravel.
- **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)
- [Comparison](#comparison)
- [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 # starts Postgres
cargo run -- migrate # apply bootstrap 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 Django-shape (`.filter()`, `.exclude()`, `.order_by()`, `.annotate()`, `.select_related()`, `.prefetch_related()`), and the **same code runs on all three backends** through the `Pool` enum.
```rust
// Filter, order, paginate
let recent = Post::objects()
.filter("published_at", Op::Lt, Utc::now())
.exclude("status", Op::Eq, "draft")
.order_by(&[("published_at", true)]) // true = DESC
.limit(20)
.fetch_pool(&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](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 can `unapply` to roll 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 -- migrate --unapply <name>
```
π [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 (incl. DRF-style bulk create), 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 DRF-shape JSON faΓ§ade (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
Django-shape 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. CSRF auto-mounts for form-driven views.
π [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](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 Django's `gettext` family in Rust: 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
`cargo run -- <cmd>` β Django's `manage.py` in Rust. Migrations (`makemigrations` / `migrate` / `inspectdb`), scaffolders (`startapp` / `make:viewset` / `make:serializer`), system commands (`check` / `check --deploy` / `shell`), 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 (base β env β local β 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)
---
## Comparison
| | Rustango | Django | Laravel | Rocket | Cot |
|---|:-:|:-:|:-:|:-:|:-:|
| ORM | β
| β
| β
| β | β
|
| Auto-migrations | β
| β
| β
| β | β
|
| Auto-admin | β
| β
| β οΈ Filament | β | β
|
| Multi-tenancy | β
| β οΈ ext | β οΈ ext | β | β |
| JWT lifecycle (refresh + blacklist + custom claims) | β
| β οΈ ext | β οΈ Sanctum/Passport | β | β |
| TOTP / 2FA | β
| β οΈ ext | β
Fortify | β | β |
| Signals | β
| β
| β
Events | β | β |
| Cache backends | β
| β
| β
| β | β οΈ optional |
| Email backends | β
| β
| β
| β | β |
| File storage | β
| β οΈ ext | β
Flysystem | β | β |
| Scheduled tasks | β
| β οΈ Celery beat | β
| β | β |
| Security headers | β
| β
| β οΈ middleware | β
Shield | β |
| Test client | β
| β
| β
| β
Client | β
|
| Project scaffolder | β
`cargo rustango new` | β
`startproject` | β
installer | β | β
`cot new` |
| File generators | β
`make:*` | β οΈ ext | β
artisan | β | β |
β
shipped Β· β οΈ partial / via extension Β· β not shipped
---
## Documentation
- **Guides & tutorials**: <https://rustango.im-fullstack.dev>
- **Runnable cookbook**: [`cookbook_blog/COOKBOOK.md`](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
MIT OR Apache-2.0