autumn generate collapses the five-file dance of "add a resource" into a single command. Four subcommands cover the cases you actually hit:

CommandWhat it produces
autumn generate modelA #[model] struct, a Diesel up.sql/down.sql pair, a schema.rs entry
autumn generate migrationA Diesel migration directory; columns are inferred when the name matches a verb
autumn generate taskA one-off operational #[task] skeleton under tasks/
autumn generate jobA #[job] background-job handler with args struct, registered_jobs() aggregator, and .jobs(…) wiring in src/main.rs
autumn generate channelA real-time broadcast channel over the Channels API — an htmx SSE live view by default, or a raw #[ws] handler with --ws
autumn generate scaffoldEverything model does plus #[repository], HTML routes, smoke test, routes![] registration
autumn generate wizardA session-backed multi-step form wizard with per-step validation and a confirm/commit/cancel flow
autumn generate adminAn AdminModel adapter for an existing model, wired to autumn-admin-plugin
autumn generate tauriA complete src-tauri/ sidecar project so the app ships as a native desktop installer (see Tauri guide)

The generators only emit code that uses macros and conventions Autumn already ships (#[model], #[repository], #[get]/#[post], the i64-PK convention, Diesel migrations, Maud templates). They never introduce new traits or runtime concepts — once a generator has run, the generated files are ordinary user code that you should edit freely.

Five commands to a working CRUD app

This is the path that every other batteries-included framework boasts about. On a fresh machine with Rust and Postgres installed, there is one one-time prerequisite: autumn migrate delegates to the Diesel CLI, so install it once with cargo install diesel_cli --no-default-features --features postgres.

Shell
autumn new my-app
cd my-app
autumn generate scaffold Post title:String body:Text published:bool
# Before migrating: configure the database (see the note below) and
# create it if it does not exist yet:
createdb my_app
autumn migrate
autumn dev

One file edit belongs between generate and migrate: the generated autumn.toml ships with the database section commented out (look for "Uncomment to configure database:"). Uncomment it and point url at your Postgres so both autumn migrate and the running app can reach the database — without it, autumn migrate exits with ✗ No database URL found.. autumn migrate runs migrations against that database but does not create it, hence the createdb my_app above (any equivalent, such as CREATE DATABASE in psql, works too):

TOML
[database]
url = "postgres://user:pass@localhost:5432/my_app"

Visit http://localhost:3000/posts to see the generated index page. The JSON endpoint at http://localhost:3000/api/posts returns [] until rows exist; mount mutating API handlers only after adding a repository policy.

The field-type DSL

Fields are passed as name:Type tokens. Only the documented public surface is supported — anything else fails with an error that lists the supported set.

DSL tokenRust typeSchema typeSQL type
title:StringStringTextTEXT
body:TextString (alias for String)TextTEXT
count:i32i32Int4INTEGER
count:i64i64Int8BIGINT
score:f32f32Float4REAL
score:f64f64Float8DOUBLE PRECISION
published:boolboolBoolBOOLEAN
token:Uuiduuid::UuidUuidUUID
at:NaiveDateTimechrono::NaiveDateTimeTimestampTIMESTAMP
at:DateTimechrono::DateTime<chrono::Utc>TimestamptzTIMESTAMPTZ
data:Bytea (or Vec<u8>)Vec<u8>ByteaBYTEA
post:referencesi64Int8BIGINT

Wrap any of the above in Option<…> to make the column nullable (Option<String>, Option<i64>, Option<NaiveDateTime>, …). The generator emits both NULL in the migration SQL and Nullable<T> in schema.rs.

Validation and HTML5 constraints ({…} modifiers)

Add a trailing {…} block to a field to declare constraints once and have them enforced on both sides — a server-side #[validate(...)] rule and the matching client-side HTML5 input attribute:

Shell
autumn generate scaffold Post \
  'title:String{min=3,max=120}' \
  'contact:String{email}' \
  'homepage:String{url}' \
  'age:i32{min=0,max=130}'
ModifierApplies to#[validate(…)]HTML5 attribute(s)
{min=N,max=N} (String/Text)String/Textlength(min, max)minlength / maxlength
{min=N,max=N} (numeric)i32/i64/f32/f64range(min, max)min / max (type="number")
{email}String/Textemailtype="email"
{url}String/Texturltype="url"

The generated model field carries the #[validate(...)] attribute (so a bad submission is rejected through the existing changeset path as a 422 with inline per-field errors, never a 500 or a silent store), and the generated form input carries the matching HTML5 attribute (so the browser blocks bad input before it hits the network). The required signal from a non-nullable column is preserved, and a rejected submission re-renders keeping the entered values. A misspelled modifier (e.g. {maxx=5}) fails the scaffold with an error naming the offending token. Quote the whole token in bash/zsh so the shell doesn't brace-expand the comma.

Every generated table also includes:

  • id BIGSERIAL PRIMARY KEY (the i64-PK convention used everywhere else in Autumn).
  • created_at TIMESTAMP NOT NULL DEFAULT NOW() annotated #[default] on the model so it stays out of NewX.

Foreign keys with references

post:references scaffolds a foreign-key column: the declared name is rewritten to end in _id (post -> post_id), the referenced table is derived by pluralising the base name (post -> posts, matching naming::pluralize), and the column is emitted as post_id BIGINT NOT NULL REFERENCES posts(id) with an automatic index (CREATE INDEX idx_comments_post_id ON comments (post_id);) — no --index flag required. Append ? for a nullable foreign key (post:references? -> post_id: Option<i64>, column NULL):

Shell
autumn generate scaffold Comment body:Text post:references

If the referenced model doesn't exist yet (no src/models/post.rs, or a matching declaration in a single-file src/models.rs), the generator still scaffolds the column, constraint, and index — it just prints a warning that the referenced table is assumed to already exist.

belongs_to dropdowns (populated from the parent)

When the referenced model does exist, the scaffolded new/edit form renders the foreign key as a populated <select> — one <option> per parent row — instead of a text box demanding a raw numeric id, and the index/show views render the parent's display value rather than the raw *_id integer. No hand-editing required:

Shell
autumn generate model Post title:String
autumn generate scaffold Comment body:Text post:references
# → the new-comment form's "post" field is a dropdown of existing posts,
#   labeled by each post's title; the comment index/show show the post title.

The display column is chosen by heuristic: a name or title column if present, otherwise the first String/Text column, falling back to the id only when the parent has no string column. Override it explicitly with a {label:col} modifier:

Shell
autumn generate scaffold Comment body:Text 'post:references{label:slug}'

A nullable reference (post:references?) renders a blank "— Unset —" first option so the selection can be cleared, and its index/show views render a dash when unset. (Index/show use a simple per-view fetch; the N+1-safe batched variant is issue #835.)

references only supports the i64/BIGSERIAL primary-key convention. If the referenced model is found but was generated with --id uuid, the generator fails fast with an error instead of emitting a migration that would break at autumn migrate time with a BIGINT-vs-UUID type mismatch — hand-write the migration for a UUID foreign key instead.

Composite foreign keys, cascade policy (ON DELETE/ON UPDATE), and runtime association traversal (belongs_to/has_many) are not in scope for this token — see issue #835 for the latter.

autumn generate model

Shell
autumn generate model Post title:String body:Text published:bool

Produces:

Code
src/models/post.rs                              # #[model] struct
src/models/mod.rs                               # `pub mod post;` (created or appended)
src/schema.rs                                   # diesel::table! { posts (id) { ... } }
migrations/<timestamp>_create_posts/up.sql      # CREATE TABLE posts (...)
migrations/<timestamp>_create_posts/down.sql    # DROP TABLE posts;

The generated src/models/post.rs:

Rust
//! Generated by `autumn generate`.

use crate::schema::posts;

#[autumn_web::model]
pub struct Post {
    #[id]
    pub id: i64,
    pub title: String,
    pub body: String,
    pub published: bool,
    #[default]
    pub created_at: chrono::NaiveDateTime,
}
Generated fileExisting concept it maps to
src/models/post.rsThe #[autumn_web::model] macro
migrations/.../up.sqlDiesel migrations consumed by autumn migrate
src/schema.rsThe Diesel table! block referenced by #[model]
src/models/mod.rsStandard Rust module aggregator

autumn generate migration

For schema changes that aren't a brand-new table.

Shell
# Empty migration — you fill in the SQL.
autumn generate migration BackfillSomething

# AddXxxToYyy — emits ALTER TABLE yyys ADD COLUMN per field
autumn generate migration AddPublishedToPosts published:bool

# RemoveXxxFromYyy — emits ALTER TABLE yyys DROP COLUMN per field
autumn generate migration RemoveBodyFromPosts body:String

The name detection is purely cosmetic — Autumn treats both Post and Posts as the table posts. If your name doesn't match Add…To… or Remove…From…, the generator just emits empty up.sql and down.sql files for you to fill in.

Generated safety comments

When autumn generate migration produces SQL that could be dangerous for a rolling deploy, it prepends an -- autumn-safety: comment to the statement:

Sql
-- autumn-safety: potentially-blocking
ALTER TABLE posts ADD COLUMN score INTEGER NOT NULL;
Sql
-- autumn-safety: destructive
ALTER TABLE posts DROP COLUMN body;

These comments are purely informational; they do not change runtime behavior. autumn migrate check strips them before classifying statements so they do not produce duplicate findings.

Expand/contract: safe column rename or removal

The naive approach — autumn generate migration RenameBodyToContent then hand- editing the SQL to RENAME COLUMN body TO content — produces an irreversible finding from autumn migrate check because old replicas still running the prior code will error on any query that references the old name.

The expand/contract pattern splits the change into two consecutive deploys:

Step 1 — Expand (add the new column alongside the old one):

Shell
autumn generate migration AddContentToPosts content:String

Edit the generated up.sql to copy existing data:

Sql
ALTER TABLE posts ADD COLUMN content TEXT;
UPDATE posts SET content = body WHERE content IS NULL;

Deploy this. All replicas now see both body and content. Update application code to dual-write both columns and read from content.

Step 2 — Contract (remove the old column once all replicas run the new code):

Shell
autumn generate migration RemoveBodyFromPosts body:String

The generated up.sql will contain:

Sql
-- autumn-safety: destructive
ALTER TABLE posts DROP COLUMN body;

Run autumn migrate check — the finding will now be destructive, not irreversible, because the column rename is already complete. This migration is safe to apply because no running code references body any longer.

The same two-step pattern applies to column type changes and to removing columns with foreign-key references.

Rolling back with autumn migrate down

Every autumn generate migration run creates a down.sql file alongside up.sql. autumn migrate down is the command that honours it.

Shell
# Revert the most recently applied user migration (default: --steps 1):
autumn migrate down

# Revert the last 3 user migrations in newest-first order:
autumn migrate down --steps 3

# Revert user migrations until 20260101000000 is the latest applied.
# VERSION must be a currently applied user migration; framework migrations are
# forward-only and cannot be used as a boundary.
autumn migrate down --to 20260101000000

# Required when AUTUMN_ENV=prod:
autumn migrate down --yes-i-mean-prod

# Enable maintenance mode around the rollback, then disable it on success:
autumn migrate --with-maintenance down

Framework migrations are forward-only

Framework-owned migrations (the ones Autumn ships internally) are never rolled back by autumn migrate down. They are listed separately in autumn migrate status and have no down.sql. This design is intentional — rolling back framework schema changes would break the framework features that depend on them.

Safety guards

Before touching the database, autumn migrate down checks:

  1. Production guard — If AUTUMN_ENV is prod or production, the command refuses unless --yes-i-mean-prod is passed. (An empty AUTUMN_ENV falls back to the legacy AUTUMN_PROFILE.)
  2. down.sql preflight — Every migration in the plan must have a non-empty, non-comment down.sql. If any are missing or blank, the command names them and exits non-zero without touching the database. A migration recorded as applied but no longer present locally is also surfaced as non-revertable rather than silently skipped.

Listing the applied migrations, building the plan, and reverting all happen while the migration advisory lock is held, so two concurrent down runs are serialized and neither double-reverts.

Sharded deployments

Like autumn migrate run, autumn migrate down operates on the control database plus every configured shard by default, and honours --shard <name> and --control-only to scope to a single target. Targets are rolled back in order and the command is fail-fast: if a later target fails (for example a runtime down.sql error, or shards sitting at divergent migration states), the earlier targets have already been rolled back. Re-running down then plans from each target's current state, so scope the command with --shard / --control-only when you need to roll a single database back in isolation. (A missing or empty down.sql is caught by preflight before any target is mutated, since all targets share one migrations/ directory.)

autumn migrate check also classifies down.sql files (in addition to up.sql), so you can catch unsafe rollback SQL — such as DROP TABLE or a DROP INDEX CONCURRENTLY that is missing its run_in_transaction = false opt-out — before an incident.

Observability

autumn migrate status shows rollback availability for every applied user migration:

Code
  ✓ 20260101000000_create_posts
  ✗ 20260102000000_add_body_to_posts  (no executable down.sql — not revertable)

This makes the rollback path visible before an incident, so you know which migrations can be safely reverted.

autumn generate task

For operational scripts that should run through the full Autumn app context.

Shell
autumn generate task cleanup_users

Produces:

Code
tasks/cleanup_users.rs                         # #[task] async function skeleton

The generated task uses TaskArgs<T> for CLI flags:

Rust
#[derive(Debug, Deserialize)]
struct CleanupUsersArgs {
    #[serde(default)]
    pub dry_run: bool,
}

#[autumn_web::task]
pub async fn cleanup_users(TaskArgs(args): TaskArgs<CleanupUsersArgs>) -> AutumnResult<()> {
    // ...
    Ok(())
}

Register the function with .one_off_tasks(one_off_tasks![...]) before running it with autumn task cleanup_users --dry-run.

autumn generate job

For background work that should survive process restarts, be retried on failure, and be visible in /actuator/jobs.

Shell
autumn generate job SendWelcomeEmail user_id:i64 email:String

Produces:

Code
src/jobs/send_welcome_email.rs    # #[job] handler + SendWelcomeEmailArgs struct
src/jobs/mod.rs                   # registered_jobs() aggregator (created or appended)
src/main.rs                       # mod jobs; + .jobs(jobs::registered_jobs()) added in place

The generated src/jobs/send_welcome_email.rs:

Rust
use autumn_web::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendWelcomeEmailArgs {
    pub user_id: i64,
    pub email: String,
}

#[job(name = "send_welcome_email", max_attempts = 5, backoff_ms = 500)]
pub async fn send_welcome_email(
    _state: AppState,
    args: SendWelcomeEmailArgs,
) -> AutumnResult<()> {
    // TODO: implement send_welcome_email
    let _ = args;
    Ok(())
}

The #[job] macro generates a companion struct SendWelcomeEmailJob with:

  • SendWelcomeEmailJob::NAME — the job's registered name ("send_welcome_email").
  • SendWelcomeEmailJob::enqueue(args).await? — at-least-once enqueue (use from most handlers).
  • autumn_web::job::enqueue_on_conn(SendWelcomeEmailJob::NAME, args, conn).await? — transactional enqueue (enqueues only if the surrounding DB transaction commits; use when the job outcome must be atomic with a DB write).

The generated src/jobs/mod.rs aggregator:

Rust
pub mod send_welcome_email;

#[must_use]
pub fn registered_jobs() -> Vec<autumn_web::job::JobInfo> {
    autumn_web::jobs![send_welcome_email::send_welcome_email]
}

Running autumn generate job a second time with a different name augments mod.rs and registered_jobs() in place — it never duplicates an entry.

The .jobs(jobs::registered_jobs()) call added to src/main.rs wires the aggregator into the job runtime and automatically populates /actuator/jobs with every registered job.

Slow live job verification

Shell
cargo test -p autumn-cli --test generate generated_job_cargo_checks -- --ignored --exact

This scaffolds a fresh project, runs autumn generate job, and asserts that cargo check --tests passes with no hand-editing required.

autumn generate channel

For a live feature (chat, notifications, a live-updating list) built entirely on Autumn's existing realtime stack — the Channels pub/sub API, SSE, and #[ws] upgrade routes. No new transport is invented; the generator only wires up what already ships.

Shell
autumn generate channel Chat

Produces:

Code
src/channels/chat.rs      # GET /chat (live view), GET /chat/events (SSE), POST /chat/messages
src/channels/mod.rs       # pub mod chat; (created or appended)
src/main.rs               # mod channels; + routes![...] entries added in place
tests/chat_channel.rs     # smoke test: publishes a message, asserts a subscriber receives it

SSE-over-htmx is the default transport — GET /chat renders a view wired to htmx's sse-connect/sse-swap, so browser tabs update live with zero client JS authored by the user:

Rust
#[get("/chat/events")]
pub async fn chat_events(State(state): State<AppState>) -> impl IntoResponse {
    autumn_web::sse::stream(&state, TOPIC)
}

#[post("/chat/messages")]
pub async fn chat_publish(
    State(state): State<AppState>,
    Form(form): Form<ChatForm>,
) -> AutumnResult<&'static str> {
    let fragment = message_fragment(&form.message).into_string();
    state.broadcast().publish(TOPIC, fragment)?;
    Ok("published")
}

Pass --ws to emit a raw #[ws] WebSocket handler instead, for clients that need a bidirectional socket rather than SSE + form posts:

Shell
autumn generate channel Chat --ws

Either transport adds the "ws" feature to the autumn-web dependency in Cargo.toml — channels, SSE, and #[ws] are all gated behind it.

The generated smoke test is a real assertion, not a stub: it publishes through the in-process TestApp, subscribes to the same topic, and asserts the message arrives — no Postgres/Docker required, so it runs on every cargo test.

Slow live channel verification

Shell
cargo test -p autumn-cli --test generate generated_channel_cargo_checks -- --ignored --exact
cargo test -p autumn-cli --test generate generated_channel_ws_cargo_checks -- --ignored --exact
cargo test -p autumn-cli --test generate generated_channel_smoke_test_passes -- --ignored --exact

These scaffold a fresh project, run autumn generate channel (both transports), and assert cargo check --tests passes with no hand-editing — plus one gate that actually runs the generated smoke test with cargo test to confirm it passes on first run.

autumn generate scaffold

Everything model produces, plus:

  • src/repositories/<snake>.rs — a #[repository(Model, api = "/api/<plural>")] block that auto-generates CRUD methods plus JSON REST handlers.
  • src/repositories/mod.rs — module aggregator.
  • src/routes/<plural>.rs — Maud HTML handlers for index, show, new_form, create, edit_form, and update. (Skipped if --api is set).
  • src/routes/mod.rs — module aggregator. (Skipped if --api is set).
  • tests/<snake>.rs — a real, in-process smoke test built on autumn_web::test::{TestApp, TestClient, TestDb}: it boots a throwaway Postgres database, fires a request at a stand-in for the scaffolded index route, and asserts a real response — no running server, no env var, no silent skip. cargo test reports it as ignored with an explicit reason (Docker isn't assumed to be available); run cargo test -- --ignored to execute it for real. --api scaffolds get the JSON equivalent, asserting against GET /api/<plural>.
  • src/main.rs — the mod declarations plus routes![…] entries get added in place. Existing entries are preserved; rerunning the generator with the same arguments is a no-op. By default, the scaffold registers only read-only API routes (GET /api/<plural> and GET /api/<plural>/{id}); mount POST/PUT/DELETE handlers only after adding a repository policy. For --api scaffolds, all 5 JSON endpoints (GET index/show, POST, PUT, DELETE) are automatically registered.

No-JavaScript edit and delete flows

The scaffolded HTML routes accept ordinary browser form submissions because Autumn's method-override middleware rewrites a POST carrying _method=PUT|PATCH|DELETE into the declared method before route matching. That means you can keep your generated handlers as #[put] / #[delete] and still serve clients with JavaScript disabled — no parallel POST-only routes required.

Use autumn_web::form::method_input (or ChangesetForm::form_tag with "delete" / "put" / "patch") inside generated edit views and any custom edit/delete buttons you add later:

Rust
use autumn_web::form::method_input;
use autumn_web::security::CsrfToken;

#[get("/bookmarks/{id}/edit")]
async fn edit_form(id: Path<i64>, csrf: Option<CsrfToken>) -> Markup {
    html! {
        // Delete button as a plain HTML form — works without htmx.
        form method="post" action=(format!("/bookmarks/{}", *id)) {
            (method_input("DELETE"))
            @if let Some(token) = csrf.as_ref() {
                input type="hidden" name="_csrf" value=(token.token());
            }
            button type="submit" { "Delete" }
        }
    }
}

autumn routes and /actuator/routes keep reporting the declared method (PUT, PATCH, or DELETE); the rewrite is a transport concession, not a routing one. CSRF protection still treats the overridden mutation as unsafe and rejects submissions without a valid token with 403 Forbidden.

Metadata flags let you keep common model and repository polish in the generation step:

Shell
autumn generate scaffold Bookmark url:String title:String tag:String alive:bool \
  --index url \
  --index tag \
  --validate url=url \
  --validate title=length:min=1,max=200 \
  --default alive=true \
  --query find_by_tag:tag \
  --query find_by_alive:alive
FlagEffect
--index FIELDAdds #[indexed] and CREATE INDEX idx_<table>_<field> .... Repeatable.
--validate FIELD=RULEAdds #[validate(...)] and the validator dependency. Supported rules: url, email, and length:min=N,max=N on String / Text fields.
--default FIELD=VALUEAdds #[default] and a SQL DEFAULT for bool, string/text, integer, and float fields. i32 defaults must fit PostgreSQL's INTEGER range. Defaulted fields are omitted from generated HTML forms and update columns because the model macro keeps them out of NewX.
--query METHOD:FIELDAdds a derived repository method such as find_by_tag(tag: String) -> Vec<Model>. The find_by_ suffix must match FIELD.
--apiGenerates a JSON API-only scaffold (skips HTML routes/templates, registers 5 REST JSON routes, and generates a JSON-based smoke test).
Generated fileExisting concept it maps to
src/models/<name>.rs#[autumn_web::model]
src/repositories/<name>.rs#[autumn_web::repository]
src/routes/<plural>.rs#[get]/#[post] route macros returning Maud Markup
src/main.rs routes![…]The routes! collection macro
migrations/<ts>_create_<plural>/Diesel migrations
src/schema.rsDiesel table! blocks
tests/<name>.rsStandard cargo test integration test

Shipped example

The examples/bookmarks app is regenerated from the current scaffold shape:

Shell
autumn new bookmarks
cd bookmarks
autumn generate scaffold Bookmark url:String title:String tag:String alive:bool \
  --index url \
  --index tag \
  --validate url=url \
  --validate title=length:min=1,max=200 \
  --default alive=true \
  --query find_by_tag:tag \
  --query find_by_alive:alive

It is the reference for what autumn generate scaffold produces in practice after a user makes ordinary app-specific edits. The committed follow-up diff is intentionally small and documents which gaps are outside the generic generator:

Bookmarks additionDisposition
Tailwind layout, htmx delete buttons, and public local-demo write formsUI and access-policy choices; replace the generated route templates.
Hourly #[scheduled] link checkerOperational workflow; generate or write a task separately.
Mounting POST/PUT/DELETE JSON API routesApplication policy; scaffold keeps only read APIs registered by default.

Reusable scaffold config (autumn.generate.toml)

Long scaffolds with many metadata flags can be checked in as a TOML file so the intent is reviewable and reproducible without spelunking shell history. Create a file at any path — autumn.generate.toml is the conventional name — with one [scaffold.<ResourceName>] section per resource:

TOML
[scaffold.Bookmark]
fields      = ["url:String", "title:String", "tag:String", "alive:bool"]
indexes     = ["url", "tag"]
validations = ["url=url", "title=length:min=1,max=200"]
defaults    = ["alive=true"]
queries     = ["find_by_tag:tag", "find_by_alive:alive"]
api         = true # Optional: JSON API-only scaffold

Pass the file with --config:

Shell
autumn generate scaffold Bookmark --config autumn.generate.toml

All the same keys are supported as their CLI counterparts — see the metadata flags table above for the accepted syntax of each.

Precedence rules (CLI wins): if a CLI flag is supplied alongside --config, it completely replaces the corresponding TOML list for that key. An empty CLI slice (i.e. the flag was not passed) falls back to the TOML value. This matches normal CLI ergonomics where the explicit flag is always authoritative:

ScenarioEffective value
TOML onlyTOML list
CLI only (no --config)CLI list
Both, CLI non-emptyCLI list (TOML ignored for that key)
Both, CLI empty / flag absentTOML list

This applies independently to each key: you can keep fields and validations from TOML while overriding indexes on the CLI for a one-off variant.

The config is additive, not a replacement — existing CLI flags always work without a config file, and the config never changes the output of any previously working invocation.

Slow live scaffold verification

The CLI test suite includes two ignored scaffold checks:

Shell
# Compile-check the generated app and its generated smoke test (CLI flags).
cargo test -p autumn-cli --test generate generated_scaffold_cargo_checks -- --ignored --exact

# Compile-check a config-file-driven scaffold (--config flag).
cargo test -p autumn-cli --test generate generated_scaffold_config_cargo_checks -- --ignored --exact

# Boot Postgres, run `autumn migrate`, start the generated server, and
# verify GET /posts and GET /api/posts over real HTTP.
cargo test -p autumn-cli --test generate generated_scaffold_serves_posts_index_and_json_api -- --ignored --exact

The live HTTP test requires Docker access for the Postgres testcontainer and the diesel CLI on PATH, because autumn migrate delegates to diesel migration run.

WebAuthn native dependency note

autumn generate auth --passkeys enables the webauthn feature and adds webauthn-rs. That dependency currently builds through OpenSSL. On Ubuntu CI the system OpenSSL toolchain is already available, but Windows developers need to install the OpenSSL libraries through vcpkg and set VCPKG_ROOT so openssl-sys can find them.

The release SemVer gate checks autumn-web optional public feature APIs, so this native dependency must be present on machines that run scripts/check-semver.sh locally.

autumn generate wizard

Multi-step forms where each step is validated before the user advances. Session-backed: step data survives page refreshes and back-button navigation without requiring the user to re-enter earlier steps.

Shell
autumn generate wizard checkout shipping payment review

Produces:

Code
src/wizards/checkout.rs        # step structs + GET/POST handlers + confirm/commit/cancel
src/wizards/mod.rs             # pub mod checkout;  (created or appended)
tests/checkout_wizard.rs       # ignored integration test skeletons

Step names must be valid Rust identifiers (letters, digits, underscores; no hyphens). The names confirm, commit, and cancel are reserved. A minimum of two steps is required.

For each step the generator emits:

  • A {PascalStep}Form struct with Serialize, Deserialize, Validate, and Default.
  • A GET /{name}/{step} handler that guards, re-populates from session, and renders the form.
  • A POST /{name}/{step} handler that validates, saves to session, and redirects — or returns 422 with errors.

Plus three fixed handlers:

  • GET /{name}/confirm — summary page; guards that all steps are complete.
  • POST /{name}/commit — assembles all step data, performs the write, clears session.
  • POST /{name}/cancel — clears session state and redirects.

Mount the routes in src/main.rs and add mod wizards;.

See the Wizards guide for the full runtime API reference and the examples/bookmarks app for a worked example.

Common flags

Every generator accepts:

  • --dry-run — print the file plan and exit. Nothing is written; existing files are not touched. Useful for previewing what the generator will do.
  • --force — overwrite existing files. By default, the generator refuses to clobber and surfaces a would overwrite <path> error listing every collision. mod.rs and schema.rs are always treated as modify-in-place edits and don't trigger collisions.

autumn db pull — scaffold models from an existing database

The generators above are greenfield: you describe a table with the name:Type DSL and they emit a brand-new one. If you already run a Postgres database, autumn db pull goes the other direction — it introspects your live schema and emits the matching Autumn artifacts, so you can adopt autumn-web incrementally instead of rewriting every table by hand.

Shell
# Pull every table in the public schema:
autumn db pull

# Pull specific tables, and also emit a #[repository] per table:
autumn db pull posts comments --with-repository

It connects using the same resolution autumn migrate uses (database.primary_url / database.url in autumn.toml, or AUTUMN_DATABASE__PRIMARY_URL / AUTUMN_DATABASE__URL / DATABASE_URL), and for each selected table emits, through the same file-emission machinery as the other generators:

  • a #[model] struct in src/models/<name>.rs,
  • a diesel::table! block in src/schema.rs,
  • the pub mod <name>; aggregator line, and mod models; / mod schema; declarations in src/main.rs,
  • optionally, a #[repository(Model)] trait in src/repositories/<name>.rs (with --with-repository).

This is read-only: no migration is written and no data is touched — the tables already exist. Column types are the inverse of the field-type DSL table (int8i64, textString, timestamptzchrono::DateTime, …); nullable columns become Option<T>. The primary key is annotated #[id] and a created_at column is annotated #[default], so a table created by autumn generate model and then re-derived by db pull produces a field-for-field equivalent model. A column whose SQL type is outside the supported set fails with an error naming the column rather than silently dropping it.

--dry-run, --force, and collision-refusal behave exactly as in the autumn generate family: existing model/repository files are not clobbered without --force, and mod.rs / schema.rs are modified in place. Under --force, an existing schema.rs block for a pulled table is replaced with the freshly introspected one so the model and schema can't drift apart on a re-pull.

Brownfield specifics:

  • Framework tables are skipped. An unscoped autumn db pull ignores Autumn's own tables (autumn_* / _autumn*, api_tokens, …) so it works on a database that has already run autumn migrate. Name a table explicitly to pull it anyway.
  • Defaults and read-only columns. A created_at column with a database default, and stored generated columns (GENERATED ALWAYS AS … STORED), are annotated #[default] so they stay out of inserts/updates. An ordinary column with a default (e.g. status TEXT DEFAULT 'draft') stays settable.
  • Irregular plurals. When the table name isn't the model macro's naive Struct + "s" inference (e.g. people, categories), db pull emits an explicit #[autumn_web::model(table = "...")] so the model compiles against the generated schema block.
  • --with-repository requires the id/i64 PK convention. Tables keyed by a uuid, a non-id column, or a composite key still get a model, but the repository is skipped (the #[repository] macro assumes an i64 id).
  • Unsupported shapes fail loudly. Tables without a primary key, columns whose names aren't valid identifiers (e.g. type), unmapped SQL types, and two tables that collapse to the same model module all stop the pull with a clear error instead of emitting broken code.

Out of scope for db pull: foreign-key/association inference, generating routes or admin adapters, non-Postgres backends, and SQL views / materialized views / partitioned tables.

What's intentionally not here

The generators are deliberately scoped to one resource per invocation and to the existing public macro surface. Out of scope (track separately if you need them):

  • Authentication scaffolding. Auth has its own session, CSRF, and #[secured] story; bundling it here would balloon scope.
  • Generators for optional plugin crates. Those plugins ship their own generators on their own timeline.
  • Harvest workflow scaffolding. autumn-harvest is a companion workflow project with its own release train, so core web generators do not depend on it.
  • Custom user-provided templates / template overrides.
  • Test scaffolding beyond the single smoke test.
  • Multi-resource scaffolds (autumn generate scaffold Blog Post Comment). One resource per invocation; chaining is the user's job.