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 (pluralised, 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;indexlists,new/editrender the create/edit forms,showfinds by id,create/updateread the form and save,destroydeletes.Views/Post/{Index,New,Show,Edit}.plume: a list (with a "New" link), a create form, a detail view (with Edit/Delete), and a pre-filled edit form (method-overridden to PATCH), using your sharedLayout. 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 withold/Errorfilled). Each resource's views are grouped in their ownViews/<Name>/folder (PascalCase, like the rest of the tree) so the directory stays tidy as the app grows. (The@componentnames stay globally unique (PostIndex,PostNew,PostShow,PostEdit) because they compile to top-level render functions; the folder just organises files.)- A model factory and a test suite (see Testing).
The scaffold wires in the conveniences you'd otherwise add by hand:
- Named routes: a
PostRoutesenum declares each path once; the controller registers with it and builds its redirect URLs from it (PostRoutes.show.path(item.id)). See Routing. - Validation with re-render:
createvalidates the input (.requiredon every field, plus.integer/.decimalfor numeric ones); on failure it re-renders the New form with status 422, the submitted values repopulated (value="{oldTitle}") and an inline<span class="field-error">message per field viainput.errors.first("title"). See Forms. - Flash messages: create/update/destroy redirect with
.flash("Post created")(and "updated" / "deleted"), and the Index view renders the.flashbanner. 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
kv,databaseandsecretscapabilities inplumekit.toml(secrets backsAUTH_SECRET; the generator offers to flip them for you). - Call
installAuth(app)inbuildApp(); it registers the identity middleware and the routes (/register,/login,/logout,/forgot,/reset, plusGET /verifyandPOST /verify/resend). - Set
AUTH_SECRET(wrangler secret put AUTH_SECRET, or your env) before deploying. - Run
plumekit migrate(the migration file is auto-discovered).
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 # → Sources/App/Jobs/SendEmailJob.swift (auto-discovered)$plumekit generate seeder Demo # → Database/Seeders/DemoSeeder.swift; run with `plumekit seed`See Controllers, Plume views, Middleware, Jobs and the CLI reference.