Skip to content

Repository files navigation

Rustango β€” the batteries-included web framework for Rust

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


Install

[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.

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

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

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.

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.

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:

The auto-admin: a Posts list with filter facets, search, bulk actions, and pagination β€” all from one admin(...) block

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

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

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

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

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


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

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

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.

About

Fullstack Rust Web Framework

Resources

Security policy

Stars

18 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages