Rustango docs
← Getting started

Scaffolding

Rustango has two layers of code generation, so you rarely wire boilerplate by hand:

  1. The project generator — cargo rustango new creates a whole new project from a template.
  2. In-project generators — manage startapp and the manage make:* family add apps, views, serializers, jobs, and more inside an existing project.

cargo rustango new scaffolds a complete, ready-to-run project — Cargo manifest, config tiers, Docker, migrations, and src — in one command

Table of contents


Install the generator

cargo rustango is a Cargo subcommand. Install it once, globally:

cargo install cargo-rustango

That puts a cargo-rustango binary on your PATH; Cargo then exposes it as cargo rustango, one global command you can run from anywhere. The same install adds cargo-rgo, so cargo rgo … is a shorter way to type any cargo rustango … command.

The generator's own version is the one your project gets

cargo rustango new writes rustango = "MAJOR.MINOR" into the generated Cargo.toml, taken from the generator's version, not from whatever is newest on crates.io. Whichever generator you install is the version your project pins.

That is almost always what you want, and it is why the install command above is unpinned: the newest generator writes the newest pin, and the two cannot drift apart.

It is worth knowing when you deliberately want an older release — to match a project already on it, or to reproduce a report. Pin the generator, not the project:

cargo install cargo-rustango --version 0.57.0

Check which one you have with cargo rustango --version. Upgrading later is the same command with --force, and it only affects projects you generate afterwards — an existing project's pin is a line in its own Cargo.toml, which you edit yourself.


Create a project: cargo rustango new

cargo rustango new <name> [--template api|fullstack|tenant]
                          [--backend postgres|sqlite|mysql]
                          [--features <list>]
  • <name> — the project (and crate) name. It must be a valid Cargo crate name ([A-Za-z_][A-Za-z0-9_-]*), and the target directory must not already exist.
  • --template / -t — which starter to scaffold (default: fullstack).
  • --backend / -b — which database the project runs on (default: postgres).
  • --features / -F — extra Rustango features, comma- or space-separated.
  • --alias rgo — name the dependency rgo instead of rustango (see A shorter name).
  • --interactive / -i — pick from menus instead. A bare cargo rustango new on a terminal does the same.
  • --help / -h, --version — usage and version.

Run it with no arguments and it asks:

  rustango — new project
  (enter accepts the default; ctrl-c aborts)

  Project name: shop

  Template
    1) fullstack  ORM + auto-admin + forms — the usual starting point  (default)
    2) api        bare ORM + axum, no admin UI — for JSON-only services
    3) tenant     multi-tenancy: tenant registry + operator console
  > 3

  Database
    1) postgres  every feature, including schema-mode tenancy  (default)
    2) sqlite    a file beside the project — no server to run
    3) mysql     MySQL 8.0+ / MariaDB
  > 1

  Extra features  (numbers, e.g. `1 3 4` — enter for none)
     1) csrf         CSRF protection middleware for form POSTs
     2) sso          OIDC single sign-on for application users
     …
  > 1 2

  Same thing without the wizard:
    cargo rustango new shop --template tenant --backend postgres --features csrf,sso

  Create it? [Y/n]

The wizard sets exactly the fields the flags set and prints the equivalent command line before it writes anything — so using it once teaches the flags, and there is only one code path deciding what a project contains. Off a terminal (a script, a CI job) it fails with a message instead of waiting for an answer nobody is there to give.

The three templates

Each maps to one of Rustango's three app shapes:

TemplateWhat you getReach for it when
apiBare ORM + Axum, no adminJSON-only services and microservices
fullstack (default)ORM + the auto-adminA typical web app with a back-office
tenantMulti-tenancy + operator console + per-tenant appsSaaS hosting many isolated tenants
cargo rustango new myblog                      # fullstack (the default)
cargo rustango new api_demo  --template api
cargo rustango new shop      --template tenant

Choosing a database: --backend

cargo rustango new edge --backend sqlite
cargo rustango new shop --backend mysql

--backend decides what cargo run uses, and shapes the whole project to match — the DATABASE_URL in .env.example, the services in docker-compose.yml, the url in each settings tier, and the README's run instructions. Pick sqlite and there is no database service at all: it is a file beside the project, created by the first cargo run -- migrate.

All three backends stay wired up in the generated [features], so the other two remain one flag away:

cargo run --no-default-features --features sqlite

Turning on more of the framework: --features

cargo rustango new saas --template tenant --features csrf,sso,cache-redis

A template turns on a sensible set; --features adds the opt-ins none of them reach:

FeatureWhat it adds
csrfCSRF protection middleware for form POSTs
sso / admin-ssoOIDC single sign-on, for application users / for the admin site
passkeyWebAuthn / passkey authentication
cache-redis / cache-pageRedis cache backend / whole-page response caching
jobs / jobs-postgresBackground job queue, in-process / database-backed so it survives restarts
schedulerFixed-interval background tasks
email-smtpSMTP transport for the email framework
mcpModel Context Protocol server for AI agents
http2Serve cleartext HTTP/2 (h2c) next to HTTP/1
testkit / test_utilsTest-only schema builders, factories, and constructors

--features tenancy alone picks --template tenant; with another template it is refused, because only the tenant main.rs serves tenancy. cargo rustango new --help prints this list. Backends are not valid here — pass --backend instead; naming one as a feature is refused, because it would pin the framework's backend while the project's own feature stayed off, and #[derive(Model)] gates its emissions on the project's features.

A shorter name: rgo

cargo rgo new shop --alias rgo

--alias rgo writes the dependency under the name rgo, and the generated code uses rgo:: paths and #[rgo(...)] attributes. The crate rgo on crates.io belongs to someone else, so this is a rename of the rustango package:

[dependencies]
rgo = { package = "rustango", version = "0.62", default-features = false, features = ["batteries"] }

[features]
postgres = ["rgo/postgres"]
use rgo::prelude::*;

#[derive(Model, Debug, Clone)]
#[rgo(table = "item", display = "name")]
pub struct Item {
    #[rgo(primary_key)]
    pub id: Auto<i64>,
    #[rgo(max_length = 64)]
    pub name: String,
}

#[rgo(...)] means exactly what #[rustango(...)] means, and works whatever you call the dependency. One struct or field uses one name: mixing #[rustango(...)] and #[rgo(...)] on the same item is a compile error. manage startapp and the make:* generators read Cargo.toml and write rgo:: paths and #[rgo(...)] too.


What gets generated

Every template writes a self-contained Cargo project:

<name>/
  Cargo.toml            # the rustango dependency + features for this template
  .env.example          # copy to .env (DATABASE_URL, RUSTANGO_SESSION_SECRET, …)
  .gitignore
  rust-toolchain.toml   # selects the `stable` toolchain + rustfmt/clippy/rust-analyzer
  docker-compose.yml    # the database service (none for sqlite) + a cargo-watch dev container
  Dockerfile            # production image (multi-stage, release, non-root)
  Dockerfile.dev        # the dev image docker-compose.yml builds
  .dockerignore
  README.md
  config/
    default.toml        # settings shared across every environment
    dev_settings.toml   # per-tier overrides …
    staging_settings.toml
    prod_settings.toml
  migrations/           # JSON migration files (committed to git)
  system/migrations/    # the framework's own tables, generated on first migrate (commit them)
  src/
    lib.rs              # the app as a library: `pub mod models; pub mod urls; pub mod views;`
    main.rs             # the single binary — HTTP server + every manage verb
    models.rs           # your #[derive(Model)] structs
    views.rs            # request handlers ("views")
    urls.rs             # pub fn api() -> Router that aggregates your routes

One binary for everything

src/main.rs is the only entrypoint. It boots the HTTP server and dispatches every manage verb — there is no separate manage script or src/bin/manage.rs. The modules live in the library target (src/lib.rs), so main.rs reaches them by crate name, and so can a worker binary under src/bin/. The api template's main.rs, trimmed:

use myapp::urls;

#[rustango::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _ = dotenvy::dotenv();
    rustango::manage::Cli::new()
        .api(urls::api())
        .with_health()   // /health + /ready endpoints (every template)
        .run()
        .await
}

So cargo run starts the server, and cargo run -- <verb> runs migrations, generators, and the rest.

How the templates differ inside main.rs / urls.rs:

  • api — no admin; urls::api() simply aggregates your own routes.
  • fullstack — same urls.rs, plus admin_prefix(&settings) and an admin_router(pool, &settings) helper that main.rs mounts with Cli::nest_with(urls::admin_prefix(&settings), …), built from the server's pool. The prefix is [admin] url_prefix (or [routes] admin_url), else /admin. main.rs loads the settings once and passes them to the Cli with with_loaded_settings. It takes rustango::sql::Pool, so it names no driver, and sits behind a login: create the account with cargo run -- create-admin <username> --superuser (see getting started, Step 11).
  • tenant — main.rs adds .tenancy(), serving the operator console at the apex domain and each tenant under its own subdomain, and .with_tenant_provisioning("migrations") for the console's New tenant and Database servers pages (delete it to turn them off; saving a server needs RUSTANGO_SECRET_KEY). The framework's own tables are generated into a system/migrations/ folder from the compiled models on the first cargo run -- migrate — no hand-shipped bootstrap JSON, so the very first migrate works with no extra setup.

Layered configuration

Settings load config/default.toml first, then config/<RUSTANGO_ENV>_settings.toml on top. RUSTANGO_ENV defaults to dev, so a freshly scaffolded cargo run works with no edits; set RUSTANGO_ENV=prod in production to pick up prod_settings.toml.

The environment always wins over the files. RUSTANGO__SECTION__KEY overrides [section] key, and a few keys also have a bare env var that wins over both: DATABASE_URL (or the DB_* vars) for [database] url, RUSTANGO_BIND for [server] bind, RUSTANGO_APEX_DOMAIN for [tenancy] apex_domain. So the database URL comes from DATABASE_URL / DB_*, then RUSTANGO__DATABASE__URL, then [database] url; the prod tier refuses a URL from a settings file. cargo run -- check --settings lists every key and where its value comes from, with secrets hidden.

The dev and staging files set a [brand] tagline of dev and staging. To change it, or to set your own name and logo, see Branding and the admin's config keys.

Settings that do not take effect on their own

Not every key is read by the framework. Keys that did nothing were removed in 0.62.0 ([sso], [auth.jwt] issuer / audience, [admin] primary_color / csrf_cookie_secure / session_timeout_minutes); a file or RUSTANGO__* var that still sets one fails to load and names what to use instead.

KeyStatus
[audit] retention_daysRead by audit-cleanup when given neither --days nor --keep-last; schedule that verb.
[jobs], [i18n], [cache], [mail], [mcp], [auth.jwt] TTLs, [admin]User-wired: read only by the constructor you call (jobs::inmemory_from_settings, Translator::from_settings, cache::from_settings_async, email::from_settings, mcp::secure_tenant_router_from_settings, auth_routes::Config::with_jwt_settings, admin::Builder::from_settings).

First run

cd <name>
cp .env.example .env                     # then change `postgres` -> `localhost` in DATABASE_URL
docker compose up -d --no-deps postgres  # only the database: the rust service holds 8080
cargo run -- makemigrations              # generate the framework's (and your) tables
cargo run -- migrate                     # apply them
cargo run -- create-admin <username> --superuser  # fullstack: the admin login
cargo run                                # serve
cargo run -- --help                      # see every manage verb

Add a feature module: manage startapp

Scaffold a self-contained module of related models, views, and routes:

cargo run -- startapp blog

It writes src/blog/ containing mod.rs, models.rs (a starter model named after the singularized app — blog → Blog), views.rs, urls.rs, and tests.rs, then declares the module in src/lib.rs (pub mod blog;; mod blog; in src/main.rs on a project without a lib.rs) and merges its routes into urls::api().

Options:

  • --into <dir> — scaffold under a base directory other than src/ (e.g. a workspace member).
  • --with-manage-bin — also emit a src/bin/manage.rs (for layouts that prefer a separate manage binary).

Generate single files: the make:* commands

Inside a project, the make:* verbs scaffold one file at a time. The full per-flag reference lives in the manage CLI reference; the common shapes are:

CommandGenerates
make:viewset <Name> [--model <M>]A CRUD ViewSet — the six REST routes over one model
make:serializer <Name> [--model <M>]A serializer for request/response shaping
make:api_routes <app>An API route aggregator for an app
make:form <Name>An HTML form with validation
make:job <Name>A background job handler
make:scheduled <Name>A fixed-interval task for scheduler::Scheduler
make:worker <Name>A worker binary under src/bin/ that drains the job queue
make:notification <Name>A multi-channel notification
make:middleware <Name>A middleware skeleton
make:test <Name>A test module using the in-process test client
cargo run -- make:viewset PostViewSet --model Post
cargo run -- make:serializer PostSerializer --model Post
cargo run -- make:test post_smoke

A typical flow

cargo rustango new myblog                              # 1. scaffold the project
cd myblog
cargo run -- startapp blog                             # 2. add a feature module
# …add fields to src/blog/models.rs…
cargo run -- makemigrations                            # 3. generate a migration
cargo run -- migrate                                   # 4. apply it
cargo run -- make:viewset PostViewSet --model Post     # 5. expose a JSON API
cargo run                                              # 6. serve