autumn-admin-plugin 0.10.0

Out-of-the-box admin panel plugin for autumn-web applications
Documentation

autumn-admin-plugin

autumn-admin-plugin adds a server-rendered admin panel to an autumn-web application. Register one or more models, mount the plugin, and it serves a CRUD UI with list/detail/edit screens, search, filtering, bulk actions, CSRF integration, and HTMX-driven interactions with no frontend build step.

Features

  • Mounts an admin UI under /admin by default
  • Generates list, create, detail, edit, delete, and bulk-action flows
  • Includes a built-in jobs dashboard for queued, running, completed, and failed work
  • Uses Maud + HTMX and works under Autumn's default Content-Security-Policy
  • Reads and writes model data through a small AdminModel trait
  • Requires an authenticated session with the "admin" role by default

Installation

autumn plugin add autumn-admin-plugin

One command adds the dependency at a version compatible with your app's autumn-web, mounts the plugin in your autumn_web::app() builder chain, and prints any configuration still needed. It is safe to re-run, and it refuses — before touching any file — to install into an app on an incompatible autumn-web version. See docs/plugins.md.

Manual install

If you would rather wire it yourself (or autumn plugin add could not find your builder chain and printed these lines for you):

[dependencies]
autumn-web = { version = "0.10", features = ["db", "flash", "htmx", "maud"] }
autumn-admin-plugin = "0.10"

autumn-admin-plugin expects a configured Autumn database pool: model operations receive the app's pool. The built-in jobs dashboard can render without a database pool when no model route is accessed.

Quick Start

use autumn_admin_plugin::{prelude::*, AdminPlugin};

struct ProjectAdmin;
// Implement `AdminModel` for your type.
// Supply field metadata plus `list`, `get`, `create`, `update`, and `delete`.

#[autumn_web::main]
async fn main() {
    autumn_web::app()
        .plugin(AdminPlugin::new().register(ProjectAdmin))
        .run()
        .await;
}

What the Plugin Mounts

When mounted at the default /admin prefix, the plugin serves:

  • GET /admin/ — dashboard
  • GET /admin/jobs — background jobs dashboard
  • GET /admin/jobs/counters — HTMX counter fragment, refreshed at most every 2s
  • POST /admin/jobs/{id}/retry — retry a failed job
  • POST /admin/jobs/{id}/discard — discard a failed job
  • POST /admin/jobs/{id}/cancel — cancel an enqueued job
  • GET /admin/{slug} — paginated list view
  • POST /admin/{slug} — create record
  • GET /admin/{slug}/new — new-record form
  • GET /admin/{slug}/{id} — detail view
  • POST /admin/{slug}/{id} — update record
  • DELETE /admin/{slug}/{id} — delete record
  • GET /admin/{slug}/{id}/edit — edit form
  • POST /admin/{slug}/actions — bulk actions

The plugin also serves its same-origin JavaScript through AppBuilder::plugin_assets, at a content-hashed URL under /static/_plugins/autumn-admin/ (admin.<hash>.js), so long-lived caching stays safe across deploys.

Database Backends

The plugin core is backend-agnostic. It takes the app's autumn_web::RuntimeConnection pool, which is Postgres by default and SQLite under the autumn-web/sqlite feature. An app on either backend can register its own models.

The three built-in models are Postgres-only:

Model Manages Audit table
tokens::TokenAdminModel api_tokens —
experiments::ExperimentAdminModel autumn_experiments autumn_experiment_changes
feature_flags::FeatureFlagAdminModel autumn_feature_flags feature_flag_changes

Do not register those three on SQLite. Their migrations use Postgres-only DDL, so the tables do not exist on SQLite. Autumn's experiment and feature-flag stores also refuse a SQLite target.

On SQLite each of the three refuses every call with an error that names the model and points here. Before issue #2108 the crate did not compile at all on SQLite, so this was a build error; now it is a clear run-time one.

AdminModel Contract

Each registered model supplies:

  • A URL slug and singular/plural display names
  • A field schema via Vec<AdminField>
  • list, get, create, update, and delete operations

AdminField covers the common form/display shapes: Text, TextArea, Integer, Float, Boolean, Date, DateTime, Select, Hidden, Password, and Json.

Optional hooks let you customize:

  • actions() for extra bulk actions beyond the built-in delete action
  • execute_action() to implement those custom actions
  • record_display() for breadcrumbs and page titles
  • per_page() and count() for pagination behavior

All values flow through serde_json::Value so the plugin stays object-safe and does not need to know your application's concrete model types.

Writing a model that runs on both backends

Your own models choose their own SQL. Four rules keep it portable.

  1. Write $N placeholders in ascending order. Use each placeholder one time. Postgres reads the digits. SQLite gives each distinct $name the next free index, in order of first appearance. If you write SET name = $2 ... WHERE id = $1, SQLite binds the id into name. To repeat a value, bind it two times under two placeholders.
  2. Do not use ILIKE. Write LOWER(col) LIKE LOWER($1). Both backends then match without case for ASCII text. SQLite's lower() folds ASCII only, so non-ASCII text stays case-sensitive there.
  3. Do not use a ::type cast, an ANY($1) array bind, or a writable CTE. SQLite has none of the three. Use CAST(x AS TEXT), an IN list, and separate statements in one transaction.
  4. Use CURRENT_TIMESTAMP, not NOW(). Read a timestamp column as Timestamp into a NaiveDateTime. The Timestamptz SQL type is Postgres-only.

tests/custom_admin_model.rs is a worked example. The same test body runs on both backends.

Configuration

AdminPlugin::new() defaults to:

  • Prefix: /admin
  • Required role: "admin"
  • Session auth key: "user_id"
  • Actuator prefix: /actuator

You can override those defaults with:

  • prefix(...)
  • require_role(...)
  • auth_session_key(...)
  • actuator_prefix(...)

Example:

let plugin = AdminPlugin::new()
    .prefix("/backoffice")
    .actuator_prefix("/ops")
    .auth_session_key("uid")
    .require_role(Some("staff".to_owned()));

Security Notes

  • The plugin assumes Autumn session/auth middleware is already configured
  • Role checks run before any admin handler by default
  • CSRF tokens are rendered automatically when Autumn's CsrfLayer is enabled
  • No inline JavaScript is used; the UI is compatible with Autumn's default CSP
  • Password fields are treated as write-only and never rendered back to users

Status

This crate is intended as the first-party admin plugin for autumn-web. The API is pragmatic and functional, but still young enough that you should expect incremental improvements around model ergonomics, docs, and batteries-included examples.