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

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 Β· in-repo guides Β· API reference 🍳 Cookbook: cookbook_blog/COOKBOOK.md β€” a runnable, test-backed recipe for every feature below.


Install

[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

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())
}
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

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
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 run).

Add an app and a model:

cargo run -- startapp blog                   # scaffolds src/blog/ with a starter model + admin block + smoke test
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>>,
}
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 Β· scaffolding.


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.

// 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 Β· models Β· runnable ORM recipes

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.

cargo run -- makemigrations
cargo run -- migrate
cargo run -- migrate --unapply <name>

πŸ“– Adopt an existing schema 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:

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

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:

#[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 Β· serializers Β· JWT Β· OpenAPI Β· QUERY method

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

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

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 Β· sessions Β· backends Β· API keys Β· decorators Β· flows

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 Β· middleware catalog

Caching, email, storage, jobs

  • Caching β€” in-memory / Redis backends, get_or_set memoization, 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), Media rows, presigned uploads, collections, and tags. files
  • 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

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
  • 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

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

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


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

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