rustango 0.46.0

Django-shaped batteries-included web framework for Rust: ORM + migrations + auto-admin + multi-tenancy + audit log + auth (sessions, JWT, OAuth2/OIDC, HMAC) + APIs (ViewSet, OpenAPI auto-derive, JSON:API) + jobs (in-mem + Postgres) + email + media (S3 / R2 / B2 / MinIO + presigned uploads + collections + tags) + production middleware (CSRF, CSP, rate-limiting, compression, idempotency, etc.).
Documentation
<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