autumn generate collapses the five-file dance of "add a resource" into a
single command. Four subcommands cover the cases you actually hit:
| Command | What it produces |
|---|---|
autumn generate model | A #[model] struct, a Diesel up.sql/down.sql pair, a schema.rs entry |
autumn generate migration | A Diesel migration directory; columns are inferred when the name matches a verb |
autumn generate task | A one-off operational #[task] skeleton under tasks/ |
autumn generate job | A #[job] background-job handler with args struct, registered_jobs() aggregator, and .jobs(…) wiring in src/main.rs |
autumn generate channel | A real-time broadcast channel over the Channels API — an htmx SSE live view by default, or a raw #[ws] handler with --ws |
autumn generate scaffold | Everything model does plus #[repository], HTML routes, smoke test, routes![] registration |
autumn generate wizard | A session-backed multi-step form wizard with per-step validation and a confirm/commit/cancel flow |
autumn generate admin | An AdminModel adapter for an existing model, wired to autumn-admin-plugin |
autumn generate tauri | A 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.
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):
[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 token | Rust type | Schema type | SQL type |
|---|---|---|---|
title:String | String | Text | TEXT |
body:Text | String (alias for String) | Text | TEXT |
count:i32 | i32 | Int4 | INTEGER |
count:i64 | i64 | Int8 | BIGINT |
score:f32 | f32 | Float4 | REAL |
score:f64 | f64 | Float8 | DOUBLE PRECISION |
published:bool | bool | Bool | BOOLEAN |
token:Uuid | uuid::Uuid | Uuid | UUID |
at:NaiveDateTime | chrono::NaiveDateTime | Timestamp | TIMESTAMP |
at:DateTime | chrono::DateTime<chrono::Utc> | Timestamptz | TIMESTAMPTZ |
data:Bytea (or Vec<u8>) | Vec<u8> | Bytea | BYTEA |
post:references | i64 | Int8 | BIGINT |
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:
autumn generate scaffold Post \
'title:String{min=3,max=120}' \
'contact:String{email}' \
'homepage:String{url}' \
'age:i32{min=0,max=130}'
| Modifier | Applies to | #[validate(…)] | HTML5 attribute(s) |
|---|---|---|---|
{min=N,max=N} (String/Text) | String/Text | length(min, max) | minlength / maxlength |
{min=N,max=N} (numeric) | i32/i64/f32/f64 | range(min, max) | min / max (type="number") |
{email} | String/Text | email | type="email" |
{url} | String/Text | url | type="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(thei64-PK convention used everywhere else in Autumn).created_at TIMESTAMP NOT NULL DEFAULT NOW()annotated#[default]on the model so it stays out ofNewX.
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):
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:
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:
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
autumn generate model Post title:String body:Text published:bool
Produces:
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:
//! 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 file | Existing concept it maps to |
|---|---|
src/models/post.rs | The #[autumn_web::model] macro |
migrations/.../up.sql | Diesel migrations consumed by autumn migrate |
src/schema.rs | The Diesel table! block referenced by #[model] |
src/models/mod.rs | Standard Rust module aggregator |
autumn generate migration
For schema changes that aren't a brand-new table.
# 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:
-- autumn-safety: potentially-blocking
ALTER TABLE posts ADD COLUMN score INTEGER NOT NULL;
-- 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):
autumn generate migration AddContentToPosts content:String
Edit the generated up.sql to copy existing data:
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):
autumn generate migration RemoveBodyFromPosts body:String
The generated up.sql will contain:
-- 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.
# 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:
- Production guard — If
AUTUMN_ENVisprodorproduction, the command refuses unless--yes-i-mean-prodis passed. (An emptyAUTUMN_ENVfalls back to the legacyAUTUMN_PROFILE.) - 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:
✓ 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.
autumn generate task cleanup_users
Produces:
tasks/cleanup_users.rs # #[task] async function skeleton
The generated task uses TaskArgs<T> for CLI flags:
#[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.
autumn generate job SendWelcomeEmail user_id:i64 email:String
Produces:
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:
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:
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
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.
autumn generate channel Chat
Produces:
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:
#[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:
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
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 forindex,show,new_form,create,edit_form, andupdate. (Skipped if--apiis set).src/routes/mod.rs— module aggregator. (Skipped if--apiis set).tests/<snake>.rs— a real, in-process smoke test built onautumn_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 testreports it asignoredwith an explicit reason (Docker isn't assumed to be available); runcargo test -- --ignoredto execute it for real.--apiscaffolds get the JSON equivalent, asserting againstGET /api/<plural>.src/main.rs— themoddeclarations plusroutes![…]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>andGET /api/<plural>/{id}); mountPOST/PUT/DELETEhandlers only after adding a repository policy. For--apiscaffolds, 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:
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:
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
| Flag | Effect |
|---|---|
--index FIELD | Adds #[indexed] and CREATE INDEX idx_<table>_<field> .... Repeatable. |
--validate FIELD=RULE | Adds #[validate(...)] and the validator dependency. Supported rules: url, email, and length:min=N,max=N on String / Text fields. |
--default FIELD=VALUE | Adds #[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:FIELD | Adds a derived repository method such as find_by_tag(tag: String) -> Vec<Model>. The find_by_ suffix must match FIELD. |
--api | Generates a JSON API-only scaffold (skips HTML routes/templates, registers 5 REST JSON routes, and generates a JSON-based smoke test). |
| Generated file | Existing 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.rs | Diesel table! blocks |
tests/<name>.rs | Standard cargo test integration test |
Shipped example
The examples/bookmarks app is regenerated from
the current scaffold shape:
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 addition | Disposition |
|---|---|
| Tailwind layout, htmx delete buttons, and public local-demo write forms | UI and access-policy choices; replace the generated route templates. |
Hourly #[scheduled] link checker | Operational workflow; generate or write a task separately. |
Mounting POST/PUT/DELETE JSON API routes | Application 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:
[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:
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:
| Scenario | Effective value |
|---|---|
| TOML only | TOML list |
CLI only (no --config) | CLI list |
| Both, CLI non-empty | CLI list (TOML ignored for that key) |
| Both, CLI empty / flag absent | TOML 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:
# 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.
autumn generate wizard checkout shipping payment review
Produces:
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}Formstruct withSerialize,Deserialize,Validate, andDefault. - 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 awould overwrite <path>error listing every collision.mod.rsandschema.rsare 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.
# 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 insrc/models/<name>.rs, - a
diesel::table!block insrc/schema.rs, - the
pub mod <name>;aggregator line, andmod models;/mod schema;declarations insrc/main.rs, - optionally, a
#[repository(Model)]trait insrc/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 (int8 → i64, text → String, timestamptz
→ chrono::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 pullignores Autumn's own tables (autumn_*/_autumn*,api_tokens, …) so it works on a database that has already runautumn migrate. Name a table explicitly to pull it anyway. - Defaults and read-only columns. A
created_atcolumn 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 pullemits an explicit#[autumn_web::model(table = "...")]so the model compiles against the generated schema block. --with-repositoryrequires theid/i64PK convention. Tables keyed by auuid, a non-idcolumn, or a composite key still get a model, but the repository is skipped (the#[repository]macro assumes ani64 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-harvestis 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.