rustango 0.27.7

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
[package]
name = "rustango"
description = "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.)."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
keywords.workspace = true
categories.workspace = true
# Drop runnable demos + integration-test files from the published
# tarball. `src/admin/templates/` and `src/tenancy/{templates,static}/`
# stay — they're baked into the binary via include_str! / include_bytes!.
exclude = ["examples/", "tests/"]

[lints]
workspace = true

[features]
# Out of the box: ORM + auto-admin + Postgres + layered config +
# public forms framework. Opt in to multi-tenancy with
# `features = ["tenancy"]`. Drop `default-features` for the bare
# ORM (core + query + sql + migrate).
default = ["postgres", "manage", "admin", "config", "forms", "serializer", "cache", "signals", "email", "storage", "scheduler", "secrets", "totp", "webhook", "webhook-delivery", "api_keys", "passwords", "signed_url", "notifications", "jobs", "jobs-postgres", "auth_flows", "sse", "websocket", "oauth2", "http-client", "compression", "openapi", "csp-nonce", "sessions", "hmac-auth", "jwt", "uploads", "storage-s3", "media", "runserver"]
postgres = ["sqlx/postgres"]

# Unified `cargo run` / `cargo run -- <verb>` dispatcher. Pulls the
# minimum axum + tokio surface so bare-API projects (without admin)
# can still use `rustango::manage::Cli::new().api(...).run().await`.
# v0.16+. Default-on so `cargo rustango new` projects work with no
# extra opt-ins.
manage = ["dep:axum", "dep:tokio", "runtime"]

# MySQL 8.4+ backend. Lands fully in rustango v0.23.0-batch2 (when the
# `MySqlDialect` impl + DDL writer dispatch ship); the `mysql` feature
# in v0.23.0-batch1 wires the sqlx side so apps can already opt-in.
# Trying to actually `Pool::connect("mysql://...")` in batch1 returns a
# clear "not yet implemented in this version" error — apps building
# against `mysql` ahead of batch2 get a soft-error, not a panic.
mysql = ["sqlx/mysql"]

# SQLite 3.35+ backend. v0.27 Phase 1 ships the `Sqlite` dialect impl
# + writer support — every SQL writer in `crate::sql::writers` produces
# valid SQLite SQL through the dialect seam. Pool::Sqlite variant +
# bi-dialect macro `__rustango_from_sqlite_row` decoder land in
# subsequent phases (mirroring the v0.23 MySQL rollout); apps building
# against the `sqlite` feature today get the dialect for SQL emission +
# DDL but must execute statements through their own `sqlx::SqlitePool`
# until Phase 2 ships.
sqlite = ["sqlx/sqlite"]

# Layered TOML config (`rustango::config`). Loads `config/default.toml`
# → `config/{env}.toml` → env-var overrides. Slice 8.3.
config = ["dep:toml"]

# Public forms framework (`rustango::forms`) — value parsing for HTML
# form payloads, shared between admin and user route handlers. Slice
# 8.4A ships the parsers; slice 8.4B added `#[derive(Form)]`. Slice
# 8.4C lights up CSRF protection behind the `csrf` sub-feature so
# parser-only users don't pay the cookie/rand cost.
forms = []

# Serializer layer (`rustango::serializer`) — `#[derive(Serializer)]` +
# `ModelSerializer` trait. Produces typed JSON output from model instances
# with field control (read_only, write_only, source, skip) and cross-field
# validation. Implies `forms` so serializer errors share `FormErrors`.
serializer = ["forms"]

# Caching layer (`rustango::cache`) — pluggable `Cache` trait with
# `NullCache` (no-op) and `InMemoryCache` (tokio RwLock + TTL). Redis
# backend is behind the separate `cache-redis` feature.
cache = ["dep:async-trait", "dep:tokio"]

# Signals layer (`rustango::signals`) — Django-shape pre_save / post_save
# / pre_delete / post_delete async dispatch. Receivers register globally
# per model type; senders fire signals after write paths.
signals = ["dep:tokio"]

# Email backends (`rustango::email`) — Mailer trait + ConsoleMailer +
# InMemoryMailer + NullMailer. SMTP and external transports plug in via
# the trait.
email = ["dep:async-trait"]

# Multi-channel notifications layer (`rustango::notifications`) — Laravel-shape
# fan-out across mail / database / log / broadcast. Implies email + cache.
notifications = ["email", "cache"]

# Background job queue (`rustango::jobs`) — Job trait, in-memory queue,
# worker pool, retry with exponential backoff, dead-letter callback.
jobs = ["dep:async-trait", "dep:tokio"]

# Postgres-backed job queue (`rustango::jobs::pg::PgJobQueue`) using
# `SELECT … FOR UPDATE SKIP LOCKED` for safe multi-process / multi-replica
# pickup. Implies `jobs` + `postgres` and the always-on `chrono` workspace dep.
jobs-postgres = ["jobs", "postgres"]

# Pre-built auth flows (`rustango::auth_flows`) — password reset, email
# verification, magic-link login. Composes signed_url + email helpers.
auth_flows = ["signed_url"]

# Broadcast event bus (`rustango::sse`) — fan-out for SSE / WebSocket /
# signal-driven push. tokio::sync::broadcast under the hood.
sse = ["dep:tokio"]

# WebSocket handler scaffold (`rustango::ws`) — fan-out via the SSE
# EventBus, axum upgrade handler with auto JSON encode/decode + ping/pong
# keep-alive. Implies `admin` (axum) + `sse` (broadcast bus). Adds
# tokio-tungstenite via the `axum/ws` feature.
websocket = ["admin", "sse", "axum/ws"]

# OAuth2 / OIDC swiss-knife (`rustango::oauth2`) — works for pure OAuth2
# providers (GitHub, Discord) AND OIDC providers (Google, Microsoft,
# Apple, Keycloak) via the `/userinfo` endpoint as identity source.
# Per-tenant via `OAuth2Registry`. Pulls reqwest (rustls — no openssl).
oauth2 = ["dep:reqwest", "dep:urlencoding", "dep:tokio", "dep:hmac", "dep:sha2", "dep:base64", "dep:rand", "dep:subtle", "dep:async-trait"]

# Opinionated HTTP client (`rustango::http_client`) — reqwest wrapper
# with sane timeouts, exponential-backoff retry on idempotent verbs +
# 5xx/timeout/connect failures, and a default User-Agent. Standalone
# wrapper that any rustango app can use directly.
http-client = ["dep:reqwest", "dep:tokio"]

# Response compression middleware (`rustango::compression`) — gzip +
# deflate via flate2 (miniz_oxide by default, no C deps). Honors the
# client's `Accept-Encoding`, skips already-compressed and binary
# content-types. Implies `admin` (axum-only).
compression = ["admin", "dep:flate2"]

# Per-request CSP nonce middleware (`rustango::csp_nonce`) — generates
# a fresh CSPRNG nonce per request, stuffs it into request extensions
# for handlers to consume, and substitutes it into the
# Content-Security-Policy header so inline `<script nonce="…">` blocks
# pass strict CSP. Implies `admin` (axum-only) + rand for the nonce.
csp-nonce = ["admin", "dep:rand"]

# Server-side sessions (`rustango::sessions`) — opaque cookie ID
# backed by any `Cache` implementation. Implies `cache` + rand for
# the ID + base64 for encoding it.
sessions = ["cache", "dep:rand", "dep:base64"]

# HMAC-signed request authentication (`rustango::hmac_auth`) for
# service-to-service traffic. AWS-style canonical request signed with
# SHA-256, replay-bounded by an X-Date tolerance window. Implies
# `admin` (axum) + the existing webhook crypto deps.
hmac-auth = ["admin", "dep:hmac", "dep:sha2", "dep:subtle", "dep:base64"]

# Standalone HS256 JWT (`rustango::jwt`) — sign / verify / decode for
# magic links, microservice tokens, third-party SSO. Standalone
# alternative to `tenancy::jwt_lifecycle` (which adds refresh +
# blacklist + sliding rotation but is gated on tenancy).
jwt = ["dep:hmac", "dep:sha2", "dep:subtle", "dep:base64"]

# Multipart file upload helper (`rustango::uploads`) — wraps
# axum's multipart extractor + the `storage::Storage` trait so file
# uploads are a one-call API with size + extension guards. Pulls
# `multer` transitively via `axum/multipart`.
uploads = ["admin", "storage", "axum/multipart"]

# First-class media model (`rustango::media`) — a Postgres-backed
# Media row that references a file in the Storage layer. Handles
# direct browser upload (via S3 presigning), CDN-aware URLs,
# soft delete + orphan purge sweep. Implies `storage` + `postgres`.
media = ["storage", "postgres"]

# S3-compatible storage backend (`rustango::storage::s3::S3Storage`).
# Hand-rolled SigV4 over reqwest — works for AWS S3, Cloudflare R2,
# Backblaze B2, MinIO, and any other S3-compatible API. No
# `aws-sdk-s3` dependency.
storage-s3 = ["storage", "dep:reqwest", "dep:hmac", "dep:sha2"]

# OpenAPI 3.1 spec builder + Swagger UI router (`rustango::openapi`).
# Pure-Rust types you assemble into a spec, serialized to JSON via serde.
# Optional axum mount at `/openapi.json` + `/docs` (Swagger UI from a
# CDN). Forwards through to the proc-macro so `#[derive(Serializer)]`
# also emits an `OpenApiSchema` impl.
openapi = ["rustango-macros/openapi"]

# File storage backends (`rustango::storage`) — Storage trait + LocalStorage
# (filesystem) + InMemoryStorage (tests). S3/GCS/Azure plug in via the trait.
storage = ["dep:async-trait", "dep:tokio"]

# In-process scheduled task runner (`rustango::scheduler`) — fire async jobs
# at fixed intervals with panic isolation per task.
scheduler = ["dep:tokio"]

# Secrets manager (`rustango::secrets`) — Secrets trait + EnvSecrets +
# InMemorySecrets. Vault / AWS / GCP plug in via the trait.
secrets = ["dep:async-trait"]

# TOTP / 2FA (`rustango::totp`) — RFC 6238 time-based one-time passwords
# for second-factor authentication. SHA-1 (Google Authenticator default).
totp = ["dep:hmac", "dep:sha1", "dep:rand", "dep:subtle"]

# Webhook signature verification (`rustango::webhook`) — HMAC-SHA256
# constant-time verification for inbound webhooks (Stripe, GitHub, etc.).
webhook = ["dep:hmac", "dep:sha2", "dep:subtle", "dep:base64"]

# Outbound webhook delivery (`rustango::webhook_delivery`) — POSTs an
# HMAC-signed JSON payload to a subscriber URL via the background job
# queue (so retries with exponential backoff are free). Implies
# `webhook` (signing format) + `jobs` (retry machinery) + reqwest.
webhook-delivery = ["webhook", "jobs", "dep:reqwest"]

# API keys (`rustango::api_keys`) — standalone generate/hash/verify
# helpers for `{prefix}.{secret}` keys with argon2id hashing.
api_keys = ["dep:argon2", "dep:password-hash", "dep:rand"]

# Generic password helpers (`rustango::passwords`) — argon2id hash/verify
# + minimal strength check. Standalone alternative to tenancy::password.
passwords = ["dep:argon2", "dep:password-hash"]

# Signed URL helpers (`rustango::signed_url`) — HMAC-SHA256 signed URLs
# with optional expiry. For magic-link login, password resets, etc.
signed_url = ["dep:hmac", "dep:sha2", "dep:subtle", "dep:base64"]

# Redis cache backend (`rustango::cache::RedisCache`). Adds the `redis`
# async connection-manager client. Implies `cache`.
cache-redis = ["cache", "dep:redis"]

# CSRF protection (`rustango::forms::csrf`) — double-submit-cookie
# axum middleware. Lives behind its own flag so `forms`-only users
# (parsers without CSRF) don't pull cookie + rand + tower
# transitively.
csrf = ["forms", "dep:axum", "dep:base64", "dep:cookie", "dep:rand", "dep:tower"]

# Auto-admin HTTP layer (`rustango::admin`). Pulls in axum + Tera +
# the supporting middleware deps. Implies `forms` + `csrf` so the
# admin's CRUD handlers reuse the shared parsers and ship CSRF on
# every mutation by default.
admin = [
    "forms",
    "csrf",
    "dep:axum",
    "dep:tera",
    "dep:base64",
    "dep:http",
    "dep:http-body-util",
    "dep:serde_urlencoded",
    "dep:tokio",
]

# Multi-tenancy module (`rustango::tenancy`). Implies `admin` (the
# tenant admin builder wraps `rustango::admin`). Pulls in Argon2 +
# HMAC-SHA256 + cookie + tower for the per-tenant + operator
# authentication paths. Implies `storage` + `uploads` so the operator
# console can persist per-tenant brand assets (logo / favicon) on
# disk and serve them via the tenancy router.
tenancy = [
    "admin",
    "runtime",
    "storage",
    "uploads",
    "dep:async-trait",
    "dep:argon2",
    "dep:cookie",
    "dep:hmac",
    "dep:password-hash",
    "dep:rand",
    "dep:rpassword",
    "dep:serde_urlencoded",
    "dep:sha2",
    "dep:subtle",
    "dep:tokio",
    "dep:tower",
]

# `#[rustango::main]` runserver shim (slice 9.0 / 0.8.2). Pulls in
# `tracing-subscriber` so the macro can install the default
# `info,sqlx=warn` env-filter logger before the user body runs. Apps
# that don't want this dep can opt out by skipping the macro and
# using `#[tokio::main]` directly.
runtime = ["dep:tracing-subscriber", "dep:tracing-appender"]

# Minimal axum runserver — `rustango::server::AppBuilder`. Bi-dialect
# single-pool bootstrap (SQLite / Postgres / MySQL). Implies axum +
# tokio. The full multi-tenant `Builder` lives behind `tenancy` and
# pulls in much more.
runserver = ["dep:axum", "dep:tokio"]

[dependencies]
# Proc-macros — separate by Rust language requirement.
rustango-macros = { version = "0.27.7", path = "../rustango-macros" }

# Always-on: core ORM + query layer + SQL writer + migration runner.
chrono     = { workspace = true }
indexmap   = { workspace = true }
inventory  = { workspace = true }
serde      = { workspace = true }
serde_json = { workspace = true }
sqlx       = { workspace = true }
thiserror  = { workspace = true }
tracing    = { workspace = true }
uuid       = { workspace = true }

# `config` feature dep — optional.
toml = { workspace = true, optional = true }

# `admin` feature deps — optional.
axum           = { workspace = true, optional = true }
base64         = { workspace = true, optional = true }
http           = { workspace = true, optional = true }
http-body-util = { workspace = true, optional = true }
tera           = { workspace = true, optional = true }

# `tenancy` feature deps — optional.
argon2           = { workspace = true, optional = true }
async-trait      = { workspace = true, optional = true }
cookie           = { workspace = true, optional = true }
hmac             = { workspace = true, optional = true }
password-hash    = { workspace = true, optional = true }
rand             = { workspace = true, optional = true }
rpassword        = { workspace = true, optional = true }
serde_urlencoded = { workspace = true, optional = true }
sha1             = { workspace = true, optional = true }
sha2             = { workspace = true, optional = true }
subtle           = { workspace = true, optional = true }
tokio            = { workspace = true, optional = true }
tower            = { workspace = true, optional = true }
redis            = { workspace = true, optional = true }

# `oauth2` feature deps — optional.
reqwest     = { workspace = true, optional = true }
urlencoding = { workspace = true, optional = true }

# `compression` feature dep — optional.
flate2 = { workspace = true, optional = true }

# `runtime` feature dep — used by `#[rustango::main]` to install a
# default tracing subscriber (env-filter) before the user body runs.
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "json"], optional = true }
tracing-appender = { workspace = true, optional = true }

[dev-dependencies]
tokio          = { workspace = true }
chrono         = { workspace = true }
uuid           = { workspace = true }
serde_json     = { workspace = true }
tower          = { workspace = true }
http-body-util = { workspace = true }
axum           = { workspace = true }
base64         = { workspace = true }
dotenvy        = { workspace = true }
tempfile       = "3"

# All example crates removed. Single-file examples are auto-detected
# by Cargo; multi-file or required-features examples are declared
# here.

# Multi-tenant blog demo (operator console + tenant admin + viewsets).
# Auto-discovered by Cargo because it lives under `examples/blog_demo/`
# with a `main.rs`, but its imports (rustango::tenancy, ::extractors,
# #[viewset(...)]) require the `tenancy` feature — without this gate,
# bare `cargo test` would compile it under default features and fail.
[[example]]
name              = "blog_demo"
path              = "examples/blog_demo/main.rs"
required-features = ["tenancy"]

# SQLite ORM demo — single-file example, gated on the `sqlite` feature
# so default `cargo build --examples` doesn't try to compile it.
[[example]]
name              = "sqlite_orm_demo"
path              = "examples/sqlite_orm_demo.rs"
required-features = ["sqlite"]

# AppBuilder + SQLite — minimal runnable bootstrap shape.
[[example]]
name              = "sqlite_app_demo"
path              = "examples/sqlite_app_demo.rs"
required-features = ["sqlite", "runserver"]