Generators
plumekit generate <kind> (alias g) scaffolds common pieces of an app. Generators never overwrite an existing file, and each one prints how to wire what it created (a route to register, a middleware to use, …). Migrations and seeders are picked up automatically. Run them from the project root.
| Command | Creates |
|---|---|
generate resource <Name> [field:type …] | A full CRUD resource: model, controller (all seven actions), index/new/show/edit views, and a migration file (auto-discovered); prints the route to register. |
generate auth | Email + password auth: User model, register/login/logout/forgot/reset plus email verification; works for browser sessions and JSON APIs. |
generate notifications | Two-channel notifications: a UserNotification model (the in-app inbox) plus a notify(...) helper that also emails when the mailer is bound. |
generate model <Name> [field:type …] | An @Model type plus its migration file. |
generate controller <Name> | A RESTful Controller (the seven actions: index/new/create/show/edit/update/destroy). |
generate migration <Name> | A timestamped migration file (schema builder) under Database/Migrations/. |
generate view <Name> | A standalone Plume component (Views/<Name>.plume). |
generate middleware <Name> | A Middleware struct. |
generate job <Name> | A background Job under Sources/App/Jobs/ — auto-registered on the next build (no manual wiring). |
generate seeder <Name> | A Seeder (in Database/Seeders/). |
generate test <Name> | A test suite in Tests/AppTests/. See Testing. |
generate ci --provider <github|gitlab|forgejo> | CI workflows (test on PR, deploy on push). See Deploying. |
Field types
field:type pairs accept string (the default), text, int, int64, double, bool, and blob. They map to the Swift property type and, in generated migrations, the SQL column type (TEXT / INTEGER / REAL / BLOB). Table and column names match what @Model derives (pluralized, snake_cased).
resource
The full-resource scaffold: everything for a resource, as a working starting point:
$plumekit generate resource Post title:string body:text published:boolIt creates:
Sources/App/Models/Post.swift: the@Model.Sources/App/Controllers/PostController.swift: aControllerwith working CRUD;Views/Post/{Index,New,Show,Edit}.plume: a list (with a "New" link), a create form,- A model factory and a test suite (see Testing).
index lists, new/edit render the create/edit forms, show finds by id, create/update read the form and save, destroy deletes.
a detail view (with Edit/Delete), and a pre-filled edit form (method-overridden to PATCH), using your shared Layout. The New and Edit forms repopulate submitted values and show per-field messages when a save fails validation (the controller re-renders New/Edit at 422 with old*/*Error filled). Each resource's views are grouped in their own Views/<Name>/ folder (PascalCase, like the rest of the tree) so the directory stays tidy as the app grows. (The @component names stay globally unique (PostIndex, PostNew, PostShow, PostEdit) because they compile to top-level render functions; the folder just organizes files.)
The scaffold wires in the conveniences you'd otherwise add by hand:
- Named routes: a
PostRoutesenum declares each path once; the controller - Validation with re-render:
createvalidates the input (.requiredon - Flash messages: create/update/destroy redirect with
registers with it and builds its redirect URLs from it (PostRoutes.show.path(item.id)). See Routing.
every field, plus .integer/.decimal for numeric ones); on failure it re-renders the index with status 422, the submitted values repopulated (value="{oldTitle}"), and an inline <span class="field-error"> message per field via input.errors.first("title"). See Forms.
.flash("Post created") (and "updated" / "deleted"), and the Index view renders the .flash banner. See Routing.
and writes the migration file, then prints the route to register:
app.resources("posts", PostController())Requires the database capability. Run plumekit migrate and the migration is picked up automatically.
auth
A complete email + password auth scaffold that works for both browser sessions (a signed, HTTP-only cookie) and API clients (a bearer token). Identity resolves the same way for both, so every route serves both kinds of client:
$plumekit generate authIt creates the User, PasswordReset, and EmailVerification models (in Sources/App/Models/; the users table is the source of truth), Sources/App/Controllers/Auth.swift (the authenticator, session manager, and an AuthController with register / login / logout / forgot-password / reset / verify), the four page views in Views/Auth/, and the two email bodies in Views/Emails/ (verification + password reset; emails are their own kind of view, so they get their own folder). It prints the migrations and the wiring steps:
- Enable the
kvanddatabasecapabilities inplumekit.toml. - Call
installAuth(app)inbuildApp(); it registers the identity middleware and - Set
AUTH_SECRET(wrangler secret put AUTH_SECRET, or your env) before deploying. - Run
plumekit migrate(the migration file is auto-discovered).
the routes (/register, /login, /logout, /forgot, /reset, plus GET /verify and POST /verify/resend).
In any handler, request.currentUser is the signed-in user id and request.isAuthenticated the flag. A browser gets a session cookie and a redirect; a client sending Accept: application/json gets {"token": "…"} and passes it back as Authorization: Bearer …. Forgot-password stores a one-time token and emails the reset link as a Plume-view email (Views/Emails/ResetEmail.plume) when the mailer is bound; in local dev, with no mailer, the link is logged instead. The scaffold builds on the primitives in Auth, which you can drop down to for OAuth, policies, etc.
Email verification is scaffolded in: registration creates an EmailVerification token and emails the link as a Plume-view email (Views/Emails/VerifyEmail.plume, rendered through the scaffold's Mailer.send(view:text:) helper, see Mailer); without a mailer binding the link is logged, so dev keeps working. GET /verify?token=… stamps User.verifiedAt (one-time, 24 h expiry, flash confirmation) and POST /verify/resend re-sends. Gate verified-only routes with:
if let blocked = try await requireVerified(request) { return blocked }The users migration includes verified_at INTEGER NOT NULL DEFAULT 0.
notifications
Two-channel notifications: an in-app inbox plus email when the mailer is bound:
$plumekit generate notificationsIt creates a UserNotification @Model (the inbox) and a notify(userID:email:title:body:) helper that writes the inbox row and also emails when the mailer capability is bound. Read a user's inbox with UserNotification.for(userID) and mark entries read with markRead(). The migration file is written for you and picked up on the next plumekit migrate.
model & migration
$plumekit generate model Post title:string views:int published:boolWrites the @Model and a migration file that creates its table. generate migration <Name> writes a blank migration file for a schema change not tied to a new model. Both land under Database/Migrations/ and run automatically. See Migrations.
controller, view, middleware, job, seeder
Each writes one file and prints how to wire it:
$plumekit generate controller Post # → app.resources("posts", PostController())$plumekit generate view Sidebar # → Views/Sidebar.plume$plumekit generate middleware RateLimit # → app.use(RateLimitMiddleware())$plumekit generate job SendEmail # → registry.register(SendEmailJob.self) in buildJobs()$plumekit generate seeder Demo # → Database/Seeders/DemoSeeder.swift; run with `plumekit seed`See Controllers, Plume views, Middleware, Jobs, and the CLI reference.