# Rullst book: full text > Rullst is a modular, strictly typed Rust framework suite (Tokio, Axum, Tower, SQLx) for full-stack web applications: Active Record ORM, server-rendered html! views with HTMX, authentication, security middleware and the cargo-rullst CLI. This index lists every page of the Rullst book in reading order; llms-full.txt holds their full Markdown. --- # Welcome to Rullst Source: https://rullst.github.io/Rullst/book/index.html # Intelligent, Security-Conscious, and Designed for Effortless Productivity Because With Rullst, We Rule! Rullst is a modular Rust framework suite built on Tokio, Axum, Tower, and SQLx for full-stack applications and the product workflows around them. Version 13 is in prerelease: `13.0.0-alpha.4` is published and is the 13.0 feature freeze. The v12 line reached end of life on 16 October 2026. Use the capability and release documents to distinguish implemented behavior from roadmap work and application-specific responsibilities. This book is built from the `main` branch, which can be ahead of the latest prerelease; the **v13 Preview** chapters describe features that are new in v13. **Welcome to the Rullst documentation.** Build a small application, understand the code behind it, then explore the tools that fit your next idea. > **New here? [Start with the guided learning path →](https://rullst.github.io/Rullst/book/start-here.html)** > > Choose a generated application, a hand-written first route or a JSON API. > Each path tells you what to run, what to expect and where to go next. ## Documentation Hub - [✨ Why Rullst?](https://rullst.github.io/Rullst/book/why-Rullst.html) - [📖 Getting Started & Starters](https://rullst.github.io/Rullst/book/1-getting-started.html) - [🤖 Rullst AI: Developing with Autonomous Agents](https://rullst.github.io/Rullst/book/2-rullst-ai.html) - [📊 Rullst Studio: Real-Time Monitoring & Radar](https://rullst.github.io/Rullst/book/3-rullst-studio.html) - [⚙️ Rullst Nexus: Explicit Admin CMS](https://rullst.github.io/Rullst/book/4-rullst-nexus.html) - [💳 Rullst Capital: SaaS Billing & Revenue Dashboard](https://rullst.github.io/Rullst/book/5-rullst-capital.html) - [🧠 Integrating AI into Rullst](https://rullst.github.io/Rullst/book/6-ai-integration-tutorial.html) - [🍳 Rullst Cookbook & Tutorials](https://rullst.github.io/Rullst/book/tutorials/01-hello-world.html) - [📜 Framework Spec](https://rullst.github.io/Rullst/book/spec.html) - [🗺️ Blueprints Roadmap](https://rullst.github.io/Rullst/book/blueprints_roadmap.html) - [💻 CLI Reference](https://rullst.github.io/Rullst/book/cli_reference.html) - [🛡️ Audit Report](https://github.com/Rullst/Rullst/blob/main/AUDIT.md) - [📦 View on Crates.io](https://crates.io/crates/rullst) ## For AI assistants The site publishes [`llms.txt`](https://rullst.github.io/Rullst/llms.txt), an index of every page of this book in reading order, and [`llms-full.txt`](https://rullst.github.io/Rullst/llms-full.txt), the full Markdown of those pages in one file. Both are generated from this book's sources each time it is published, so they describe the same `main` branch. ## Learn the foundations, then explore - 🚀 **Measured performance:** Criterion suites and CI track regressions; application latency must be measured on the target workload. - 🧩 **Server-rendered UI:** `html!` and HTMX-oriented scaffolds are the audited default. LiveView and Wasm Island primitives exist, while turnkey Leptos/Dioxus adapters remain roadmap work. - 📖 **Interactive Scalar Docs (`/docs`):** `cargo rullst make:scalar` scaffolds an OpenAPI viewer for your reviewed specification. - ☸️ **Kubernetes manifests:** `cargo rullst make:k8s` scaffolds manifests that probe `/health` and `/ready`. - 📡 **Rullst Radar & Prometheus:** Process/Tokio observations and a Prometheus text exporter, with unavailable probes reported explicitly. - 🛡️ **Local AI option:** Use an Ollama endpoint when prompts should stay on infrastructure you control; network isolation remains a deployment duty. - 📦 **Batteries Included:** ORM (rullst-orm), Auth, Revenue Dashboard, Jobs, Mail, Scheduler, Cache, Security. --- # Start Here: Your First Idea in Rust Source: https://rullst.github.io/Rullst/book/start-here.html # Your first idea, running in Rust You do not need to understand every Rullst crate before writing your first application. Start small, make one thing work, and learn what each layer does as your product needs it. > **This guide targets the v13 prerelease, 13.0.0-alpha.4.** The v12 line > reached end of life on 16 October 2026 and a new 12.x project no longer > resolves its dependencies, so install the CLI and pin the framework with the > exact version, as [Getting Started](https://rullst.github.io/Rullst/book/1-getting-started.html) shows: Cargo never > selects a prerelease on its own. Upgrading an existing application? Follow > the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html). This book is built from `main`, > where v13 is in development. ## Pick your starting line | What you want to do | Start here | Your first visible result | | --- | --- | --- | | “Take me from an empty folder to a complete app.” | [Zero to a complete app](https://rullst.github.io/Rullst/book/tutorials/zero-to-complete-app.html), about an hour | Sign-in, owner-only CRUD pages, tests, a security report and a production build | | “Give me an application I can explore.” | [CLI installation and starters](https://rullst.github.io/Rullst/book/1-getting-started.html) | Generated Rust, a local web page and a development loop | | “Show me how the framework actually works.” | [Zero to Hello Rullst](https://rullst.github.io/Rullst/book/tutorials/01-hello-world.html) | One complete typed route you write yourself | | “I need a backend for another client.” | [Your first JSON REST API](https://rullst.github.io/Rullst/book/tutorials/rest-api-quickstart.html) | A real HTTP response you can inspect with curl | For the simplest first experiment, choose **Blank + SQLite**, or Blank with `--no-database` if you do not need persistence yet. The larger SaaS starter, and the LMS, ERP, Portfolio and Blog [examples](https://rullst.github.io/Rullst/book/examples.html), have more moving parts and longer first builds. You can explore them after your toolchain and local workflow are working. ## A small loop that teaches a lot 1. **Run it.** Reach the local page or JSON endpoint before adding features. 2. **Find the code.** Open `src/main.rs`, then the generated controllers, models and pages that exist in your chosen starter. 3. **Make one visible change.** Edit a heading or response field. With `cargo rullst dev`, watch the rebuild and process restart; with plain `cargo run`, stop and restart it yourself. 4. **Break something safely.** Introduce a small syntax error in your local experiment. Read the compiler diagnostic, correct it and save again. The supervisor keeps the old application running when compilation fails. 5. **Verify the result.** Reload the page or repeat the request. A successful build is only the first check; the response should do what you intended. See [exact restart and state boundaries](https://rullst.github.io/Rullst/book/tutorials/51-authenticated-hot-reload.html). Do not use production databases or credentials for this exercise. ## Build your understanding, one layer at a time Looking for a type, a feature or a connected REST learning path? Use the [API reference index](https://rullst.github.io/Rullst/book/api-reference.html) to move between the guides and the versioned Rust documentation. | Next question | Guide | | --- | --- | | Where do generated files go? | [CLI generators](https://rullst.github.io/Rullst/book/tutorials/02-cli-generators.html) | | How do I save and read data? | [Active Record CRUD](https://rullst.github.io/Rullst/book/tutorials/03-active-record-crud.html) and [migrations](https://rullst.github.io/Rullst/book/tutorials/05-migrations-and-seeds.html) | | How does the page become interactive? | [HTML and HTMX](https://rullst.github.io/Rullst/book/tutorials/06-htmx-zero-bundle.html) | | How do requests reach my handlers? | [Routes and middleware](https://rullst.github.io/Rullst/book/tutorials/08-routing-and-middlewares.html) | | Who is allowed to access a record? | [Ownership, RBAC and IDOR](https://rullst.github.io/Rullst/book/tutorials/13-rbac-authorization.html) | | How do I update the framework later? | [Assisted upgrades](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html) | | Which crates are stable, and which are experimental? | [Maturity tiers](https://rullst.github.io/Rullst/book/maturity.html) | You can use an AI assistant while learning. Ask it to explain the generated files, point to the exact APIs and show a failing test before a bug fix. Treat its output as a proposed change, not as proof. Rullst's [architecture specification](https://rullst.github.io/Rullst/book/spec.html) is the common reference for both of you. ## When the first run does not work | What you see | Check first | | --- | --- | | `cargo rullst` is unknown | Install the matching CLI, then reopen your terminal if its binary directory is not on `PATH`. | | The first build is taking a long time | Source builds compile Rust dependencies. Keep the output visible; elapsed time is not a reliable failure signal. | | A database connection fails | Confirm the selected backend and local `.env` values. PostgreSQL/MySQL/MariaDB need a running service; SQLite does not need a separate server. | | The app cannot bind its port | Stop the conflicting process you own, or configure another port and restart the development command. | | Optional persistence feels confusing | Selecting none is valid. Add only the specialized stores your application actually needs. | | A command differs from a screenshot | Use the installed CLI's `--help` and the matching source documentation. Screenshots are recorded examples. | If you are still stuck, share the command, operating system, Rust version, framework commit and a minimal reproduction in an [issue](https://github.com/Rullst/Rullst/issues) or on [Discord](https://discord.com/invite/2ntKFtsSjw). Remove secrets and personal data first. Send security vulnerabilities privately using the [security policy](https://github.com/Rullst/Rullst/security/policy). ## Before this becomes a real product A generated app is a foundation, not a deployment approval. Review authentication, ownership, secrets, database backups and migrations, external provider setup and release status. Start with the [security architecture](https://rullst.github.io/Rullst/book/security-architecture.html), choose features using the [capability status](https://rullst.github.io/Rullst/book/capability-status.html), check each crate's [maturity tier](https://rullst.github.io/Rullst/book/maturity.html) (Core, Extension or Experimental), and measure your own workload. **Ready? [Create your first application →](https://rullst.github.io/Rullst/book/1-getting-started.html)** --- # Getting Started & Starters Source: https://rullst.github.io/Rullst/book/1-getting-started.html # Getting Started Welcome to the **Rullst** Getting Started guide! **Your goal:** install the matching v13 CLI, generate a small application, open it locally and make your first change. Prefer to write the first route yourself? Use [Zero to Hello Rullst](https://rullst.github.io/Rullst/book/tutorials/01-hello-world.html). Rullst is a strictly typed Rust framework suite for full-stack applications, designed around explicit APIs, measurable performance, and defense-in-depth defaults. ## 1. Installation First, ensure you have Rust installed. The official and recommended way is to visit [rustup.rs](https://rustup.rs/). **For macOS and Linux:** ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` **For Windows:** Download and run `rustup-init.exe` from the website. Next, install the **Rullst CLI** from the same release train as the framework. This book is built from `main`, where v13 is in development. The v12 line reached end of life on 16 October 2026 (12.3.0 was its last release, and a new 12.x project no longer resolves its dependencies after crates.io yanked the `zip` releases its Studio needs), so install the v13 prerelease. Name its exact version: Cargo never selects a prerelease on its own, so neither a plain `cargo install cargo-rullst` nor `^13` installs it: ```bash cargo install cargo-rullst --version =13.0.0-alpha.4 --locked ``` Pin the framework the same way in `Cargo.toml` (`"=13.0.0-alpha.4"`); the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html) explains why and what changed. Source reviewers who intentionally need checkout-local changes can instead clone this repository and install the CLI from that exact checkout. On `main` this installs the v13 development CLI: ```bash git clone --branch main https://github.com/Rullst/Rullst.git cd Rullst cargo install --locked --path cargo-rullst cd .. ``` When installed from source, the CLI reuses the exact checkout from which it was compiled, even when project creation is invoked from another directory, provided that checkout has not been moved or deleted. Generated manifests therefore contain absolute path dependencies and are not portable. Running from the repository root remains an explicit fallback. Prefer the published prerelease and its matching registry packages for ordinary applications. Cargo installs the `cargo-rullst` and `rullst` executables into `$CARGO_HOME/bin` (`~/.cargo/bin` by default). To install elsewhere, set `CARGO_INSTALL_ROOT` or pass `--root `. To remove the CLI, run: ```bash cargo uninstall cargo-rullst ``` Add the same `--root ` if you installed with one. ## 2. Creating Your First Project Run the create wizard with the name of your application (or run `cargo rullst` alone and choose **Create New Project** on its home screen): ```bash cargo rullst new my_app ``` The wizard asks its questions in a terminal and writes nothing until you confirm. Esc goes back one question. 1. **Starter**: **Blank**, **API** or **SaaS**, each with a one-line description and a preview of the files it generates (see [Rullst Starters](https://rullst.github.io/Rullst/book/1-getting-started.html#3-rullst-starters)). 2. **Database**: the primary database. **SQLite** is the simplest local first run; PostgreSQL, MySQL and MariaDB need their database service running. 3. **Features**: optional features such as AI, Redis or a storage add-on. Selecting none is valid; a storage add-on is not another primary database. 4. **Review**: the files and the commands it will run. Choose **Create the project**, **Back** to change an answer, or **Cancel** to exit without creating anything. For a database-backed app, the CLI then compiles the new application once and runs its initial migration, and prints three numbered next steps: `cd my_app`, `cargo rullst dev` and the local URL of the generated welcome page, by default `http://127.0.0.1:3000`: ```bash cd my_app cargo rullst dev ``` Open the local URL reported by the command. Find the generated page code, change a heading and save. Confirm that the changed page appears after the successful rebuild/restart. Stop development with `Ctrl+C` before moving or removing the project directory. `cargo rullst new my_app --default --dry-run` prints the plan, its file tree and the commands without creating anything, and `cargo rullst tour` walks through the main commands without changing a project. Without a terminal (in CI or a script), `new` needs `--default`; flags such as `--blueprint` and `--database` answer their questions. See the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-new-name). Prefer a small deterministic starter without navigating the wizard? ```bash cargo rullst new first_app --default --blueprint blank --database sqlite \ --skip-initial-migration cd first_app cargo rullst dev ``` The first build compiles every dependency and can take several minutes (about two and a half minutes for the Blank starter on the machine measured in [Build times](https://rullst.github.io/Rullst/book/build-times.html), which also has tips). `--skip-initial-migration` defers the generator's bootstrap work; the development command runs the initial migration before starting a generated database-backed app. Never point this learning project at a production database. > [!TIP] > The `cargo rullst dev` command compiles the project and starts the local > server. Saving changes triggers a real rebuild and supervised process restart; > a failed build leaves the previous application serving. The same-origin > browser client refreshes after the new process responds with its generation > marker. In-memory state resets. No hot-reload scaffold option is required. > See the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-dev). > > The CLI deliberately generates one audited application profile: Active > Record with server-rendered `html!` views and HTMX enhancement. The framework > still exposes lower-level repository and client-runtime foundations for > application-owned integration. See the [Architecture Choices Guide](https://rullst.github.io/Rullst/book/architecture-decisions.html). ## 3. Rullst Starters `cargo rullst new` generates one of three starters. The wizard asks which one (or pass `--blueprint blank`, `api` or `saas`); each uses the Rullst color scheme and server-rendered HTML/HTMX patterns, and allocation and latency depend on the generated page and runtime. ### Blank **Use Case:** Custom, from-scratch development. This is the minimal template powered by server-rendered `html!` views and HTMX without a project-local JavaScript bundle. It includes a simple reactive counter to demonstrate server-driven communication. Other frontend foundations remain available as application APIs, but are not advertised as equivalent generated starters. ### API **Use Case:** Headless JSON services. The Blank starter without HTML (`--blueprint api`, or `--api`): JSON routes, an example write route protected by a bearer `API_TOKEN` written to `.env`, and the same production security baseline. It can run without a database (`--no-database`) or with Turso as its primary. ### SaaS **Use Case:** Subscription-based products and billing. An opinionated SaaS starting point, pre-wired with: - User authentication (login, signup, session management, sign out everywhere, password change and password reset by email with links built from `RULLST_PUBLIC_ORIGIN`). - Stripe pricing panels and subscription checkout views. - Secure user dashboard and a Nexus admin for signed-in administrators. ### Larger example applications Up to 13.0.0-alpha.3 the CLI also generated LMS, Blog, Portfolio and ERP starters. Since 13.0.0-alpha.4 they are complete applications in the repository's [`examples/`](https://rullst.github.io/Rullst/book/examples.html) directory — `examples/lms` (courses, lessons, an accessible player, enrollment and owner-bound progress), `examples/erp` (products, stock and orders), `examples/portfolio` (a developer showcase edited in Nexus) and the `examples/blog` showcase — tested in the workspace. Copy one to start a bigger application; `cargo rullst new --blueprint lms` (or `erp`, `portfolio`, `blog`) creates nothing and names the example instead. --- > [!TIP] > **Starter evolution:** Starters are continuously checked by generator > smoke tests. Treat generated code as an application starting point: inspect its > configuration and rerun `cargo check` and security tests after customization. ### Local Studio and Nexus access The SaaS starter (and the LMS, ERP, Portfolio and Blog examples) expose visible buttons for both control surfaces during local development: - **Nexus** is mounted at `/nexus` and accepts only a verified loopback peer in a debug build, so the first local click needs no placeholder password. - **Studio** starts as a separate debug-only service at `http://127.0.0.1:5555`. This convenience cannot be enabled in a release binary through `RULLST_ENV` or the legacy `APP_ENV` alias. A release build does not start the generated Studio task, and Nexus needs a real sign-in: - **SaaS** (and the LMS example) admit the user signed in to the application whose `users.is_admin` column is true, checked on every request; no extra credentials. Grant the first administrator with your database client (`UPDATE users SET is_admin = TRUE WHERE email = 'you@example.com';`); that administrator can grant others from the Users model in Nexus. See [using the application's sign-in](https://rullst.github.io/Rullst/book/4-rullst-nexus.html#using-the-applications-sign-in). - **The Portfolio and ERP examples** require unique `NEXUS_ADMIN_USERNAME` and `NEXUS_ADMIN_PASSWORD` values (Basic Auth behind verified TLS). Without them the application starts without `/nexus` (the ERP without its back office) and prints one line naming the missing variable; see [serving the application without Nexus](https://rullst.github.io/Rullst/book/4-rullst-nexus.html#serving-the-application-without-nexus). The blog example needs the same variables in a release build and does not start without them. Put production Nexus behind a verified TLS boundary; do not expose Studio publicly. Studio has no production mode: it runs only in debug builds and on loopback. ## 4. Database configuration and specialized stores The wizard deliberately makes two separate decisions: 1. Choose the primary **SQLx Active Record** backend: **SQLite**, **PostgreSQL**, **MySQL**, or **MariaDB**. MariaDB uses the MySQL wire protocol but has its own executable container contract. The Blank starter (web page or JSON API) can instead select **Turso/libSQL** as its primary database, through a separate adapter that has no Nexus, Studio database pages, `Schema` migrations, `artisan!` or SQLx query builder. 2. Optionally add side stores: **Turso/libSQL** edge SQL, **MongoDB** documents, **DuckDB** analytics, **SurrealDB** documents/graph reads, **Qdrant** vectors or **Redis** structures. They use their own APIs and do not replace the primary pool. [Database Support](https://rullst.github.io/Rullst/book/database-support.html) lists what each backend works with; read it before you choose. After generation, the default flow compiles the new application once and runs its initial migration. A clean first build can take several minutes for the larger SaaS starter. Use `--skip-initial-migration` when the network database is not yet configured, then run `cargo rullst db:migrate` from the generated project. `cargo rullst dev` applies pending migrations when it starts, but a deployed server never does: run the binary's `db:migrate` command on each release, as described in [Before the first deploy](https://rullst.github.io/Rullst/book/tutorials/26-one-click-paas-deploy.html#before-the-first-deploy). The non-interactive flags mirror the second step: ```bash cargo rullst new edge_app --default --database mariadb --turso --mongodb \ --skip-initial-migration ``` Automation can pin the supported starter, database and optional runtime capabilities. The application architecture is intentionally fixed to Active Record plus server-rendered `html!`/HTMX: ```bash cargo rullst new subscriptions --default --blueprint saas \ --database postgres --ai \ --skip-initial-migration ``` For a blank application with no primary relational database, use the explicit `--no-database` flag. It cannot be combined with `--database`. Generated SQLx profiles disable the default features of Rullst's umbrella and `rullst-orm` dependencies and select exactly one strict relational backend, so a chosen PostgreSQL/MySQL/MariaDB profile is not accidentally compiled through an implicit SQLite default. To create a Turso-primary API using the current bounded blank starter: ```bash cargo rullst new edge_app --default --api --database turso \ --skip-initial-migration cd edge_app cargo rullst db:migrate ``` The generated `.env` selects a persistent, real-SQL offline fallback and does not invent a SQLx `DATABASE_URL`: ```env TURSO_DATABASE_URL=mock_local TURSO_AUTH_TOKEN= TURSO_OFFLINE_PATH=turso-development.db ``` To use Turso Cloud, put the token in its own variable rather than in the URL: ```bash turso db create my-app-db turso db tokens create my-app-db ``` ```env TURSO_DATABASE_URL=libsql://my-app-db-username.turso.io TURSO_AUTH_TOKEN=replace-with-a-secret-token ``` The familiar derive explicitly selects the Turso backend: ```rust #[derive(Debug, Clone, rullst_orm::Orm)] #[orm(table = "users", backend = "turso")] struct User { id: i64, name: String, } ``` `User::all()`, `find`, `save`, `create`, `delete`, `count`, filtering, ordering and pagination then execute through the primary `TursoOrm` store. `make:model --migration`, `make:migration`, `db:migrate`, `db:status`, and `db:rollback` preserve this backend. The current first-class scaffold contract is deliberately limited to the Blank and API starters; the SQLx-specific SaaS starter rejects `--database turso` until ported. Transparent embedded-replica synchronization is not claimed. `TursoStore` remains available for prepared SQL, bounded result materialization, transactions and checksummed migrations in either primary or additive configurations. See [Polyglot Persistence](https://rullst.github.io/Rullst/book/polyglot-persistence.html) for complete setup, security constraints and examples for every optional store. --- # Build Times Source: https://rullst.github.io/Rullst/book/build-times.html # Build Times Most of the time spent waiting on a Rullst application is `cargo build` or `cargo rullst dev` rebuilding it after an edit. This page states what a generated project does to keep those builds short, the measured effect on the three starters, and other things you can try. ## What `cargo rullst new` writes Since 13.0.0-alpha.4 every generated `Cargo.toml` has a development profile next to the [release profile](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-new-name): ```toml [profile.dev] debug = "line-tables-only" [profile.dev.package."*"] debug = false ``` - **Your package keeps line tables.** Panic messages and the frames of your own code in a `RUST_BACKTRACE=1` backtrace still show the file and line (`at ./src/main.rs:2:11`). Frames inside dependencies show the function name without a file and line. - **Dependencies are built without debug information.** Their debug information was about half of a debug binary and 41 % of `target/`: the API starter's binary went from 83.7 MB to 41.5 MB and its `target/` directory from 2.47 GB to 1.45 GB. - **Nothing else changes.** Optimisation level, incremental compilation, overflow checks and debug assertions keep Cargo's development defaults. `cargo test` uses the same settings (Cargo's `test` profile inherits `dev`), and release builds are unchanged. - **For a debugger** that shows local variables, set `debug = true` in both sections (or delete them). Cargo then rebuilds everything once. The SaaS starter also no longer declares `rullst-connect`: its code does not use it, and `cargo rullst make:oauth` enables the facade's `oauth-*` feature instead. Its first build compiles 12 fewer packages. The generated `.cargo/config.toml` is unchanged: on Linux it sets `-C split-debuginfo=unpacked` and selects mold or lld when the generating machine has one, and Git ignores it. With the profile above, split debug information made no measurable difference on Linux (API starter: 146.0 s with it and 149.1 s without for a cold build, 1.34 s and 1.40 s for a rebuild after an edit). macOS already uses unpacked debug information by default and was not measured. ## Measurements Measured on 29 October 2026 with the 13.0.0-alpha.4 development source. The numbers describe these starters on this one machine; they are not a prediction for other applications or hardware. - **Applications:** `cargo rullst new --default --skip-initial-migration`, plus `--api` or `--blueprint saas` (SQLite; `rullst` features `orm`, `strict-sqlite` and `studio`, and `nexus`, `auth`, `mail` and `capital` for SaaS), with path dependencies on the framework source and the repository's `Cargo.lock`, built `--offline`. *Before* is the same source without the two changes above. - **Toolchain:** Rust 1.99.1, `x86_64-unknown-linux-gnu`, linked by rustc's bundled `rust-lld` (LLD 23.1.1; the default for this target since Rust 1.90). Neither mold nor lld was installed, so the generated `.cargo/config.toml` selected no linker. - **Machine:** AMD Ryzen 5 7520U (8 logical CPUs), 3.4 GiB RAM, NVMe disk, `CARGO_BUILD_JOBS=2`, a shared workstation; no other Cargo process ran during a measurement. - **Commands:** wall-clock time from `/usr/bin/time` of `cargo build --offline` and `cargo check --offline`. *Cold* starts from an empty `target/` (dependencies already downloaded). *Edit* changes one string literal in a handler (`src/main.rs` for Blank and API, `src/controllers/auth_controller.rs` for SaaS) and rebuilds; *touch* only updates that file's timestamp. Cold builds: mean of two runs; rebuilds: median of five. | Starter | Cold `cargo build` | Rebuild after edit | Rebuild after touch | Cold `cargo check` | `cargo check` after touch | `target/` after the build | Binary | Packages compiled | | :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | Blank, before | 175.4 s | 2.56 s | 1.72 s | 127.4 s | 0.83 s | 2.48 GB | 83.9 MB | 310 | | Blank, generated | 147.4 s (−16 %) | 1.35 s (−47 %) | 1.09 s | 124.2 s | 0.62 s | 1.45 GB (−41 %) | 41.7 MB | 310 | | API, before | 173.5 s | 2.32 s | 1.38 s | 128.1 s | 1.02 s | 2.47 GB | 83.7 MB | 310 | | API, generated | 147.5 s (−15 %) | 1.34 s (−42 %) | 1.22 s | 123.6 s | 0.50 s | 1.45 GB (−41 %) | 41.5 MB | 310 | | SaaS, before | 202.0 s | 4.29 s | 3.11 s | 137.9 s | 1.28 s | 2.99 GB | 113.7 MB | 349 | | SaaS, generated | 163.2 s (−19 %) | 2.14 s (−50 %) | 2.19 s | 132.6 s | 1.05 s | 1.74 GB (−42 %) | 65.7 MB | 337 | Cold checks are one run each; checks after touch are the median of three. `cargo check` generates no code and links nothing, so the profile changes it little; its `target/` still went from 1.11 to 0.81 GB (SaaS: 1.26 to 0.92 GB). Removing `rullst-connect` alone, with full debug information, took the cold SaaS build from 203.7 s to 194.1 s and its cold check from 137.9 s to 135.2 s (one run each). Other development settings, measured the same way on the API starter (one run each): | `[profile.dev]` | Cold `cargo build` | Rebuild after edit | Binary | `target/` | | :--- | ---: | ---: | ---: | ---: | | Cargo's default (full debug information), as generated before | 173.9 s | 2.61 s | 83.7 MB | 2.47 GB | | The same, without the generated `.cargo/config.toml` (as on a fresh clone or in CI) | 177.0 s | 5.52 s | 269.0 MB | 2.76 GB | | `debug = "line-tables-only"` for every package | 156.5 s | 1.78 s | 71.3 MB | 1.93 GB | | **Generated:** line tables for the application, none for dependencies | 146.0 s | 1.34 s | 41.5 MB | 1.45 GB | | `debug = false` for every package | 146.8 s | 1.34 s | 35.2 MB | 1.44 GB | Line tables for your own package cost nothing measurable, while those of the dependencies cost about ten seconds of every cold build. Without split debug information (no `.cargo/config.toml`), the full-debug link was also the largest process of the build: 1.2 GiB, against 0.7 to 0.8 GiB for every other setting. ### Where a cold build spends its time `cargo build --timings` writes `target/cargo-timings/cargo-timing.html` with the time of every compiled unit. In the generated API and SaaS builds above the longest single unit was the build script of `aws-lc-sys`, the cryptography library behind the HTTPS client, at about 52 s; then `rullst-core` (about 13 s), `jsonschema` (about 9 s) and the SQLite library bundled by `libsqlite3-sys` (about 6 s). Some crates, SQLite among them, are compiled twice: once for SQLx's compile-time macros and once for the application. None of this depends on your code, so it is paid again only after `cargo clean`, a toolchain update or a change of features or profile. Rullst keeps `aws-lc-rs` as the TLS crypto provider, for rustls' post-quantum hybrid key exchange, so the `aws-lc-sys` build script stays in a project's first build. Later builds reuse it, and the dependency layer of the generated Dockerfile (the cargo-chef approach) caches it in images; moving to `ring` is not planned. Compiling SQLite and the other affected crates only once, and keeping `rullst-ai` out of the SaaS starter (Nexus pulls it in today), are planned for 13.0.0-rc.1 if they need no breaking change, otherwise for 13.1. ## Tips - **Use `cargo check` while you edit.** It skips code generation and linking: a check after an edit took 0.6 to 1.0 s in these projects (one run each), and a cold check was 23 to 31 seconds shorter than a cold build. rust-analyzer in your editor runs the same check when you save. - **Keep `cargo rullst dev` running.** It reuses `target/`, so after the first build only your package is rebuilt on each save (the *edit* column above), then it restarts the server; restart time was not measured here. - **Linker.** On `x86_64-unknown-linux-gnu`, Rust 1.90 and later already link with the bundled `rust-lld`, which all the measurements above used. On other hosts (for example aarch64 Linux) mold or lld may shorten links: on Linux, `cargo rullst doctor` suggests one when rustc does not already use LLD and no linker is configured, and a project generated on a machine with one selects it in `.cargo/config.toml`. Not measured here. - **sccache** can share compiled dependencies between projects on the same machine (`RUSTC_WRAPPER=sccache`). Not measured here. ## Existing applications `cargo rullst upgrade` does not edit build profiles. To adopt the faster development builds, copy the `[profile.dev]` and `[profile.dev.package."*"]` sections above into your `Cargo.toml`. In an application created from the SaaS starter before 13.0.0-alpha.4, you can remove the `rullst-connect` dependency when no source file uses `rullst_connect`; `cargo check` tells you whether one does. --- # Database Support Source: https://rullst.github.io/Rullst/book/database-support.html # Database Support Read this page before you choose a database. It is the one place that says what each backend works with in Rullst 13.0. Rullst integrates one family of databases fully: SQLite, PostgreSQL and MySQL/MariaDB, through SQLx. `#[derive(Orm)]` models and their query builder, `Schema` migrations, `artisan!` and the `db:*` commands, `Server::with_db`, Nexus and Studio's database pages all use that SQLx connection pool. Turso/libSQL can be the primary database of the Blank starter, but through a separate adapter (`TursoOrm`, `TursoModel`, `TursoRepository`, `TursoMigration`) that does not plug into any of those parts. Integrating it is roadmap work, not part of 13.0. MongoDB, DuckDB, SurrealDB, Qdrant and Redis data structures are side stores: you add them next to the primary database and call them through their own APIs. They never replace the primary database, and there is no transaction across stores. ## Capability table | Backend | Primary database | `#[derive(Orm)]` and query builder | `Schema` migrations | `artisan!` and `db:*` | `Server::with_db` | Nexus | Studio | Transactions | `cargo rullst new` | Live CI test | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | SQLite | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Every starter; the default (`--database sqlite`) | No server (in-process): `matrix_sqlite`, Studio `data_browser_mutation_sqlite`, Nexus `tenant_audit` | | PostgreSQL | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Every starter (`--database postgres`) | `matrix_postgres`, Studio `data_browser_mutation_postgres`, Nexus `sql_matrix_postgres` | | MySQL / MariaDB | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes, but DDL commits implicitly (note 4) | Every starter (`--database mysql` or `--database mariadb`) | `matrix_mysql` (MySQL 8.0), `matrix_mariadb`, Studio `data_browser_mutation_mysql` and `_mariadb`, Nexus `sql_matrix_mysql` (MySQL only) | | Turso / libSQL | Blank starter only (note 1) | No: a separate model API (note 2) | No: SQL `TursoMigration`s (note 3) | No `artisan!`; the Blank starter answers `db:migrate`, `db:rollback` and `db:status` itself (note 3) | No | No | No (note 5) | Statement batches only (`TursoStore::transaction`) | Primary: Blank, web page or `--api` (`--database turso`). Side store: `--turso` with any starter | `matrix_turso` against `libsql-server` v0.24.32 | | MongoDB | No | No (`MongoDbStore`) | No | No | No | No | No | No | Side store: `--mongodb` with any starter | `matrix_mongodb` | | DuckDB | No | No (`DuckDbStore`) | No | No | No | No | No | No | Side store: `--duckdb` with any starter | None: an in-process engine, covered by unit tests | | SurrealDB | No | No (`SurrealDbStore`) | No | No | No | No | No | No | Side store: `--surrealdb` with any starter | `matrix_surrealdb` (SurrealDB 3.2.4) | | Qdrant | No | No (`QdrantStore`) | No | No | No | No | No | No | Side store: `--qdrant` with any starter | `matrix_qdrant` | | Redis data | No | No (`RedisDataStore`) | No | No | No | No | No (note 6) | No: each command is atomic on its own | Side store: `--redis` with any starter (it also enables Redis queues and cache) | `matrix_redis_data` | Notes: 1. `cargo rullst new` refuses `--database turso` with the SaaS starter (Blank and API accept it). SaaS uses SQLx models, `Schema` migrations and Nexus, so it needs a SQLx database. 2. `#[derive(Orm)]` with `#[orm(backend = "turso")]` generates the `TursoModel` contract instead of SQLx code: `all`, `find`, `save`, `create`, `delete`, `count` and `paginate`, plus filtered reads through `TursoOrm::repository::()?.query()` (`where_eq`, `order_by`, `limit`, `offset`, `get`, `first`, `count`). It has no SQLx query builder, and the derive refuses relations, soft deletes, hooks, scopes, tenant columns, policies, auditing, Scout search and embeddings at compile time. 3. A Turso migration is a `TursoMigration` holding SQL statements, checksummed and reversible with `with_down`; the `Schema` and `Blueprint` builders never apply to Turso. `cargo rullst make:migration` writes one, and `make:migration:auto` refuses Turso projects. The Blank starter's `main.rs` answers `db:migrate`, `db:rollback` and `db:status`, and `db:seed` reports that no seeders exist; `cargo rullst db:*` runs them through `cargo run`. 4. MySQL and MariaDB commit each DDL statement implicitly, so a migration's schema changes do not roll back with it; data statements are transactional on all three SQLx backends. See [Tutorial 05](https://rullst.github.io/Rullst/book/tutorials/05-migrations-and-seeds.html). 5. Studio still starts in a Turso Blank project, but its database, migration and data browser pages read only a SQLx `DATABASE_URL`, so they report that no database is configured. 6. Studio's cache page shows the Core `Cache` handed to it (memory or Redis), not `RedisDataStore` structures. ## Where the live tests run The live tests start the database in a container (Testcontainers) in the **Run provider matrices** step of the Rust CI workflow's Linux workspace shard. `matrix_postgres`, `matrix_mysql` and `matrix_sqlite` also run in the **ORM strict-postgres**, **ORM strict-mysql** and **ORM strict-sqlite** jobs. The tests live in `rullst-orm/tests/`, `rullst-studio/tests/` and `rullst-nexus/tests/`, and each pins its container image. A live test proves the adapter's contract against that server version; it does not certify a hosted service, backups, replication or failover. ## Choosing - Start with SQLite; move to PostgreSQL, MySQL or MariaDB for a shared server. Every starter and every framework feature works with these. - Choose Turso as the primary database only for a Blank starter (a web page or a JSON API) where you are ready to write SQL migrations and use the `TursoModel` API without Nexus, Studio's database pages, `Schema` or the SQLx query builder. - Add a side store when the application needs documents, analytics, graph reads, vectors or Redis structures next to its primary database. Enable only the adapters you use: each one is its own Cargo feature (`orm-turso`, `orm-mongodb`, `orm-duckdb`, `orm-surrealdb`, `orm-qdrant`, `orm-redis` on `rullst`), and DuckDB's bundled C++ build makes a first build noticeably longer. The APIs of Turso and the side stores are described in [Polyglot Persistence](https://rullst.github.io/Rullst/book/polyglot-persistence.html); the SQLx ORM in [rullst-orm](https://rullst.github.io/Rullst/book/crates/orm.html); migrations in [Tutorial 05](https://rullst.github.io/Rullst/book/tutorials/05-migrations-and-seeds.html); every Cargo feature in the [feature matrix](https://rullst.github.io/Rullst/book/feature-matrix.html). --- # API Reference & REST Learning Path Source: https://rullst.github.io/Rullst/book/api-reference.html # Find the API you need There are two different meanings of **API** in this documentation: - **Rust API:** the framework types, traits, functions and Cargo features your application imports. Use the versioned rustdoc references below for exact signatures and follow the book for application-level context. - **HTTP API:** the endpoints your application exposes to a browser, mobile client or another service. Start with the REST learning path below; an OpenAPI document describes your endpoints, not the entire Rust framework. The Rust reference links target the published **13.0.0-alpha.4** prerelease packages; the v12 line reached end of life on 16 October 2026 (see the [release record](https://rullst.github.io/Rullst/book/v12.html)). Versioned docs.rs pages become available after registry publication and rustdoc builds. This book is built from `main`, which can be ahead of that prerelease; for unreleased APIs, build local documentation from the checkout. The index connects existing guides without certifying every public symbol or v13 roadmap item. Check [capability boundaries](https://rullst.github.io/Rullst/book/capability-status.html) before interpreting a provider or blueprint name as complete product support. ## Rust reference: start with a task | What you need | Application guide | Exact v13 prerelease Rust reference | | --- | --- | --- | | Choose imports and start an application | [First application](https://rullst.github.io/Rullst/book/tutorials/01-hello-world.html) | [rullst facade](https://docs.rs/rullst/13.0.0-alpha.4/rullst/) | | Route requests and compose middleware | [Routing](https://rullst.github.io/Rullst/book/tutorials/08-routing-and-middlewares.html), [Core](https://rullst.github.io/Rullst/book/crates/core.html) | [rullst-core](https://docs.rs/rullst-core/13.0.0-alpha.4/rullst_core/) | | Query data and manage persistence | [ORM](https://rullst.github.io/Rullst/book/crates/orm.html), [CRUD](https://rullst.github.io/Rullst/book/tutorials/03-active-record-crud.html), [backend boundaries](https://rullst.github.io/Rullst/book/polyglot-persistence.html) | [rullst-orm](https://docs.rs/rullst-orm/13.0.0-alpha.4/rullst_orm/) | | Authenticate users and authorize access | [Auth](https://rullst.github.io/Rullst/book/crates/auth.html), [ownership and roles](https://rullst.github.io/Rullst/book/tutorials/13-rbac-authorization.html) | [rullst-auth](https://docs.rs/rullst-auth/13.0.0-alpha.4/rullst_auth/) | | Compose request defenses | [Security](https://rullst.github.io/Rullst/book/crates/security.html), [security architecture](https://rullst.github.io/Rullst/book/security-architecture.html) | [rullst-security](https://docs.rs/rullst-security/13.0.0-alpha.4/rullst_security/) | | Integrate social sign-in | [Connect](https://rullst.github.io/Rullst/book/crates/connect.html) | [rullst-connect](https://docs.rs/rullst-connect/13.0.0-alpha.4/rullst_connect/) | | Integrate payments and signed webhooks | [Capital](https://rullst.github.io/Rullst/book/crates/capital.html), [billing tutorial](https://rullst.github.io/Rullst/book/tutorials/19-saas-billing-capital.html) | [rullst-capital](https://docs.rs/rullst-capital/13.0.0-alpha.4/rullst_capital/) | | Call local or cloud language models | [AI](https://rullst.github.io/Rullst/book/crates/ai.html) | [rullst-ai](https://docs.rs/rullst-ai/13.0.0-alpha.4/rullst_ai/) | | Send transactional email | [Mail](https://rullst.github.io/Rullst/book/crates/mail.html) | [rullst-mail](https://docs.rs/rullst-mail/13.0.0-alpha.4/rullst_mail/) | | Publish and consume messages | [Messaging](https://rullst.github.io/Rullst/book/crates/messaging.html) | [rullst-messaging](https://docs.rs/rullst-messaging/13.0.0-alpha.4/rullst_messaging/) | | Inspect the local app or compose an admin interface | [Studio](https://rullst.github.io/Rullst/book/crates/studio.html), [Nexus](https://rullst.github.io/Rullst/book/crates/nexus.html) | [rullst-studio](https://docs.rs/rullst-studio/13.0.0-alpha.4/rullst_studio/), [rullst-nexus](https://docs.rs/rullst-nexus/13.0.0-alpha.4/rullst_nexus/) | | Generate code or prepare a framework upgrade | [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html), [assisted upgrades](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html) | [cargo-rullst](https://docs.rs/cargo-rullst/13.0.0-alpha.4/cargo_rullst/) | Search within the selected crate's rustdoc page for an exact symbol. Read its module context and feature requirements as well as its signature. A facade re-export and a direct dependency can expose different features; enabling a feature does not configure credentials, an external service or authorization. For your application's selected feature set, `cargo doc --no-deps --open` builds its local documentation; remove `--no-deps` to include dependency APIs. Run Cargo commands only in a trusted checkout: build scripts and procedural macros can execute code during documentation builds too. ## HTTP/REST: a connected learning path 1. [Return typed JSON](https://rullst.github.io/Rullst/book/tutorials/rest-api-quickstart.html). Run the endpoint and inspect its status, headers and body with curl. 2. [Compose routing and middleware](https://rullst.github.io/Rullst/book/tutorials/08-routing-and-middlewares.html). Understand which layer receives or rejects a request first. 3. [Validate input](https://rullst.github.io/Rullst/book/tutorials/07-forms-and-validation.html). This guide is form-oriented; its validation principles do not turn it into a complete JSON validation/error-handling tutorial. Define the JSON error contract explicitly. 4. [Persist records](https://rullst.github.io/Rullst/book/tutorials/03-active-record-crud.html) and [manage migrations](https://rullst.github.io/Rullst/book/tutorials/05-migrations-and-seeds.html). Design bounded pagination and test failed writes as well as successful reads. 5. Choose [session or token authentication](https://rullst.github.io/Rullst/book/tutorials/12-jwt-and-session-auth.html) and enforce [ownership/role authorization](https://rullst.github.io/Rullst/book/tutorials/13-rbac-authorization.html). Authentication alone must not grant access to another user's records. 6. [Expose a reviewed OpenAPI specification](https://rullst.github.io/Rullst/book/tutorials/24-scalar-api-docs.html). The viewer does not infer a complete specification or security policy from arbitrary handlers. Read its CSP, asset and missing-specification boundaries. 7. Review [deployment and security boundaries](https://rullst.github.io/Rullst/book/security-architecture.html) and [the compatibility policy](https://rullst.github.io/Rullst/book/compatibility-policy.html) before exposing the app. For your own API, test malformed and oversized input, authentication failure, cross-owner/tenant denial, missing records, bounded pagination and error responses that do not expose credentials or database details. Public demos and a compiling example are not substitutes for these application tests. The v13 prerelease [schema-first API candidate](https://rullst.github.io/Rullst/book/typed-api.html) adds an explicit bounded OpenAPI profile, generated Rust codecs and a compiled TypeScript HTTP consumer. Its authenticated fixture proves the stated wire/ownership boundary; it does not close the broader CRUD/documentation programme below. ## What still needs improvement The existing chapters are useful but are not yet one complete, consistently tested REST product walkthrough. A coherent CRUD example with JSON validation, typed error responses, pagination, authorization negatives and matching OpenAPI is still needed. Per-symbol reference reviews also need explicit feature, error, concurrency and migration coverage. The [API documentation plan](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#api-documentation-quality) tracks that work separately from this navigation index. --- # Why Rullst? Source: https://rullst.github.io/Rullst/book/why-Rullst.html # Why Rullst? Rullst is an opinionated, Axum-based full-stack framework for teams that want the productivity of a coordinated application platform without hiding Rust's types or the underlying ecosystem. Its strongest distinction is not that every individual feature is unique. It is the combination of compile-time-oriented APIs, explicit security boundaries, first-party backend capabilities, offline development contracts, and one coordinated CLI and release train. > **Version status:** the Rullst v12 release line reached end of life on > 16 October 2026; v13 is in prerelease (`13.0.0-alpha.4`, installed with an > exact version as [Getting Started](https://rullst.github.io/Rullst/book/1-getting-started.html) shows). This page > lists implemented or explicitly bounded capabilities; > it is not a production-readiness certificate. The > [framework specification](https://rullst.github.io/Rullst/book/spec.html) and > [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html) hold the detailed contracts; where > they and this summary disagree, they win. ## The short answer Choose Rullst when you want to build a backend-oriented Rust application with: - an explicit Axum, Tokio, Tower, and SQLx foundation; - compile-time-generated routes, HTML, and models, with compiler-visible diagnostics instead of runtime reflection; - a coordinated ORM, authentication, security, jobs, mail, AI, billing, administration, observability, and CLI toolchain; - secure, fail-closed defaults for privileged and production-facing surfaces; - deterministic offline paths for supported external-provider integrations; - server-rendered interfaces that do not require a project-local SPA bundle; - standard escape hatches when a framework abstraction is not the right fit. ## What makes the combination distinctive ### 1. AI-native means explicit and inspectable Rullst is designed so that both humans and coding agents can reason about an application from its source. Common routes, models, HTML trees, policies, and scaffolds are represented by typed Rust or macro input rather than runtime class scanning or hidden reflection. This does not make generated code automatically correct. It makes important structure available to the compiler, review tools, and the CLI. Macro diagnostics, compile-fail tests, generated-project gates, and the AST-based IDOR scanner turn that structure into evidence. The AI layer follows the same rule. Its bounded RAG pipeline makes tenant context, retrieval, context limits, source metadata, and audit explicit in one typed operation. It rejects differently tagged or unsafe passages and refuses ungrounded generation when retrieval is empty. The included cosine retriever is clearly process-local; production datastore authorization and durability are not hidden behind an “automatic AI” claim. Conversational memory follows that explicit model too: a static-dispatch tenant-bound contract has a bounded offline store and an opt-in SQL adapter for SQLite, PostgreSQL, MySQL, and MariaDB. Each successful turn commits the user and assistant messages atomically, while revision compare-and-swap rejects stale cross-process writers instead of silently scrambling history or automatically repeating a billable model request. ### 2. A broad platform that keeps its foundations visible Rullst coordinates a large backend surface in one versioned workspace, but it does not replace its foundations with proprietary runtime primitives: - applications can mount ordinary `axum::Router` values; - middleware remains compatible with Tower composition; - Tokio remains the asynchronous runtime; - SQLx pools and raw parameterized queries remain available beside the ORM. The [Axum and SQLx interoperability guide](https://rullst.github.io/Rullst/book/axum-sqlx-migration.html) documents the supported escape hatches. Framework conveniences can still require migration work, so Rullst describes this as reduced lock-in rather than "zero lock-in." ### 3. Security boundaries are part of framework design The framework treats authorization and privileged tooling as architectural inputs rather than deployment footnotes. Current bounded contracts include: - fail-closed production environment and secret validation; - CSRF, secure headers, request heuristics, DLP, abuse controls, and security telemetry primitives; - route-scoped JSON Schema 2020-12 or OpenAPI 3.1-component enforcement with bounded offline compilation, local-only references and linear-time regexes; - explainable aggregate threat assessment plus an opt-in authenticated, subject-bound, expiring and locally one-shot proof-of-work gate; - Argon2id password hashing, expiring encrypted sessions, RBAC, model policies, and explicit owner-or-role guards; - a typed OAuth/OIDC session transaction that keeps PKCE verifiers and OIDC nonces server-side, expires them after ten minutes, and consumes them before callback validation; - a provider-generic automatic token coordinator that binds refreshes to the original user, redacts credentials, detects a bounded expiry window and prevents overlapping provider calls while waiters reuse a valid transition; - an authenticated Nexus admin surface and a loopback/debug-constrained Studio; - one bounded signed-payment-webhook verifier exposed through both Axum and opt-in Actix middleware adapters; - versioned AES-256-GCM ORM field encryption with authenticated context; - CI gates against `panic!`, `unwrap()`, and `expect()` in declared production targets. These are defense-in-depth controls, not a claim that an application is secure without its own authorization model, deployment controls, reviews, and tests. The Sentinel is deliberately deterministic rather than marketed as autonomous AI: the host still owns aggregate collection, identity, accessible fallback, distributed replay state and enforcement. See the [security architecture](https://rullst.github.io/Rullst/book/security-architecture.html) and [v12 security claims](https://rullst.github.io/Rullst/book/v12-security-claims.html). ### 4. Persistence depth without a fictional universal database API The relational ORM provides generated Active Record and query APIs, transactions, migrations, relations, tenant scopes, policies, soft deletion, auditing, encrypted fields, typed pgvector helpers, structured telemetry, and an opt-in transactional outbox. The primary relational matrix covers SQLite, PostgreSQL, MySQL, and MariaDB. Turso/libSQL has a separate typed primary profile. MongoDB, DuckDB, SurrealDB, Qdrant, Redis, and external search engines use capability-specific adapters so their different consistency and query models stay visible. MongoDB and SurrealDB share only a narrow identifier-preserving recovery boundary: a bounded encrypted snapshot can be rehearsed across both, while writer quiescence, schema provisioning, key custody and durable backup remain explicit application/operator work. This is deliberate: Rullst prefers several honest, bounded APIs over one API that pretends documents, OLAP, graphs, vectors, key-value structures, and relational transactions have identical semantics. Read the [polyglot persistence](https://rullst.github.io/Rullst/book/polyglot-persistence.html), [transactional outbox](https://rullst.github.io/Rullst/book/tutorials/38-transactional-outbox.html), and [Scout search](https://rullst.github.io/Rullst/book/tutorials/39-scout-search.html) guides. The same boundary discipline now applies to messaging. `rullst-messaging` defines one versioned envelope, bounded topic/group metadata, idempotent publication, consumer groups, expiring acknowledgement leases, retry and dead-letter semantics, with a deterministic concurrent in-memory contract. A canonical bounded v1 envelope codec and allowlisted W3C trace-context boundary make byte compatibility and correlation behavior executable. The codec still does not open a remote connection or map broker-specific semantics. SQLite can explicitly protect header values and payloads with row-bound AES-256-GCM and a bounded rotation keyring while keeping visible metadata and operator duties honest. The opt-in relational outbox relay also proves its unavoidable publish-before-ACK crash window as an exact idempotent replay instead of calling two systems one atomic transaction. That makes offline tests and future adapter conformance explicit without pretending the local broker is Kafka, RabbitMQ, NATS, Redis Streams, or a cloud service. Applications still own topic authorization and idempotency of remote side effects. See [Bounded Brokered Messaging](https://rullst.github.io/Rullst/book/tutorials/49-brokered-messaging.html). ### 5. External services remain usable during offline development Supported AI, OAuth, mail, billing, search, and persistence adapters expose deterministic offline behavior when configured with their documented empty or `mock_*` credentials. This lets generated projects, tests, examples, and local sandboxes run without silently contacting a third party. Mail scheduling follows the same bounded approach: SQLite and Redis persist a due time and never claim it early, while unsupported real direct transports reject a future message instead of delivering it immediately. Offline fixtures may retain that timestamp for assertions. Polling delay, at-least-once delivery, and provider acceptance remain visible operational boundaries. The optional native AWS SES v2 path follows the provider's real authentication model instead of disguising a bearer request as AWS support. It delegates regional SigV4 and credential-provider rotation to the official AWS SDK while retaining the same pre-flight, HTML/text, attachment/CID and RFC 8058 message contract. Its local signed-request fixture proves integration shape, not AWS account readiness, reputation or inbox delivery. Mocks prove the local application contract, not the live provider contract. Provider-specific production support still requires the applicable live or protocol tests, credentials, policy, and operational evidence. ### 6. Framework upgrades are a product feature `cargo rullst upgrade --dry-run` inventories a project and reports a versioned migration plan. `cargo rullst upgrade` snapshots controlled files, updates the coordinated Rullst dependency train, applies supported compiler fixes, runs a Cargo check gate, and restores those files when the gate fails. It intentionally refuses to invent database migrations, business rules, or security policy. The useful distinction is a bounded, recoverable upgrade transaction with human and machine-readable reports. See the [assisted upgrade tutorial](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html). ### 7. Server-first UI with optional escalation The compile-time `html!` macro escapes supported dynamic text and attribute values and returns `Markup`, so views nest without re-escaping, while `RawHtml` makes an unescaped boundary for trusted outside HTML explicit. HTMX-oriented server rendering is the default path for applications that do not need a project-local SPA bundle. LiveView and selected Wasm islands are optional strategies with separate maturity and operational requirements, and a template engine such as Tera is the application's own choice. Rullst does not claim that every frontend mode is interchangeable or bundle-free. The same server-rendered application becomes installable without a second codebase: `cargo rullst make:pwa` adds a web app manifest, a service worker with an offline page and an update prompt that work under the default Content-Security-Policy, and never keeps `no-store` or private responses on the device. The same site can be packaged for Google Play as a Trusted Web Activity and for the Microsoft Store with PWABuilder; iOS users add it to the home screen from Safari. Store review, signing keys and device testing remain the application's work; see the [PWA guide](https://rullst.github.io/Rullst/book/tutorials/43-progressive-web-apps.html). ### 8. Local control surfaces use real application signals Studio is a local developer control room with explicit unavailable states when a probe is not connected. Nexus generates an authenticated administration surface from registered model metadata and applies server-side field policy. Tenant-owned models can explicitly bind every built-in CRUD/search/batch path to a trusted Core tenant context, while an opt-in required audit row shares the mutation transaction and rolls it back on audit failure. The host still owns identity/membership and immutable external evidence; Nexus does not disguise a same-database mutable table as an append-only audit service. Studio's relational browser can also perform deliberately narrow primitive row edits/deletions: the write surface exists only behind the verified local request capability, binds values, identifies one row by its inspected complete primary key and runs against four relational engines. It is not presented as a replacement for application tenant authorization or production administration. Studio flag toggles also invalidate already-warm database flag drivers in the same process without an unbounded key registry; cross-process invalidation remains an explicit application transport. The queue monitor follows an explicit retention rule as well. SQLite removes successful payloads by default, while applications that need operational history can opt into a validated, atomically pruned limit and purge it from Studio. Rullst does not silently trade privacy for a more impressive dashboard. The ORM emits secret-free structured spans for generated and raw query entrypoints, streams, transaction outcomes, and Rullst-owned pool acquisition timing. The host owns subscriber initialization, sampling, collector security, and retention. See the [telemetry guide](https://rullst.github.io/Rullst/book/telemetry-guide.html). ### 9. Scaffolding is treated as shipped code The CLI can generate projects, routes, models, migrations, auth, mail, billing, deployment manifests, and other bounded application slices. Rullst tests representative generated projects and maintains structural matrices instead of assuming that a template is correct because its source file compiles inside the CLI crate. The LMS example (`examples/lms`, the CLI's LMS starter up to 13.0.0-alpha.3) is more than a landing-page mock: it has a course catalog with modules and lessons, sign-in and registration, enrollment, owner-bound idempotent lesson progress and a Nexus admin. Its accessible lesson presentation supports bounded video/audio metadata, requires a WebVTT captions track for video and renders escaped transcripts. In v13 this small application replaces the complete Academy scaffold that v12 generated (assessment, scores, leaderboard, automation, notifications and school boundaries). It is a foundation for a learning product, not a claim that Rullst generates pedagogy, content, speech recognition or native-device behavior. For example, `make:billing --model Workspace` now materializes distinct SQLx and Turso-primary persistence profiles. Both are generated, linted, migrated and exercised through subscription ownership and collision negatives; the test does not turn provider sandbox validation or distributed reconciliation into an automatic claim. The billing facade follows the same bounded design: an explicit generic subscription handle delegates pause/cancel without erasing the provider type, while its grace-period value validates time bounds but leaves persistence, authorization and entitlement policy visible in application code. Its shared-quota boundary likewise derives one subject from trusted tenant state, reserves idempotently before creation and can commit the counter and a relational domain insert in the same caller-owned transaction. SQLite plus live PostgreSQL/MySQL/MariaDB contention contracts prove that concurrent workspace members stop at the configured limit; membership and tier reconciliation remain visible host responsibilities. Coupon and trial management follows the same evidence boundary: coupon IDs are bounded/redacted, Stripe binds the expanded applied discount, and 1–730-day relative trial updates have stable explicit-clock retries plus bound Stripe protocol fixtures. Providers without a reviewed live operation fail explicitly instead of presenting a local no-op as remote success. The same CLI includes inspection, toolchain diagnostics, migration assistance, SBOM generation, and a bounded static route-access scanner. Generated output remains application code: review it, test it, and keep it under version control. ### 10. Ambition is separated from evidence Rullst keeps ambitious ideas, but labels them as implemented, partial, experimental, offline mock, roadmap, or not recommended. Benchmarks are reported for their measured workload instead of becoming universal performance slogans. Hardware, provider, regulatory, and certification claims remain incomplete until the corresponding external evidence exists. This distinction is essential for a broad framework. The [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html) records the boundary. Dated comparative claims belong in reproducible, sourced research rather than in the active product documentation. ## Where Rullst is a particularly good fit Rullst is worth evaluating for: - backend-heavy SaaS and multi-tenant applications; - server-rendered products that still need jobs, realtime, mail, billing, and administration; - Rust teams that prefer one coordinated release train over assembling every application subsystem independently; - projects that need relational data plus explicit document, analytics, search, vector, graph, or key-value capabilities; - organizations that value offline reproducibility, secure scaffolds, and machine-readable engineering evidence; - teams using coding agents that benefit from explicit APIs, bounded files, strong types, and a normative specification. ## When another choice may be better Rullst may not be the best choice when: - only a small HTTP router or middleware library is needed; - a browser-component ecosystem is the primary product architecture; - long production history, a very large plugin ecosystem, or commercial support matters more than Rust-native integration; - the application requires a capability that the ledger still marks partial, experimental, or roadmap; - the team does not want the maintenance surface of a batteries-included framework. In those cases, using Axum directly, another Rust application framework, a frontend-centered framework, or a mature platform in another ecosystem can be the more responsible decision. ## How to evaluate Rullst today 1. Start with [Zero to Hello Rullst](https://rullst.github.io/Rullst/book/tutorials/01-hello-world.html) or [Zero to a complete app](https://rullst.github.io/Rullst/book/tutorials/zero-to-complete-app.html). 2. Check the exact capability you need in the [specification](https://rullst.github.io/Rullst/book/spec.html) and [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html). 3. Enable only the required Cargo features and inspect their dependency graph. 4. Run the documented tests against your database, provider, proxy, and threat model. 5. Pin an immutable release or commit for evaluation; do not deploy from a moving development branch. This page describes the v13 prerelease line (`13.0.0-alpha.4`). It remains a concise map of the strongest implemented ideas, not a substitute for the detailed contracts or application-specific validation. --- # The Rullst Philosophy Source: https://rullst.github.io/Rullst/book/philosophy.html # The Rullst philosophy Rullst exists to make ambitious software feel possible in Rust without hiding the language, the generated code, or the boundaries that keep an application safe. It aims to be productive and broad, but its convenience must remain inspectable: static dispatch, compile-time generation, typed errors, explicit middleware, and ordinary Rust escape hatches are deliberate choices. ## A story that stayed with us A widely reported account of Rust's origin begins with a broken elevator in Graydon Hoare's apartment building. Its software had crashed, leaving him to climb the stairs. Rust itself began as Hoare's personal project in 2006 and grew into a language designed to combine systems-level performance with much stronger memory-safety guarantees. The elevator account is an origin story reported by [MIT Technology Review](https://www.technologyreview.com/2023/02/14/1067869/rust-worlds-fastest-growing-programming-language/), not a claim that every elevator failure was a memory bug or that Rust can make all software infallible. The lesson Rullst takes from it is narrower and more useful: when an entire class of failure can be prevented by design, prevention is better than asking every developer to rediscover the same danger. ## Why Venelouis started Rullst Rullst also has its own practical origin. Long before modern AI systems became capable coding partners, Venelouis wanted to build an education platform. He first learned what was possible by operating a Moodle installation on a VPS. Later, Laravel and AI helped him create a far more ambitious education product than he had previously believed he could build. That experience made Laravel's greatest strength tangible: a framework can let one person concentrate on the product instead of repeatedly assembling its foundations. Venelouis then wanted to rebuild that kind of product in Rust to use fewer resources, gain performance, and inherit Rust's stronger compile-time guarantees. He could not find a Rust framework that combined the particular Laravel-like product workflow, breadth, approachability, and explicit security boundaries he wanted. Rullst began as an attempt to build that missing bridge. Rullst is not a Laravel clone, and Rust should not be forced to behave like PHP. The inspiration is the feeling that a complete product is within reach; the implementation follows Rust's strengths rather than concealing them. ## Why Rullst jumps from version 5 to version 12 The version jump records the history of the ecosystem rather than six hidden major releases of the unified framework. When Venelouis began recreating `creio.eu` in Rust with AI assistance, the Gemini model he was using at the time told him that there was no direct Rust counterpart to Laravel Socialite for the workflow he needed. Instead of abandoning that part of the product, he created the first independent Rullst crate. That project became `rullst-connect`, which still carries the identity and social-login mission of that original work. The crates initially evolved in separate repositories and at different speeds. By the time the umbrella Rullst framework was at version 5, `rullst-connect` had already reached version 11. Bringing the projects into one monorepo made their dependency changes, cross-crate compatibility tests, and coordinated releases easier to maintain. Aligning every publishable package on one version line was the natural next step. A clean reset to 1.0 might have described the beginning of this unified era, but a registry history cannot be reset that way: crates.io does not permit a published version to be overwritten or removed, and lower new versions would not erase the already published higher ones. Rullst therefore advances the whole ecosystem directly to version 12, monotonically beyond Connect's version 11. From the version 12 line onward, coordinated releases can follow the usual Semantic Versioning sequence. The jump is thus an act of package alignment and honest continuity, not a claim that standalone Rullst versions 6 through 11 were released as complete framework generations. ## Core tenets 1. **Productive, not magical.** Rullst coordinates routing, data, identity, security helpers, background work, AI, and developer tooling through APIs and generated source that users can inspect and replace. 2. **Simple to begin, explicit when it matters.** Good defaults should remove repetitive setup. Authorization, tenancy, provider behavior, persistence, deployment, and recovery must still expose their real application-owned decisions. 3. **Prevent failure classes by design.** Typed errors, bounded inputs, parameterized queries, compile-time generation, and fail-closed behavior are preferred to conventions that only work when every caller remembers them. This reduces risk; it does not create a universal security guarantee. 4. **Built for humans and AIs.** Stable vocabulary, focused modules, static contracts, executable examples, and documented limits give human developers and coding agents a shared map of the system. AI assistance never replaces review, testing, or accountable approval. 5. **Evidence before claims.** A feature claim must name its implemented scope and residual boundary. A test belongs to the revision and environment it exercised. Benchmarks describe measured workloads, not universal rankings. 6. **Emotional productivity matters.** The framework should make builders feel capable, curious, and supported. Removing boilerplate is valuable when it leaves more attention for users, learning, and the purpose of the product. ## The ambition and the boundary The long-term ambition is deliberately large: make Rullst a foundation from which people can build many kinds of applications without surrendering Rust's performance or explicitness. That ambition is a direction, not a promise that one framework can finish every product, operate every provider, or replace the judgment of its developers. Rullst will therefore grow through bounded, reviewable capabilities. Where the repository has proof, the documentation should show it. Where real devices, provider accounts, production operations, independent review, or application policy are still required, the documentation should say so plainly. The goal is not to look complete. It is to keep becoming more useful without losing the trust of the people building with it. --- # Simple Capability Status Source: https://rullst.github.io/Rullst/book/capability-status.html # Simple capability status This is the compact view of Rullst's canonical M1–M41 programme. It is derived from the root [ROADMAP](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md); that roadmap and the [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html) retain the evidence and limitations. The labels here deliberately do not turn partial foundations into completed features. Package stability is a separate question: the [maturity tiers](https://rullst.github.io/Rullst/book/maturity.html) classify every crate as Core, Extension or Experimental and state what each tier promises within 13.x. ## v12 stable snapshot — 15 September 2026 Stable `12.0.0` was published from commit `eb11f892`. See the [release record](https://rullst.github.io/Rullst/book/v12.html) for the immutable tag and publication receipt. The v12 line reached end of life on 16 October 2026 (12.3.0 was its last release); this section keeps the record of its stable release. The [Rust CI scorecard](https://github.com/Rullst/Rullst/actions/runs/34895536764) for that commit awarded **94/100 (A)** to the repository, with all fifteen non-IoT packages at A and IoT at its approved B exception. These are repository-owned audit scores constrained by test results, not a percentage of all planned functionality or an independent certification. The [quality scorecard](https://rullst.github.io/Rullst/book/quality-scorecard.html) explains the scale. The [stable coverage run](https://github.com/Rullst/Rullst/actions/runs/34895523751) recorded the following local LLVM/LCOV gates before the Codecov upload: | Coverage view | Stable-source measurement | Required floor | | :--- | :--- | :--- | | Whole repository | **90.3220%** (79,813/88,365 lines) | **90%** | | Framework libraries | **90.6186%** (60,014/66,227 lines across 436 files) | **90%** | These are distinct path sets, not values to average. The public Codecov badge tracks the current branch and may change as its source and report processing change. Do not relabel an older checkpoint as the latest measurement. The [release audit](https://rullst.github.io/Rullst/book/v12-release-audit.html) links the stable platform, property, Miri, Kani, fuzzing, sanitizer and publication results. New maintenance commits must satisfy their applicable gates; the published release cannot certify future source changes. ## `rullst` facade versus `rullst-core` | Package | Role | Typical user choice | | :--- | :--- | :--- | | `rullst-core` | Low-level runtime engine: HTTP server, routes, lifecycle, queue/realtime, storage/cache and the default browser-security baseline. It deliberately does not aggregate every domain crate. | Use directly when a library/application wants only the runtime primitives and explicit dependencies. | | `rullst` | Ergonomic umbrella facade. Cargo features re-export Core plus selected ORM, Auth, Connect, Security, AI, Mail, Capital, Studio, Nexus, Messaging and IoT APIs (and, from the 13.0.0-alpha.1 prerelease, Privacy) through one dependency. It also exposes the browser/WASM surface used by web-first applications; PWA packaging (`make:pwa`) is a CLI workflow, not a re-exported crate. Its maturity cannot exceed the crates selected underneath it. | Use for most Rullst applications and enable only the required features. | ## Documentation maintenance gate Stable v12 completed the documentation release gate: the mdBook and Rust snippets sourced from public tutorials built, local links and anchors were validated, commands/features/version examples were reconciled with the frozen manifests, and upgrade/provider boundaries were reviewed. v13 changes must keep those checks green: the Documentation workflow builds the book, checks `llms.txt` and validates local links and anchors, and the guides listed in `rullst/src/book_doctests.rs` compile as doctests. A green documentation build proves structural consistency, not that every external service or store workflow was homologated. ## Canonical milestones | ID | Capability | Simple status | | :---: | :--- | :--- | | M1 | CLI and `make:*` generator matrix | 🟡 Still to implement — partial | | M2 | Fast linkers and measured build-time improvements | 🟡 Still to implement — partial | | M3 | Escape hatches, granular features, diagnostics, and ejection | 🟡 Still to implement — partial | | M4 | `make:resource` and local error console | ✅ Implemented — scoped | | M5 | mdBook, OpenAPI, and typed client generation | 🟡 Still to implement — partial | | M6 | ORM parity and Turso/libSQL profile | 🟡 Still to implement — partial | | M7 | Portable edge runtime, distributed data, and safe upgrades | 🟡 Still to implement — partial | | M8 | Explain-and-approve index recommendations | ⏳ Still to implement — not started | | M9 | Local auth, OAuth/OIDC, TOTP, passkeys, and WebAuthn | 🟡 Still to implement — partial | | M10 | Mail, DTO validation, distributed rate limits, and Shield | 🟡 Still to implement — partial | | M11 | Nexus, billing, and entitlements (Omni was retired in 13.0.0-alpha.3) | 🟡 Still to implement — partial | | M12 | Defence-in-depth security programme | 🟡 Still to implement — continuous/partial | | M13 | Audited PQC protocols and sandboxed Wasm extensions | ⏳ Still to implement — not started | | M14 | HTMX-first SSR and real Leptos/Dioxus interoperability | 🟡 Still to implement — partial | | M15 | Runtime queues/cache/scheduler plus brokered messaging | 🟡 Still to implement — local foundation and admitted standalone Redis profile; other adapters open | | M16 | Wasm islands and `#[client_component]` protocol | 🟡 Still to implement — partial | | M17 | Realtime, object storage, media, and packages | 🟡 Still to implement — partial | | M18 | LiveView-style server-driven UI | 🟡 Still to implement — partial | | M19 | Radar, agent schemas, spans, and Prometheus | ✅ Implemented — bounded | | M20 | Persistent event stream and verifiable ledger semantics | ⏳ Still to implement — not started | | M21 | Omni frontend protocol and mobile bridge | 🟡 Still to implement — partial; 13.0.0-alpha.3 replaced the Tauri shells with installable PWAs (`make:pwa`) | | M22 | Human-reviewed agentic DevOps recommendations | 🟡 Still to implement — partial | | M23 | Diagnostic auto-healing recommendations | 🟡 Still to implement — partial | | M24 | `no_std` IoT frames/packet encoders, signed OTA gate, and durable-counter CAS boundary | 🟡 Still to implement — partial | | M25 | Embassy-based async embedded integration | ⏳ Still to implement — not started | | M26 | Guided deployment (`cargo rullst deploy` targets a Linux VM; the PaaS targets were removed in 13.0.0-alpha.4) | 🟡 Still to implement — partial | | M27 | Kubernetes scaffolding and health/readiness probes | ✅ Implemented — scaffolding scope | | M28 | Compile-time DI and `Inject` | ✅ Implemented — foundation | | M29 | Scalar playground and complete OpenAPI generation | 🟡 Still to implement — partial | | M30 | Tonic/gRPC and Protobuf support | ⏳ Not implemented — the `make:grpc` generator was removed in 13.0.0-alpha.4; applications use their own `tonic` setup | | M31 | Aerospace/autonomous/defence systems | ⏳ Separate safety-critical programme; outside the general framework suite | | M32 | Axum/Tower escape hatches and proc-macro diagnostics | ✅ Implemented — bounded | | M33 | Server-side declarative SaaS entitlements | 🟡 v13 prerelease foundation — typed current-state plan authorization with an authenticated SaaS consumer; the declarative `#[rullst::gate]`/`GateGuard` model remains open | | M34 | Schema-driven TypeScript/React/Dart/Swift SDKs | 🟡 v13 prerelease foundation — one bounded OpenAPI 3.1 profile generates Rust codecs and a strict TypeScript client; React, Dart and Swift remain open | | M35 | Distributed OpenTelemetry waterfall in Studio | 🟡 Still to implement — partial | | M36 | Read-only explainable natural-language SQL assistant | ⏳ Still to implement — not started | | M37 | Reviewable one-click error-console patch workflow | 🟡 v13 prerelease preview — the error page records a bounded panic context, makes no network request and hands the error to the developer's own assistant through a copyable prompt and the recorded-context path (`cargo rullst ai fix` was removed in 13.0.0-alpha.4); a one-click flow and recording in the hot-reload mode remain open | | M38 | Vendor-specific SQLite replica/synchronization profile | ⏳ Still to implement — not started | | M39 | Optional self-hosted `rullst-gateway` load balancer | ⏳ Not implemented — abandoned on 8 October 2026 in favour of generated Caddy/nginx configuration; kept in the not-started count | | M40 | `rullst-labs` contracts with a bring-your-own runner | 🟡 v13 prerelease foundation — trusted job/lease/grading contracts and a documented runner contract; the `rullst-labs-runner` candidate was removed from 13.0, so execution is application-owned; full offensive CTF arenas require external isolated infrastructure | | M41 | Privacy controls and proportional age assurance | 🟡 v13 prerelease foundation — policy, signed evidence, asynchronous replay, SQLite and PostgreSQL adapters; combined hosted acceptance, consumer journeys and broader privacy work remain open | The labels match the milestone rows of the root roadmap: **5 implemented, 27 partial, and 8 not started** inside the 40-milestone framework programme. M31 is excluded because it is a separately governed safety-critical programme. The 35 milestones without strict closure have different sizes and overlap existing published capabilities; their count does not measure remaining engineering effort or release blockers. Assigning every partial row half credit would not establish a completion percentage. The [v12 stable record](https://rullst.github.io/Rullst/book/v12.html) preserves the completed release identity; new capability work goes to v13. The [safe update experience](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#safe-update-experience) is a top v13 priority; `cargo rullst upgrade` and the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html) are its current tools. The [26 September delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html) selects concrete increments from this wider programme and records their acceptance requirements. ## Claims that are impossible as framework guarantees No useful capability above is dismissed merely because it is difficult. The `impossible` label is reserved for absolute wording that code in this repository cannot honestly establish: | Absolute claim | Status | | :--- | :--- | | 100% uptime or zero data loss in every deployment | 🚫 Impossible as a framework guarantee | | Exactly-once arbitrary external side effects | 🚫 Impossible without destination-level idempotency/transactions | | Zero latency, zero overhead, zero allocations, or universal sub-100ms builds | 🚫 Impossible as a universal guarantee | | Total memory safety or universal panic-freedom across dependencies, FFI, generated apps, and every input | 🚫 Impossible to prove from this repository alone | | Automatic fiscal, security, privacy, App Store, or hardware certification | 🚫 Requires independent authorities, environments, and evidence | | Universal one-click zero-downtime deployment | 🚫 DNS, credentials, migrations, providers, and rollback remain operational inputs | | “Best/fastest/most secure framework in the world” | 🚫 Not a technical property without dated, reproducible comparative evidence | | Unattended production mutation that is always safe | 🚫 Approval, scoped authority, audit, recovery, and application policy cannot be removed | Use the [quality scorecard](https://rullst.github.io/Rullst/book/quality-scorecard.html) for per-commit engineering evidence. It is intentionally separate from this functionality view. --- # Maturity Tiers: Core, Extension, Experimental Source: https://rullst.github.io/Rullst/book/maturity.html # Maturity tiers **Decision recorded on 8 October 2026 for the v13 line, before `13.0.0-alpha.1`.** Every Rullst package, and every CLI generator whose result depends on an outside toolchain or platform, has one of three maturity tiers. A tier tells you how stable a package is and how much of it has been validated. It does not tell you whether a feature exists. For that, see the [capability status](https://rullst.github.io/Rullst/book/capability-status.html) and the [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html). | Tier | Who needs it | Promise in one line | | :--- | :--- | :--- | | **Core** | Every application | Supported: stable within 13.x, with the widest platform validation. | | **Extension** | Applications that opt in | Supported: the same 13.x stability promise, for the documented scope of each crate. | | **Experimental** | Applications that opt in and accept change | May change between 13.x releases and is not validated in every scenario. | > **Prerelease note.** Until stable `13.0.0` is published, every v13 package is > a prerelease and may change in any prerelease. `13.0.0-alpha.4` is the 13.0 > feature freeze: `13.0.0-rc.1` carries only fixes and documentation. The > promises below begin with `13.0.0`. [`SECURITY.md`](https://github.com/Rullst/Rullst/blob/main/SECURITY.md) lists > which published versions receive security fixes. ## What "supported" promises Core and Extension packages are **supported**. In practice, that means: - **Stability within 13.x.** Documented public Rust APIs, Cargo feature names, CLI commands and flags, configuration keys and serialized contracts follow the [compatibility policy](https://rullst.github.io/Rullst/book/compatibility-policy.html). A 13.x minor or patch release does not intentionally break them. A removal is deprecated first and happens in a later major release. The policy's security exception still applies: an unsound or unsafe API can be disabled sooner, with an advisory. - **Validation.** The documented scope runs in the repository's required checks: tests, strict Clippy, feature boundaries, the zero-panic and unsafe policies, dependency audits and package checks. The workspace runs on Linux for every pull request and on the full Linux, macOS and Windows matrix nightly; Core, the `rullst` facade and the macros also compile for WebAssembly. External services (payment, mail, AI and identity providers), real devices and production topologies count as validated only where the crate's own documentation says so. - **Support.** Bug reports are triaged and fixes ship in a 13.x release. A known regression in a Core package blocks the release that would contain it. An Extension regression is fixed or documented as a known issue before release. Security reports follow [`SECURITY.md`](https://github.com/Rullst/Rullst/blob/main/SECURITY.md). Rullst has a sole maintainer (see [`GOVERNANCE.md`](https://github.com/Rullst/Rullst/blob/main/GOVERNANCE.md)) and offers no paid support or response-time commitment beyond `SECURITY.md`. **Core vs Extension.** The difference is reach, not quality. Core packages are in every application's dependency graph, so they get the widest platform validation and the highest priority. Extensions are optional; you add them when your application needs them. Within an Extension, the documentation of an individual adapter or provider can give it a different label, and that label wins. For example, `rullst-capital` supports Stripe and marks InfinitePay as experimental. ## What "experimental" promises Experimental packages are optional and opt-in. **Experimental does not mean broken.** They pass the same gates as supported packages: tests, strict Clippy, the zero-panic and unsafe policies, dependency audits and, where applicable, fuzzing. What the label tells you: - **The API can still change.** Public APIs, Cargo features, storage schemas and generated output may change between 13.x releases, including in ways that stop your code from compiling. Each breaking change is listed in the changelog and in the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html). - **Some real-world scenarios are not validated yet.** Each experimental package lists its gaps in the table below and on its own page. Examples: physical hardware, live provider accounts, real devices and multi-host deployments. Tests cover the documented contract; they do not certify those environments. - **Support is best effort.** Bug reports and contributions are welcome, and security reports are handled under `SECURITY.md` like any other package. A security fix may change an experimental API. A non-security regression does not block a release. To use an experimental package today, pin an exact version in `Cargo.toml` and read the changelog before you upgrade. Check that the documented scope covers what your application needs. ## Packages | Package | Tier | Scope notes | | :--- | :--- | :--- | | `rullst-core` | Core | HTTP runtime, routing, lifecycle, queues/realtime and the default security baseline. | | `rullst-macros` | Core | `html!`, model and runtime macros. | | `rullst-orm` | Core | Relational models, migrations and transactions. Optional store adapters document their own validation boundaries. | | `rullst-orm-macros` | Core | Typed ORM code generation. | | `rullst-security` | Core | Defense-in-depth middleware, guards and audit helpers. | | `rullst` (facade) | Core | Its own API is Core. A feature that re-exports another crate has that crate's tier: `iot` and every `privacy` feature are experimental. | | `cargo-rullst` | Core | Some generators have their own label; see [CLI generators](https://rullst.github.io/Rullst/book/maturity.html#cli-generators). | | `rullst-auth` | Extension | Passwords, sessions, passkeys and authorization helpers. | | `rullst-connect` | Extension | OAuth2/OIDC identity integrations. | | `rullst-mail` | Extension | Transactional email and delivery controls; see the [crate page](https://rullst.github.io/Rullst/book/crates/mail.html) for the supported drivers. | | `rullst-capital` | Extension | Payments and billing: Stripe (supported) and InfinitePay (experimental). | | `rullst-ai` | Extension | Guarded LLM clients and retrieval; provider coverage is in the [AI provider matrix](https://rullst.github.io/Rullst/book/ai-provider-capabilities.html). | | `rullst-nexus` | Extension | Admin CMS for registered models. | | `rullst-studio` | Extension | Local developer control room. | | `rullst-messaging` | Extension | Broker-neutral messaging, durable local state and the [Redis Streams profile](https://rullst.github.io/Rullst/book/redis-messaging.html). | | `rullst-iot` | Experimental | Not validated on physical hardware and has no network transport. See the [module status](https://rullst.github.io/Rullst/book/crates/iot.html#module-status). | | `rullst-privacy` | Experimental | First published in the 13.0.0-alpha.1 prerelease. No live age providers or verified guardianship; see the [privacy roadmap](https://rullst.github.io/Rullst/book/privacy-age-assurance-roadmap.html). | | `rullst-supervision` | Experimental | The host owns capture, models and reviewer workflow. Read [Responsible use](https://rullst.github.io/Rullst/book/supervision.html#responsible-use) first. | | `rullst-media` | Experimental | Interoperability with real Bunny Stream accounts, transcoding and CDN delivery is unvalidated; the opt-in `s3` adapter (S3, R2, MinIO) is validated against the offline mock only. See the [managed-video candidate](https://rullst.github.io/Rullst/book/managed-video.html). | | `rullst-labs` | Experimental | In the 13.0 release inventory since `13.0.0-alpha.1`. Contracts only; it never executes learner code. Bring your own runner; see the [runner contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html). | ## CLI generators `cargo-rullst` is a Core package, and commands without a label here have the CLI's Core tier, within the boundaries described for each command in the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html). A command that the reference marks as a v13 preview or candidate is not yet in a stable release. A generator that adds an Extension or Experimental crate to your application cannot promise more than that crate. For example, `make:privacy` and `make:age-gate` generate consumers of the experimental `rullst-privacy`. | Generator | Commands | Tier | Why | | :--- | :--- | :--- | :--- | | IoT | `make:iot` | Experimental | Generates a telemetry module on top of the experimental `rullst-iot`. | | Green report | `green` | Experimental | Measured size, memory, dependency and benchmark CPU figures and a labelled SCI estimate; outside the pull-request mutation gate. See the [green guide](https://rullst.github.io/Rullst/book/green.html). | For an experimental generator, the generated files, their layout, and the command's flags may change between 13.x releases. Review the diff when you regenerate. 13.0.0-alpha.4 removed the experimental Foundry, Nix, Buildah and gRPC generators and the Fly.io, Railway and Render targets of `deploy`; see [Advanced deployment](https://rullst.github.io/Rullst/book/advanced-deployment.html) for those setups. It also removed the terminal AI assistant (`cargo rullst ai`; use your own assistant with the project's `AGENTS.md` and the site's [`llms.txt`](https://rullst.github.io/Rullst/llms.txt)) and renamed `footprint` to `green`. `cargo rullst new` offers three starters with the CLI's Core tier: Blank, API and SaaS. 13.0.0-alpha.4 moved the LMS, Blog, Portfolio and ERP starters out of the CLI; the LMS, ERP, Portfolio and Blog applications in `examples/` replace them. These [examples](https://rullst.github.io/Rullst/book/examples.html) are applications to copy from and carry no tier. ## How a tier changes - **Promotion** (Experimental to Extension) can happen in any 13.x minor release. It requires recorded evidence for the gaps listed above, an API expected to stay stable, complete documentation and a maintainer who commits to supporting it. The changelog and this page record each promotion. - **A supported package keeps its promise for the rest of 13.x.** Moving it to Experimental, or retiring it, is a major-release change. It is announced through deprecation, as the compatibility policy requires. - An experimental package may be redesigned, extracted or retired in a later release. Any such change appears in the changelog and migration guide. The [v13 maintenance scope](https://rullst.github.io/Rullst/book/v13-maintenance-scope.html) explains the investment decisions behind these tiers. For the two Core security crates, see [which security layer to use, and when](https://rullst.github.io/Rullst/book/security-layers.html). --- # Zero to a Complete App (one-hour path) Source: https://rullst.github.io/Rullst/book/tutorials/zero-to-complete-app.html # Zero to a complete app **What you will build:** *Notes*, a small web app where people register, sign in and keep private notes. Along the way you will add a model and its migration, CRUD pages with HTMX and form validation, routes that only a note's owner can open, tests, a security report, a development loop and a production build. Plan for about an hour, most of it waiting for the first build. This page is one path from an empty folder to a working app. Each step links to the tutorial that explains it in depth. > **How this page was recorded.** The commands and outputs come from a run on > 8 October 2026 with the 13.0 development CLI (`13.0.0-alpha.1`), SQLite and > Linux. On 29 October 2026 the steps that need no build were repeated with > `13.0.0-alpha.4`: creating the app, the two generators, the security report > and the IDOR check printed what this page shows, and the files under > `zero-to-complete-app/` follow that release's SaaS starter. The test list in > step 6 is taken from the alpha.4 sources; build times and sizes are from the > first run. Output is shortened with `…`, and your timestamps will differ. ## 1. Install Rust and the CLI Install Rust from [rustup.rs](https://rustup.rs/), then the v13 prerelease CLI. Cargo never selects a prerelease on its own, so name the exact version, as described in [Getting Started](https://rullst.github.io/Rullst/book/1-getting-started.html#1-installation): ```bash cargo install cargo-rullst --version =13.0.0-alpha.4 --locked ``` ## 2. Create the app from the SaaS starter The SaaS starter already has registration, sign-in, a session middleware, CSRF protection, security headers and Stripe billing that runs offline until you add real keys, so you can focus on your own feature. Create it with SQLite, which needs no database server: ```bash cargo rullst new notes --default --blueprint saas --database sqlite cd notes ``` The first build compiles every dependency and takes several minutes (see [Build times](https://rullst.github.io/Rullst/book/build-times.html) for measured figures). It ends with: ```text 📦 Bootstrapping Database... ✅ Database tables created successfully. ✅ Initialized a Git repository (.gitignore keeps .env and target/ out). ✨ Project 'notes' created successfully! Application profile: Zero-Bundle HTMX (html! SSR) ORM profile: Active Record Next steps 1 cd notes 2 cargo rullst dev builds, applies migrations and serves with live reload 3 open http://127.0.0.1:3000 your generated welcome page Prefer a live dashboard? cargo rullst dash · new to Rullst? cargo rullst tour ``` Commit the generated files now. The security report in step 7 reads the files Git tracks, and the generated `.gitignore` already leaves out `.env` and the SQLite database: ```bash git add -A git commit -m "Generated SaaS starter" ``` ## 3. Add a model with a migration ```bash cargo rullst make:model Note --migration ``` ```text 🛠️ Generating Rullst model: Note... ✨ Model 'Note' successfully created at 'src/models/note.rs'! ✨ Rust migration successfully created at 'src/migrations/m20261008071503_create_notes.rs'! … Next steps → cargo rullst db:migrate apply the new migration → cargo rullst make:controller Note serve the model over HTTP ``` Give the model its fields in `src/models/note.rs`. `user_id` records who wrote the note; it always comes from the session, never from the form. Keep the `snapshot` field the generator added: with it, `save()` of a loaded note writes only the columns that changed: ```rust,ignore use rullst::db::{FromRow, Orm, RowSnapshot}; #[derive(Debug, Clone, FromRow, Orm)] #[orm(table = "notes")] pub struct Note { pub id: i32, /// The id of the user who wrote the note; set from the session, never from the form. pub user_id: i32, pub title: String, pub body: String, // Remembers the loaded values, so `save()` writes only the columns this // record changed. Build new records with `snapshot: RowSnapshot::default()`. #[sqlx(skip)] pub snapshot: RowSnapshot, } ``` Add the same columns to the generated migration, `src/migrations/m_create_notes.rs`: ```rust,ignore use rullst::db::schema::{Schema, Migration}; use rullst::db::async_trait; pub struct MigrationImpl; #[async_trait] impl Migration for MigrationImpl { fn name(&self) -> &'static str { "m20261008071503_create_notes" } async fn up(&self) -> Result<(), rullst::orm::Error> { Schema::create("notes", |table| { table.id(); table.integer("user_id").not_null(); table.string("title").not_null(); table.string("body").not_null(); table.timestamps(); }).await } async fn down(&self) -> Result<(), rullst::orm::Error> { Schema::drop_if_exists("notes").await } } ``` Apply it: ```bash cargo rullst db:migrate ``` ```text ⏳ Running 'cargo run -- db:migrate'... … Migrating: m20261008071503_create_notes Migrated: m20261008071503_create_notes ``` More on models and schema changes: [Active Record CRUD](https://rullst.github.io/Rullst/book/tutorials/03-active-record-crud.html) and [migrations](https://rullst.github.io/Rullst/book/tutorials/05-migrations-and-seeds.html). ## 4. CRUD pages with HTMX and validation Generate the controller. Its form checks use the derive macro of the `validator` crate, which the starter already declares at the version Rullst uses, 0.21: ```bash cargo rullst make:controller Notes ``` Replace the generated placeholder in `src/controllers/notes_controller.rs` with the complete controller: ```rust,ignore use rullst::db::RowSnapshot; use rullst::html::Markup; use rullst::security::{CspNonce, CsrfToken}; use rullst::server::{Extension, IntoResponse, Path, Redirect, Response, StatusCode}; use rullst::{html, HtmxRequest, HtmxResponse, Validate, ValidatedForm}; use rullst_security::{RbacGuard, UserContext}; use serde::Deserialize; use crate::models::note::Note; /// The fields a user may submit. `user_id` is not here: it comes from the session. #[derive(Debug, Deserialize, Validate)] pub struct NoteForm { #[validate(length(min = 1, max = 120, message = "Give the note a title (1 to 120 characters)."))] pub title: String, #[validate(length(max = 5000, message = "Keep the note under 5000 characters."))] pub body: String, } /// Lets only the note's owner through. Another user's note answers 404, so /// its existence is not revealed. pub fn authorize(user_id: i32, note: &Note) -> Result<(), StatusCode> { let user = UserContext::new(user_id.to_string(), Vec::new()); RbacGuard::authorize_owner_or_role(&user, ¬e.user_id.to_string(), "admin") .map_err(|_| StatusCode::NOT_FOUND) } async fn find_owned(user_id: i32, id: i32) -> Result { let note = Note::find(id) .await .map_err(|_| StatusCode::SERVICE_UNAVAILABLE)? .ok_or(StatusCode::NOT_FOUND)?; authorize(user_id, ¬e)?; Ok(note) } /// After a successful write, HTMX gets an `HX-Redirect`; a plain form post a 303. fn back_to_list(htmx: &HtmxRequest) -> Response { if htmx.is_htmx { HtmxResponse::new("").redirect("/notes").into_response() } else { Redirect::to("/notes").into_response() } } fn page(nonce: &CspNonce, title: &str, content: Markup) -> Markup { html! { {title}
{content}
} } fn note_form(action: &str, csrf: &CsrfToken, title: &str, body: &str, button: &str) -> Markup { html! {
} } /// GET /notes: the signed-in user's notes and a form to add one. pub async fn index( Extension(user_id): Extension, Extension(csrf): Extension, Extension(nonce): Extension, ) -> Result { let notes = Note::query() .where_eq("user_id", user_id) .order_by_desc("id") .get() .await .map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?; let items: Markup = notes .iter() .map(|note| { let href = format!("/notes/{}", note.id); html! {
  • {note.title.as_str()}
  • } }) .collect(); let content = html! {

    "My notes"

      {items}

    "New note"

    {note_form("/notes", &csrf, "", "", "Add note")} }; Ok(page(&nonce, "My notes", content)) } /// POST /notes: validates the form, then saves a note owned by the current user. pub async fn store( htmx: HtmxRequest, Extension(user_id): Extension, ValidatedForm(form): ValidatedForm, ) -> Result { let mut note = Note { id: 0, user_id, title: form.title, body: form.body, snapshot: RowSnapshot::default(), }; note.save().await.map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?; Ok(back_to_list(&htmx)) } /// GET /notes/{id}: one note, with edit and delete forms. Owner only. pub async fn show( Path(id): Path, Extension(user_id): Extension, Extension(csrf): Extension, Extension(nonce): Extension, ) -> Result { let note = find_owned(user_id, id).await?; let action = format!("/notes/{id}"); let delete = format!("/notes/{id}/delete"); let content = html! {

    "← All notes"

    {note.title.as_str()}

    {note_form(&action, &csrf, ¬e.title, ¬e.body, "Save")}
    }; Ok(page(&nonce, ¬e.title, content)) } /// POST /notes/{id}: validates and saves changes. Owner only. pub async fn update( htmx: HtmxRequest, Path(id): Path, Extension(user_id): Extension, ValidatedForm(form): ValidatedForm, ) -> Result { let mut note = find_owned(user_id, id).await?; note.title = form.title; note.body = form.body; note.save().await.map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?; Ok(back_to_list(&htmx)) } /// POST /notes/{id}/delete: deletes the note. Owner only. pub async fn destroy( htmx: HtmxRequest, Path(id): Path, Extension(user_id): Extension, ) -> Result { let note = find_owned(user_id, id).await?; note.delete().await.map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?; Ok(back_to_list(&htmx)) } #[cfg(test)] mod tests { use super::*; fn note_of(user_id: i32) -> Note { Note { id: 1, user_id, title: "Groceries".into(), body: String::new(), snapshot: RowSnapshot::default(), } } #[test] fn only_the_owner_may_open_a_note() { assert_eq!(authorize(7, ¬e_of(7)), Ok(())); assert_eq!(authorize(8, ¬e_of(7)), Err(StatusCode::NOT_FOUND)); } #[test] fn an_empty_title_is_rejected() { let form = NoteForm { title: String::new(), body: "text".into() }; assert!(form.validate().is_err()); let form = NoteForm { title: "Groceries".into(), body: String::new() }; assert!(form.validate().is_ok()); } } ``` What it does: - `index` lists only the signed-in user's notes and renders a form. `store`, `update` and `destroy` save, change and delete notes; `show` opens one. - `ValidatedForm` rejects an empty or overlong title before the handler runs. For an HTMX request it answers with an error fragment, which `hx-target="#errors"` places under the form; the REST status travels in the `X-Rullst-Validation-Status` header. - After a successful write, HTMX receives an `HX-Redirect` back to the list, and a plain form post a `303` redirect, so the forms work without JavaScript too. - `html!` escapes the values it interpolates: a note titled `Buy ` is rendered as `Buy <milk>`. It returns `Markup`, so `note_form` and the collected list items are inserted into the page as HTML, and the handlers return the page directly. Now mount the routes in the `router()` function of `src/main.rs`. Add them **before** the `csrf_middleware` layer, so every form post needs the CSRF token the pages include, and wrap each one in the starter's `auth_middleware`: ```rust,ignore // Notes: signed-in users only. Each note belongs to the user who wrote it. .route("/notes", rullst::routing::get(controllers::notes_controller::index) .post(controllers::notes_controller::store) .layer(rullst::server::from_fn(middlewares::auth_middleware::auth_middleware))) // rullst-access: owner — find_owned() calls RbacGuard::authorize_owner_or_role .route("/notes/{id}", rullst::routing::get(controllers::notes_controller::show) .post(controllers::notes_controller::update) .layer(rullst::server::from_fn(middlewares::auth_middleware::auth_middleware))) // rullst-access: owner — find_owned() calls RbacGuard::authorize_owner_or_role .route("/notes/{id}/delete", rullst::routing::post(controllers::notes_controller::destroy) .layer(rullst::server::from_fn(middlewares::auth_middleware::auth_middleware))) .layer(rullst::server::from_fn(rullst::security::csrf_middleware)) ``` The whole file is in [`zero-to-complete-app/main.rs`](https://github.com/Rullst/Rullst/blob/main/docs/src/tutorials/zero-to-complete-app/main.rs). More on [HTMX rendering](https://rullst.github.io/Rullst/book/tutorials/06-htmx-zero-bundle.html), [forms and validation](https://rullst.github.io/Rullst/book/tutorials/07-forms-and-validation.html) and [routing](https://rullst.github.io/Rullst/book/tutorials/08-routing-and-middlewares.html). ## 5. Sign-in and owner-only routes Sign-in comes from the starter: `/register` and `/login` set an encrypted session cookie, and `auth_middleware` turns it into the user id that the handlers read as `Extension`. Anyone else is redirected to `/login`. The cookie carries the account's session epoch: the dashboard's *Sign out everywhere* button (`POST /logout/everywhere`) increments it, which ends the session on every device. The starter also has *Change password* (`/password/change`), which keeps only the current browser signed in, and *Forgot your password?* (`/password/forgot`), which emails a single-use link that expires in 30 minutes (`RESET_TOKEN_LIFETIME_MINUTES`, from 10 minutes to 24 hours); see [Tutorial 11](https://rullst.github.io/Rullst/book/tutorials/11-authentication-system.html#what-the-scaffold-currently-enforces). Being signed in is not enough to open a note, though: `/notes/{id}` must check that the note belongs to that user. Otherwise anyone could read another user's note by changing the number in the URL (an IDOR). `find_owned` loads the note and calls `RbacGuard::authorize_owner_or_role`; another user's note answers `404`, so it does not even reveal that the note exists. The `// rullst-access: owner — reason` comment above each route with an `{id}` records that decision. `cargo rullst audit --idor` and the security report in step 7 fail when a route with a path parameter has no such comment, or when a non-public classification has no matching guard in the code. With two accounts, Alice and Bob, the recorded run observed: | Request | Result | | --- | --- | | `GET /notes` without signing in | `303` to `/login` | | Alice: `GET /notes/1` (her note) | `200` | | Bob: `GET /notes/1` | `404` | | Bob: `POST /notes/1/delete` | `404`, and the note is kept | | `POST /notes` without the CSRF token | `403` | | Alice: create with an empty title (HTMX) | `200` with `X-Rullst-Validation-Status: 422` and the message *Give the note a title (1 to 120 characters).* | More on [authentication](https://rullst.github.io/Rullst/book/tutorials/11-authentication-system.html) and [ownership, RBAC and IDOR](https://rullst.github.io/Rullst/book/tutorials/13-rbac-authorization.html). ## 6. Run the tests Every new project ships `src/security_tests.rs`. Its tests build the app from `router()`, wrap it in the baseline that `Server` adds in production, and check offline that responses carry the security headers, that a write is refused without the CSRF token and accepted with it, and that the WAF refuses an injection probe but accepts ordinary prose. The SaaS starter also checks that repeated sign-in attempts are rate limited. The tests request `/health`, `/logout` and `/login`; your notes routes sit behind the same baseline because they are part of `router()`. The starter's sign-in controller and its Nexus access check have unit tests of their own. The notes controller adds two unit tests: one proves that only the owner passes `authorize`, the other that an empty title fails validation. None of these tests needs a database or the network: ```bash cargo test ``` ```text running 9 tests test controllers::auth_controller::tests::length_limits_count_what_the_form_counts ... ok test controllers::auth_controller::tests::registration_timestamps_are_current_utc_instants ... ok test controllers::notes_controller::tests::only_the_owner_may_open_a_note ... ok test controllers::notes_controller::tests::an_empty_title_is_rejected ... ok test middlewares::nexus_access::tests::a_request_without_a_session_is_sent_to_sign_in ... ok test security_tests::production_responses_carry_the_security_headers ... ok test security_tests::the_waf_refuses_injection_and_accepts_prose ... ok test security_tests::csrf_refuses_a_write_without_the_token ... ok test security_tests::repeated_sign_in_attempts_are_rate_limited ... ok test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in … ``` The order of the lines can differ from run to run. ## 7. Check the app with the security report ```bash cargo rullst audit --report ``` ```text Rullst security report (OWASP ASVS 5.0.0 Level 1 mapping) Evidence for a reviewer; it does not replace a manual review or a penetration test. Security checks ✓ Security headers and CSP — no findings ✓ CSRF — no findings ✓ Session and auth cookies — no findings ✓ Rate limiting on authentication routes — no findings ✓ Committed secrets — no findings ✓ Vulnerable dependencies — no findings ✓ IDOR / BOLA route classification — no findings Personal data ! Inventory — 2 field(s) listed, 2 need review (name heuristic) Accessibility checks ✓ Images have a text alternative — no findings ✓ Form controls have a label — no findings ✓ Pages declare their language — no findings NOT EVALUATED: 63 of 70 ASVS Level 1 requirements are outside these static checks. Report written to SECURITY_REPORT.md ``` `SECURITY_REPORT.md` lists the two fields to review: the `email` columns of the starter's `User` and `BillingCustomer` models. The vulnerable-dependency check needs `cargo-audit` installed. To see the IDOR check work, delete one `rullst-access` comment and run `cargo rullst audit --idor`: it fails and names the route. See [the security report guide](https://rullst.github.io/Rullst/book/security-report.html). ## 8. Develop with live reload ```bash cargo rullst dev ``` ```text Tip: run `cargo rullst dash` for live requests/s, latency, errors and database metrics. Building the application... Running initial db:migrate... Nothing to migrate. 📊 Rullst Studio running on http://127.0.0.1:5555 🚀 SaaS server starting on port 3000... … Rullst framework serving on http://127.0.0.1:3000 … Auto-reload: watching source, assets and configuration; successful builds restart the application. ``` Open `http://127.0.0.1:3000/register`, create an account, then go to `http://127.0.0.1:3000/notes`. Saving a source file rebuilds and restarts the app; a failed build keeps the previous one running. `cargo rullst dash` runs the same loop inside a terminal dashboard with live requests, latency, errors and database metrics. When one request repeats the same ORM operation three or more times, the dashboard adds a **POSSIBLE N+1 QUERIES** panel with the route, the operation and the count; see the [N+1 query warning](https://rullst.github.io/Rullst/book/cli_reference.html#n1-query-warning). Stop either with `Ctrl+C`. See [supervised auto-reload](https://rullst.github.io/Rullst/book/tutorials/51-authenticated-hot-reload.html) and the [`dash` reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-dash). ## 9. Build for production and deploy ```bash cargo rullst build ``` ```text 🚀 Starting Rullst production build pipeline (Release Mode: true)... ⚙️ Executing cargo build --release... Finished `release` profile [optimized] target(s) in 5m 48s 📦 Pre-compressing static assets in static/ directory... … ✨ Pre-compression finished: processed 2 files, generated 2 .br files and 2 .zst files. 🎉 Rullst production build completed successfully! ``` The release binary is `target/release/notes` (15.3 MiB in the recorded run). Cargo builds it with the `[profile.release]` that `cargo rullst new` wrote into `Cargo.toml`: thin LTO, one codegen unit and stripped symbols. `panic` stays `unwind`, so one panicking handler cannot stop the server. The build also writes Brotli and Zstandard copies of the files in `static/`, which the server prefers; run it again after changing an asset. The `/static` mount also sets cache headers. A file name with a content hash, such as `app.3f9a2c1b.css`, is cached for a year as `immutable`; other files, such as the starter's `rullst.css`, get `Cache-Control: no-cache` and are revalidated with their `ETag`. Started with the development `.env`, the binary answered `200` on `/health`, redirected `/notes` to `/login`, and served `/static/rullst.css` Brotli-compressed with `no-cache` and an `ETag`. Measure the build with `cargo rullst green` (Experimental). On its own, `cargo rullst green --path /health` starts the release binary with `RULLST_ENV=production`, which the SaaS starter refuses until the production billing settings exist: ```text error: App could not be started What happened the app exited during start-up (exit status: 1). Last stderr lines: Error: BillingConfigError("production billing requires real credentials, a strong webhook secret, an HTTPS redirect URL, and an explicit plan allowlist") ``` Until then, either pass `--env staging`, which the starter accepts with its mock billing credentials, or start `./target/release/notes` with the development `.env` and measure it from a second terminal: ```bash cargo rullst green --url http://127.0.0.1:3000 --path /health --bench ``` The report lists what it measured, each with its method: the binary size of the running executable, the idle resident memory, the number of third-party packages in `Cargo.lock` and, because of `--bench`, the app's CPU time during a 10-second loopback load and the CPU time per request. The container image size is `NOT MEASURED` until you name an image with `--image`. The figures describe one run on one machine, with the load generator on the same machine; compare runs only on the same setup. green measures no energy: a carbon figure appears only as an SCI estimate from your own `--cpu-watts` and `--grid-intensity`. See [efficiency figures for an app](https://rullst.github.io/Rullst/book/green.html). Before it serves real users: - create a production environment file with `RULLST_ENV=production`, a new `APP_KEY` and the billing settings described in the generated `BILLING.md`; never reuse the development `.env`; - check that file with `cargo rullst deploy:doctor --env-file .env.production`. Run against the development `.env`, it fails with `[FAIL] environment_target`, because that file does not set `RULLST_ENV=production`; - choose a platform: [guided container deployment](https://rullst.github.io/Rullst/book/tutorials/26-one-click-paas-deploy.html) (`cargo rullst deploy` writes a `Dockerfile`, `compose.yaml` and `Caddyfile` for a Linux VM; see [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html)), [Kubernetes](https://rullst.github.io/Rullst/book/tutorials/25-kubernetes-deployment.html), or a container from `cargo rullst dockerize`. Run migrations once per release, before starting the new version. ## Where next - Read the generated `AGENTS.md` before asking an AI assistant to change the app, and see [the CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html) for every command. - Add [background jobs](https://rullst.github.io/Rullst/book/tutorials/20-background-jobs-queues.html), [email](https://rullst.github.io/Rullst/book/crates/mail.html) or [real billing](https://rullst.github.io/Rullst/book/tutorials/19-saas-billing-capital.html). Work that can wait, such as reports and exports, can be [deferred to an allowed time window](https://rullst.github.io/Rullst/book/crates/core.html#deferrable-jobs-and-time-windows). - Before launch, read the [security architecture](https://rullst.github.io/Rullst/book/security-architecture.html) and check each crate's [maturity tier](https://rullst.github.io/Rullst/book/maturity.html). --- # 01. Hello Rullst! Source: https://rullst.github.io/Rullst/book/tutorials/01-hello-world.html # Tutorial 01: From Zero to Hello Rullst 🚀 **What you will build:** one complete web application with a typed route and server-rendered HTML. No database account or AI provider is needed. By the end, you will be able to point to the handler that produced the page in your browser. [Choose a different starting path](https://rullst.github.io/Rullst/book/start-here.html) · [Next: CLI generators](https://rullst.github.io/Rullst/book/tutorials/02-cli-generators.html) This tutorial takes a new developer from installing Rust to a running Rullst web application. It targets the v13 prerelease `13.0.0-alpha.4`. The v12 line reached end of life on 16 October 2026 (see the [release record](https://rullst.github.io/Rullst/book/v12.html)); existing v12 applications follow the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html). ## 1. Install Rust and Cargo On Linux or macOS, use the official rustup installer: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` On Windows, download rustup from [rustup.rs](https://rustup.rs) or run: ```powershell winget install --id Rustlang.Rustup ``` Restart the terminal if necessary, then verify the toolchain: ```bash rustc --version cargo --version ``` Rullst v13 requires Rust 1.96.0 or newer, the MSRV recorded in the [compatibility policy](https://rullst.github.io/Rullst/book/compatibility-policy.html). Run `rustup update` if `rustc --version` reports an older release. ## 2. Create a project ```bash cargo new my_first_app cd my_first_app ``` Every command below must run in this directory, where `Cargo.toml` lives. ## 3. Add Rullst Add the v13 prerelease from crates.io. Name its exact version: Cargo never selects a prerelease on its own, and the v12 line reached end of life on 16 October 2026, so a plain `cargo add rullst` would pick an unmaintained 12.x: ```bash cargo add rullst@=13.0.0-alpha.4 cargo add tokio --features full ``` Cargo records the resolved dependency graph in `Cargo.lock`. Commit that file for an application so its checkout remains reproducible, and review future dependency updates before deployment. ## 4. Define the first route Replace `src/main.rs` with: ```rust,no_run use rullst::{html, routes, Markup, Server}; async fn home() -> Markup { html! {

    "Hello, Rullst! 📜🦀"

    "Your first typed route is running."

    } } #[tokio::main] async fn main() -> Result<(), Box> { let app = routes![ get("/" => home) ]; Server::new(app).run(3000).await?; Ok(()) } ``` The fallible server startup uses `?`, so bind and runtime failures are returned to the process instead of causing a framework-originated panic. ## 5. Run the application Start the server: ```bash cargo run ``` The first build compiles every dependency and can take several minutes ([Build times](https://rullst.github.io/Rullst/book/build-times.html)). When the terminal prints `Rullst framework serving on http://127.0.0.1:3000`, open [http://127.0.0.1:3000](http://127.0.0.1:3000). Stop the server with `Ctrl+C`. **Make it yours:** change the heading to your project name, restart with `cargo run`, and confirm the response changed. The example's utility class names do not install Tailwind by themselves; seeing an unstyled page is not a server failure. The Blank and SaaS starters of the CLI ship a local stylesheet (`static/rullst.css`). If the port is already occupied, stop the conflicting process you own or start on another port with `PORT=3001 cargo run`: the `PORT` variable (from the environment, then `.env`) takes precedence over the `.run(3000)` argument. Keep the terminal output: it is the first place to look for a build or startup diagnostic. ## 6. Continue with the CLI The CLI can generate complete starters and project modules. Install the v13 prerelease from crates.io (name the exact version; Cargo never picks a prerelease on its own): ```bash cargo install cargo-rullst --version =13.0.0-alpha.4 --locked cargo rullst --version ``` The second command prints `rullst 13.0.0-alpha.4`. [Tutorial 02](https://rullst.github.io/Rullst/book/tutorials/02-cli-generators.html) creates a project with it; the hand-written `my_first_app` is not needed there. The CLI's `new` generator targets the CLI's own framework version. A CLI built from a source checkout of `main` is the v13 development line: it emits absolute path dependencies to that exact checkout, including when invoked elsewhere. Keep such a checkout in place and review its sources before sharing a generated project. See the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html) for every command and boundary. ## Key takeaways - The `rullst::html!` macro generates ordinary Rust string-building code and escapes dynamic values. It returns `Markup`, which a handler returns as `text/html` and another `html!` inserts as HTML. Rendering still performs the allocations/work implied by the generated template. - Boolean HTML attributes inside `html!` are written bare (`required`) or conditionally (`checked?={is_done}` writes `checked` or nothing). Presence is the value, so `disabled="false"` would still disable the control. - Fallible handlers can return `Result` using an application-defined error that converts the relevant typed framework/domain errors; server startup propagates `ServerError` with `?`. - The moving `main` branch is an integration source, not an immutable release artifact. For a reproducible application, pin the exact crates.io version (`=13.0.0-alpha.4`) and commit `Cargo.lock`. --- # REST API Quickstart Source: https://rullst.github.io/Rullst/book/tutorials/rest-api-quickstart.html # Create Your First Rullst REST API **Your result:** a typed JSON response you can verify from another terminal. Start here when your frontend is a separate app, mobile client or integration. [Explore all beginner paths](https://rullst.github.io/Rullst/book/start-here.html) or the [complete API learning-path index](https://rullst.github.io/Rullst/book/api-reference.html#httprest-a-connected-learning-path). This quickstart creates one runnable JSON endpoint without a database or an AI provider. It targets the v13 prerelease `13.0.0-alpha.4`; the v12 line reached end of life (see the [release record](https://rullst.github.io/Rullst/book/v12.html)). You need Rust 1.96.0 or newer ([Tutorial 01](https://rullst.github.io/Rullst/book/tutorials/01-hello-world.html#1-install-rust-and-cargo) shows how to install it). ## 1. Create the application ```bash cargo new first_rullst_api cd first_rullst_api cargo add rullst@=13.0.0-alpha.4 cargo add tokio --features macros,rt-multi-thread cargo add serde --features derive ``` The exact requirement keeps this tutorial on the v13 prerelease (Cargo never selects a prerelease on its own, and the v12 line reached end of life on 16 October 2026). Commit the generated `Cargo.lock` so subsequent locked builds use the same resolved package versions; it does not pin an unrelated moving Git branch or automatically update the app. ## 2. Add a JSON route Replace `src/main.rs` with: ```rust,no_run use rullst::{Server, ServerError, routes, server::Json}; use serde::Serialize; #[derive(Serialize)] struct HealthResponse { status: &'static str, framework: &'static str, } async fn health() -> Json { Json(HealthResponse { status: "ok", framework: "Rullst", }) } #[tokio::main] async fn main() -> Result<(), ServerError> { let app = routes![ get("/api/health" => health), ] .layer(rullst::server::from_fn( rullst::security::headers_middleware, )); Server::new(app).run(3000).await } ``` The response is serialized from a typed Rust value. The secure-header layer is included explicitly because transport, proxy, authentication, authorization, CSRF, and application-specific input policy remain deployment responsibilities; a JSON response alone is not a production security boundary. ## 3. Run and verify it Start the application: ```bash cargo run ``` From another terminal, request the route: ```bash curl -i http://127.0.0.1:3000/api/health ``` The body is: ```json {"status":"ok","framework":"Rullst"} ``` Stop the server with `Ctrl+C`. ## 4. Prefer a generated API foundation? After [installing the v13 CLI](https://rullst.github.io/Rullst/book/1-getting-started.html#1-installation), it can scaffold a headless API directly. Use a different directory from the hand-written example above: ```bash cargo rullst new generated_api --default --api --no-database \ --skip-initial-migration cd generated_api cargo run ``` This is the API starter (the Blank starter without HTML). Its routes are registered in `router()` in `src/main.rs`, not copied from the health handler above: `GET /` answers with a JSON welcome message (`curl http://127.0.0.1:3000/`), `POST /api/messages` requires `Authorization: Bearer ` with the random token written to `.env`, and `/health` and `/ready` serve container probes. Use `cargo rullst make:controller project --api` for additional JSON controllers. Generated parameterized data routes still require explicit ownership or tenant authorization; see [RBAC and IDOR protection](https://rullst.github.io/Rullst/book/tutorials/13-rbac-authorization.html). --- # 02. CLI Automation & Generators Source: https://rullst.github.io/Rullst/book/tutorials/02-cli-generators.html # Tutorial 02: CLI Automation & Generators ⚡ **Your goal:** generate a small piece of the application, find the resulting files and understand what still needs your code. You need the CLI installed in [Tutorial 01](https://rullst.github.io/Rullst/book/tutorials/01-hello-world.html#6-continue-with-the-cli) (`cargo rullst --version` prints `rullst 13.0.0-alpha.4`). Rullst provides opinionated code generators (`make:*`) for controllers, models, migrations, and resources. The current generators register generated Rust modules when possible; they do not add application routes. `make:controller` and `make:model` also refresh the generated project map (`.llms.txt`, `.rullst/context-map.json`) and `diagram.md`; hand-written versions of those files are kept. Review every generated file and mount the intended routes yourself. --- ## Create the project Create a Blank starter with SQLite and enter it: ```bash cargo rullst new shop --default --blueprint blank --database sqlite \ --skip-initial-migration cd shop ``` `--default` skips the wizard's questions. The project has `Cargo.toml`, `Rullst.toml`, `.env`, `src/main.rs` (the routes, in `router()`) and a first `users` migration. Run every command below from this directory; the following tutorials use the same project unless they say otherwise. To see the welcome page, run `cargo rullst dev` and open `http://127.0.0.1:3000`; the first build can take several minutes. --- ## 🛠️ Essential Scaffolding Commands Run the three commands in order: each one adds different files. ### 1. Generate a Controller ```bash cargo rullst make:controller ProductsController ``` Creates `src/controllers/products_controller.rs` with placeholder `index`, `show`, `store`, `update`, and `delete` handlers and registers the module (`pub mod controllers;` in `src/main.rs`). Mount its page by adding a line to the `routes![...]` list in `router()`: ```rust,ignore get("/products" => controllers::products_controller::index), ``` After `cargo rullst dev` rebuilds, `http://127.0.0.1:3000/products` shows the generated placeholder page. ### 2. Generate a Model & Migration ```bash cargo rullst make:model Product --migration ``` Creates the model in `src/models/product.rs` and a timestamped Rust migration (`src/migrations/m_create_products.rs`) that `src/migrations/mod.rs` registers. The model starts with an `id` and a `snapshot` field, the migration with `id` and timestamps; [Tutorial 03](https://rullst.github.io/Rullst/book/tutorials/03-active-record-crud.html) adds the columns. `cargo rullst dev` applies pending migrations when it starts; `cargo rullst db:migrate` applies them without starting the server ([Tutorial 05](https://rullst.github.io/Rullst/book/tutorials/05-migrations-and-seeds.html)). ### 3. Generate a Full-Stack Resource Starting Point ```bash cargo rullst make:resource Category ``` Scaffolds a model (`src/models/category.rs`), its migration, a controller (`src/controllers/category_controller.rs`) and HTML view placeholders (`views/category/index.html` and `views/category/form.html`) in one command. Use a name that has no model yet: for an existing model or table the command skips the create-table migration. The generated controller is not a complete authorized CRUD implementation. --- ## 🔍 Static CLI Code Inspection Inspect recognizable source declarations without launching a server: ```bash # Inspect recognizable routes! declarations cargo rullst inspect routes # Print ORM models and field types cargo rullst inspect models ``` Add `--json` for a versioned machine-readable document (see the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html)). --- ## 💡 Key Takeaways - Scaffolding generators maintain architectural consistency across team members. - `cargo rullst inspect` performs bounded source-text inspection. Route output recognizes explicit single-line `routes!` entries (`get`, `post`, `put` and `delete`); model output lists public declarations from `src/models`. It is not complete macro expansion or a Rust semantic analysis. - Generators never add routes: mount each handler in `router()` yourself. --- # 03. Active Record CRUD Operations Source: https://rullst.github.io/Rullst/book/tutorials/03-active-record-crud.html # Tutorial 03: Active Record CRUD Operations 🗄️ `rullst-orm` derives a typed query builder and persistence methods from a Rust struct. The SQLx-backed model uses an `i32` primary key named `id`; a zero value means that `save()` inserts, while a non-zero value means that it updates. This tutorial continues in the `shop` project of [Tutorial 02](https://rullst.github.io/Rullst/book/tutorials/02-cli-generators.html), whose `cargo rullst make:model Product --migration` wrote `src/models/product.rs` and `src/migrations/m_create_products.rs`. --- ## Step 1: Define an Active Record model Give the generated migration the columns the model will use. In its `up()`: ```rust,ignore Schema::create("products", |table| { table.id(); table.string("name").not_null(); table.integer("price_cents").not_null(); table.timestamps(); }).await ``` Then add the matching fields to `src/models/product.rs`: ```rust use rullst::db::{FromRow, Orm, RowSnapshot}; #[derive(Debug, Clone, FromRow, Orm)] #[orm(table = "products")] pub struct Product { pub id: i32, pub name: String, pub price_cents: i32, // Remembers the loaded values, so `save()` writes only the columns this // record changed. Build new records with `snapshot: RowSnapshot::default()`. #[sqlx(skip)] pub snapshot: RowSnapshot, } ``` Apply the migration with `cargo rullst db:migrate` (or restart `cargo rullst dev`) before using the model. The model leaves out `created_at`/`updated_at`, so inserts keep their database defaults. If `cargo rullst dev` already applied the migration while it had only an `id`, run `cargo rullst db:rollback` (it reverts the last batch of migrations, including this one) and then `cargo rullst db:migrate`; [Tutorial 05](https://rullst.github.io/Rullst/book/tutorials/05-migrations-and-seeds.html) explains both commands. --- ## Step 2: Perform CRUD operations Call these methods from an `async` handler or seeder, for example in a controller of [Tutorial 02](https://rullst.github.io/Rullst/book/tutorials/02-cli-generators.html). ### Create ```rust,no_run # use rullst::db::{FromRow, Orm, RowSnapshot}; # #[derive(Debug, Clone, FromRow, Orm)] # #[orm(table = "products")] # struct Product { id: i32, name: String, price_cents: i32, #[sqlx(skip)] snapshot: RowSnapshot } # async fn create_product() -> Result<(), rullst::orm::Error> { let mut product = Product { id: 0, name: "Starter kit".to_string(), price_cents: 2_500, snapshot: RowSnapshot::default(), }; product.save().await?; // `product.id` now contains the inserted primary key. # Ok(()) # } ``` ### Read and filter ```rust,no_run # use rullst::db::{FromRow, Orm, RowSnapshot}; # #[derive(Debug, Clone, FromRow, Orm)] # #[orm(table = "products")] # struct Product { id: i32, name: String, price_cents: i32, #[sqlx(skip)] snapshot: RowSnapshot } # async fn read_products() -> Result<(), rullst::orm::Error> { let product = Product::find(1).await?; // Result, rullst::orm::Error> let kits = Product::query() .where_like("name", "%kit%") .get() .await?; # let _ = (product, kits); # Ok(()) # } ``` Builder values are bound as query parameters. Column names are validated as identifiers, but they should still be application-owned constants rather than untrusted request input. ### Update ```rust,no_run # use rullst::db::{FromRow, Orm, RowSnapshot}; # #[derive(Debug, Clone, FromRow, Orm)] # #[orm(table = "products")] # struct Product { id: i32, name: String, price_cents: i32, #[sqlx(skip)] snapshot: RowSnapshot } # async fn update_product() -> Result<(), rullst::orm::Error> { if let Some(mut product) = Product::find(1).await? { product.price_cents = 2_000; // The snapshot makes this an UPDATE of `price_cents` only. product.save().await?; } # Ok(()) # } ``` ### Delete ```rust,no_run # use rullst::db::{FromRow, Orm, RowSnapshot}; # #[derive(Debug, Clone, FromRow, Orm)] # #[orm(table = "products")] # struct Product { id: i32, name: String, price_cents: i32, #[sqlx(skip)] snapshot: RowSnapshot } # async fn delete_product() -> Result<(), rullst::orm::Error> { if let Some(product) = Product::find(1).await? { product.delete().await?; } # Ok(()) # } ``` Without a `RowSnapshot` field, `save()` on a loaded record writes every column, so the last writer wins; see the [ORM guide](https://rullst.github.io/Rullst/book/crates/orm.html#concurrent-updates-and-counters) for dirty tracking and `update_partial()`. --- ## Soft deletes A `deleted_at: Option` field opts the model into the default soft-delete contract. For a different sentinel, configure it explicitly and ensure the migration uses the same representation: ```rust use rullst_orm::{FromRow, Orm}; #[derive(Debug, Clone, FromRow, Orm)] #[orm( table = "users", soft_delete(field = "is_deleted", value = "0", delval = "1") )] pub struct SoftUser { pub id: i32, pub name: String, pub is_deleted: i32, } ``` `query()` applies the model's configured scopes. `unscoped()` is an explicit administrative escape hatch and should not be used directly from request data. `restore()` clears the soft-delete marker and `force_delete()` removes the row permanently. Both check the tenant and the policy (`can_restore` / `can_force_delete`) first and then run in a savepoint of the task-scoped `Orm::transaction`, or in their own transaction, like `delete()`: - `force_delete()` runs the `before_delete`/`after_delete` hooks and the `deleting`/`deleted` observers, writes a `force_deleted` audit entry on auditable models, and after commit invalidates the query cache, publishes the Redis `deleted` event, calls `committed(Deleted)` and removes the Scout document. It does not cascade to `cascade_soft_delete` relations. - `restore()` re-reads the restored row, calls the `updated` and `saved` observers with it, writes a `restored` audit entry, and after commit invalidates the cache, publishes the Redis `updated`/`saved` events, calls `committed(Updated)` and re-indexes the Scout document. The save hooks and the `saving`/`updating` observers are not called, because restore writes only the soft-delete column. Restoring a missing or live row is a no-op, and deleting an already trashed row again fails without changing its deletion time. --- ## Key takeaways - `save`, `find`, `all`, `query`, and `delete` are generated for SQLx-backed `Orm` models. - Creation uses a normal Rust struct, not a JSON map. - CRUD errors are returned as `rullst::orm::Error` (the `rullst_orm::Error` type); a missing row is `Ok(None)`. Saving or deleting a handle whose row was deleted meanwhile returns `RecordNotFound` instead of silently succeeding. - Use a caller-owned transaction and the generated `*_with_tx` methods when a business operation must commit multiple writes atomically. --- # 04. Data Mapper & Repository Pattern Source: https://rullst.github.io/Rullst/book/tutorials/04-repository-pattern.html # Tutorial 04: Data Mapper & Repository Pattern 🏗️ For domain-heavy code, Rullst exposes a small `Repository` contract. The framework does not invent SQL for this trait: your adapter owns its pool, queries, transactions, and error type. This keeps persistence behavior explicit and makes an in-memory implementation straightforward in unit tests. `GenericRepository` is currently only a zero-state marker/helper. It does not accept a pool and does not provide methods such as `find_one_by`. --- ## Step 1: Implement a PostgreSQL repository ```rust,no_run use rullst_orm::{async_trait, sqlx, FromRow, Repository}; #[derive(Debug, Clone, FromRow)] pub struct User { pub id: i64, pub name: String, pub email: String, } #[derive(Clone)] pub struct PgUserRepository { pool: sqlx::PgPool, } impl PgUserRepository { pub fn new(pool: sqlx::PgPool) -> Self { Self { pool } } pub async fn find_by_email( &self, email: &str, ) -> Result, sqlx::Error> { sqlx::query_as::<_, User>( "SELECT id, name, email FROM users WHERE email = $1", ) .bind(email) .fetch_optional(&self.pool) .await } } #[async_trait] impl Repository for PgUserRepository { type Id = i64; type Error = sqlx::Error; async fn find_by_id(&self, id: i64) -> Result, Self::Error> { sqlx::query_as::<_, User>( "SELECT id, name, email FROM users WHERE id = $1", ) .bind(id) .fetch_optional(&self.pool) .await } async fn find_all(&self) -> Result, Self::Error> { sqlx::query_as::<_, User>( "SELECT id, name, email FROM users ORDER BY id", ) .fetch_all(&self.pool) .await } async fn save(&self, user: &User) -> Result<(), Self::Error> { sqlx::query( "INSERT INTO users (id, name, email) VALUES ($1, $2, $3) \ ON CONFLICT (id) DO UPDATE SET name = $2, email = $3", ) .bind(user.id) .bind(&user.name) .bind(&user.email) .execute(&self.pool) .await?; Ok(()) } async fn delete(&self, id: i64) -> Result<(), Self::Error> { sqlx::query("DELETE FROM users WHERE id = $1") .bind(id) .execute(&self.pool) .await?; Ok(()) } } ``` All values are parameterized. SQL identifiers and the query shape remain application-owned source code. This adapter targets PostgreSQL (`PgPool`, `$1` placeholders) and a `users` table with `BIGINT` ids. Inside a generated project, `rullst::db::Orm::pool()?` returns the application's pool as `RullstPool`, which is `sqlx::SqlitePool` with the SQLite starter and `sqlx::PgPool` with PostgreSQL: clone it into the adapter and write the queries for that database. --- ## Step 2: Inject the concrete adapter through Axum state ```rust,ignore use std::sync::Arc; use rullst::web::axum::{ extract::{Query, State}, http::StatusCode, Json, }; use serde::Deserialize; #[derive(Deserialize)] pub struct EmailQuery { email: String, } pub async fn get_user_by_email( State(repository): State>, Query(query): Query, ) -> Result, StatusCode> { repository .find_by_email(&query.email) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .map(Json) .ok_or(StatusCode::NOT_FOUND) } ``` `rullst::Router` has no `with_state`: build this route on an `axum::Router`, call `.with_state(Arc::new(repository))` and mount it with `merge_axum`, as [Tutorial 08](https://rullst.github.io/Rullst/book/tutorials/08-routing-and-middlewares.html#step-3-share-application-state) shows. Returning `Json` also requires `User` to derive `serde::Serialize`. Production handlers should map internal database errors to an application error without returning query or credential details to clients. --- ## Key takeaways - Use derived Active Record models for direct typed CRUD. - Use `Repository` when the domain needs an explicit persistence boundary. - The repository implementation, not the trait, determines the supported backend and SQL dialect. - Add tenant/owner predicates inside repository queries where the resource is tenant- or user-scoped; dependency injection is not an authorization check. --- # 05. Database Migrations & Seeds Source: https://rullst.github.io/Rullst/book/tutorials/05-migrations-and-seeds.html # Tutorial 05: Database Migrations & Seeders 🗄️ Rullst uses timestamped Rust migration modules for SQLx-primary projects. Turso primary projects receive explicit, reversible `TursoMigration` statements instead. This tutorial shows the SQLx path: `Schema` works with SQLite, PostgreSQL and MySQL/MariaDB only, as [Database Support](https://rullst.github.io/Rullst/book/database-support.html) lists. --- ## Step 1: Create a migration Run this in the `shop` project of [Tutorial 02](https://rullst.github.io/Rullst/book/tutorials/02-cli-generators.html), which already has `products` and `categories` tables: ```bash cargo rullst make:migration create_catalog_items_table ``` The command creates `src/migrations/m_create_catalog_items_table.rs` and regenerates `src/migrations/mod.rs`. Edit the generated `up` and `down` methods: ```rust,no_run use rullst_orm::{async_trait, schema::{ForeignKeyAction, Migration, Schema}}; pub struct MigrationImpl; #[async_trait] impl Migration for MigrationImpl { fn name(&self) -> &'static str { "m20260901000000_create_catalog_items_table" } async fn up(&self) -> Result<(), rullst_orm::Error> { Schema::create("catalog_items", |table| { table.id(); table.string("name").not_null(); // MySQL/MariaDB cannot index a TEXT column, so the indexed SKU // has a bounded type. table.string("sku").not_null().unique().col_type = "VARCHAR(64)".to_string(); table .foreign_id("category_id", "categories") .on_delete(ForeignKeyAction::Restrict); table.integer("price_cents").not_null(); table.check("price_cents >= 0"); table.date("available_on"); table.index(&["category_id", "available_on"]); table.timestamps(); }) .await } async fn down(&self) -> Result<(), rullst_orm::Error> { Schema::drop_if_exists("catalog_items").await } } ``` Keep the generated timestamp/name in `name()`; the runner uses that stable value to record migration state. The foreign key needs the referenced table: `categories` comes from `cargo rullst make:resource Category`, and this migration, generated later, has a later timestamp and runs after it. `unique()`, `index(...)`, `foreign_id(...)`/`foreign(...)` and `check(...)` declare constraints and indexes with the table (see the [ORM guide](https://rullst.github.io/Rullst/book/crates/orm.html#constraints-indexes-and-foreign-keys) for every option and the generated names). PostgreSQL and SQLite create the indexes with `CREATE INDEX IF NOT EXISTS` right after the table; MySQL/MariaDB declare them inside `CREATE TABLE`. `table.timestamps()` creates nullable `created_at`/`updated_at` UTC-instant columns that default to the current time: `TIMESTAMPTZ` on PostgreSQL, `DATETIME(6)` on MySQL/MariaDB and RFC 3339 text on SQLite. Read them as `rullst::orm::chrono::DateTime` model fields. `date()`, `time()`, `timestamp()` and `timestamp_tz()` add single date/time columns for `NaiveDate`, `NaiveTime`, `NaiveDateTime` and `DateTime` fields. They need a strict driver feature (`strict-postgres`, `strict-mysql` or `strict-sqlite`), which `cargo rullst new` selects: the multi-driver SQLx `Any` pool cannot read date/time values, so with it `timestamps()` keeps `TEXT` columns for `String` fields and the other date/time helpers fail on PostgreSQL and MySQL/MariaDB. To change an existing table, use `Schema::table`: ```rust,no_run use rullst_orm::schema::{ForeignKeyAction, Schema}; # async fn up() -> Result<(), rullst_orm::Error> { Schema::table("products", |table| { table.integer("category_id"); table .foreign("category_id") .references("categories", "id") .on_delete(ForeignKeyAction::SetNull); table.unique_index(&["name", "category_id"]); }) .await # } ``` SQLite cannot add constraints to an existing table: a foreign key works there only on a nullable column added in the same call, and a CHECK, a foreign key on an existing column or a `NOT NULL` column without a default return `Error::UnsupportedSchemaChange` before anything runs. `table.float(...)` maps to an `f64` model field: it emits `DOUBLE PRECISION` on PostgreSQL, `DOUBLE` on MySQL/MariaDB and `REAL` on SQLite. PostgreSQL columns created by earlier Rullst versions were single-precision `REAL`; migrate them explicitly (`ALTER TABLE products ALTER COLUMN price TYPE DOUBLE PRECISION`) before relying on `f64` precision. `table.boolean(...)` maps to a `bool` model field. PostgreSQL receives a native `BOOLEAN` (an integer default of `0`/`1` becomes `FALSE`/`TRUE`); MySQL/MariaDB and SQLite keep an `INTEGER` 0/1 flag. PostgreSQL columns created by earlier versions were `INTEGER`; migrate them with `ALTER TABLE t ALTER COLUMN c TYPE BOOLEAN USING c <> 0`. Text defaults (`ColumnDefault::Text`) and `table.enum_col(...)` variants are embedded in the DDL as single-quoted literals with doubled single quotes. `Schema::create` rejects such text when it contains a backslash or a control character, because MySQL/MariaDB treat a backslash inside a quoted literal as an escape character by default. --- ## Step 2: Run, inspect, and roll back migrations ```bash cargo rullst db:migrate cargo rullst db:status cargo rullst db:rollback ``` `db:rollback` runs `down()` for the last recorded batch, in reverse order. The SQLx migration runner does **not** wrap the whole batch in one database transaction. By default it runs each `up()`/`down()` and then records it in the `migrations` table as separate statements, so a crash or lost connection in between leaves a change applied but unrecorded. A migration that returns `true` from `within_transaction()` (v13 prerelease API) runs `up()`/`down()` and its record inside one `Orm::transaction` instead. The template written by the ORM's own artisan `make:migration` command opts in; add the method to migrations generated by `cargo rullst make:migration` yourself: ```rust,ignore fn within_transaction(&self) -> bool { true } ``` Only statements that join that transaction are covered: `Schema` calls, generated model methods, `rullst_orm::execute_query!` and raw SQL sent with `Orm::raw(sql).execute()` (or `.map_to()`). `Orm::raw` is the raw-SQL path that stays in the transaction: ```rust,ignore async fn up(&self) -> Result<(), rullst_orm::Error> { Schema::create("tags", |table| { table.id(); table.string("name").not_null(); }) .await?; // Runs in the migration's transaction, so it sees the new table and rolls // back with it. Orm::raw("CREATE UNIQUE INDEX tags_name_key ON tags (name)") .execute() .await?; Ok(()) } ``` `Orm::pool()` and `Orm::read_pool()` would hand out another connection that cannot see the transaction's uncommitted tables and does not roll back with it. Since 13.0.0-alpha.3 they return `Error::Validation`, naming the migration, while such a migration's `up()`/`down()` runs, so the migration fails and rolls back unrecorded instead of half-applying (`Orm::begin_transaction()` fails the same way). Keep the default `false` for a migration that sends queries such as `sqlx::query(sql).execute(Orm::pool()?)`. A pool obtained before the migration ran, another crate's pool or a task spawned from `up()` is not detected and still runs outside the transaction. PostgreSQL and SQLite roll the DDL back with the record; MySQL and MariaDB commit each DDL statement implicitly, so there only data changes after the last DDL statement are atomic with the record. Statements that refuse to run in a transaction block, such as PostgreSQL `CREATE INDEX CONCURRENTLY`, also need `false`. Make every migration reversible and test both directions against each supported database. These commands use the same database as the running server: the process `DATABASE_URL`, then `DATABASE_URL` in `./.env` (never overriding the process environment), then `[database].url` in `Rullst.toml`. If none is set, the command fails instead of creating a new SQLite file, and a database that cannot be initialized also fails the command with exit status 1. Each migration's tracking row is removed as soon as its `down()` succeeds. If a later `down()` fails, the rollback stops with that error: the migrations it already reverted are no longer recorded as applied, while the failed migration and the rest of the batch stay recorded. Fix the cause and run `db:rollback` again to continue from that point. `db:migrate` and `db:rollback` hold a runner lock for the whole run on PostgreSQL (`pg_advisory_lock`) and MySQL/MariaDB (`GET_LOCK`), on a dedicated connection outside the pool. Replicas that start together therefore apply each pending migration once: a second runner waits, then finds it already recorded. The lock is released when the run ends or its process exits. A newly created `migrations` table also declares the migration name `UNIQUE`. SQLite has no cross-process advisory lock, so run a single migrator per SQLite database. --- ## Step 3: Define and register a seeder Create `src/seeds.rs` with a seeder for the `Product` model of [Tutorial 03](https://rullst.github.io/Rullst/book/tutorials/03-active-record-crud.html), and declare `mod seeds;` in `src/main.rs`: ```rust,ignore use rullst::db::{async_trait, RowSnapshot, Seeder}; use crate::models::product::Product; pub struct CatalogSeeder; #[async_trait] impl Seeder for CatalogSeeder { async fn run(&self) -> Result<(), rullst::orm::Error> { // Idempotent: a second `db:seed` finds the row and adds nothing. let existing = Product::query() .where_eq("name", "Starter kit") .first() .await?; if existing.is_some() { return Ok(()); } let mut product = Product { id: 0, name: "Starter kit".to_string(), price_cents: 2_500, snapshot: RowSnapshot::default(), }; product.save().await } } pub fn get_seeders() -> Vec> { vec![Box::new(CatalogSeeder)] } ``` Register migrations and seeders before starting the server: in `main()`, replace the starter's `rullst::artisan!(crate::migrations::get_migrations());` with ```rust,ignore rullst::artisan!( crate::migrations::get_migrations(), crate::seeds::get_seeders() ); ``` Without `artisan!`, `Server::run` still recognizes `db:migrate`, `db:rollback`, `db:status` and `db:seed`, but it has no registry to run them against: the process exits with status 1 and asks for `rullst::artisan!` rather than reporting "Nothing to migrate." as a success. Then run: ```bash cargo rullst db:migrate cargo rullst db:seed ``` `db:seed` first checks the registered migrations against the `migrations` table. While one is pending it runs no seeder and fails, instead of letting a seeder fail on a table that does not exist yet: ```text Validation error: 1 migration(s) pending, starting with `m20261012_create_users`: run `db:migrate` before `db:seed` ``` Seeders execute sequentially. Make development/CI seeders idempotent, as above, if the command may run more than once. Never commit real passwords or provider secrets; for authentication records, hash a test password with `rullst-auth` or create a non-login fixture. --- ## Key takeaways - Migrations are Rust modules, not split `up/down` SQL files. - Migration names and order are generated deterministically from timestamps. - Batch tracking exists, but batch-wide transactional rollback is not implied. - `db:seed` executes only seeders explicitly registered with `artisan!`, and only after every registered migration is applied. --- # 06. HTMX-oriented Server Rendering Source: https://rullst.github.io/Rullst/book/tutorials/06-htmx-zero-bundle.html # Tutorial 06: HTMX-oriented server rendering 🎨 The Blank and SaaS starters render HTML on the server and use HTMX attributes for targeted requests and fragment swaps. It does not require a project-local SPA bundle, but HTMX itself is browser JavaScript. The CLI's HTML starters write a pinned copy to `static/htmx-1.9.12.min.js`, which `rullst::htmx::render_page` loads from the same origin; an application without those generated assets must serve that file itself. The application's CSP must permit it. --- ## 🛠️ Step 1: Render HTMX Components In your controller or view: ```rust use rullst::html; use rullst::html::Markup; pub async fn search_users() -> Markup { let results = vec!["Alice", "Bob", "Charlie"]; let rows: Markup = results .into_iter() .map(|name| html! {
  • {name}
  • }) .collect(); html! {
      {rows}
    } } ``` `html!` returns `Markup`, escaped HTML that a handler can return directly (`text/html; charset=utf-8`). Each `name` is escaped, and the collected rows are inserted into the list as HTML. Mount the handler in `router()` of `src/main.rs` with `get("/api/users/search" => search_users),`. ### Composing views Interpolating a `Markup` inserts it as it is, so a view built by another `html!` call nests without a wrapper. A `String`, `&str` or number is always escaped, so request data stays text. Declare view helpers `-> Markup`, collect lists with `.collect::()` and use `Option` for parts that may be absent: ```rust use rullst::html; use rullst::html::Markup; fn badge(label: &str) -> Markup { html! { {label} } } let name = ""; let flagged = true; // `name` is escaped; the badge is inserted as HTML. let row = html! {
  • {name}{badge("new")}
  • }; assert_eq!(row, "
  • <Ana>new
  • "); // `None` renders nothing. let note = flagged.then(|| html! { "flagged" }); assert_eq!(html! {

    {note}

    }, "

    flagged

    "); ``` `rullst::html::RawHtml` (or `Markup::from_trusted`) is only for trusted HTML produced outside `html!`, such as a sanitizer's output; never wrap request data in it. A full page can start with ``, which `html!` emits as written, and element names may be Rust keywords, such as SVG's ``. --- ## 💻 Step 2: Wire HTMX Attributes in Front-End HTML ```html
    Start typing to search...
    ``` Put this markup in a page that loads HTMX: the starter's `home` handler returns its page through `rullst::htmx::render_page_with_lang`, which adds the same-origin HTMX script. The search handler is a **GET** route because it only reads a list. GET is not a workaround for CSRF. Commands that change data, send messages or incur provider charges must not be converted to GET to avoid validation. For POST forms, including an AI chat, follow the [CSRF form contract](https://rullst.github.io/Rullst/book/tutorials/07-forms-and-validation.html#step-3-send-the-csrf-token-with-browser-forms). HTMX does not automatically copy Rullst's CSRF cookie into a header or make HTTP error bodies visible in the target element. --- ## 🧪 Step 3: Snapshot-test rendered HTML `rullst::testing::assert_html_snapshot!` (v13) compares rendered HTML with `tests/snapshots/.html` in the crate that calls it, in unit and integration tests alike. Both sides are normalised first: whitespace-only text between tags is dropped, every tag that follows another tag starts a new line, other whitespace runs collapse to one space, and quoted attribute values and the content of `pre`, `textarea`, `script` and `style` stay as written. `SnapshotOptions` can also replace per-request values with documented placeholders: `mask_nonce()` writes `{NONCE}` for `nonce="…"` attributes and `'nonce-…'` CSP sources, and `mask_csrf_token()` writes `{CSRF_TOKEN}` for a `name="_token"` field, `` and `X-CSRF-Token` values. ```rust,no_run use rullst::html; use rullst::testing::{SnapshotOptions, assert_html_snapshot}; fn search_form(nonce: &str, csrf_token: &str) -> rullst::html::Markup { html! {
    } } // In a #[test] function: assert_html_snapshot!( "search/form", search_form("per-response-nonce", "per-request-token"), SnapshotOptions::new().mask_nonce().mask_csrf_token() ); ``` A missing snapshot fails and names the file to create; a changed one fails with a compact line diff (`-` snapshot, `+` rendered). Run the tests with `RULLST_UPDATE_SNAPSHOTS=1` to write missing snapshots and overwrite changed ones, then review the files and commit them. Snapshot names use ASCII letters, digits, `_`, `-` and `.`, with `/` between folders; a name that would leave `tests/snapshots` is refused. The helper is test code: it panics like `assert_eq!` and has no production use. --- ## 💡 Key Takeaways - **Small application-owned client surface:** business logic can remain on the server while HTMX coordinates browser requests. - **Partial rendering:** handlers can return fragments instead of full pages. Measure page weight and latency for the actual application; no fixed size or load-time guarantee follows from the rendering style. --- # 07. Forms & DTO Validation Source: https://rullst.github.io/Rullst/book/tutorials/07-forms-and-validation.html # Tutorial 07: Forms & DTO Validation 📝 Rullst provides `ValidatedForm` and `ValidatedJson` extractors. They parse the request and run `validator` constraints before the handler is called. Invalid payloads become bounded `400`, `413`, `415` or `422` JSON responses. HTMX requests (`HX-Request: true`) instead receive an HTML error fragment with `200 OK`, because HTMX 1.x and 2.x swap only successful responses by default; the `X-Rullst-Validation-Status` header carries the REST status. htmx swaps the fragment into the form's `hx-target` with its `hx-swap`, so point `hx-target` at an error or result container rather than at the form itself. A body that cannot be parsed gets a fixed message: `413` "Request body is too large." for a body over the extractor limit, `415` "Unsupported request content type." for a wrong content type, and `400` "The submitted data could not be read or has an invalid format." for a malformed body or an unknown enum variant (`422` stays reserved for failed `validator` constraints). The deserializer's own text can echo request input, so it is logged at `debug` level on the `rullst::validation` target and not returned. Every message and field name in the HTMX fragment is HTML-escaped, including custom validator messages. Errors inside `#[validate(nested)]` structs and lists are reported under dotted and indexed paths such as `address.zip` and `items[0].name`. `ValidationError`'s `Display` and `Debug` list only those paths and validator codes, never the rejected values, so logging the error cannot leak a password. --- ## Step 1: Define a validated DTO ```rust use serde::Deserialize; use rullst::Validate; #[derive(Debug, Deserialize, Validate)] pub struct CreateUserForm { #[validate(length(min = 3, max = 100))] pub name: String, #[validate(email)] pub email: String, #[validate(length(min = 12, max = 72))] pub password: String, } ``` `#[derive(Validate)]` expands to `::validator::…` paths, so the application needs a direct `validator` dependency. Projects generated by the v13 CLI already declare it; in an older project add the version Rullst uses: ```toml validator = { version = "0.21", features = ["derive"] } ``` `length` counts characters, while password hashing refuses more than 72 **bytes**: a 72-character password with accented or non-Latin characters passes this validator and then fails to hash. Check `form.password.len() <= 72` (bytes) in the handler as well, as the controller generated by `cargo rullst auth` does. Add a request-body limit at the router/proxy boundary as validation happens after body extraction. --- ## Step 2: Validate and hash before persistence The following handler assumes the `User` model (`src/models/user.rs`) that `cargo rullst auth` generates in [Tutorial 11](https://rullst.github.io/Rullst/book/tutorials/11-authentication-system.html): ```rust,ignore use rullst::{html, server::StatusCode, Markup, ValidatedForm}; use rullst::auth::hash_password_async; use rullst::db::RowSnapshot; use rullst::orm::chrono::Utc; pub async fn store( ValidatedForm(form): ValidatedForm, ) -> Result { // `length` counted characters; hashing refuses more than 72 bytes. if form.password.len() > 72 { return Err(StatusCode::UNPROCESSABLE_ENTITY); } let password_hash = hash_password_async(form.password) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // The ORM inserts every field, so the model carries the `timestamps()` // values instead of relying on the column defaults. let now = Utc::now(); let mut user = crate::models::user::User { id: 0, name: form.name.trim().to_owned(), email: form.email.trim().to_ascii_lowercase(), password_hash: Some(password_hash), oauth_provider: None, oauth_id: None, created_at: now, updated_at: now, // `#[orm(db_default)]` columns: inserts leave them to the database. is_admin: false, session_epoch: 0, snapshot: RowSnapshot::default(), }; user.save() .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(html! {

    "User created successfully"

    }) } ``` This is a continuation snippet because the generated model lives in the application crate. The controller generated by `cargo rullst auth` sets the timestamps the same way with its `utc_timestamp` helper and also rejects a duplicate email before inserting. The checked extractor implementation itself is covered by `rullst-core` tests. For a JSON endpoint, replace `ValidatedForm` with `ValidatedJson`; the DTO and handler body remain the same. --- ## Step 3: Send the CSRF token with browser forms Production/staging `Server` requests use the security baseline, including double-submit CSRF validation. A POST needs both the `rullst_csrf` cookie and its matching value in a form `_token` field or `X-CSRF-Token` header. Extract the request-scoped token on the page's GET handler. On a visitor's first request, this is the same token that the middleware will set in the response cookie; reading the incoming cookie alone would miss it. ```rust use rullst::{html, security::CsrfToken, server::Extension, Markup}; pub async fn message_form( Extension(csrf): Extension, ) -> Markup { html! {
    } } ``` Mount GET and POST handlers in the protected application and serve the pinned HTMX asset from the same origin. This is a form-rendering snippet, not a full messaging service: `/messages` still needs a bounded validated DTO, application authorization and the intended domain operation. The hidden field works with both a normal URL-encoded form submission and HTMX; no global cookie-forwarding script is required. JSON requests instead need the matching header, while the browser still sends the cookie. Do not forward tokens to third-party URLs. Serve form pages from ordinary paths. A `GET` for a path that looks like a static file never creates the token or sets the cookie: `/robots.txt`, `/sitemap.xml`, `/favicon.ico`, anything under `/static/`, and any path ending in `.txt`, `.xml`, `.ico`, `.json`, `.css`, `.js`, `.png`, `.jpg`, `.jpeg`, `.svg`, `.webp` or `.wasm`. Since 13.0.0-alpha.2 such a page still receives `CsrfToken` when the browser already has a valid `rullst_csrf` cookie from another page, but on a first visit there is none, and the `Extension` extractor fails with `500`. The development server does not automatically apply the production CSRF/WAF stack; its WAF only logs the requests production would refuse. For this extractor in development, explicitly compose `rullst::server::from_fn(rullst::security::csrf_middleware)` on the application router, as the generated form-enabled starters do. Nested CSRF composition with the production server is request-idempotent. Do not make the extractor optional and silently render an empty token. Exercise the production baseline over HTTPS before deployment; do not remove `Secure` to accommodate a production HTTP URL. ### Show failures instead of a silent spinner For the pinned HTMX 1.9.12 used by these examples, ordinary HTTP errors do not automatically replace the target content (`ValidatedForm` failures are the exception described above). Put the following in an application-owned, same-origin external script loaded with `defer`. This avoids requiring inline event handlers or weakening Content Security Policy: ```javascript const form = document.getElementById('message-form'); const status = document.getElementById('message-result'); if (form && status) { const showFailure = (event) => { if (event.detail.elt !== form) return; status.textContent = 'Message not sent. Reload the page and try again.'; }; for (const name of ['htmx:responseError', 'htmx:sendError', 'htmx:timeout']) { form.addEventListener(name, showFailure); } form.addEventListener('htmx:afterRequest', (event) => { // Validation fragments are swapped with 200 and this header. const rejected = event.detail.xhr.getResponseHeader('X-Rullst-Validation-Status'); if (event.detail.elt === form && event.detail.successful && !rejected) { const input = form.elements.namedItem('message'); if (input instanceof HTMLInputElement) input.value = ''; } }); } ``` Keep the message on failure so the visitor can retry. Never insert an arbitrary server error body using `innerHTML`. Resetting only after success is also clearer than relying on event-order assumptions. In HTMX 1.9.12, form values have already been collected before `htmx:beforeRequest`; clearing an input there alone does not establish that an empty message was transmitted. ### Diagnose a 403 before changing security policy | Response | Inspect | | --- | --- | | `CSRF token cookie missing` | The POST's Cookie header: the cookie may be absent, duplicated or malformed. Check that the preceding GET set it, that the browser accepted it, and that scheme/host/path and credential settings match. A hidden field cannot replace the cookie. | | `Invalid or missing CSRF token` | The form's `_token` value and URL-encoded content type. Confirm the input belongs to the submitted form and matches the current cookie. | | `Invalid CSRF token` | The explicit header, which takes precedence over the form. Remove a stale custom header rather than expecting the valid form field to override it. | | `Missing CSRF token: a multipart/form-data form must send its _token field before any file field …` | A file-upload form whose hidden `_token` input comes after a file input (or after 64 KiB of other fields). Move `_token` to the top of the form or send `X-CSRF-Token`; see [form uploads](https://rullst.github.io/Rullst/book/form-uploads.html#csrf). | Do not log token values or session cookies. `HX-Request: true` is not a security exemption. Public browser chat endpoints retain CSRF and need separate abuse, rate and cost limits. The existing `csrf_signed_webhook_paths` configuration is only for exact POST paths authenticated by mandatory signed-webhook validation; it is not an AI-chat allowlist and does not verify signatures by itself. Core's `htmx_csrf_contract` regression tests exercise first-visit cookie/form agreement, ordinary and HTMX submissions, preservation of form data and rejection before handler execution. These in-process HTTP tests do not certify a reverse proxy, browser cookie policy, a live AI provider or application auth. ## Key takeaways - Validation is not sanitization and not authorization; enforce all three at their respective boundaries. - Never store, log, or echo a plaintext password. Use the asynchronous Argon2id helper in request handlers. - Enforce a unique database index on normalized email rather than relying only on a pre-insert lookup. - Keep internal validation/database details out of production client errors. --- # 08. Controllers & Middlewares Source: https://rullst.github.io/Rullst/book/tutorials/08-routing-and-middlewares.html # Tutorial 08: Controllers, Routing & Middleware 🚦 Rullst's `Router` wraps Axum's router while preserving explicit Tower middleware composition. This example uses the Axum 0.8 request and `Next` types. --- ## Step 1: Create a custom middleware Generate the file with the CLI: ```bash cargo rullst make:middleware Logger ``` It writes `src/middlewares/logger_middleware.rs` with an empty `logger_middleware` function and registers the module. Fill in the body, for example: ```rust use rullst::web::axum::{ extract::Request, middleware::Next, response::Response, }; pub async fn logger_middleware(req: Request, next: Next) -> Response { let method = req.method().clone(); let path = req.uri().path().to_owned(); let response = next.run(req).await; // Prefer structured tracing in production; do not log query strings, // cookies, authorization headers, or request bodies by default. println!("[HTTP] {method} {path} -> {}", response.status()); response } ``` --- ## Step 2: Organize sub-routers ```rust,no_run use rullst::{Router, Server, ServerError}; use rullst::routing::{get, post}; use rullst::web::axum::middleware; async fn list_users() -> &'static str { "users" } async fn create_user() -> &'static str { "created" } async fn login() -> &'static str { "login" } // Stand-in for the generated `logger_middleware` of Step 1. async fn logger_middleware( request: rullst::web::axum::extract::Request, next: rullst::web::axum::middleware::Next, ) -> rullst::web::axum::response::Response { next.run(request).await } #[tokio::main] async fn main() -> Result<(), ServerError> { let api = Router::new() .route("/users", get(list_users).post(create_user)) .layer(middleware::from_fn(logger_middleware)); let auth = Router::new().route("/login", post(login)); let app = Router::new() .nest("/api/v1", api) .nest("/auth", auth); Server::new(app).run(3000).await } ``` In the application, import the generated function (`use crate::middlewares::logger_middleware::logger_middleware;`) and pass it to `from_fn` the same way. A layer covers only the routes added before it. Use `nest_axum` or `merge_axum` when integrating a third-party raw `axum::Router`. --- ## Step 3: Share application state `rullst::Router` wraps an `axum::Router<()>`, whose handlers cannot take `State`, so it has no `with_state`. Pass shared state in one of two ways: - **As a request extension.** `.layer(Extension(state))` inserts the value into every request, and handlers read it with `Extension`. Rullst's own middleware passes the CSRF token, the CSP nonce and the signed-in user id this way. Like every layer, it covers only the routes added before it. - **With Axum's `State`.** Build the routes that use `State` as an `axum::Router`, call its `with_state`, and mount the result with `merge_axum` (or `nest_axum` under a prefix). ```rust,no_run use rullst::{Router, Server, ServerError}; use rullst::routing::get; use rullst::web::axum::{self, Extension, extract::State}; use std::sync::Arc; #[derive(Clone)] struct AppState { site_name: Arc, } async fn home(Extension(state): Extension) -> String { format!("Welcome to {}", state.site_name) } async fn about(State(state): State) -> String { format!("About {}", state.site_name) } #[tokio::main] async fn main() -> Result<(), ServerError> { let state = AppState { site_name: Arc::from("Rullst") }; let stateful = axum::Router::new() .route("/about", get(about)) .with_state(state.clone()); let app = Router::new() .route("/", get(home)) .layer(Extension(state)) .merge_axum(stateful); Server::new(app).run(3000).await } ``` Keep values in the state cheap to clone (`Arc`, connection pools): each request clones it. --- ## Key takeaways - Middleware order is security-sensitive. Use the canonical production baseline for secure headers, CSRF, CORS, and WAF rather than assembling those controls ad hoc. - Authentication middleware establishes identity; handlers/repositories must still enforce resource ownership or role authorization. - Apply request-body and concurrency limits before handlers that parse expensive or attacker-controlled input. --- # 09. Environment Config Source: https://rullst.github.io/Rullst/book/tutorials/09-environment-config.html # Tutorial 09: Environment Management & Configuration ⚙️ Rullst provides a validated runtime environment shared by its subsystems. New projects use `RULLST_ENV`; `APP_ENV` remains a legacy compatibility alias. --- ## 🛠️ Step 1: Define Environment Variables in `.env` `cargo rullst new` already writes `.env` with `RULLST_ENV=development`, a random `APP_KEY` and the starter's `DATABASE_URL`, plus a `.env.example` with a placeholder key. A hand-written project adds the file itself: ```dotenv RULLST_ENV=development PORT=3000 APP_KEY=REPLACE_WITH_YOUR_32_CHAR_RANDOM_KEY DATABASE_URL=postgres://postgres:password@localhost:5432/my_app ``` Replace the placeholder with at least 32 bytes of random data, for example the output of `openssl rand -base64 32`: `rullst-auth`, which encrypts session cookies with `APP_KEY`, refuses shorter keys and the documented placeholders. `PORT` overrides the port passed to `Server::run`. --- ## 💻 Step 2: Access Configuration in Rust ```rust,no_run use rullst::config::RullstConfig; fn report_environment() -> Result<(), Box> { let environment = RullstConfig::global().environment()?; // The process environment first, then `./.env`, as `Server` reads it. let database_is_configured = rullst::config::project_setting("DATABASE_URL")?.is_some(); println!( "Booting Rullst in {environment} mode; database configured: {database_is_configured}" ); Ok(()) } ``` Do not log `DATABASE_URL`: it commonly embeds a username and password. Report only whether configuration is present, and redact sensitive fields in structured telemetry. The generated server bootstrap reads `.env` before applying its runtime policy. Neither `Server` nor the Artisan commands (`db:migrate`, `db:seed`, `studio`) load `.env` into the process environment, so `std::env::var` sees only real process variables there; read application settings with `rullst::config::project_setting`, which checks the process environment and then `./.env`, and behaves the same in the server and in a seeder. Standalone utilities must load a dotenv file themselves or receive exported process variables. Environment precedence is exact: the process `RULLST_ENV`, legacy `APP_ENV`, then `RULLST_ENV` or `APP_ENV` in `.env`, then `[app].env` in `Rullst.toml`. `Server` never exports `.env` into the process environment; `RullstConfig::environment()` includes the `.env` step once a `Server` has started in the process, so it reports the environment the server enforces. Before that, and in standalone utilities, it reads only the process variables and `[app].env`, as `Environment::detect` always does. Unknown values are configuration errors rather than silently becoming development. `rullst-auth` cookie helpers also consult `.env` below the process variables and skip reading it when either is set; a malformed `.env` yields a fixed error that never quotes file content. A malformed or mistyped `Rullst.toml` fails with only its line and column, so the error can be logged without exposing `app_key` or connection strings. --- ## Step 3: Configure the Browser Security Baseline `Server` applies the same public `apply_security_baseline` composition in staging and production. CORS is deny-by-omission: list exact origins without a trailing slash, and enable credentialed cross-origin requests only when the application genuinely needs them. ```toml [security] csrf_same_site = "Strict" cors_allow_origins = ["https://academy.example"] cors_allow_credentials = false csp = "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'nonce-{NONCE}'; style-src 'self' 'nonce-{NONCE}'" coep = "require-corp" ``` Wildcard, path-bearing, credential-bearing, queried or duplicate CORS origins are configuration errors, as are spellings a browser never sends: an uppercase scheme or host, or an explicit default port (`:443` for HTTPS, `:80` for HTTP). CORS compares the configured origin with the `Origin` header byte for byte. When credentials are enabled, Core still grants them only to an origin in the exact allowlist. Test the final policy behind the real TLS proxy because an intermediary can change headers and cookie behavior. `coep` accepts only `require-corp`, `credentialless`, or `unsafe-none`. Keep the strict `require-corp` default for same-origin assets. If an application must load reviewed cross-origin media whose server does not emit CORP, it can explicitly select `credentialless` and update the relevant CSP source directive; the browser then omits credentials from those cross-origin no-CORS requests. `unsafe-none` lowers cross-origin isolation and should be chosen only after an application threat-model review. COEP configuration never makes an arbitrary remote origin trustworthy. The same baseline adds `Cache-Control: no-store` to every response that has no `Cache-Control` header of its own, so pages with personal data are not stored by browsers or shared caches. Responses that should be cached, such as file downloads or calendar and RSS feeds, must set their own header; the standard `/static` mount already does. --- ## 💡 Key Takeaways - Never hardcode credentials, database passwords, or JWT secrets in `.rs` source code. - Add `.env` to `.gitignore` and commit `.env.example` to document required keys for team members. - Prefer `RULLST_ENV`; keep `APP_ENV` only while migrating an existing project. --- # 10. Static Assets & Compression Source: https://rullst.github.io/Rullst/book/tutorials/10-static-assets-and-compression.html # Tutorial 10: Static Assets & Pre-Compression 📦 The standard `Server` serves an existing `static/` directory at `/static`. Production builds can create Brotli and Zstandard sidecars for eligible text and Wasm assets in that directory. --- ## Step 1: Use the standard static directory ```text static/ ├── css/ │ └── app.css ├── js/ │ └── app.js └── favicon.svg ``` Reference those files through `/static/...`, for example `/static/css/app.css`. `Server::run` mounts the directory when it exists; no additional `ServeDir` layer is required for this standard path. ```rust,no_run use rullst::{routes, routing::get, Server}; #[tokio::main] async fn main() -> Result<(), Box> { let app = routes![get("/" => || async { "Rullst" })]; Server::new(app).run(3000).await?; Ok(()) } ``` If you mount a different directory manually through Axum/Tower, its routing and pre-compressed negotiation become application responsibilities. --- ## Step 2: Build production sidecars ```bash cargo rullst build ``` The release-mode command builds the application and creates `.br` and `.zst` siblings for `html`, `css`, `js`, `json`, `svg`, `wasm`, `xml`, and `txt` files under `static/`. The standard server negotiates Brotli through `ServeDir` and Zstandard through its static middleware. The server serves an existing sibling without comparing it with its source, so an asset edited after the last build would keep reaching browsers that accept `br` or `zstd` in its old form. `cargo rullst dev` therefore deletes every `.br`/`.zst` sibling that is not newer than its source when it starts and before each rebuild, and reports how many it removed. A sibling without a source file is kept, because it may be hand-made. Outside `cargo rullst dev`, rerun `cargo rullst build` after editing assets and before `docker build` copies `static/` into an image. The Zstandard middleware serves `name.zst` only when `Accept-Encoding` lists `zstd` with a quality above zero (`zstd;q=0` is a refusal) and the path below `/static/` has only plain segments. Paths with `.`, `..`, empty segments, backslashes or percent-encoding skip the `.zst` lookup and go to `ServeDir` uncompressed. `Content-Encoding: zstd` is added only to `2xx` and `304` responses. A hand-made `.zst` is used only for a type the middleware can label (the generated types plus `.htm`, `.mjs`, `.cjs`, `.map`, `.webmanifest`, `.csv`, `.pdf`, `.ttf` and `.otf`); for any other file the uncompressed asset is served, never a `.zst` body typed `application/octet-stream`. Verify deployed behavior rather than assuming negotiation worked: ```bash curl --compressed -I -H 'Accept-Encoding: br' \ http://127.0.0.1:3000/static/css/app.css curl -I -H 'Accept-Encoding: zstd' \ http://127.0.0.1:3000/static/css/app.css ``` Check `Content-Encoding`, `Content-Type`, cache headers, and `Vary` through the actual TLS proxy/CDN. Pre-compression avoids compression work per request; it does not eliminate file I/O or network latency. --- ## Step 3: Cache headers Since v13 the standard static mount sets `Cache-Control` itself (Rullst does not rename files; your asset tool does): | Request path below `/static/` | `Cache-Control` | | :--- | :--- | | Fingerprinted name: a `.`- or `-`-separated segment of 8–64 lowercase hex characters, with at least one digit and one letter, after the first part and before the extension (`app.3f9a2c1b.css`, `chunk-3f9a2c1b.js`, `app.3f9a2c1b.js.map`) | `public, max-age=31536000, immutable` | | Any other file (`app.css`, `report-20261008.pdf`) | `no-cache` | `no-cache` lets browsers and caches store the file but revalidate it before each use. `ServeDir` sends `ETag` and `Last-Modified` and answers a matching `If-None-Match` or `If-Modified-Since` with `304 Not Modified` and no body. The policy is set on `2xx` and `304` responses only, for every encoding (`br`, `zstd` or none). A missing file and every dynamic response keep the production security baseline's `Cache-Control: no-store`. An immutable file is never asked for again until it expires, so give an asset a new hashed name whenever its content changes, and do not hand-name a mutable file with a hash-like segment. Check the headers through your proxy or CDN: ```bash curl -sI http://127.0.0.1:3000/static/css/app.css | grep -i -E 'cache-control|etag' curl -sI -H 'If-None-Match: ""' \ http://127.0.0.1:3000/static/css/app.css # HTTP/1.1 304 Not Modified ``` ### Downloads and feeds Dynamic responses keep `Cache-Control: no-store` unless the route chooses a policy. Since 13.0.0-alpha.2, `rullst::cache_control` labels the `2xx` and `304` responses of a route that do not already have a `Cache-Control`; errors keep `no-store`: ```rust,no_run use rullst::{Router, cache_control, routing::get}; use std::time::Duration; # async fn calendar_feed() -> &'static str { "BEGIN:VCALENDAR" } # async fn latest_report() -> &'static str { "report" } let app = Router::new() // The same for every visitor: browsers and CDNs may keep it 5 minutes. .route( "/calendar.ics", get(calendar_feed).layer(cache_control::public(Duration::from_secs(300))), ) // A user's own download: only their browser may keep it for an hour. .route( "/reports/latest", get(latest_report).layer(cache_control::private(Duration::from_secs(3600))), ); ``` A handler can also return the policy with one response: `(CacheControl::private(Duration::from_secs(60)), body)`. Use `public` only when the response is the same for every visitor, because a shared cache serves one visitor's copy to the next. The CSRF middleware never sets its `rullst_csrf` cookie on a response that shared caches may store (`public` or `s-maxage` without `private` or `no-store`), so a feed fetched without cookies stays cacheable and never hands out a visitor's token. --- ## Key takeaways - Use `static/` for the framework's standard asset path and build integration. - Keep source files alongside generated sidecars in the deployed artifact. - Content-hashed names are cached for a year as `immutable`; other static files are revalidated with `ETag`/`Last-Modified`. --- # 11. Authentication Scaffolding Source: https://rullst.github.io/Rullst/book/tutorials/11-authentication-system.html # Tutorial 11: Authentication Scaffolding 🔒 The authentication generator creates a reviewable starting point for cookie-session login and registration. It is application source code, not a hosted identity service or a claim that every production policy is automatic. --- ## Step 1: Run the generator The SaaS starter already contains this code. In a Blank or API project with a database, such as the `shop` project of [Tutorial 02](https://rullst.github.io/Rullst/book/tutorials/02-cli-generators.html), run from the project root: ```bash cargo rullst auth ``` Those starters ship a minimal `users` migration (`src/migrations/m20260601000000_create_users_table.rs`), and the command refuses to add a second one. In a learning project whose `users` table holds no data, delete that migration file; if `cargo rullst dev` or `cargo rullst db:migrate` already ran, also delete the local SQLite database (`db.sqlite`, with any `db.sqlite-wal`/`db.sqlite-shm` beside it), which the next run recreates with every migration. Then run `cargo rullst auth` again. In an application with real accounts, keep the migration and add the columns with `cargo rullst make:migration` instead, as the error message says. The current command creates: - `src/models/user.rs` and `src/models/password_reset.rs`; - `src/migrations/m_create_users_table.rs`, `src/migrations/m_create_password_reset_tokens_table.rs` and the migrations module; - `src/controllers/auth_controller.rs` and `src/controllers/password_controller.rs`; - `src/middlewares/auth_middleware.rs`; - `src/pages/auth.rs`; - the corresponding module declarations, including `controllers`, `middlewares`, `models` and `pages` in `src/lib.rs` (or `src/main.rs`); and - a best-effort refresh of the generated `.llms.txt`, `.rullst/context-map.json` and `diagram.md` (a hand-written `diagram.md` is left unchanged). It also enables the `orm`, `auth` and `mail` umbrella features in `Cargo.toml`. It fails before writing anything when one of those files exists, when a `*_create_password_reset_tokens_table.rs` migration exists, or when a `*_create_users.rs`/`*_create_users_table.rs` migration already creates the users table (see above). Turso-primary projects are not supported, and neither is a project without a database migration runner (a `--no-database` starter): create the project with a database so `src/main.rs` declares `pub mod migrations;` and calls `rullst::artisan!(crate::migrations::get_migrations())`. It does not support an `auth --api` flag, and it does not silently register application routes. Review the generated diff before editing it. --- ## Step 2: Register the generated handlers Wire the generated view, submit, logout, and authenticated routes in the application router. The exact route tree is an application decision; a typical mapping is: ```rust,ignore use rullst::{routes, routing::post, server::from_fn}; use crate::controllers::auth_controller; use crate::middlewares::auth_middleware::auth_middleware; let public_auth = routes![ get("/login" => auth_controller::login_view), get("/register" => auth_controller::register_view), post("/logout" => auth_controller::logout), ] // Each credential submission runs Argon2id: budget it per client. .route("/login", post(auth_controller::login_submit) .layer(from_fn(auth_controller::credential_rate_limit))) .route("/register", post(auth_controller::register_submit) .layer(from_fn(auth_controller::credential_rate_limit))); let signed_in = routes![ get("/dashboard" => auth_controller::dashboard), // Ends the session on every device. post("/logout/everywhere" => auth_controller::logout_everywhere), ] .layer(from_fn(auth_middleware)); ``` Mount the password flows the same way: the reset routes are public, the change routes need `auth_middleware`, and every submission is rate limited. ```rust,ignore use crate::controllers::password_controller; let password_reset = routes![ get("/password/forgot" => password_controller::forgot_password_view), get("/password/reset" => password_controller::reset_password_view), ] // Reset requests send mail and resets run Argon2id. .route("/password/forgot", post(password_controller::forgot_password_submit) .layer(from_fn(auth_controller::credential_rate_limit))) .route("/password/reset", post(password_controller::reset_password_submit) .layer(from_fn(auth_controller::credential_rate_limit))); let password_change = routes![ get("/password/change" => password_controller::change_password_view), ] .route("/password/change", post(password_controller::change_password_submit) .layer(from_fn(auth_controller::credential_rate_limit))) .layer(from_fn(auth_middleware)); ``` Add each group to the starter's router in `router()` (`src/main.rs`) with `.merge_axum(public_auth.into_axum())` and so on, before its `.layer(rullst::server::from_fn(rullst::security::csrf_middleware))` line so that CSRF protection covers them. The SaaS starter's `router()` shows the same routes registered one by one. `credential_rate_limit` allows ten submissions per minute for each connecting peer. Behind a reverse proxy, configure `[security] trusted_proxies` so clients do not share the proxy's budget. Reset emails go through the `Mail` facade: set `MAIL_DRIVER` and `MAIL_FROM` for staging and production, and `RULLST_PUBLIC_ORIGIN` to the application's HTTPS origin (for example `https://app.example.com`), which every reset link uses. Links are never built from the request's `Host` header. Without `RULLST_PUBLIC_ORIGIN`, development links point to `http://localhost:3000`, and staging and production log an error instead of sending a link. In development the default mail driver logs only metadata, so a debug build without `MAIL_DRIVER` also prints the reset link to the terminal. The emails name your product `APP_NAME` (process environment, then `.env`) or, when it is unset, the `APPLICATION_NAME` constant at the top of `password_controller.rs`, which starts as the package name. They are written in the request's language: the locale `LOCALES.layer()` negotiated when the application uses `rullst::i18n`, otherwise the browser's `Accept-Language`, matched by language to English, Brazilian Portuguese or Spanish, otherwise `DEFAULT_MAIL_LOCALE` (English), which you can edit there too. The generated form pages expect the Core CSRF and CSP-nonce extensions. Apply the canonical security baseline and place authenticated routes behind the generated authentication middleware. --- ## Step 3: Migrate and test ```bash cargo rullst db:migrate cargo rullst dev ``` Before production, exercise registration, login, logout, sign-out everywhere, password change and reset, invalid credentials, duplicate email, expired/tampered/revoked cookies and reset links, CSRF failure, rate limiting, mail delivery from your provider, and key rotation. Configure a strong `APP_KEY` through a secret manager and HTTPS at the edge. --- ## What the scaffold currently enforces - Passwords are hashed with Argon2id on Tokio's blocking pool; plaintext is not written to the user model. At most four hashes or verifications run at once (each holds about 19 MiB); a submission that waits two seconds without capacity receives 503. Adjust `MAX_CONCURRENT_PASSWORD_WORK` to the host. - Registration accepts passwords of at least 12 characters and at most 72 bytes, and normalizes email. - Login performs a dummy password verification for unknown users to reduce the obvious account-enumeration timing difference. - Session values use authenticated encryption and are emitted as cookie headers through `rullst-auth` helpers. - The `users.session_epoch` column (0 for a new account) goes into every login cookie, and `auth_middleware` refuses a cookie whose epoch differs. `User::revoke_sessions` increments it in one `UPDATE`, ending every cookie of the account; the *Sign out everywhere* button (`logout_everywhere`) calls it. Plain logout clears only the current browser's cookie. Call `User::revoke_sessions` when you deactivate an account. See [encrypted sessions](https://rullst.github.io/Rullst/book/crates/auth.html#revocable-cookies-the-session-epoch). - **Change password** (`/password/change`, signed in) asks for the current password, the new one and a confirmation. A wrong current password gets the generic "The current password is incorrect". The new password follows the registration policy. `User::change_password` stores the new hash and increments the session epoch in one `UPDATE` that only applies while the account is still at the epoch of the asking cookie; that browser receives a cookie with the new epoch and every other device is signed out. A reset link requested before the change stops working. - **Forgot password** (`/password/forgot`) answers with the same page, after at least 400 ms, whether or not the email has an account. For an account it creates a `PasswordResetToken` (32 random bytes from the operating system), stores only its SHA-256 digest in `password_reset_tokens` with a 30-minute expiry, and emails the link in the background. The table keeps one token per account: a new request replaces the previous token, but requests for the same account less than 60 seconds apart send nothing. - **Reset link lifetime**: `RESET_TOKEN_LIFETIME_MINUTES` in `src/models/password_reset.rs` (30 as generated) sets it. Change it there: a compile-time assertion keeps it from 10 minutes to 24 hours (1440), and the stored expiry, the email ("This link expires in 30 minutes.", or "2 hours" for whole hours) and the forgot-password answer all follow it. Below 10 minutes, mail that a provider greylists or queues for a few minutes arrives after its link expired, and the user can only ask again. Above 24 hours, the link stays a working way into the account for as long as it sits in a mailbox that is synchronized to several devices, forwarded or read by someone else, while a day already covers a user who reads mail once a day. Single use and replacement by a newer request narrow that exposure but do not remove it, which is why the [OWASP Forgot Password Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html) asks for short-lived tokens. - **Reset password** (`/password/reset?token=…`) shows the form only for a live token, without caching and with `Referrer-Policy: no-referrer`. Submitting it deletes the token, stores the new hash and increments the session epoch in one transaction, so the link works once and every device is signed out. The user is not signed in: the page redirects to `/login?reset=done`. Both flows email a "password changed" notice. These controls do not replace application review. Add account verification, abuse controls, audit policy, MFA/passkeys, per-device session management, and privacy/retention behavior according to the product's threat model. A reset link proves only access to the mailbox; for high-value accounts, add a second factor to recovery. --- # 12. JWT vs Session Auth Source: https://rullst.github.io/Rullst/book/tutorials/12-jwt-and-session-auth.html # Tutorial 12: JWT and Cookie Sessions 🔑 Cookie sessions and bearer JWTs are transport choices with different operational trade-offs. Neither is automatically “for web” or “for mobile,” and neither removes the need for TLS, authorization, rotation, expiry, and revocation design. --- ## Step 1: Generate JWT middleware ```bash cargo rullst make:jwt ``` This creates `src/middlewares/jwt_middleware.rs`, registers its module, and adds the direct `jsonwebtoken`, `chrono`, and `serde` dependencies when missing. Configure a high-entropy secret plus exact issuer and audience values: ```bash export JWT_SECRET="$(openssl rand -base64 48)" export JWT_ISSUER="https://identity.example.test" export JWT_AUDIENCE="rullst-api" ``` The generated HS256 validator requires `sub`, `iss`, `aud`, `iat`, and `exp`, checks expiry, and rejects secrets shorter than 32 bytes, with fewer than 12 distinct characters, or starting with `mock_`. It reads these variables with `std::env::var` on each request, so they must be set in the process environment: a value only in `.env` is not seen, and while one is missing a request that carries a bearer token receives `500`. Keep the secret out of source control and logs. The same file provides `generate_token(user_id)`, which issues a token with these settings that expires after 24 hours. Call it after your own credential check, never for an unauthenticated caller. --- ## Step 2: Protect a route group ```rust,ignore use rullst::{Router, routing::get}; use rullst::web::axum::middleware; use crate::middlewares::jwt_middleware::jwt_middleware; pub fn protected_routes() -> Router { Router::new() .route("/profile", get(user_profile)) .route("/orders", get(user_orders)) .layer(middleware::from_fn(jwt_middleware)) } ``` Valid claims are inserted into request extensions. The handler must still map `sub` to a current account and enforce resource/tenant authorization. For long-lived systems, design key rotation and immediate revocation rather than assuming expiry alone is sufficient. --- ## Choosing deliberately - An `HttpOnly`, `Secure`, appropriately `SameSite` cookie reduces direct token reads by browser JavaScript. It does not neutralize XSS: injected code can still act as the user, and cookie requests need CSRF protection. - A bearer token is convenient for interoperable API clients, but storage in a browser or native app is an application security decision. A stolen bearer token can be replayed until rejected or expired. - Cookie and JWT strategies can coexist at different boundaries, but keep one authoritative identity/session lifecycle and test logout/revocation behavior. --- # 13. RBAC & IDOR Protection Source: https://rullst.github.io/Rullst/book/tutorials/13-rbac-authorization.html # Tutorial 13: RBAC, Ownership, and IDOR/BOLA Protection 🛡️ `rullst-security` provides role, owner, and tenant checks over a trusted `UserContext`. The framework cannot infer ownership from a route parameter: the application must load the resource and pass its stored owner/tenant identifiers to the guard. --- ## Step 1: Authorize a trusted user context ```rust use rullst_security::{RbacGuard, SecurityError, UserContext}; pub fn authorize_admin(user: &UserContext) -> Result<(), SecurityError> { RbacGuard::authorize(user, "admin") } ``` Construct `UserContext` only after authentication. Roles, permissions, and tenant membership must come from trusted server-side state, not request headers or JSON supplied by the caller. In a project with the [`cargo rullst auth`](https://rullst.github.io/Rullst/book/tutorials/11-authentication-system.html) scaffold, `auth_middleware` inserts the signed-in account id as `Extension`; build the context with `UserContext::new(user_id.to_string(), roles)` from roles your application stores. Generated projects already depend on `rullst-security`. --- ## Step 2: Check the resource record, not request ownership ```rust use rullst_security::{RbacGuard, SecurityError, UserContext}; pub struct DocumentAccess { pub owner_user_id: String, pub tenant_id: String, } pub fn authorize_document_update( user: &UserContext, stored: &DocumentAccess, ) -> Result<(), SecurityError> { RbacGuard::authorize_tenant_owner_or_role( user, &stored.tenant_id, &stored.owner_user_id, "document-editor", ) } ``` Load `DocumentAccess` with a parameterized query by the route's document ID. Do not accept `owner_user_id` or `tenant_id` from the update payload. The tenant guard is evaluated first and roles — including `admin` — do not bypass a tenant mismatch. For particularly sensitive paths, make the database query itself tenant-scoped and then apply the guard as a second boundary. Return the same not-found/forbidden shape where revealing resource existence would leak information. --- ## Key takeaways - `authorize` checks a role (and recognizes the framework's `admin` role). - `authorize_owner_or_role` is safe only when the owner ID came from trusted resource state. - Use `authorize_tenant_owner_or_role` for tenant-bound resources. - A helper contributes to IDOR/BOLA prevention only when every parameterized resource route invokes it or an equivalent scoped repository policy. --- # 14. RASP Security Layer Source: https://rullst.github.io/Rullst/book/tutorials/14-rasp-security-layer.html # Tutorial 14: RASP — Runtime Application Self-Protection ⚡ `rullst-security::rasp` applies bounded heuristic signatures to request targets, non-secret headers, and supported textual bodies. Its current signatures cover common SQL injection, path traversal, SSRF, shell/RCE, and JNDI indicators. It does not claim general exploit detection. The `powershell` keyword is not matched against the stock PowerShell product token in `User-Agent` (`PowerShell/7.4.1`, `WindowsPowerShell/5.1…`), so `Invoke-WebRequest` and `Invoke-RestMethod` clients are served, while execution syntax such as `powershell -enc` or `powershell.exe` in that header still blocks. --- ## 🛠️ Step 1: Mount `RaspSecurityLayer` in `main.rs` ```rust,no_run use rullst::web::axum::Router; use rullst_security::rasp::RaspSecurityLayer; use rullst::Server; #[tokio::main] async fn main() -> Result<(), rullst::ServerError> { let app = Router::new() // ... routes .layer(RaspSecurityLayer::default()); Server::new(app.into()).run(3000).await } ``` --- ## 🧪 Step 2: Test Malicious Attack Payload Interception Send a percent-encoded `UNION SELECT` payload in the query string: ```bash curl -i "http://localhost:3000/api/users?query=1%27%20UNION%20SELECT%20password%20FROM%20users" ``` For a recognized bounded signature, the layer returns `403 Forbidden` and adds a process-local event to `SecurityStore`. A Studio instance running in the same process can display that event at `http://127.0.0.1:5555/studio/security`. The body inspector accepts identity-encoded UTF-8 text, JSON, form, and XML media types up to 1 MiB. It fails closed for oversized declared bodies and encoded textual bodies that it cannot inspect. Put an independent request-body limit outside this layer as well. --- ## 🚧 What RASP does not inspect - `Cookie` and `Authorization` headers, `multipart/form-data` uploads and binary bodies. - Payloads hidden by double percent-encoding, HTML entities, SQL comments between keywords, alternative syntax or splitting across several fields. - Anything without a listed signature. It also refuses ordinary text that contains a signature, such as a forum post that quotes `../` or `/bin/sh`. Test your own forms and API payloads with the layer mounted. In staging and production, `Server` already runs the Core WAF, which overlaps with RASP and buffers the same body. See [which security layer to use, and when](https://rullst.github.io/Rullst/book/security-layers.html#request-inspection-core-waf-vs-rasp) before adding both. ## 💡 Key Takeaways - Inspection has runtime cost and uses bounded pattern heuristics, with possible false positives and false negatives. - RASP is defense in depth; parameterized SQL, validation, authorization, body limits, and dependency review remain required. --- # 15. Vault & Field Encryption Source: https://rullst.github.io/Rullst/book/tutorials/15-rullst-vault-encryption.html # Tutorial 15: Vault and ORM field encryption Rullst can encrypt `String` and `Option` model fields before they reach the database and decrypt them when a generated ORM query loads the model. The implementation uses AES-256-GCM with a fresh random nonce and an authenticated, versioned envelope. ## 1. Configure a current key Generate 32 random bytes and store them as a prefixed base64 value. Do not commit this value to source control: ```bash export RULLST_ENCRYPTION_KEY="base64:$(openssl rand -base64 32)" export RULLST_ENCRYPTION_KEY_ID="production-2026-01" ``` `RULLST_ENCRYPTION_KEY` accepts `base64:`, `hex:`, or a legacy raw value containing exactly 32 UTF-8 bytes. A secret manager or KMS-backed deployment adapter should inject it at runtime. Rullst does not provide key custody. `RULLST_ENCRYPTION_KEY`, `RULLST_ENCRYPTION_KEY_ID` and `RULLST_ENCRYPTION_KEYRING` are each read from the process environment first and, when the process does not set them, from `.env` in the current working directory, the precedence `APP_KEY` and `rullst::config::project_setting` use. The file is read on each lookup and never loaded into the process environment. In development a line such as `RULLST_ENCRYPTION_KEY=base64:...` in the project's `.env` is therefore enough; keep `.env` out of version control and inject the key through the environment in production. ## 2. Mark model fields ```rust use rullst_orm::FromRow; #[derive(Clone, Debug, FromRow, rullst_orm::Orm)] #[orm(table = "users")] pub struct User { pub id: i32, pub email: String, #[orm(encrypted)] pub tax_id: String, #[orm(encrypted)] pub recovery_note: Option, } ``` Generated `save`, `update_partial`, `find`, `all`, query-builder loads and streams preserve plaintext in the Rust model while storing an envelope shaped like this in the SQL column: ```text RULLST:v2::: ``` The table and column names are authenticated as additional data. Copying a ciphertext into a different annotated column therefore fails decryption. Generated secondary copies do not undo that protection. `to_json()`, audit rows and committed-event payloads (including Redis `orm:events:*` messages) carry the fixed marker `"***"` instead of the value, and Scout documents omit the field. `save_to_redis` writes the same authenticated envelope as the SQL column and `get_from_redis` decrypts it, while the `.remember(...)` query cache stores the ciphertext read from the database. Audit rows, indexes and Redis hashes written before this behavior may contain plaintext: purge, reindex or rewrite them. ### `SecretString` fields `rullst_orm::SecretString` is an alternative for a field that should be encrypted by its SQLx codec and redacted from `Debug`. Its serde form is never plaintext: `Serialize` writes a `RULLST:v2` envelope under the current key (and fails without one), and `Deserialize` decrypts such an envelope with the current key or keyring while still accepting an ordinary string as input. Generated audit, event and search projections write `"***"` instead, and the `.remember(...)` query cache stores the envelope. A `#[derive(Serialize)]` on the model therefore produces ciphertext for this field; call `reveal_audited()` where the plaintext is deliberately required. ## 3. Rotate a key without downtime Set the new current key and keep old readable keys in a JSON keyring: ```bash export RULLST_ENCRYPTION_KEY="base64:" export RULLST_ENCRYPTION_KEY_ID="production-2027-01" export RULLST_ENCRYPTION_KEYRING='{ "production-2026-01": "base64:" }' ``` Reads select the key named by the envelope. A normal full `save()` rewrites all annotated values with the current key. Keep every key needed by existing rows until a separately monitored migration has rewritten and verified them; then remove the retired key. ## 4. Understand query limits AES-GCM encryption is randomized, so the same plaintext produces different ciphertexts. Generated queries reject encrypted fields in `WHERE`, `ORDER BY`, `GROUP BY`, and explicit `SELECT` clauses rather than silently returning wrong results. `pluck_string` supports non-null encrypted strings; load the model for nullable encrypted strings. For lookup, add a separate application-designed blind-index column and assess its equality-leakage and key-rotation trade-offs. Rullst does not generate a blind index automatically. Raw SQL is an explicit escape hatch and does not automatically encrypt bindings or decrypt arbitrary projections. ## 5. Reduce secret lifetime in memory `VaultSecret` redacts `Debug`/`Display` and calls `Zeroize` on the wrapped value when dropped: ```rust use rullst_security::VaultSecret; fn use_api_key() { let Ok(value) = std::env::var("EXAMPLE_PROVIDER_KEY") else { return; }; let api_key = VaultSecret::new(value); send_request(api_key.expose_secret()); } # fn send_request(_: &str) {} ``` Zeroization is defense in depth, not secure memory. It cannot erase prior copies or prevent a debugger, core dump, swap, allocator behavior, or another process-memory capture while the secret is live. ## Operational checklist - Back up the database and keyring together, with separate access controls. - Test restore and rotation before retiring any key. - Restrict environment and crash-dump access. - Never log plaintext model fields or expose them through serialization by accident. Generated `to_json()` already replaces encrypted fields with `"***"`; add `#[orm(hidden)]` when a field must be omitted entirely. A `#[derive(Serialize)]` on the model is your own serializer and still emits the decrypted value. - Treat authentication failure as possible corruption, wrong context, wrong key, or tampering; do not replace it with an empty value. --- # 16. LiveView Server-Driven UI Source: https://rullst.github.io/Rullst/book/tutorials/16-liveview-server-driven-ui.html # Tutorial 16: LiveView Server-Driven UI (`rullst::live`) ⚡ Build a per-connection Rust component that receives JSON events and sends rendered HTML over WebSockets. The browser still needs the HTMX WebSocket extension (or a compatible client transport). This tutorial documents the legacy per-connection API. For the v13 prerelease candidate with explicit authorization, revision conflicts, bounded reconnection and recovery after process restart, use [Recoverable Live UI](https://rullst.github.io/Rullst/book/live-recovery.html). Its browser module and application trait are separate; `make:live` does not automatically supply that contract. --- ## 🛠️ Step 1: Scaffold a LiveComponent ```bash cargo rullst make:live CounterComponent ``` This creates `src/live/counter_component.rs` and declares it in `src/live/mod.rs`. Declare `mod live;` in the crate root yourself; the command does not edit `src/main.rs` or `src/lib.rs`. --- ## 💻 Step 2: Implement the Component Lifecycle Edit the generated `src/live/counter_component.rs`, for example: ```rust use rullst::async_trait; use rullst::live::LiveComponent; use serde_json::Value; #[derive(Default)] pub struct CounterComponent { pub count: i32, } #[async_trait] impl LiveComponent for CounterComponent { async fn mount(&mut self) { self.count = 10; } async fn handle_event(&mut self, payload: Value) { if let Some(action) = payload.get("action").and_then(|v| v.as_str()) { match action { "increment" => self.count += 1, "decrement" => self.count -= 1, _ => {} } } } fn render(&self) -> String { format!( r#"

    Counter: {}

    "#, self.count ) } } ``` --- ## 💻 Step 3: Mount Component in a Controller This route fragment belongs in the same application module where `CounterComponent` is in scope: ```rust,ignore use rullst::html::Markup; use rullst::live::Live; use crate::live::counter_component::CounterComponent; // `Live::mount` returns `Markup`: return it as the page, or interpolate it // into an `html!` layout as `{component}`. pub async fn page_handler() -> Markup { Live::mount::("/ws/counter").await } ``` Register the page and the matching WebSocket route, for example in the `routes![...]` list of `router()`: ```rust,ignore use rullst::live::live_ws_handler; let app = rullst::routes![ get("/counter" => page_handler), get("/ws/counter" => live_ws_handler::), ]; ``` The page must also load HTMX and its WebSocket extension (see the takeaways). The command's own hint uses `/ws/counter_component`; any path works as long as `Live::mount` and the route name the same one. --- ## 💡 Key Takeaways - Event payloads travel over WebSocket connections; `render()` produces updated HTML fragments. - The current component state lives in one socket task. Authentication, authorization, reconnect/replay, backpressure and multi-process state remain application concerns. - `live_ws_handler` accepts WebSocket frames and messages of at most 64 KiB; a larger message closes the session before it is parsed. Origin checks, connection caps and idle timeouts are still route-level policy. - Include and pin the HTMX WebSocket extension; Rullst does not inject that browser dependency automatically. --- # 17. Real-Time WebSockets & Presence Source: https://rullst.github.io/Rullst/book/tutorials/17-websockets-realtime-chat.html # Tutorial 17: In-process realtime and presence Rullst Core provides bounded in-process broadcast channels and presence state. These primitives do not create an HTTP WebSocket endpoint by themselves and do not synchronize independent server processes. ## Subscribe before publishing ```rust use std::sync::Arc; use rullst::realtime::{BroadcastManager, RealtimeError, RealtimeMessage}; async fn local_exchange() -> Result { let manager = Arc::new(BroadcastManager::new()); let mut receiver = manager.get_or_create("room:general").subscribe(); manager.publish( "room:general", "message.created", r#"{"sender":"alice","content":"hello"}"#, )?; receiver .recv() .await .map_err(|error| RealtimeError::BroadcastError(error.to_string())) } ``` `Channel` uses `tokio::sync::broadcast`: a slow receiver can lag, and publishing without any receiver returns `RealtimeError::BroadcastError`. The registry keeps a channel only while it has a subscriber or a caller holds the `Arc` from `get_or_create`. Publishing never creates a channel, and idle channels are released by a publish that finds no receivers or by an amortized sweep when new channels are created; a released name gets a new channel on its next `get_or_create`. `PresenceTracker` counts connections: call `user_left` once for every `user_joined` (for example when each socket closes), so a user with two open tabs stays online until both close. It removes a room when its last user leaves. ## Bind channels and presence to an authenticated tenant Construct `TenantContext` only from trusted authentication/membership state. The wrappers validate logical names and ensure that identical room names use different backend namespaces: ```rust use std::sync::Arc; use rullst::realtime::{ BroadcastManager, PresenceTracker, TenantPresence, TenantRealtime, }; use rullst::security::TenantContext; fn publish_for_school() -> Result<(), Box> { let context = TenantContext::try_new("school-alpha")?; let realtime = TenantRealtime::from_context( Arc::new(BroadcastManager::new()), &context, ); let presence = TenantPresence::from_context( Arc::new(PresenceTracker::new()), &context, ); let mut receiver = realtime.subscribe("course/42")?; presence.user_joined("course/42", "learner-7")?; realtime.publish("course/42", "lesson.completed", r#"{"lesson_id":9}"#)?; assert_eq!(presence.count_online("course/42")?, 1); assert!(receiver.try_recv().is_ok()); Ok(()) } ``` The tenant wrapper limits names and payloads (64 KiB), but the application still owns authentication, room-level authorization, connection lifecycle and replay. Use `rullst::live::live_ws_handler` for the separate per-connection LiveComponent flow ([Tutorial 16](https://rullst.github.io/Rullst/book/tutorials/16-liveview-server-driven-ui.html)), or build an Axum WebSocket handler around these primitives. A distributed transport is still roadmap work. --- # 18. Wasm Islands in Pure Rust Source: https://rullst.github.io/Rullst/book/tutorials/18-wasm-islands-rust.html # Tutorial 18: Wasm Island foundation Rullst can generate a dual-target Island function: native builds emit a host element with serialized props, while `wasm32-unknown-unknown` builds export a hydration function. ## Generate the component ```bash cargo rullst make:island InteractiveChart ``` The generated `src/islands/interactive_chart.rs` uses the supported macro: ```rust use rullst::island; #[cfg(target_arch = "wasm32")] use wasm_bindgen::{closure::Closure, JsCast}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct InteractiveChartProps { pub initial_value: i32, } #[island] pub fn interactive_chart(props: InteractiveChartProps) { #[cfg(not(target_arch = "wasm32"))] { format!( "", props.initial_value, props.initial_value ) } #[cfg(target_arch = "wasm32")] { let mut value = props.initial_value; element.set_text_content(Some(&format!("Count: {value}"))); let button = element.clone(); let closure = Closure::::new(move || { value = value.saturating_add(1); button.set_text_content(Some(&format!("Count: {value}"))); }); if element .add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()) .is_ok() { closure.forget(); } String::new() } } ``` The `element` binding in the Wasm block is supplied by `#[island]`. The command also declares the module in `src/islands/mod.rs`, adds `pub mod islands;` to `src/lib.rs` (creating that file when it is absent), and adds any missing `serde`, `serde_json`, `wasm-bindgen` and `web-sys` dependencies. On the server the island function returns `rullst::html::Markup`: the component's HTML wrapped in its `data-island` element. Place it in a page by interpolating the call inside `html!`, as `{interactive_chart(props)}`; it is inserted as HTML, not escaped. The module belongs to the library target (`src/lib.rs`), so the starters' `src/main.rs` imports it through the package name, with `-` written as `_`: `use ::islands::interactive_chart::{interactive_chart, InteractiveChartProps};`. ## Build and load the artifact ```bash cargo rullst build:client ``` The command parses `Cargo.toml`, adds `cdylib` to the `[lib]` `crate-type` array when needed (writing `["cdylib", "rlib"]` when the key is absent), installs/checks the Wasm target and `wasm-bindgen-cli`, builds the library, locates the artifact using `lib.name` or `package.name`, writes bindings under `static/`, and generates the `static/rullst-islands.js` hydration orchestrator. Review these manifest, network, and toolchain side effects in CI and pin the required tools for reproducible releases. Load the orchestrator from the page with ``, as the command output shows. This is a useful foundation, not a complete frontend framework: routing, application state, accessibility, CSP-compatible asset delivery, cache busting, error reporting and browser E2E remain application/release work. --- # 19. SaaS Billing with Capital Source: https://rullst.github.io/Rullst/book/tutorials/19-saas-billing-capital.html # Tutorial 19: SaaS billing with Rullst Capital 💳 This tutorial creates a checkout adapter boundary and explains how to feed the optional local Studio view without treating it as an accounting system. ## 1. Scaffold and configure the selected adapter ```bash cargo rullst make:billing --model Workspace ``` The command detects a relational SQLx or Turso-primary project, adds the exact `orm` and `capital` facade features once, generates a reversible matching migration, registers the models/controller/page modules, and refuses to overwrite an earlier billing scaffold. The generated runtime supports the Stripe live integration or explicit offline fixtures; `BILLING_PROVIDER` accepts only `stripe`. To bill through another gateway, implement the Capital provider traits in the application as described in [Writing your own payment provider](https://rullst.github.io/Rullst/book/capital-custom-provider.html). v13 removed the other built-in adapters; see the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html). The generated Stripe flow persists an opaque local owner, account/test-live scope, immutable customer and checkout intents, session IDs, and event receipts. Signed Checkout/subscription notifications reconcile current provider state. Database revision fencing prevents delayed reads from replacing newer state, and event completion shares the subscription transaction. Configure `BILLING_PROVIDER=stripe`, `BILLING_ACCOUNT_ID`, `BILLING_API_KEY`, `BILLING_WEBHOOK_SECRET`, an HTTPS `BILLING_REDIRECT_URL`, and the explicit `BILLING_ALLOWED_PLAN_IDS` recurring-price allowlist in the process environment or the project's `.env` (in v13 the generated code reads the process environment first, then `.env`, through `rullst::config::project_setting`). Follow the generated `BILLING.md` for webhook event selection, pinned API version, CSP, recovery and migration details. Keep credentials outside source control. Test and live keys use separate persisted namespaces; mixed real/mock credentials fail closed. Live keys also require `BILLING_LIVE_ACKNOWLEDGEMENT=I_UNDERSTAND_REAL_CHARGES`; without it the generated live path stays unavailable. Existing controllers are application-owned: merge the generated modules and add the billing-state/event tables through a new migration. Updating a dependency does not rewrite existing controllers or safely backfill legacy customer ownership. ## 2. Create a checkout session Mount the generated authenticated checkout POST behind session/tenant ownership and CSRF verification. The form supplies only an allowlisted plan. The generated handler persists the intent before dispatch and the session ID before its HTTP 303 redirect. Repeated requests retrieve and reuse an open session. Older unknown outcomes use read-only recovery rather than new keys after provider retention. When calling the low-level adapter directly, persist and authorize the same customer/attempt boundary explicitly: ```rust,no_run use rullst_capital::{CapitalError, StripeCheckoutRequest, StripeProvider}; async fn checkout_url( stripe: &StripeProvider, persisted_attempt: &StripeCheckoutRequest, ) -> Result { let session = stripe.create_subscription_checkout(persisted_attempt).await?; // Persist session.id() and its digest before returning the URL to the browser. Ok(session.url().to_owned()) } ``` Email is optional contact data, never the webhook owner key. The ID-bound portal uses the same persisted customer. The application supplies authentication and membership, configured products/prices, retention, entitlement/settlement policy, and actual provider-account sandbox acceptance. Navigation and session creation never grant paid access. ### 2.1 Handle provider failure without blindly repeating a charge Built-in live adapters return a redacted `CapitalError::Provider` for outbound request construction, transport, HTTP status, bounded-response, JSON, and response-contract failures. Inspect its stable class for telemetry or a durable job decision: ```rust use rullst_capital::{CapitalError, ProviderFailureClass}; fn provider_disposition(error: &CapitalError) -> Option { match error { CapitalError::Provider(failure) => Some(failure.class()), _ => None, } } ``` Do not turn `Transient` or `RateLimited` into an unconditional loop. Persist the original command and idempotency identity, confirm that the selected operation actually forwards that identity, cap attempts with backoff/jitter, and reconcile signed provider events. Checkout creation in the legacy unified trait does not accept an application idempotency key, so reconcile before repeating it. The shared client already applies finite timeouts, disables redirects and ambient proxies, caps JSON to one MiB, and validates returned checkout URLs as absolute credential-free HTTPS. ## 3. Make a bounded immediate charge when checkout is not the right flow For a payment method already tokenized and authorized for off-session reuse at Stripe, the model deriving `Billable` can perform one fully specified charge: ```rust,no_run use rullst_capital::{Billable as _, CapitalError, StripeProvider}; async fn charge_saved_method( account: &(impl rullst_capital::Billable + Sync), stripe: &StripeProvider, ) -> Result { let receipt = account .charge_with( stripe, 2_500, "BRL", "cus_from_authoritative_state", "pm_from_authoritative_state", "order_2026_0001-attempt_1", ) .await?; Ok(receipt.charge_id().to_string()) } ``` This deliberately is not `charge(amount)`: currency, provider customer, tokenized payment method and retry identity cannot be inferred safely. The Stripe adapter uses Payment Intents with immediate off-session confirmation and the provider idempotency header. Only `succeeded` and `processing` are accepted; an amount/currency mismatch or a flow requiring customer action fails closed. Credentials beginning with `mock_` return the same deterministic receipt with the distinct non-success `ChargeStatus::Mock` for an exact retry. The mock is not a mandate, durable idempotency store or live sandbox test. Other adapters return `UnsupportedOperation` until reviewed individually. ## 4. Render and deliver the invoice only after final success Enable `capital-mail` on the umbrella crate (or `invoice-pdf` on Capital plus `capital-invoice` on Mail). Build the invoice from authoritative order state, then bind it to the returned receipt: ```rust,no_run use chrono::Utc; use rullst::capital::{ChargeReceipt, Invoice, InvoiceItem}; use rullst::mail::PaidInvoiceDelivery; async fn deliver_invoice(receipt: &ChargeReceipt) -> Result<(), Box> { let invoice = Invoice { invoice_id: "INV-2026-0001".to_string(), customer_email: "customer@example.com".to_string(), date: Utc::now(), items: vec![InvoiceItem { description: "Pro subscription".to_string(), amount: 25.00, }], total: 25.00, currency: "BRL".to_string(), }; let paid = invoice.bind_succeeded_charge(receipt)?; // Real providers require a sender on a domain verified for the account. let delivery = PaidInvoiceDelivery::prepare(&paid)?.from("billing@example.com")?; // In production, atomically claim this stable key in a durable outbox. let _delivery_key = delivery.delivery_key(); delivery.send().await?; Ok(()) } ``` The binding rejects `Processing`, `Mock`, a mismatched recipient, amount or currency. The default PDF is paginated, bounded to sixteen MiB and supports WinAnsi text (including common Portuguese characters); pass a checked TTF/OTF to Capital for other scripts. Mail applies its mandatory pre-flight before the facade queues or sends the HTML message and attachment; `from` re-runs it for the verified sender, which every real transport requires: none invents one. This helper does not subscribe to webhooks by itself. Reconcile the provider event, build the authoritative invoice and insert `delivery_key` under a unique database constraint in the same application workflow. Mail delivery remains at least once: a crash and retry can still require provider/application deduplication. ## 5. Report metered usage without confusing provider identities Use the provider-specific static trait for new code. Stripe Meter Events need a customer ID and configured event name, not a subscription-item ID: ```rust,no_run use rullst_capital::{ CapitalError, MeteredBillingProvider as _, StripeMeterEvent, StripeProvider, }; async fn report_ai_exercises(stripe: &StripeProvider) -> Result<(), CapitalError> { let event = StripeMeterEvent::new( "cus_from_authoritative_state", "ai_exercises", 3, "usage:school-7:attempt-99", )?; let receipt = stripe.report_metered_usage(&event).await?; assert_eq!(receipt.quantity(), 3); Ok(()) } ``` Stripe receives the identifier but enforces it only within a rolling window, so keep a durable application record of submitted events. A custom adapter whose provider receives no event key reports `UsageDeduplication::ApplicationOutboxRequired`; durable application deduplication is then mandatory. The adapter caps and binds responses, while provider sandbox/live acceptance, retry, invoice reconciliation and entitlement updates remain release and application work. Empty or `mock_*` API keys produce a deterministic `UsageStatus::Mock`, never billable evidence. ## 6. Verify webhooks before business processing Mount `rullst_capital::verify_webhook` on the exact provider callback route as shown in the [Capital crate guide](https://rullst.github.io/Rullst/book/5-rullst-capital.html). Apply any CSRF exemption only to that exact signed route. Never update access or subscription state from an unverified request. The default replay store protects one process. For multiple processes, `rullst-capital/webhook-sql` provides a bounded SQL ledger for SQLite, PostgreSQL, MySQL, and MariaDB. Its middleware form claims the signed payload before dispatch and therefore does not guarantee exactly-once processing after a crash. When a subscription mutation must be atomic, use the provider's verified stable event ID with `SqlWebhookReplayStore::check_and_record_event_key_with_transaction` in the same database transaction as that mutation; do not also pre-claim that event through SQL middleware. See the [payment guide](https://rullst.github.io/Rullst/book/payment-gateways-guide.html#3-cryptographically-verified-webhook-endpoint) for setup and operational boundaries. ## 7. Use a bounded subscription handle and grace period ```rust,no_run use rullst_capital::{Billable as _, CapitalError, GracePeriod, StripeProvider}; async fn pause_with_local_policy( workspace: &impl rullst_capital::Billable, provider: &StripeProvider, ) -> Result<(), CapitalError> { let grace = GracePeriod::new(1_900_000_000, 1_900_604_800)?; let handle = workspace .subscription_with(provider)? .with_grace_period(grace); handle.pause().await } ``` The grace value does not schedule the pause or grant access by itself. Persist it with authoritative subscription state, evaluate it against a trusted clock inside the entitlement check, and confirm the selected adapter's live pause or cancel semantics. The same statically dispatched handle validates coupon IDs and gives the historical relative-trial API its intended meaning: ```rust,no_run use rullst_capital::{Billable as _, CapitalError, StripeProvider}; async fn grant_a_retention_offer( workspace: &impl rullst_capital::Billable, stripe: &StripeProvider, command_created_at: i64, ) -> Result<(), CapitalError> { let subscription = workspace.subscription_with(stripe)?; subscription.apply_coupon("RETENTION_25").await?; // Fifteen whole days. Persist command_created_at and reuse it on retries. subscription .extend_trial_days_at(15, command_created_at) .await } ``` `extend_trial(15)` uses the current UTC time for interactive convenience. Workers should persist their command time and use `extend_trial_days_at` so a retry sends the identical expiration; `set_trial_end` is the explicit absolute timestamp operation. Stripe has the reviewed live coupon path and reviewed trial-update protocol fixtures; adapters without those contracts fail explicitly. Authorize the subscription owner before building the handle, serialize conflicting updates, and reconcile the signed provider webhook because trial changes can affect billing anchors and charges. ## 8. Enforce one shared workspace quota before creation Enable `quota-sql` directly, or `capital-quota-sql` on the umbrella crate. The authenticated middleware must first establish the active `TenantContext`; do not build a billing subject from an arbitrary header or request field. `Billable::quota_request` reads the limit from the subscription owner's `tier_limit` implementation. A limit of `Some(0)` (a plan that grants none of the feature) returns `QuotaError::LimitExceeded`, the same denial as a used-up limit. Give every attempted creation a stable event key, normally the ID of the application command/request rather than a random value generated on every retry. ```rust,no_run use rullst::{ capital::{Billable as _, BillingSubject, QuotaError, SqlQuotaStore}, security::TenantContext, }; async fn create_project( workspace: &impl rullst::capital::Billable, tenant: &TenantContext, quotas: &SqlQuotaStore, project_id: &str, ) -> Result> { let subject = BillingSubject::from_tenant(tenant)?; let request = workspace.quota_request( subject, "projects", format!("create-project:{project_id}"), 1, )?; let mut transaction = quotas.pool().begin().await?; let grant = match quotas .reserve_with_transaction(&mut transaction, &request) .await { Ok(grant) => grant, Err(QuotaError::LimitExceeded { .. }) => { transaction.rollback().await?; return Ok(false); } Err(error) => { transaction.rollback().await?; return Err(error.into()); } }; if grant.is_replay() { transaction.rollback().await?; return Ok(true); } let inserted = rullst::orm::sqlx::query( "INSERT INTO projects (id, workspace_id) VALUES (?, ?)", ) .bind(project_id) .bind(tenant.tenant_id.as_str()) .execute(&mut *transaction) .await; if let Err(error) = inserted { transaction.rollback().await?; return Err(error.into()); } transaction.commit().await?; Ok(true) } ``` The placeholder above is SQLite/MySQL syntax; use `$1`, `$2` for a raw PostgreSQL insert, or use the ORM operation that participates in the same transaction. `SqlQuotaStore` uses a unique event claim plus a conditional counter update, so concurrent members cannot both pass the last available unit. An exact retry returns `is_replay()` without consuming again; reusing the same key with different units or a different limit fails closed. For work that cannot share the SQL transaction, `QuotaGate::execute` still blocks the callback before an over-limit/replayed operation and releases the reservation after an ordinary callback error. A process crash between a standalone reservation and the external side effect is intentionally conservative and needs application reconciliation; the framework never risks exceeding the quota to guess whether that external effect happened. ## 9. Supply an optional local revenue snapshot `RevenueDashboardManager` does not derive money or subscribers from event names. After durable reconciliation, the application may call `update_metrics` with its authoritative snapshot and `record_event` with a bounded inspection record. The standalone Studio can display that process-local source at `/studio/capital` when it is explicitly connected. It neither auto-discovers webhook routes nor auto-syncs the application database. Provider-hosted checkout normally keeps card collection away from the application, but the final data boundary depends on the selected provider flow, application logs, analytics, and deployment. --- # 20. Background Jobs & Task Queues Source: https://rullst.github.io/Rullst/book/tutorials/20-background-jobs-queues.html # Tutorial 20: Background Jobs & Task Queues ⚙️ Rullst Core provides named JSON jobs, SQLite and Redis storage drivers, and a bounded-concurrency worker. Job handlers are closures registered against a stable name; there is no `Job` trait or process-global `Queue::dispatch` API. --- ## Step 1: Scaffold a handler module ```bash cargo rullst make:worker EmailWorker ``` This creates `src/workers/email_worker.rs`, registers it in `src/workers/mod.rs` (declared in `src/main.rs`), and provides `start_workers(worker)`, which registers every generated handler and starts the worker. Replace the generated log-only body with the application operation. Avoid logging the complete job payload, because it can contain personal or secret data. --- ## Step 2: Create a queue and keep its worker handle alive `Queue::sqlite` needs the `queue-sqlite` feature of `rullst`, which the starters do not enable: add `"queue-sqlite"` to the `features` list of the `rullst` dependency in `Cargo.toml`. The example below registers its handler by hand; with the generated module, pass the `Worker` to `workers::start_workers` instead. ```rust,no_run use rullst::queue::{Queue, QueueError, Worker, WorkerHandle}; use serde_json::json; fn start_email_worker(queue: &Queue) -> Result { let mut worker = Worker::new(queue) .max_concurrency(8) .poll_interval(250); worker.register("email", |payload| async move { let recipient = payload .get("recipient") .and_then(|value| value.as_str()) .ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::InvalidData, "missing recipient") })?; // Call an idempotent mail service with a stable delivery key here. let _ = recipient; Ok::<(), Box>(()) }); worker.run() } async fn configure() -> Result<(Queue, WorkerHandle), QueueError> { let queue = Queue::sqlite("sqlite://queue.db?mode=rwc").await?; let handle = start_email_worker(&queue)?; queue .dispatch( "email", json!({"recipient": "learner@example.test", "delivery_key": "welcome:42"}), ) .await?; Ok((queue, handle)) } ``` Dropping `WorkerHandle` stops processing. On graceful shutdown, call `handle.shutdown().await` and inspect its typed error. It stops polling and cancels running handlers; the SQLite and Redis drivers requeue each job whose handler was actually cancelled, while a handler that finished first keeps its own result. The worker never cancels a claim in flight: graceful shutdown waits for the current `pop`, and a job claimed after shutdown was requested is requeued instead of dispatched. Dropping the handle aborts immediately, so such a claim returns only through stalled-lease recovery. Use `Queue::redis(redis_url)` with the `queue-redis` feature when independent processes must share work. SQLite is durable local state and supports atomic claims, but it is not a distributed queue. --- ## Failure, retry, and delivery semantics - A handler error, panic, or timeout is recorded as failed. Core does not automatically retry it with exponential backoff. - `retry_failed_job(job_id)` explicitly returns a failed job to pending state. - SQLite and Redis recover stale processing leases and support scheduled jobs, but execution time is the first worker poll after the due timestamp. - Queue processing is at least once around crashes and external side effects. Give provider operations stable idempotency keys and reconcile ambiguous outcomes. - Successful payloads are deleted by default. SQLite completed history is an explicit bounded opt-in with its own privacy/access-control obligations. See the [Redis Architecture Guide](https://rullst.github.io/Rullst/book/redis-guide.html) for deployment boundaries. --- # 21. Explicit Local AI with Ollama Source: https://rullst.github.io/Rullst/book/tutorials/21-local-ai-ollama.html # Tutorial 21: Explicit Local AI with Ollama 🤖 Use an explicit `OllamaProvider` when a workload must not fall through to a configured cloud provider. `AiClient::auto()` is a convenience fallback chain; it is not an isolation policy when cloud API keys are also present. You need the `ai` feature of `rullst` (`cargo rullst new --ai`, or `cargo rullst add ai` in an existing project) and an Ollama server you run and provision yourself. --- ## Step 1: Configure the local endpoint Add both settings to the process environment or the project's `.env`: ```dotenv OLLAMA_HOST=http://127.0.0.1:11434 OLLAMA_MODEL=llama3:8b ``` Rullst never loads `.env` into the process environment, so the code below reads them with `rullst::config::project_setting` (the process environment first, then `.env`), the same way the generated starters read their own settings. Bind Ollama to loopback or a controlled private interface. Pull and license the chosen model (`ollama pull llama3:8b`) through a reviewed provisioning step rather than silently during a request. --- ## Step 2: Construct the provider explicitly ```rust,no_run use rullst::ai::{AiClient, AiError, providers::ollama::OllamaProvider}; /// Reads a setting from the process environment, then `.env`. fn required_setting(name: &str) -> Result { rullst::config::project_setting(name) .map_err(|error| AiError::ConfigError(error.to_string()))? .ok_or_else(|| AiError::ConfigError(format!("{name} is required"))) } pub async fn summarize_lesson(text: &str) -> Result { let host = required_setting("OLLAMA_HOST")?; let model = required_setting("OLLAMA_MODEL")?; let client = AiClient::new(OllamaProvider::new(host, model)); client .prompt(&format!( "Summarize this lesson in three factual bullet points:\n{text}" )) .await } ``` A missing setting returns `AiError::ConfigError`, and a server that cannot be reached returns `AiError::RequestError`; neither falls back to another provider. The high-level client applies Rullst's mandatory prompt/PII guardrails before dispatch. Model output remains untrusted: escape it for HTML, validate structured data, and never use an LLM verdict as the only authentication, authorization, malware, or abuse-control decision. --- ## Isolation checklist - Confirm the resolved endpoint, host firewall, container network, DNS, proxy, and telemetry configuration. - Remove cloud-provider credentials from the process when policy requires local only; an explicit provider prevents fallback but least privilege still helps. - Review application/Ollama logs, model caches, backups, crash dumps, and swap for sensitive data. - Empty or `mock_*` Ollama hosts select deterministic offline mock behavior; a mock response is test evidence, not evidence that a live model ran. “Local” does not prove an air gap. Verify the complete deployed data flow. --- # 22. RAG Systems & Vector Search Source: https://rullst.github.io/Rullst/book/tutorials/22-rag-vector-search.html # 22. RAG Systems & Vector Search > [!IMPORTANT] > Dependency examples pin the v13 prerelease `=13.0.0-alpha.4` exactly: Cargo > never selects a prerelease on its own, and the v12 line reached end of life > on 16 October 2026 (see the [release record](https://rullst.github.io/Rullst/book/v12.html)). Commit Cargo.lock > for reproducible builds. Use path dependencies only when intentionally > testing checkout-local changes. Rullst provides three deliberately separate vector paths: a deterministic in-memory index in `rullst-ai`, parameterized PostgreSQL `pgvector` queries, and a bounded Qdrant HTTP adapter in `rullst-orm`. None silently invent tenant authorization, context budgets, an embedding model, or a production RAG policy. ## In-memory retrieval `VectorIndex` is useful for bounded local datasets and tests: ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["ai"] } serde_json = "1.0" ``` ```rust use rullst::ai::VectorIndex; let mut index = VectorIndex::new(); index.add( "rullst", vec![1.0, 0.0, 0.0], serde_json::json!({"text": "Rullst is a Rust framework suite."}), ); index.add( "other", vec![0.0, 1.0, 0.0], serde_json::json!({"text": "An unrelated document."}), ); let matches = index.search(&[0.9, 0.1, 0.0], 3); assert_eq!(matches[0].1.id, "rullst"); ``` The caller supplies the embedding and the limit. This process-local index is not durable, distributed, tenant-aware, or an approximate-nearest-neighbor service. ## PostgreSQL + pgvector Enable the typed vector and concrete PostgreSQL paths: ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = [ "orm-pgvector", "strict-postgres", "ai", ] } # `#[derive(Orm)]` and `FromRow` expand to `rullst_orm::…` and `::sqlx::…` # paths, so a crate that derives models depends on both directly. rullst-orm = { version = "=13.0.0-alpha.4", default-features = false, features = ["strict-postgres"] } sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "tls-rustls", "postgres"] } ``` Projects generated with `cargo rullst new --database postgres` already list the `rullst-orm` and `sqlx` lines; add `orm-pgvector` and `ai` to their `rullst` features. Install the extension through a reviewed PostgreSQL migration and choose the dimension used by your embedding provider: ```sql CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE knowledge_chunks ( id SERIAL PRIMARY KEY, tenant_id TEXT NOT NULL, content TEXT NOT NULL, embedding vector(1536) NOT NULL ); ``` The model can use the re-exported typed `Vector`: ```rust,no_run use rullst::orm::{FromRow, Orm, Vector}; #[derive(Clone, Debug, FromRow, Orm)] #[orm(table = "knowledge_chunks", tenant_column = "tenant_id")] struct KnowledgeChunk { id: i32, tenant_id: String, content: String, embedding: Vector, } ``` Generate the query embedding through a configured guarded AI client, establish the authenticated tenant scope, and then use the native operators: ```rust,no_run use rullst::ai::AiClient; use rullst::orm::{FromRow, Orm, Vector, with_tenant}; # #[derive(Clone, Debug, FromRow, Orm)] # #[orm(table = "knowledge_chunks", tenant_column = "tenant_id")] # struct KnowledgeChunk { # id: i32, # tenant_id: String, # content: String, # embedding: Vector, # } # async fn retrieve( # client: &AiClient, # question: &str, # ) -> Result, Box> { let embedding = client.embed(question).await?; let query: Vec = embedding.iter().map(|value| f64::from(*value)).collect(); let chunks = with_tenant("tenant-42", async move { KnowledgeChunk::query() .where_similar("embedding", query.clone(), 0.8) .order_by_cosine_distance("embedding", query) .limit(5) .get() .await }) .await?; # Ok(chunks) # } ``` The two clauses use different metrics: `where_similar` keeps rows whose L2 distance (`<->`) is below the bound, while `order_by_cosine_distance` sorts by cosine distance (`<=>`). `order_by_l2_distance` and `order_by_inner_product` (`<#>`) are also available; choose the bound for the L2 metric. Vector and distance values are SQL bindings, not interpolated literals. Column names use the normal identifier validation, vectors must be finite/non-empty, and the distance must be finite and non-negative. The `pgvector` feature also supplies SQLx encode/decode for the typed field. Rullst's live contract creates the extension, inserts typed vectors, and executes L2 and cosine queries against a digest-pinned `pgvector/pgvector` container. ## Qdrant dense-vector store Use Qdrant when the application deliberately chooses a specialized external vector service rather than keeping vectors in PostgreSQL: ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = [ "orm-qdrant", "ai", ] } serde_json = "1.0" ``` ```rust,no_run use rullst::orm::{ QdrantConfig, QdrantStore, VectorCollectionName, VectorDimensions, VectorPoint, VectorQueryLimit, VectorRepository, }; use serde_json::{Map, Value}; # async fn qdrant_example() -> Result<(), Box> { let config = QdrantConfig::new( std::env::var("QDRANT_URL").unwrap_or_default(), std::env::var("QDRANT_API_KEY").unwrap_or_default(), ); let vectors = QdrantStore::connect_or_mock(config)?; let collection = VectorCollectionName::new("knowledge-v1")?; vectors .create_collection(&collection, VectorDimensions::new(3)?) .await?; let mut payload = Map::new(); payload.insert("chunk_id".into(), Value::String("chunk-42".into())); vectors .upsert( &collection, VectorPoint::new(42, vec![1.0, 0.0, 0.0], payload)?, ) .await?; let matches = vectors .search( &collection, &[0.9, 0.1, 0.0], VectorQueryLimit::new(5)?, ) .await?; # let _ = matches; # Ok(()) # } ``` Empty or `mock_*` endpoint/API-key values select the deterministic in-process backend. An explicit `QdrantConfig::unauthenticated_local` is available only for loopback self-hosting. The live contract is deliberately limited to one unnamed dense cosine vector per numeric point. It bounds identifiers, dimensions, finite/non-zero-norm vectors, 1 MiB object payloads, `top-k`, request and response memory, deadlines and redirects. It does not claim named, sparse or multivectors, arbitrary filters, hosted availability, ANN index tuning or tenant authorization. A digest-pinned Qdrant lifecycle proves the supported operations. ## Orchestrate one bounded RAG operation The compatibility `build_rag_prompt` helper only formats already-authorized text. Prefer `RagPipeline` when the application needs one typed operation for embedding, retrieval, context budgets, guarded generation, source metadata, and mandatory auditing: ```rust,no_run use rullst::ai::rag::{RagPipeline, RagRetriever}; # fn compose(client: rullst::ai::AiClient, retriever: R, audit: A) # where # R: RagRetriever, # A: rullst::ai::rag::RagAuditSink, # { let pipeline = RagPipeline::new(client, retriever, audit); # let _ = pipeline; # } ``` The retriever receives a trusted tenant context and must enforce authoritative tenant and ownership predicates in its datastore query. The pipeline also rejects differently tagged documents and refuses ungrounded generation when no safe context remains. Follow the complete [Tenant-Bound RAG tutorial](https://rullst.github.io/Rullst/book/tutorials/41-tenant-bound-rag.html) for the offline index, production adapter boundary, and secret-minimized audit contract. The application still owns embedding dimension/model compatibility, durable ingestion and deletion, citation evaluation, index tuning, authorization, output policy, observability, and recovery. --- # 23. Process Telemetry & Prometheus Source: https://rullst.github.io/Rullst/book/tutorials/23-rullst-radar-prometheus.html # Tutorial 23: Process Telemetry & Prometheus Exporter 📡 Monitor process uptime, RSS memory, CPU, Tokio scheduler-yield latency, and active tasks using Rullst Radar (`rullst::radar`) and Prometheus (`GET /metrics`). --- ## 🛠️ Step 1: Mount Prometheus Exporter In `src/main.rs`: ```rust,no_run use rullst::radar::radar_metrics_router; use rullst::{Router, Server, ServerError}; #[rullst::runtime::main] async fn main() -> Result<(), ServerError> { let app = Router::new().merge_axum(radar_metrics_router()); // Exposes GET /metrics Server::new(app).run(3000).await } ``` In a generated project, add `.merge_axum(rullst::radar::radar_metrics_router())` to the router that `main` passes to `Server::new`, then run `cargo rullst dev` and open `http://localhost:3000/metrics`. --- ## 📊 Step 2: Prometheus Metrics Scrape Output Query `GET /metrics`: ```text # HELP rullst_uptime_seconds Process uptime in seconds. # TYPE rullst_uptime_seconds counter rullst_uptime_seconds 120 # HELP rullst_memory_rss_bytes Process RSS memory consumption in bytes. # TYPE rullst_memory_rss_bytes gauge rullst_memory_rss_bytes 24536678 # HELP rullst_cpu_usage_percent Process CPU utilization percentage. # TYPE rullst_cpu_usage_percent gauge rullst_cpu_usage_percent 1.25 # HELP rullst_tokio_active_tasks Total active Tokio tasks count. # TYPE rullst_tokio_active_tasks gauge rullst_tokio_active_tasks 4 # HELP rullst_tokio_latency_microseconds Observed Tokio scheduler yield latency in microseconds. # TYPE rullst_tokio_latency_microseconds gauge rullst_tokio_latency_microseconds 42 ``` The CPU gauge needs an earlier sample in the same process, so the first collection omits it. In a debug build the generated starters also start Studio, whose Radar page shows the same process measurements: `http://127.0.0.1:5555/studio/radar`. `/metrics` is not authenticated by `radar_metrics_router()`. Restrict it with a private network, service-mesh policy, or reviewed authentication middleware; process and runtime measurements can disclose operational information. --- ## 💡 Key Takeaways - The response is a point-in-time local snapshot and allocates its text body. - Linux and Windows expose supported process RSS/CPU probes. Tokio task data is available only inside a Tokio runtime; unsupported probes are omitted. - The scheduler-yield observation is not a universal request-latency target. --- # 24. Interactive Scalar API Docs Source: https://rullst.github.io/Rullst/book/tutorials/24-scalar-api-docs.html # Tutorial 24: Interactive Scalar API Documentation 📖 Scaffold a Scalar OpenAPI documentation router served at `/docs`. --- ## 🛠️ Step 1: Scaffold Scalar Docs ```bash cargo rullst make:scalar ``` Run it in the project root. It generates `src/controllers/docs_controller.rs` and declares it in `src/controllers/mod.rs` (creating both when they are missing) and refuses to overwrite an existing controller. The command does not edit route registration: the application must merge the router returned by `docs_controller::router()`. --- ## 💻 Step 2: Mount in Application `docs_controller::router()` returns `scalar_docs_router("/openapi.json")`. This self-contained example mounts the same router: ```rust,no_run use rullst::{Router, Server}; use rullst::scalar::scalar_docs_router; #[rullst::runtime::main] async fn main() -> Result<(), rullst::ServerError> { let app = Router::new().merge_axum(scalar_docs_router("/openapi.json")); Server::new(app).run(3000).await } ``` In a generated project, declare `mod controllers;` in `src/main.rs` if it is not there yet (the Blank and API starters have no `controllers` module), and merge the router where `main` builds the server; in the Blank and API starters that is `Server::new(router().merge_axum(controllers::docs_controller::router()))`. Merge it there rather than inside `router()`: each starter's `router()` adds `headers_middleware`, whose default Content Security Policy blocks the page's remote and inline scripts, which leaves `/docs` blank. --- ## 📄 Step 3: Generate the specification and open the page ```bash cargo rullst generate:openapi cargo rullst dev ``` `generate:openapi` scans the project's routes and writes `openapi.json` in the project root. Open `http://localhost:3000/docs` in your browser to try the endpoints interactively. --- ## 💡 Key Takeaways - The current page loads a version-pinned Scalar asset from jsDelivr. A failed CDN load shows only a status page naming the OpenAPI document; it is not an offline interactive UI. - The status-only fallback prints the configured OpenAPI location as text; it does not create an executable link from the configured value. - In staging and production `rullst::Server` applies the same security headers to every route, so the default CSP blocks the remote and inline assets there. Vendor the asset and integrate the page with the application's nonce/hash policy before using it outside local development. - The router serves `/openapi.json` from the `openapi.json` file in the process working directory (`cargo rullst generate:openapi` writes it there); if it is missing or malformed, the endpoint fails with `503 Service Unavailable` instead of fabricating an empty specification. Validate the release artifact in CI. --- # 25. Kubernetes Deployment & Probes Source: https://rullst.github.io/Rullst/book/tutorials/25-kubernetes-deployment.html # Tutorial 25: Kubernetes Manifests & Health Probes ☸️ `cargo-rullst` can generate a Kubernetes starter set. The files contain project name/port defaults and must be reviewed for the target cluster, registry, secrets, storage, network policy, ingress, workload identity, and security policy before deployment. --- ## Step 1: Generate the starter manifests ```bash cargo rullst make:k8s ``` The command writes `deployment.yaml`, `service.yaml`, `configmap.yaml`, `hpa.yaml`, `ingress.yaml`, and `all-in-one.yaml` under `k8s/`. It refuses to run when any of those files already exists, so move customized manifests aside to regenerate them, and review the diff. Resource names, the image and the ingress host use the `[package]` name as a lowercase RFC 1123 label, so a package named `my_app` becomes `my-app`; tag your image with the same name. Replace the placeholder `image: :latest` with an immutable registry reference (preferably a digest). The generated ConfigMap contains non-secret settings only; use a Kubernetes Secret/external secret manager for credentials. The Ingress selects `ingressClassName: nginx` and has a `tls` section, so cert-manager's ingress-shim asks the `letsencrypt-prod` ClusterIssuer for a certificate and stores it in the `-tls` Secret. Replace the placeholder `.local` host in both `tls.hosts` and `rules` with a public DNS name you control (an ACME issuer cannot certify `.local`), and create the issuer or change the annotation. Without TLS, the production configuration's Secure session cookies, HSTS and Nexus Basic Auth over verified TLS do not work. --- ## Step 2: Mount lifecycle-aware health routes Projects created by `cargo rullst new` already merge `rullst::health::health_router()` into their router, so the generated probes answer from the first deployment: `/health` and `/ready` both return `200` while the process serves requests. To report startup, dependency and drain state on `/ready` instead, replace that merge with the lifecycle-aware router: ```rust,no_run use rullst::{ApplicationLifecycle, Router, Server}; use rullst::health::{health_router_with_lifecycle, init_health_boot_time}; #[tokio::main] async fn main() -> Result<(), Box> { init_health_boot_time(); let lifecycle = ApplicationLifecycle::with_required_components(["database", "queue"])?; // Set these only after the application's own bounded checks succeed. lifecycle.set_component_ready("database", true)?; lifecycle.set_component_ready("queue", true)?; let app = Router::new() .merge_axum(health_router_with_lifecycle(lifecycle.clone())); Server::new(app) .with_lifecycle(lifecycle) .run(3000) .await?; Ok(()) } ``` `/health` remains process-only so an external database outage does not create a restart loop. `/ready` returns `503` during startup, while any registered component is unavailable, during drain, and after stop. The JSON carries only aggregate counts; it never emits the component labels or their error messages. The `version` field of both probes is the Rullst framework version. To see which build of your application a Pod runs, use `health_router_with_lifecycle_and_version(lifecycle.clone(), env!("CARGO_PKG_VERSION"))` (or `health_router_with_version` instead of `health_router`), which adds that string as `app_version`. The component registry is immutable, accepts at most 32 validated labels, and does not execute probes by itself. The application must perform bounded, timeout-protected checks and update each bit. `Server` marks startup complete after binding, closes new application admission before graceful shutdown, and waits through Axum for accepted requests. `run_with_shutdown` accepts a supervisor future when OS signals are not the desired trigger. This is one process contract. Kubernetes removes an unready Pod from service according to its own timing; Rullst does not coordinate replica consensus, load-balancer propagation, dependency failover, `preStop`, or the Pod's `terminationGracePeriodSeconds`. Measure those together in staging. The legacy `health_router()` remains the simpler process-only pair when dependency-aware admission is not requested. --- ## Step 3: validate before applying ```bash kubectl apply --dry-run=client -f k8s/all-in-one.yaml kubectl diff -f k8s/all-in-one.yaml kubectl apply -f k8s/all-in-one.yaml ``` The generated HPA uses `autoscaling/v2` and requires a working resource metrics pipeline. Validate against the actual cluster version and admission policies. --- ## Release checklist - add pod/container security contexts compatible with the reviewed image user; - define CPU/memory requests and limits from measurements; - add PodDisruptionBudget, topology spread/anti-affinity, NetworkPolicy, and service account/workload identity as required; - terminate TLS with a configured issuer and real hostname; - provision durable storage only where application state truly needs it; and - verify probes, graceful termination, migrations, rollback, and autoscaling in a staging cluster. --- # 26. Guided Container Deployment Source: https://rullst.github.io/Rullst/book/tutorials/26-one-click-paas-deploy.html # Tutorial 26: Guided Container Deployment 🚀 `cargo rullst deploy` prepares the files you need to run your application on any Linux server (a virtual machine from any provider, or your own machine) with Docker or Podman. It writes them into your project and does not contact any provider; you review them and run them yourself. | File | Purpose | | :--- | :--- | | `Dockerfile` | Multi-stage build with a dependency-cache layer and a small non-root runtime image. | | `compose.yaml` | Your application and Caddy (automatic HTTPS), a private network and volumes. Works with `docker compose` and `podman compose`. | | `Caddyfile` | The domain Caddy serves and the proxy to the application. | Step-by-step guides that use these files: [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html), with provider pages for [Hetzner Cloud](https://rullst.github.io/Rullst/book/deploy-hetzner.html), [DigitalOcean](https://rullst.github.io/Rullst/book/deploy-digitalocean.html), [Oracle Cloud](https://rullst.github.io/Rullst/book/deploy-oracle-cloud.html) and [Google Cloud](https://rullst.github.io/Rullst/book/deploy-google-cloud.html). For Nix, Buildah, Podman Quadlet or Kubernetes see [Advanced deployment](https://rullst.github.io/Rullst/book/advanced-deployment.html). The CLI no longer has provider-specific targets (Fly.io, Railway, Render) or the Foundry SSH pipeline; container platforms can still build the generated `Dockerfile`, but their configuration is yours to write and review. --- ## Before the first deploy These staging and production defaults apply to every way of deploying: - **Migrations are a separate step by default.** `Server::run` only opens the database pool, and `cargo rullst dev` applies pending migrations only in development. Run the application binary's `db:migrate` command once per release, before the new version serves traffic: `/app/ db:migrate` in the generated image (it has no shell), or `cargo run --release -- db:migrate` from a checkout. Since 13.0.0-alpha.2 the server can instead apply them at startup: see [startup migrations](https://rullst.github.io/Rullst/book/tutorials/26-one-click-paas-deploy.html#startup-migrations) and the [container lifecycle boundary](https://rullst.github.io/Rullst/book/tutorials/26-one-click-paas-deploy.html#container-lifecycle-boundary). - **Bind address.** With `RULLST_ENV=staging` or `production`, the server listens on `0.0.0.0` unless `HOST` (or `RULLST_HOST`) is set in the process environment or `.env`. Behind a reverse proxy on the same host, set `HOST=127.0.0.1` so the plain-HTTP port is reachable only through the proxy. Inside a container keep `0.0.0.0` and publish the port only to the proxy's network. The server prints the address it listens on at startup (`Rullst framework serving on http://0.0.0.0:3000 (every network interface)`); since 13.0.0-alpha.2, when that is every interface and a trusted reverse proxy is configured (`[security] trusted_proxies` or `Server::trusted_proxies`), the same line suggests `HOST=127.0.0.1`. - **Caching.** The security baseline adds `Cache-Control: no-store` to every response without a `Cache-Control` of its own; the standard `/static` mount sets its own. Downloads, feeds and other responses that browsers or a CDN may cache choose a policy with `rullst::cache_control::public(max_age)` or `private(max_age)` (13.0.0-alpha.2) on their route, or set the header themselves; see [downloads and feeds](https://rullst.github.io/Rullst/book/tutorials/10-static-assets-and-compression.html#downloads-and-feeds). --- ## Running the generated files ```bash cargo rullst deploy ``` Then follow [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html). ### Client addresses behind the proxy Every deployment puts a reverse proxy in front of the application, so the socket peer of each request is the proxy. Rate limits, such as the SaaS starter's ten login/registration submissions per client per minute, would then treat every visitor as one client. The generated Caddy container has a fixed address on the private network in `compose.yaml`, and `cargo rullst deploy` lists that address in `Rullst.toml` (an existing `trusted_proxies` list is left unchanged, with a warning): ```toml [security] trusted_proxies = ["172.31.250.10"] ``` Keep `compose.yaml` and `Rullst.toml` in step, and change the subnet if it overlaps a network on the host. For any other proxy or platform, list the networks it connects from, as documented by that platform. List only proxy networks: any host inside them can choose the client address. ### Container lifecycle boundary The `Dockerfile` written by `cargo rullst deploy`, `cargo rullst dockerize` or `cargo rullst new --docker` builds a non-root runtime image with production host/port defaults and local assets, but never an application secret. For a SQLite project (the starters' default) the image sets `DATABASE_URL=sqlite:///app/data/rullst.db?mode=rwc`, so the database lives in `/app/data`; configure the target platform so that directory remains owned by UID/GID 10001 when attaching persistent storage (the generated `compose.yaml` mounts a volume there). Apply migrations in one bounded pre-deployment job and start application replicas only after it succeeds. Running migrations inside every replica is opt-in ([startup migrations](https://rullst.github.io/Rullst/book/tutorials/26-one-click-paas-deploy.html#startup-migrations)), not the default. Starters created by `cargo rullst new` mount `rullst::health::health_router()`, which answers `/health` and `/ready`. An application created otherwise must mount those routes before configuring probes or monitoring; a redirect to login is not a health signal. ### Startup migrations Since 13.0.0-alpha.2 the server can apply pending migrations itself, after it opens the database pool and before it binds its port. It is off by default. Register the migrations and turn it on in code, per deployment, or both: ```rust,ignore Server::new(router()) .with_migrations(crate::migrations::get_migrations()) .auto_migrate(true) // or leave it out and set RULLST_AUTO_MIGRATE=true .run(3000) .await?; ``` `RULLST_AUTO_MIGRATE` (`true` or `false`; `1`/`0`, `yes`/`no` and `on`/`off` also work), from the process environment and then `.env`, overrides `auto_migrate`, so a deployment that runs `db:migrate` as a separate job can set `RULLST_AUTO_MIGRATE=false`. Projects that `cargo rullst new` generates with an SQLx database (Blank, API and SaaS) already call `with_migrations` but not `auto_migrate`, so setting `RULLST_AUTO_MIGRATE=true` is enough; a project generated before 13.0.0-alpha.2 needs the line above in `src/main.rs`. A Turso starter applies its migrations with its own `db:migrate` command and registers none with the server, so `RULLST_AUTO_MIGRATE=true` there stops startup with `startup migrations failed: no migrations are registered; call Server::with_migrations`: leave it unset and run `db:migrate`. Replicas that start together apply each migration once: the runner holds a PostgreSQL advisory lock or a MySQL/MariaDB `GET_LOCK` lock, and for an SQLite file an exclusive lock on `.rullst-migrate.lock` (left on disk), while it reads and applies the pending migrations; the others wait and then find nothing to do. If a migration fails, or migrations are enabled without a registry or a database, the server does not start: `run` returns `ServerError::Migration` and the process exits with that error. A migration must still be safe for the previous release, which keeps serving until the new replicas are ready. --- # 27. Typed Dependency Injection Source: https://rullst.github.io/Rullst/book/tutorials/27-zero-cost-di.html # Tutorial 27: Typed Dependency Injection (`rullst::di`) 💉 Rullst DI uses Rust types as keys and avoids runtime reflection metadata. It is not literally zero-cost: registration uses a map, resolution performs a type-indexed lookup/downcast, and each injection clones an `Arc`. --- ## Step 1: Register services without embedding secrets in source ```rust,no_run use std::sync::Arc; use rullst::di::Container; use rullst::security_runtime::VaultSecret; pub struct PaymentGateway { api_key: VaultSecret, } impl PaymentGateway { pub fn is_configured(&self) -> bool { !self.api_key.expose_secret().is_empty() } } pub struct UserService { pub gateway: Arc, } pub fn configure_di() -> Result, std::env::VarError> { let key = std::env::var("PAYMENT_API_KEY")?; let mut container = Container::new(); let gateway = Arc::new(PaymentGateway { api_key: VaultSecret::new(key), }); container.register_arc(Arc::clone(&gateway)); container.register(UserService { gateway }); Ok(Arc::new(container)) } ``` `security_runtime` requires the umbrella `security` feature. The generated starters depend on `rullst-security` directly, so they can use `rullst_security::VaultSecret` instead. `std::env::var` reads only the process environment (Rullst never loads `.env` into it); use `rullst::config::project_setting` to fall back to `.env` in development. A secret manager should inject the real value in production; `VaultSecret` reduces accidental formatting and zeroizes its owned allocation on drop, but it is not key custody or secure memory. --- ## Step 2: Attach the container and extract a service ```rust,ignore use rullst::server::{Extension, Json, post}; use rullst::{Router, di::Inject}; pub async fn process_payment( Inject(user_service): Inject, ) -> Json<&'static str> { // Use a bounded provider adapter; never log the key or full request payload. let _configured = user_service.gateway.is_configured(); Json("Payment request accepted") } let container = configure_di()?; let app = Router::new() .route("/payments", post(process_payment)) .layer(Extension(container)); ``` The continuation is application code because its error type and provider adapter belong to the project. `Inject` takes `T` from the container and, when the container lacks it (or there is none), from an `Extension>` layer. If neither provides it, `Inject` fails closed with a `500` whose body is a fixed message; the missing type name is logged on the `rullst::di` target, never returned to the client. --- ## Key takeaways - Services are registered as type-safe `Arc` singletons. - DI controls construction and lookup; it does not provide authorization, transaction boundaries, or secret management. - Prefer ordinary constructors when a small service graph does not benefit from a container. --- # 29. Embedded IoT & Edge Systems Source: https://rullst.github.io/Rullst/book/tutorials/29-embedded-iot-edge.html # Tutorial 29: IoT data and frame helpers (`rullst-iot`) `rullst-iot` provides telemetry/state models and protocol frame builders that compile without `std`. Some APIs use `alloc`, so a bare-metal application must supply an allocator. It also provides bounded MQTT 5 PUBLISH (`MqttPublish`) and CoAP request (`CoapRequest`) encoders. The crate does not currently read hardware registers or provide MQTT/CoAP network transport, OPC-UA, Sparkplug B, HSM, or post-quantum implementations. > **Experimental.** `rullst-iot` and the `make:iot` generator are in the > Experimental [maturity tier](https://rullst.github.io/Rullst/book/maturity.html): the API may change between 13.x > releases and nothing has been validated on physical hardware. The > [module status](https://rullst.github.io/Rullst/book/crates/iot.html#module-status) lists which modules are working > implementations and which are only data models or contracts. ## Add the crate In a Rullst application, `cargo rullst make:iot TemperatureSensor` turns on the `iot` feature of `rullst`, writes `src/iot/temperature_sensor.rs` (a `TemperatureSensorDevice` that returns `SensorTelemetry` readings) and declares the `iot` modules. The crate is then reachable as `rullst::iot`, so the `rullst_iot::` paths below become `rullst::iot::`. A firmware crate depends on `rullst-iot` directly and turns off its default `std` feature: ```toml [dependencies] rullst-iot = { version = "=13.0.0-alpha.4", default-features = false } ``` ## Build a telemetry model and Modbus request ```rust use rullst_iot::{AnomalyDetector, ModbusFrame, SensorTelemetry}; let telemetry = SensorTelemetry::new( "sensor-01", "temperature", 38.5, 1_700_000_000, ); let detector = AnomalyDetector::new(25.0, 5.0); let state = detector.evaluate(telemetry.value); // This builds bytes for a request. Platform code must send them over a real // serial/TCP transport and handle timeouts, retries, and the response. let request = ModbusFrame::read_holding_registers(1, 0, 10); assert_eq!(request.len(), 8); let _ = state; ``` ## Signed firmware artifacts Use `OtaManifest` and `OtaManager::new_with_trusted_key` to verify a firmware artifact before selecting an inactive partition. See the [crate guide](https://rullst.github.io/Rullst/book/crates/iot.html) for the trust, persistence, and bootloader requirements that remain the integrator's responsibility. ## Experimental fixtures The opt-in `experimental-simulators` feature contains explicitly named `Simulated*` fixtures. They are deterministic test data generators, not hardware or protocol implementations. --- # 30. Framework Escape Hatch Source: https://rullst.github.io/Rullst/book/tutorials/30-framework-escape-hatch.html # Tutorial 30: Axum Escape-Hatch Snapshot (`cargo rullst eject`) 🔓 `eject` generates a minimal Axum/Tokio entry point that can begin a manual migration away from Rullst's server wrapper. It does **not** statically expand macros, copy the application's route graph, convert middleware, or remove Rullst dependencies automatically. --- ## Step 1: Generate a separate starting point ```bash cargo rullst eject ``` The default output is `src/ejected_main.rs`; the existing `src/main.rs` remains unchanged. Cargo does not build `src/ejected_main.rs` (it is not a binary target): review it and compare it with `src/main.rs`; only `--force` (Step 2) replaces the entry point. The generated template serves a placeholder route from `application_routes()` and wraps it in the configured Rullst security baseline, the same `apply_security_baseline` step `rullst::Server` performs with `Rullst.toml`. Axum is reached through the `rullst::web::axum` re-export, so the template compiles without adding Axum to `Cargo.toml`. Without its header comment and start-up message, the template is: ```rust,no_run use rullst::RullstConfig; use rullst::web::axum::{Router, response::Html, routing::get}; use std::net::SocketAddr; /// Mount the application's routes here. fn application_routes() -> Router { Router::new().route("/", get(|| async { Html("

    Ejected Axum Server

    ") })) } #[rullst::runtime::main] async fn main() -> Result<(), Box> { let config = if std::path::Path::new("Rullst.toml").exists() { RullstConfig::load_from_file("Rullst.toml").await? } else { RullstConfig::default() }; let environment = config.environment()?; let app = rullst::apply_security_baseline(application_routes(), config.security, environment)?; let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); let listener = rullst::async_runtime::tokio::net::TcpListener::bind(addr).await?; rullst::web::axum::serve( listener, app.into_make_service_with_connect_info::(), ) .await?; Ok(()) } ``` The baseline covers secure headers, CORS, WAF/RASP and CSRF (headers, WAF and CSRF in staging/production). Static files, rate limiting, lifecycle probes, hot reload, sessions, authentication and authorization are not included. Move application routes into `application_routes()` one bounded group at a time and retain equivalent limits, telemetry, graceful shutdown, health behavior, state, and authorization tests. --- ## Step 2: Treat `--force` as a deliberate replacement ```bash cargo rullst eject --force ``` The hardened command first preserves the original entry point as `src/main.rs.rullst-backup` and refuses to overwrite an existing backup. The template does not carry over the original routes or module declarations; move them from the backup before building or deploying. Keep a normal version-control commit as the authoritative recovery path. Custom output paths are restricted to relative Rust files under `src/` and existing targets are not overwritten implicitly. --- ## Key takeaways - Ejection is a migration aid, not a semantics-preserving compiler transform. - The application remains responsible for dependency cleanup and replacements for ORM, auth, queues, Studio, Nexus, Capital, AI, and other selected crates. - Run `cargo fmt`, strict Clippy, the complete test suite, and application security/operational checks after every migrated route group. --- # 31. SaaS Deployment Preparation Source: https://rullst.github.io/Rullst/book/tutorials/31-end-to-end-saas-aws-gcp.html # Tutorial 31: SaaS deployment preparation for AWS or GCP This guide prepares a generated SaaS application for a cloud deployment. It is not an end-to-end production certification: identity, network policy, database operation, secrets, billing and recovery remain deployment responsibilities. ## 1. Materialize and verify the SaaS starter Install the published `13.0.0-alpha.4` CLI with its exact version (Cargo never selects a prerelease on its own, and the v12 line reached end of life on 16 October 2026), then generate deterministically. Do not substitute the moving `main` branch for an immutable release artifact: ```bash cargo install cargo-rullst --version =13.0.0-alpha.4 --locked cargo rullst new my_cloud_saas --default --blueprint saas --docker cd my_cloud_saas cargo fmt --all -- --check cargo clippy --all-targets --all-features -- -D warnings cargo test --all-features ``` Review every generated route, access policy, migration and environment placeholder before supplying live credentials. ## 2. Supply secrets outside source control Use AWS Secrets Manager, Google Secret Manager or an equivalent deployment boundary for values such as `APP_KEY`, `DATABASE_URL`, provider keys, webhook secrets and, if you use encrypted model fields, `RULLST_ENCRYPTION_KEY`. Do not bake `.env` into an image (the generated `.dockerignore` keeps `.env` and `.env.*` out of the build context). Set `RULLST_ENV=production` and make startup fail when a required production adapter or credential is missing. Empty and `mock_*` provider credentials are for deterministic offline development, not live operation. ## 3. Build and scan the exact image ```bash docker build --pull --tag my-cloud-saas: . docker inspect my-cloud-saas: ``` Pin the deployed image by digest. Run the application's tests and a container scanner against the exact candidate. The CLI's `deploy` command writes a `Dockerfile`, `compose.yaml` and `Caddyfile` for a Linux VM (see [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html)); AWS App Runner/ECS and Google Cloud Run configuration remains explicit cloud work. ## 4. Configure the cloud boundary For AWS or GCP, define and review: - private database connectivity, TLS and least-privilege credentials; - trusted proxy handling and the application's external origin policy; - ingress authentication, rate limits, body limits and request timeouts; - readiness/liveness behavior and a bounded shutdown grace period; - immutable image rollout and a tested rollback procedure; - logs, metrics and alerts that avoid secrets and unnecessary PII. Managed services have provider-specific scaling floors, quotas, cold starts and costs. Verify current provider documentation and load-test your selected region and topology rather than assuming scale-to-zero or a request rate. ## 5. Exercise stateful recovery Before production traffic, rehearse migrations, backup, restore, webhook replay, field-encryption key rotation, a failed rollout and database unavailability. Health endpoints only report the checks implemented by the application; they do not create zero-downtime deployment by themselves. --- # 32. Bounded Security Telemetry Source: https://rullst.github.io/Rullst/book/tutorials/32-ai-sentinel-soc-dashboard.html # Tutorial 32: Bounded security telemetry in Studio Rullst's honeypot, RASP and Studio security page provide local defense-in-depth signals. They are not an autonomous SOC, a universal blocker, an AI incident responder or a durable SIEM integration. ## 1. Compose local request controls `rullst::Server` already applies secure headers and the WAF in staging and production. This example composes the `rullst-security` layers on a plain Axum router: ```rust,no_run use axum::Router; use rullst_security::{CspSecurityLayer, HoneypotLayer, HoneypotState, RaspSecurityLayer}; use std::net::SocketAddr; # async fn run() -> Result<(), Box> { let app = Router::new() // Add application routes first. .layer(RaspSecurityLayer) .layer(CspSecurityLayer) .layer(HoneypotLayer::new(HoneypotState::default())); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?; axum::serve( listener, app.into_make_service_with_connect_info::(), ) .await?; # Ok(()) # } ``` `ConnectInfo` supplies the accepted socket peer used by the honeypot. Deployments behind a proxy must establish and test a trusted client-identity boundary; the middleware deliberately does not trust arbitrary forwarding headers. ## 2. Understand each signal - Honeypots match configured exact synthetic paths and keep bounded, expiring process-local bans. - RASP performs bounded heuristic inspection for selected patterns and can have false positives and negatives. - CSP and headers depend on the final rendered page, proxy, browser and TLS deployment. - `SecurityStore` is local telemetry. An event is not HMAC-verified or durably delivered merely because it appears in Studio. No built-in path asks an LLM to ban a peer or automatically mounts a blocking policy. The opt-in `ThreatSentinel` can classify three bounded, caller-supplied aggregate patterns and issue an HMAC-authenticated, subject-bound, expiring, one-shot **process-local** proof-of-work challenge. The application must supply trustworthy observations and subject identity, translate the outcome into an HTTP protocol, and decide where that gate belongs. It is not AI attribution, a distributed replay store, or an autonomous ban. Sensitive automated actions still require authenticated policy, limits, durable audit and human approval where appropriate. ## 3. Inspect the local Studio view In a debug build the generated starters start Studio in the same process with `rullst::studio::run_studio(5555)`, which serves only loopback peers (`LocalStudioAccess::loopback_only()`). Open `http://127.0.0.1:5555/studio/security`. The page renders this process's current `SecurityStore` telemetry and keeps unavailable sources visibly unavailable. A multi-instance deployment needs a shared, authenticated event pipeline with retry, acknowledgement, retention and dead-letter handling before it can be described as an operational SIEM. See the [Threat Radar and SOC guide](https://rullst.github.io/Rullst/book/threat-radar-soc-guide.html) and the [v12 security evidence ledger](https://rullst.github.io/Rullst/book/v12-security-claims.html) for exact boundaries. --- # 33. Auth-Bound Multi-Tenancy Source: https://rullst.github.io/Rullst/book/tutorials/33-multi-tenant-b2b-saas.html # Tutorial 33: Auth-bound multi-tenancy Rullst Core can select a tenant from a request hint only after application authentication has inserted a trusted `TenantMembership`. A hostname, header, query parameter or request body is never sufficient proof of membership. ## 1. Configure the selection layer ```rust,no_run use axum::{Extension, Router, routing::get}; use rullst_core::{ multitenant::{TenantConfig, TenantLayer, TenantStrategy}, security::TenantContext, }; async fn tenant_dashboard(Extension(tenant): Extension) -> String { format!("Selected workspace: {}", tenant.tenant_id) } fn tenant_routes() -> Router { let selection = TenantLayer::new(TenantConfig::new(TenantStrategy::Subdomain)); Router::new() .route("/dashboard", get(tenant_dashboard)) .layer(selection) } ``` An outer, application-owned authentication layer must first validate the session/token and insert `TenantMembership::try_new(...)` from trusted identity claims. Without that extension, `TenantLayer` returns `403 Forbidden`. If the requested subdomain is not in the authenticated membership set, it also returns 403. A request that names no tenant (and, with the subdomain strategy, has no `with_domain_fallback`) uses the membership's default (`TenantMembership::with_default`, or the only tenant of a single-tenant membership) and is refused with 403 when there is none. In Axum, remember that subsequently added layers run first; test the final middleware order in-process. `TenantStrategy::Header` and `TenantStrategy::Parameter` are also available, but they remain untrusted selection hints. Query parameters additionally leak more easily through history, referrers and access logs. The built-in subdomain parser reads the `Host` header, or the HTTP/2 `:authority` when no `Host` is sent, and selects the first label only for hostnames with at least three labels; a `www` label is never a tenant. When the apex domain sits under a multi-label public suffix such as `.com.br` or `.co.uk`, set `TenantConfig::with_base_domain("escola.com.br")` (added in v13): the tenant is then the label immediately to the left of that domain, and the apex itself uses `with_domain_fallback` (which only the subdomain strategy reads). Custom-domain ownership and trusted-proxy normalization remain application and deployment work. ## 2. Bind every database query Tenant selection does not rewrite arbitrary SQL or automatically add a tenant predicate to every ORM query. Bind the authenticated tenant explicitly: ```rust,no_run use rullst_core::security::TenantContext; use sqlx::{FromRow, PgPool}; #[derive(FromRow)] struct Invoice { id: i64, tenant_id: String, total_minor: i64, } async fn list_tenant_invoices( pool: &PgPool, tenant: &TenantContext, ) -> Result, sqlx::Error> { sqlx::query_as::<_, Invoice>( "SELECT id, tenant_id, total_minor FROM invoices WHERE tenant_id = $1", ) .bind(&tenant.tenant_id) .fetch_all(pool) .await } ``` Use database constraints and, where appropriate, database-native row-level security as additional defense. Negative tests must prove that one tenant cannot read, update or delete another tenant's records through every relevant route, repository, background job and administrative surface. ## 3. Operational boundaries - `TenantContext` is server-selected state, not a claim accepted from JSON. - `current_tenant_id()` is task-local convenience; passing an explicit `TenantContext` to domain/storage APIs is easier to audit. - `TenantCache`, `TenantStorage`, `TenantRealtime` and `TenantPresence` provide validated namespaces, but applications still own business authorization and remote provider policy. - Cross-process membership updates, custom-domain verification, database policy, audit retention and incident response are not automatic framework guarantees. --- # 34. Live Analytics Dashboard & WebSockets Source: https://rullst.github.io/Rullst/book/tutorials/34-realtime-dashboard-liveview-websockets.html # Tutorial 34: Live Analytics Dashboard (`rullst::live` & WebSockets) 📈 Build a per-connection analytics view with `rullst::live`. Feed it values from your own authoritative metrics source; the example below uses explicit state only to demonstrate the component lifecycle. --- ## 🛠️ Step 1: Create Analytics LiveComponent `cargo rullst make:live AnalyticsDashboard` writes `src/live/analytics_dashboard.rs` and declares it in `src/live/mod.rs`; declare `mod live;` in the crate root yourself. Replace the generated counter with: ```rust use rullst::async_trait; use rullst::live::LiveComponent; use serde_json::Value; #[derive(Default)] pub struct AnalyticsDashboard { pub revenue_mrr: f64, pub active_users: usize, } #[async_trait] impl LiveComponent for AnalyticsDashboard { async fn mount(&mut self) { // Replace these initial values with an application-owned metrics query. self.revenue_mrr = 0.0; self.active_users = 0; } async fn handle_event(&mut self, payload: Value) { if let Some(event) = payload.get("event").and_then(|v| v.as_str()) { if event == "refresh" { self.revenue_mrr += 100.00; self.active_users += 1; } } } fn render(&self) -> String { format!( r#"

    Live Analytics Stream

    MRR

    ${:.2}

    Active Users

    {}

    "#, self.revenue_mrr, self.active_users ) } } ``` --- ## 💻 Step 2: Render in View Page ```rust,ignore use rullst::html::Markup; use rullst::live::Live; use crate::live::analytics_dashboard::AnalyticsDashboard; pub async fn analytics_page() -> Markup { Live::mount::("/ws/analytics").await } ``` `Live::mount` pre-renders the component inside a wrapper that opens the WebSocket; return it as the page or interpolate it into an `html!` layout. Register the matching upgrade route on the application's `rullst::Router` (generated projects reach Axum only through `rullst::web::axum`): ```rust,ignore let app = router() .ws("/ws/analytics", rullst::live::live_ws_handler::); ``` The page must also load HTMX and a pinned HTMX WebSocket extension; Rullst does not inject that browser dependency. --- ## 💡 Key Takeaways - Rullst owns the server-side component lifecycle; a browser transport is still required. - The current implementation re-renders an HTML fragment after each valid JSON event. It does not provide distributed state, replay, authorization or a metrics source automatically. - Incoming events are limited to 64 KiB per WebSocket message; larger messages close the session. --- # 35. Performance Measurement & Resilient Operation Source: https://rullst.github.io/Rullst/book/tutorials/35-high-performance-benchmarking.html # Tutorial 35: Performance measurement and resilient operation Performance is a property of a concrete application, build, host and workload. Rullst includes Criterion microbenchmarks and production helpers, but does not promise a universal latency, throughput or availability number. ## 1. Build an application-specific release binary ```toml [profile.release] opt-level = 3 lto = "thin" codegen-units = 1 strip = "debuginfo" ``` Generated projects already ship a release profile with `lto = "thin"`, `codegen-units = 1` and `strip = "symbols"`; `strip = "debuginfo"` keeps symbol names for production backtraces. `cargo rullst build` compiles the release binary and pre-compresses static assets. Alternative linkers such as `mold` or `lld` can improve developer link time on supported hosts. `target-cpu=native` can improve a host-specific binary but makes it less portable; do not put it in a generally distributed artifact without an explicit CPU baseline. ## 2. Configure and measure the database pool ```rust use rullst_orm::Orm; # async fn configure() -> Result<(), rullst_orm::Error> { Orm::init_with_options( "postgres://app:password@127.0.0.1/app", 20, // maximum connections 10, // acquire timeout in seconds ) .await?; # Ok(()) # } ``` Call it before `Server::run`: the server keeps an already-initialized pool instead of opening its own from `DATABASE_URL`. Choose limits from database capacity and measured concurrency. More connections can increase contention and resource use instead of increasing throughput. ## 3. Run reproducible benchmarks In a checkout of the Rullst repository, run one group at a time with the command the [benchmark workflow](https://github.com/Rullst/Rullst/blob/main/.github/workflows/bench.yml) uses for it, for example: ```bash cargo bench -p rullst-core --bench core_bench cargo bench -p rullst-orm --features drivers-all --bench orm_bench ``` Record at least: - exact commit, Rust version, features and release profile; - CPU, memory, operating system and power/virtualization settings; - database/provider version and topology; - warm-up, sample count, concurrency and payload distribution; - median and tail latency, throughput, errors and resource use. Criterion microbenchmarks detect regressions in selected functions. They do not model a production network, database, proxy or user journey. For cross-framework comparisons, follow the method in the [v13 framework comparison plan](https://rullst.github.io/Rullst/book/v13-framework-comparison.html). The [public benchmark hub](https://rullst.github.io/Rullst/benches/) links the eight published groups: facade/HTTP, Core primitives, ORM, Auth, Connect, Security, AI and Capital. Nine Criterion binaries back those groups because ORM has both local and cross-ORM inputs. A crate without a microbenchmark is not automatically less mature: protocol vectors, generated-project compilation, restart/failure contracts, no_std target builds or whole-request tests can fit its dominant risk better. Add a new benchmark only with a stable operation, input, unit and interpretation. The repository also contains a cross-ORM SQLite harness: ```bash cargo bench -p rullst-orm --features strict-sqlite \ --bench orm_comparison ``` It pins Diesel and SeaORM in the lockfile and gives all three ORMs one typed SQLite connection, separate database files, the same schema/unique index, 100 equivalent rows and identical WAL/synchronous/busy-timeout policy. Criterion measures primary-key lookup, indexed filtered lookup, count, ordered list-ten and insert/delete. This makes the input inspectable; it does not make the architectures identical. Diesel's synchronous call path and the async Rullst/SeaORM executor paths remain part of what is measured. The benchmark workflow runs it and publishes the results with the other groups. Read them as regression/comparison evidence for these five SQLite shapes only, and never generalize them to networked PostgreSQL/MySQL, concurrency, memory use or complete applications. ## 4. Test failure behavior separately Availability requires deployment exercises: health/readiness semantics, timeouts, overload, cancellation, database failover, backup/restore, migration rollback and secret rotation. Miri, sanitizers, fuzzing and Kani are target-specific correctness tools; none measures availability or proves the absence of every memory error. --- # 36. Assisted Framework Upgrades Source: https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html # 36. Assisted framework upgrades Rullst v12 introduced a bounded upgrade transaction for existing applications. The goal is to make the safe, repeatable part a single command while refusing to guess about application data or security policy. > The transaction steps below were written for `12.1.0`; check [publication status](https://rullst.github.io/Rullst/book/v12.html). > The v12 line reached end of life on 16 October 2026, so a v12 application > moves to v13 with the v13 CLI. The v13 source rules and the rule > classification are described in > [v12 to v13: source findings](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#v12-to-v13-source-findings) and belong to > the `13.0.0-alpha.4` prerelease. Install the exact release and complete > the application-specific validation below before any production rollout. ## What the command can guarantee `cargo rullst upgrade` can inventory the Cargo workspace, update the Rullst release train, apply compiler suggestions, require `cargo check`, and restore the files it controls after a failure. It cannot prove that a database upgrade, authorization rule, provider integration or deployment still behaves correctly. The automatic transaction owns only: - versioned Rullst dependencies in exact Cargo workspace manifests; - the root `Cargo.lock` produced by Cargo resolution; - Rust edits proposed by `cargo fix`; - a `cargo check --workspace --all-targets --locked` gate using the application's selected features. The 12.1 updater writes managed requirements as `=VERSION`, preserving the explicitly selected release instead of allowing Cargo to choose a later patch or minor version. `cargo fix` resolves the candidate lockfile, and the final check must use that resolution unchanged. Review these pins before restoring any broader dependency-update policy in your application. It never runs migrations, changes secrets, invents tenant/ownership policy, opens Nexus or Studio, contacts application providers, or marks the result production-ready. ## 1. Prepare the application Create a branch, make the worktree reviewable, record the old test result, and back up every database. Prove the database backup can be restored before changing the framework. Install the exact CLI from the same release train as the target framework. For a v12 application, that is the v13 prerelease (Cargo never selects a prerelease on its own): ```bash cargo install cargo-rullst --version =13.0.0-alpha.4 --locked --force ``` The framework command does not update its own executable. This matters for v5: the already-published v5 CLI cannot gain the v12 migration engine retroactively, and the v13 CLI upgrades only from v12 or later. A v5 application first moves to v12 with the end-of-life 12.x CLI (`cargo install cargo-rullst --version 12.1.0 --locked --force`, as the steps below show), then to v13. Run each CLI inside the application. ## 2. Inspect without writing ```bash cargo rullst upgrade --dry-run ``` The plan shows every dependency edit and source finding. `MUST-CHANGE` (`BLOCKER` in the 12.x CLI) means a known old API or option requires a source change; `REVIEW` means the code uses an API, feature or configuration whose behaviour changed and must be revalidated. Neither label means that unreported code is automatically safe. For automation, request JSON: ```bash cargo rullst upgrade --dry-run --json > upgrade-plan.json ``` The root object uses `schema_version: "rullst.upgrade-plan.v1"` and identifies the rule catalog, exact target, manifest changes, detected source majors, findings, automatic scope and mandatory manual gates. Consumers must reject an unknown schema version rather than silently interpreting it as v1. The v13 CLI extends v1 additively (see [the v13 fields](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#json-plan-fields)); consumers must ignore keys they do not know. Use an explicit target to make the selected published version visible: ```bash cargo rullst upgrade --to 13.0.0-alpha.4 --dry-run ``` Other targets must exist in the registry and belong to the installed CLI's major train, so a CLI never applies rules it does not carry: a v12 CLI cannot plan a v13 migration, and the v13 CLI cannot downgrade a project. ## 3. Apply the transaction After resolving or accepting every finding: ```bash cargo rullst upgrade ``` Before the first write, the CLI snapshots Cargo workspace manifests, the root lockfile and Rust sources under: ```text target/rullst-upgrades// ├── files/ ├── index.tsv ├── report.md └── report.json ``` It then edits the TOML while preserving comments and relative order, runs compiler-provided fixes and executes the Cargo check gate. A failing gate restores the controlled files automatically and returns a non-zero status. To deliberately keep a partial result for diagnosis: ```bash cargo rullst upgrade --keep-on-failure ``` To restore a persisted snapshot after that mode or after an interrupted run: ```bash cargo rullst upgrade \ --restore target/rullst-upgrades/ ``` Restore accepts only a path-validated snapshot inside the current project's `target/rullst-upgrades` directory. `cargo clean` deletes `target`, so retain a normal version-control commit or copy a needed diagnostic report before cleaning. ### Recovery boundaries and 12.1.0 hardening Stop editors, watchers and other writers before restoring. File recovery does not undo build-script/test side effects or database/external-service changes. Keep an independent version-control backup; the directory under `target` is not a substitute for one. A filesystem error during application can leave some files restored and others unchanged, so always review the result. Since 12.1.0 the CLI preflights the entire backup, stages every replacement before writing originals, and rejects linked or malformed paths. It limits indexes to 8 MiB/100,000 entries, each snapshot to 64 MiB and the total to 512 MiB. Disk failure while staging leaves originals intact; failure during the later per-file apply reports progress and retains the backup. This is not an all-files atomic commit and does not defend against hostile concurrent filesystem changes. These improvements are **not in 12.0.0**. ## 4. Finish a v5 to v12 migration This step uses the v12 CLI. The v13 CLI accepts only v12 and v13 projects, so upgrade older applications to v12 first and then to v13. The v5 README used attribute-style routing and a server builder with no router or port. The scanner reports these markers instead of applying a global text replacement. Replace the old shape: ```rust,ignore #[routes] fn home() -> Response { // ... } Server::new() .route("/", get(home)) .run() .await; ``` with an explicit v12 router and typed error propagation: ```rust,no_run use rullst::{Server, response::Html, routes}; async fn home() -> Html<&'static str> { Html("Hello from v12") } #[rullst::runtime::main] async fn main() -> Result<(), Box> { let app = routes![get("/" => home)]; Server::new(app).run(3000).await?; Ok(()) } ``` Then follow the complete [v5 → v12 guide](https://rullst.github.io/Rullst/book/migration-v5-to-v12.html), including feature selection, disposable database migration/rollback, explicit Nexus and Studio boundaries, provider validation and authorization negatives. ## 5. Run the application-owned gates At minimum: ```bash cargo fmt --all -- --check cargo clippy --workspace --all-targets --all-features -- -D warnings cargo test --workspace --all-features cargo build --release ``` Also test the actual production feature set, restore/migrate/rollback against a production-shaped database copy, cross-user and cross-tenant denials, proxy/TLS identity, CSRF/CORS, Nexus/Studio exposure and every configured live provider. ## v12 to v13: source findings The v13 CLI carries the `rullst-upgrade-rules-v4` catalog. It parses the application instead of matching text: Rust files with `syn` (so comments, doc comments and string literals never match an API rule, and macro bodies such as `html!` and `routes!` are read token by token), Cargo manifests with a TOML parser, and a few generated project files (`.gitignore`, `.dockerignore`, `.cargo/config.toml`, `Foundry.toml`, `docker-compose.prod.yml`, Kubernetes YAML, an Omni shell under `omni-app/`, `rullst-client.ts`, `Dockerfile`, `.env`/`.env.example` keys and release scripts). Files are read without following symbolic links; a finding records a rule and a line, never file contents. A Rust file that does not parse, is not UTF-8 or is larger than 2 MiB is listed as `NOT SCANNED`. Each finding has a stable code (`V13-...`), a kind, a location, a one-line message and the first-column title of its row in the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#changes-from-the-published-1210-source): ```text Source findings (rullst-upgrade-rules-v4): 1 must-change, 2 review MUST-CHANGE src/views/home.rs:12 [V13-HTML-DYNAMIC-EVENT-HANDLER] a dynamic value in an `on*`/`hx-on*` attribute of `html!` no longer compiles migration-v13 row: `html!` event handlers REVIEW src/main.rs:64 [V13-RENDER-PAGE-LANGUAGE] `render_page` declares `lang="pt-BR"`; English pages should use `render_page_with_lang` migration-v13 row: Starter page language ``` - **must-change**: a compile-breaking or API-shape change that the code uses (for example a dynamic `onclick={...}` in `html!`, a `belongs_to` without `foreign_key`, a struct-level `#[sqlx(rename_all)]` on a model, a `match` on `FieldKind` without a wildcard, a utoipa 5 dependency used with Studio). - **review**: the code uses an API, feature or configuration whose behaviour changed (for example `Model::all()`, `ValidatedJson`, the `Mail` facade, `render_page`, a generated `.gitignore` without the SQLite journal files). A review rule is reported once per file (its first location); a must-change rule at every location. The [rule classification](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#v12--v13-rule-classification) lists which migration rows have rules. Findings never block `cargo rullst upgrade`, whose `cargo check` gate still restores the project when the code does not compile. The gate's error names the must-change findings and repeats the guidance of `V13-CAPITAL-REMOVED` and `V13-MAIL-REMOVED` findings, which name removed APIs but are review findings. `update project verify`, `review` and `apply` refuse a preparation that has must-change findings and carry review findings in the plan. ### Rules by release Each rule records the release that introduced the change it reports. The rules for the 12.x to 13 changes date from 13.0.0-alpha.1; a rule added later, or reworded because a later release changed what it reports, carries that release: | Release | Rules | | :--- | :--- | | 13.0.0-alpha.1 | The other 95 rules | | 13.0.0-alpha.2 | `V13-HTML-MARKUP-RETURN`, `V13-HTML-RAW-MARKUP`, `V13-ORM-CONSTRAINT-ERRORS`, `V13-TEXT-TIMESTAMPS`, `V13-POLYGLOT-FEATURE` | | 13.0.0-alpha.3 | `V13-CONNECT-GITHUB-EMAIL`, `V13-ORM-TRANSACTION-ERRORS`, `V13-ORM-SKIP-MANAGED-COLUMNS`, `V13-ORM-DEFAULT-FEATURES` (reworded: `rullst-orm` has no default driver), `V13-RULLST-DEFAULT-FEATURES`, `V13-OMNI-REMOVED`, `V13-OFFLINE-SYNC-REMOVED` | | 13.0.0-alpha.4 | `V13-DEPLOY-TARGETS-REMOVED`, `V13-DEPLOY-GENERATORS-REMOVED`, `V13-CLI-SELF-UPDATE-REMOVED`, `V13-CLI-AI-REMOVED` | A rule is dated by the first release tag whose catalog contains its code (`git show v13.0.0-alpha.2:cargo-rullst/src/generators/build/upgrade/rules/catalog.rs`, or `git log -S'"V13-..."'` on that file), checked against the CHANGELOG entry that names it; a new rule takes the next release. A unit test checks that every rule names one of these releases. The plan takes the application's Rullst version to be the oldest version that a Rullst requirement in the workspace manifests admits (`=13.0.0-alpha.3`, or `13.0.0-alpha.1`, a caret requirement) or that `Cargo.lock` holds. When that version is a 13.0 prerelease, only the rules introduced after it apply: an alpha.3 application gets the alpha.4 rules, an alpha.2 application the seven alpha.3 rules and the alpha.4 rules, and an alpha.1 application the alpha.2, alpha.3 and alpha.4 rules, because the earlier changes are already in the version it runs. The plan says what it skipped: ```text Source findings (rullst-upgrade-rules-v4): 0 must-change, 1 review Rules: introduced after 13.0.0-alpha.2, the Rullst version this project uses; earlier rules skipped: 100 (findings hidden: 3); --all-rules applies every rule ``` A v12 application, or one whose version cannot be read (path dependencies without a version and no `Cargo.lock`), gets every rule. `--all-rules` applies every rule to a prerelease application too, for example to review the 12.x to 13 changes again. Project preparations (`cargo rullst update`) always apply every rule, because their must-change findings gate execution. ### JSON plan fields `--dry-run --json` keeps every `rullst.upgrade-plan.v1` key. Each element of `source_findings` keeps `path`, `line`, `code` (the stable rule id), `severity` (`BLOCKER` or `REVIEW`) and `message`, and adds: | Key | Meaning | | :--- | :--- | | `kind` | `must-change` or `review` | | `introduced_in` | The release that introduced the rule's change, such as `13.0.0-alpha.3` | | `migration_row` | First-column title of the row in `migration-v13.md` | | `migration_url` | The published table of the v13 migration guide | The root object adds `finding_counts` (`must_change`, `review`), `unscanned_sources` (project-relative paths), `migration_guide` and `rule_selection`: `current_version` (the application's Rullst version, or `null` when it cannot be read), `all_rules` (`--all-rules`, and always `true` in project preparations), `skipped_rules` (rules introduced in or before `current_version` that were not applied) and `hidden_findings` (the findings those rules would have added). A consumer that ignores unknown keys reads the plan as before; findings of skipped rules are absent from `source_findings` and `finding_counts`. ## Fixing the findings Each finding names its rule and the first-column title of its row in the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#changes-from-the-published-1210-source), which says what changed and how to adapt. Fix them by hand, or with your editor's or terminal's AI assistant given the plan, the migration row and the project's `AGENTS.md` (`cargo rullst generate:ai-context`); review every edit it proposes. `cargo rullst ai upgrade`, which proposed such fixes in earlier 13.0 prereleases, was removed in 13.0.0-alpha.4. A practical order: 1. `cargo rullst upgrade --dry-run` and read every finding with its migration row. 2. Fix findings that also compile against v12 (most review items and the `html!`, ORM and Nexus must-change items), then run the v12 tests. 3. `cargo rullst upgrade`. When a fix needs a v13 API (for example utoipa 6 with Studio), run `cargo rullst upgrade --keep-on-failure`, fix the kept state, and finish with `cargo rullst upgrade` (no further version edits; it validates the result). 4. Run the application-owned gates below. ## Rehearsal on generated starters A rehearsal generates starters with a published CLI, points their Rullst dependencies at a checkout of the next version through `[patch.crates-io]` (the registry requirements stay, so the upgrade edits them as it would for a published release), runs `cargo rullst upgrade` with the newer CLI and then each project's build, tests and Clippy. ### 13.0.0-alpha.2 to 13.0.0-alpha.3 The October 2026 rehearsal used the published `v13.0.0-alpha.2` Linux binary, checked with `sha256sum -c cli-checksums-x86_64-unknown-linux-gnu.txt` from the same release, and a CLI built from `main`; the projects built with Rust 1.99.0. Because `main` still carried the alpha.2 version, the rehearsal exported it (`git archive`), set every package to `13.0.0-alpha.3` in that copy, patched the projects to the copy and passed `--to 13.0.0-alpha.3`. The published alpha.3 CLI targets its own version, so `--to` is not needed there. ```bash ./cargo-rullst-13.0.0-alpha.2-x86_64-unknown-linux-gnu rullst new a2-saas --default \ --blueprint saas --database sqlite --skip-initial-migration cargo build && cargo test # on 13.0.0-alpha.2; commit Cargo.lock # append [patch.crates-io] entries for every rullst package in Cargo.lock, commit cargo rullst upgrade --dry-run --to 13.0.0-alpha.3 cargo rullst upgrade --to 13.0.0-alpha.3 # edit what the findings ask for, then cargo build && cargo test && cargo clippy --all-targets -- -D warnings ``` Blank, Blank `--api`, SaaS and LMS starters built on alpha.2 without warnings, and 3, 3, 7 and 11 tests passed. Against the alpha.3 candidate: | Starter | Must-change | Review | `cargo rullst upgrade` | Manual edits | alpha.3 build, tests and Clippy | | :--- | :--- | :--- | :--- | :--- | :--- | | Blank | 0 | 0 | passed; changed only `Cargo.toml` and `Cargo.lock` | none | no warnings; 3 passed; clean | | Blank `--api` | 0 | 1 (`V13-HOT-RELOAD-MACHINE-ENDPOINTS`) | passed; changed only `Cargo.toml` and `Cargo.lock` | none | no warnings; 3 passed; clean | | SaaS | 0 | 4 (`V13-ORM-SKIP-MANAGED-COLUMNS`; `V13-REFERRER-NO-REFERRER` in two files, `V13-MAIL-FACADE-CONFIG`) | passed; changed only `Cargo.toml` and `Cargo.lock` | `#[orm(skip)]` to `#[orm(db_default)]` on `User::is_admin` and `User::session_epoch` | no warnings; 7 passed; clean | | LMS | 0 | 6 (`V13-ORM-SKIP-MANAGED-COLUMNS`; `V13-LMS-RECORD-PROGRESS` in three files, `V13-REFERRER-NO-REFERRER`, `V13-MAIL-FACADE-CONFIG`) | passed; changed only `Cargo.toml` and `Cargo.lock` | the same `User` edit | no warnings; 11 passed; clean | `V13-ORM-SKIP-MANAGED-COLUMNS` is the only finding about an alpha.3 change. The other findings locate APIs whose behaviour changed between 12.x and 13.0.0-alpha.2, so they also appear on a new alpha.2 or alpha.3 starter and need no edit in an application that already runs alpha.2. After the `User` edit the SaaS and LMS dry runs report 3 and 5 findings, all of that kind. The rehearsal's CLI applied every rule; the plan now skips the rules an alpha.2 application already runs with (see [Rules by release](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#rules-by-release)), so its default dry run lists only `V13-ORM-SKIP-MANAGED-COLUMNS` for SaaS and LMS and nothing for Blank and Blank `--api`, and `--all-rules` lists the findings in the table. An HTTP smoke test of each upgraded starter (`db:migrate`, then the server in development) passed. In SaaS and LMS, registration stored `is_admin = 0` and `session_epoch = 0` from the column defaults, "Sign out everywhere" raised the epoch and the earlier cookie was refused, signing in again worked and the loopback Nexus answered `200`. Blank and the JSON API answered `/` and `/health`, and the API's bearer `POST /api/messages` returned `201` (`401` without the token). The same starters generated by the alpha.3 CLI differ from the alpha.2 ones only in the `User` fields above, in the localized account mail and `APP_NAME` of the password controller and `.env` and the checked reset-link lifetime of `src/models/password_reset.rs` (the "Account mail language and product name" and "Generated reset link lifetime" rows; existing projects keep their code) and in the SaaS users migration, which imports through `rullst::db` instead of `rullst_orm` (both compile). The [alpha.2 to alpha.3 checklist](https://rullst.github.io/Rullst/book/migration-v13.html#upgrading-an-alpha2-application-to-alpha3) turns this into steps for a real application. Limits: SQLite only; the starters use none of the other APIs that alpha.3 removes or changes (Omni, `offline-sync`, default features, `Orm::transaction`, mock Connect credentials), so their rules ran only in the catalog's unit tests; no live mail, billing or OAuth provider was called. ### Earlier rehearsals: v12 to v13 `.github/rehearse-v12-upgrade.sh` is a manual helper (not a CI job) for the v12 to v13 path; its header documents the arguments. In October 2026 it and the published 12.3.0 binary upgraded Blank, Blog, SaaS and LMS starters of the 12.0.0, 12.1.2 and 12.3.0 CLIs to the 13.0.0-alpha.1 workspace with review findings only (5 to 14 per starter, for example `V13-GITIGNORE-DATABASES`, `V13-MODEL-ALL` and `V13-HEALTH-PROBES`) and no source edit, with one exception: the 12.3 SaaS starter still names `LemonSqueezyProvider`, so `cargo fix` failed and the project was restored until the Lemon Squeezy branches of `src/controllers/billing_controller.rs` and the `BILLING_STORE_ID` lines of `.env` and `.env.example` were removed, as `V13-CAPITAL-REMOVED` describes (that edit also compiles on 12.3). Starters of the 12.0 CLI also carry the [build files](https://rullst.github.io/Rullst/book/migration-v13.html#projects-generated-by-the-120-cli) listed in the migration guide. Those counts predate the alpha.2 and alpha.3 rules and were not re-measured. ## v12 → v13 rule classification Every row of the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#changes-from-the-published-1210-source) is classified for `rullst-upgrade-rules-v4`: - **(a) must-change**: a compile-breaking or API-shape change detectable in application source (11 rows); - **(b) review**: changed behaviour worth reviewing when the application uses the affected API, feature or configuration, which the rules locate (99 rows); - **(c) none**: no application impact, or not detectable in the application (CLI behaviour, generator output for new projects, opt-in features, fixes of inputs that previously failed) (96 rows). A row with both a must-change and a review part counts in both. The catalog has 111 rules. A row can map to several rules and a rule to several rows; when a row changes in a later release, update this table and the rule together. | Migration row | Class | Rules | Notes | | :--- | :--- | :--- | :--- | | Existing runtime APIs | (c) none | — | General statement; the session-schema migration is the next row | | Account/session registry | (b) review | `V13-AUTH-RECOVERY-MIGRATE` | | | Private object storage | (c) none | — | Opt-in `storage-s3` candidate | | R2 public URLs | (b) review | `V13-R2-PUBLIC-URL` | | | Tenant local storage | (c) none | — | Only a tenant directory below a regular file changes the error; not located | | Recoverable Live UI | (c) none | — | Opt-in module | | Distributed tracing | (b) review | `V13-OTLP-ENVIRONMENT` | | | Core queue | (b) review | `V13-QUEUE-SEMANTICS` | Unindexed 12.x Redis failures are not located | | Core scheduler, cache and realtime | (b) review | `V13-PRESENCE-COUNTING` | The `TenantCache` key encoding is not located | | Core memory cache sweeps | (c) none | — | Internal sweep cadence; reads unchanged | | Studio queue monitor | (b) review | `V13-QUEUE-DRIVER-PREVIEWS` | | | Studio API playground | (a) must-change | `V13-STUDIO-UTOIPA-6` | Must-change when a manifest declares utoipa < 6 or utoipa-axum < 0.3 | | Studio local access and table view | (b) review | `V13-STUDIO-TABLE-VALUES` | | | LMS blueprint | (c) none | — | Generator output; existing applications keep their code | | Consumer generators | (c) none | — | Generator commands | | `html!` event handlers | (a) must-change | `V13-HTML-DYNAMIC-EVENT-HANDLER` | | | `html!` returns `Markup` | (a) must-change | `V13-HTML-MARKUP-RETURN`, `V13-HTML-RAW-MARKUP` | Located in function signatures and `RawHtml` calls, including `html!` bodies; a `String` collected from `html!` output, an `#[island]` call or `Live::mount` output is reported where `RawHtml` wraps it | | Core request validation | (b) review | `V13-VALIDATION-STATUS` | | | Core feature flags and scheduler | (b) review | `V13-SCHEDULER-WEEKDAYS` | A/B splits: the two feature-flag rules below; the bucket reassignment depends on flag configuration and is not located | | Memory feature-flag splits | (b) review | `V13-MEMORY-FEATURE-SPLITS` | | | Database feature-flag splits | (b) review | `V13-DB-FEATURE-SPLITS` | | | Security headers | (b) review | `V13-REFERRER-NO-REFERRER` | | | Core WAF signatures | (c) none | — | Only requests that previously failed with 403 change | | Core security header precedence | (c) none | — | Explicit header values in handlers and inner layers are not located | | AI provider streaming and stop reasons | (b) review | `V13-AI-GEMINI-STOP-REASONS` | | | AI image-beacon guardrail | (c) none | — | Guardrail hardening without an application API change | | AI RAG tenant tags | (b) review | `V13-AI-RAG-TENANT-TAGS` | | | Anthropic provider output | (b) review | `V13-AI-ANTHROPIC-OUTPUT` | | | OpenAI provider output | (b) review | `V13-AI-OPENAI-OUTPUT` | | | Ollama host | (c) none | — | Only values that previously failed change meaning | | AI chat memory keys on MySQL/MariaDB | (b) review | `V13-AI-CHAT-MEMORY-KEYS` | The database backend is not located | | Core CSRF on HEAD | (c) none | — | HEAD previously failed with 500 | | Hot-reload machine endpoints | (b) review | `V13-HOT-RELOAD-MACHINE-ENDPOINTS` | | | Core JSON PII masking | (b) review | `V13-PII-MASKING` | | | Core PII masking of range responses | (b) review | `V13-PII-MASKING` | | | Security DLP, RASP and honeypot | (b) review | `V13-SECURITY-DLP-HONEYPOT` | | | Security audit log lines | (b) review | `V13-SECURITY-AUDIT-LOG-LINES` | | | Connect generic OIDC | (b) review | `V13-OIDC-OPTIONAL-NAME` | | | Age assurance | (c) none | — | Opt-in APIs and generator | | Optional consent and export | (c) none | — | Opt-in generator | | Project context | (c) none | — | CLI generator | | Typed API generation | (c) none | — | Opt-in CLI profile | | CLI generator output | (b) review | `V13-TS-CLIENT-REGENERATE` | Other generator changes affect new output only | | CLI generator robustness | (b) review | `V13-TS-CLIENT-REGENERATE`, `V13-GITIGNORE-DATABASES` | Other generator changes affect new output only | | ORM database-managed columns | (b) review | `V13-ORM-SKIP-MANAGED-COLUMNS` | Reports `#[orm(skip)]` on `is_admin`/`session_epoch` without `#[sqlx(skip)]`, as alpha.2 starters declared them | | ORM dirty tracking | (c) none | — | Opt-in `RowSnapshot` field | | ORM partial updates | (b) review | `V13-ORM-PARTIAL-UPDATE` | | | ORM derive checks | (a) must-change | `V13-ORM-BELONGS-TO-KEY`, `V13-ORM-IGNORED-RELATION-KEY`, `V13-ORM-SEARCHABLE-TABLE` | | | ORM queries and cache | (b) review | `V13-ORM-SQLX-JSON-SERIALIZE`, `V13-ORM-QUERY-CACHE-KEY` | Count, chunk-order and stream changes are not located | | ORM query-cache index | (b) review | `V13-ORM-CACHE-PREFIX` | | | ORM Redis model hashes | (b) review | `V13-ORM-REDIS-HASHES` | | | ORM outbox keys on MySQL/MariaDB | (b) review | `V13-OUTBOX-MYSQL-KEYS` | The database backend is not located | | ORM audit payloads on MySQL/MariaDB | (b) review | `V13-ORM-AUDIT-PAYLOADS` | Auditable models and audit-table setup; the database backend is not located | | Turso migrations | (b) review | `V13-TURSO-ROLLBACK-DRIFT` | | | Schema table names on PostgreSQL | (b) review | `V13-SCHEMA-PG-TABLE-CASE` | | | Legacy `SecretString` ciphertext | (c) none | — | Older values become readable; no source change | | `SecretString` client input | (b) review | `V13-SECRET-STRING-CLIENT-INPUT` | | | ORM protected values and `SecretString` serialization | (b) review | `V13-ORM-PROTECTED-VALUES` | Encrypted and masked model fields, and `SecretString` fields of serialized structs and models | | Offline Redis mock | (b) review | `V13-REDIS-MOCK-TIE-ORDER` | | | SQLite DSN paths | (c) none | — | Only stray files of earlier versions | | Auto-healing diagnostics | (b) review | `V13-AUTO-HEALING-DIAGNOSTICS` | | | ORM migrations | (c) none | — | Existing migrations keep their behaviour; `db:seed` refusing pending migrations is command behaviour | | ORM transactional migrations and the pool | (c) none | — | `within_transaction()` is a v13 API, so v12 migrations are unaffected | | ORM instance mutations | (b) review | `V13-ORM-MISSING-ROW` | Plain `delete()` calls are not located | | ORM encryption key settings | (c) none | — | Only `.env` entries that the process environment lacks change; `.env` is not read | | ORM constraint errors | (b) review | `V13-ORM-CONSTRAINT-ERRORS` | Code that matches `DatabaseError` | | ORM transaction errors | (b) review | `V13-ORM-TRANSACTION-ERRORS` | `Orm::transaction` calls in files that mention `DatabaseError`; `cargo check` reports closure error types without `From` | | ORM query values | (c) none | — | Only exhaustive `match`es on `RullstValue` break, and `cargo check` reports them | | ORM date/time columns | (b) review | `V13-TEXT-TIMESTAMPS` | `String` `created_at`/`updated_at` fields of SQLx models; whether the table's migration calls `timestamps()` is not checked | | Schema builder constraints | (c) none | — | Only `Column` struct literals break, and `cargo check` reports them | | ORM sandbox tests | (a) must-change | `V13-ORM-SANDBOX-TEST` | Flags every sandbox test as review; `cargo check` reports the signatures that no longer compile | | ORM model SQLx options | (a) must-change | `V13-ORM-SQLX-STRUCT-OPTION` | | | ORM trashed scopes | (b) review | `V13-ORM-ONLY-TRASHED` | | | ORM bulk soft deletes | (b) review | `V13-ORM-TRASHED-DELETE-ALL` | | | ORM joined scopes | (b) review | `V13-ORM-SQL-TEXT` | | | ORM chunk bounds | (b) review | `V13-ORM-CHUNK-BOUNDS` | | | ORM eager pivot order | (c) none | — | The order was unspecified before | | PersonalData reports | (b) review | `V13-PERSONAL-DATA-REPORT` | | | Nexus derive | (a) must-change | `V13-NEXUS-PRIMARY-KEY`, `V13-NEXUS-FIELD-KIND-NUMBER` | The field-kind rule is a review | | Nexus composite keys | (c) none | — | Opt-in composite keys; only `RegistryEntry` struct literals break, and `cargo check` reports them | | Nexus dates and times | (c) none | — | Derived field kinds change without a source edit; comparisons with `FieldKind::DateTime` are not located | | Nexus access in the starters | (c) none | — | Generator output; alpha.2 `#[orm(skip)]` fields are reported under ORM database-managed columns | | Nexus credentials | (b) review | `V13-NEXUS-DOTENV` | | | Nexus panel | (a) must-change | `V13-NEXUS-FIELD-KIND-MATCH` | Other panel changes are runtime behaviour without an application API | | CLI migration catalog | (c) none | — | CLI | | CLI `upgrade` downgrades | (c) none | — | CLI | | CLI Foundry service environment | (b) review | `V13-FOUNDRY-SERVICE` | | | CLI Foundry build artifact | (b) review | `V13-FOUNDRY-SERVICE` | | | CLI generators | (b) review | `V13-MFA-CLIENT-SECRET` | Other changes affect new generator output only | | CLI operations | (b) review | `V13-FOUNDRY-SERVICE` | Script exit statuses are not located | | CLI diagnostics and side effects | (c) none | — | CLI | | CLI home screen | (c) none | — | CLI | | CLI security audit | (c) none | — | CLI | | CLI dashboard audit | (c) none | — | CLI | | Core deferrable jobs | (c) none | — | Opt-in APIs | | Core scheduler time zones | (c) none | — | Opt-in `scheduler-tz` feature | | Dash N+1 warning | (c) none | — | CLI | | CLI security report | (c) none | — | CLI | | Generated security tests | (c) none | — | Generator output for new projects | | Blank JSON API starter | (c) none | — | Generator output; existing API projects keep their behaviour | | CLI audit lockfile | (c) none | — | CLI | | RASP shell signatures | (c) none | — | Request matching; not detectable in source | | CLI footprint report | (c) none | — | New CLI command; renamed `green` (CLI `ai` removed, `footprint` renamed `green` row) | | CLI `dev`/`dash` shutdown | (c) none | — | CLI | | CLI `dev --ts-sync` | (c) none | — | CLI | | CLI `dash` live metrics | (c) none | — | CLI | | Core development telemetry | (b) review | `V13-DEV-TELEMETRY-ROUTE` | | | Core development error console | (c) none | — | Development-only page; `handle_autofix` callers are not located | | CLI `ai fix` and `ai review` | (c) none | — | Added and removed within the 13.0 prereleases | | CLI project updates on SELinux | (c) none | — | CLI | | CLI `pkg` in a virtual workspace | (c) none | — | CLI | | CLI `new` repository | (c) none | — | CLI | | CLI generated `validator` dependency | (c) none | — | Generator output; `cargo check` reports a missing `validator` dependency | | CLI `make:model` imports | (c) none | — | Generator output for new models | | CLI `dockerize` and `generate:buildah` | (c) none | — | Affected generated files are not distinguishable | | CLI errors and exit codes | (c) none | — | CLI output and scripts | | CLI `doctor` | (c) none | — | CLI | | CLI output additions | (c) none | — | CLI | | SaaS plan gates | (c) none | — | Opt-in generated module | | Capital providers and NFS-e removed | (b) review | `V13-CAPITAL-REMOVED` | Compile-breaking; reported for review because a name such as `FiscalError` can also be an application type; also locates `BILLING_PROVIDER=lemonsqueezy` and `BILLING_STORE_ID` in `.env` and `.env.example` | | Capital quota keys on MySQL/MariaDB | (b) review | `V13-CAPITAL-QUOTA-KEYS` | `SqlQuotaStore`/`SqlQuotaBackend`; the database backend is not located | | Capital zero tier limit | (b) review | `V13-CAPITAL-ZERO-TIER` | | | Capital provider subscription IDs | (c) none | — | Rejects dot-only identifiers before a request | | Capital invoice amounts | (c) none | — | Only amounts below one minor unit change | | Messaging outbox relay key | (b) review | `V13-OUTBOX-RELAY-KEY` | | | Messaging encrypted SQLite startup | (c) none | — | Correct keyrings are unaffected | | Messaging Redis Streams candidate | (c) none | — | No 12.x release contains the adapter | | Mail providers removed | (b) review | `V13-MAIL-REMOVED` | Compile-breaking; also locates removed `MAIL_DRIVER` values and settings in Rust strings, `.env`, `.env.example` and `Rullst.toml` | | Mail driver default | (b) review | `V13-MAIL-FACADE-CONFIG` | | | Mail sender | (b) review | `V13-MAIL-FACADE-CONFIG` | | | Mail attachment inspection | (b) review | `V13-MAIL-ATTACHMENT-INSPECTION` | | | Mail link checks | (c) none | — | Depends on message content | | Mail facade settings | (b) review | `V13-MAIL-FACADE-CONFIG` | | | Mail queued attachments | (b) review | `V13-MAIL-QUEUED-ATTACHMENTS` | `Mail::init_queue`, `Mail::enqueue`/`enqueue_for_tenant` and `register_mail_handler` | | Mail SES addresses | (c) none | — | Requests SES rejected now succeed | | Mail Resend scheduling | (b) review | `V13-MAIL-RESEND-SCHEDULE` | | | Mail tracking recipient | (b) review | `V13-MAIL-TRACKING-RECIPIENT` | | | Mail plain-text fallback | (b) review | `V13-MAIL-TEXT-FALLBACK` | | | Labs runner | (a) must-change | `V13-LABS-RUNNER-REMOVED` | | | Education candidates | (c) none | — | New v13 packages, first published in 13.0.0-alpha.1 | | Labs package | (c) none | — | New package in the release inventory | | Media S3 storage | (c) none | — | New opt-in feature of a package first published in 13.0.0-alpha.1 | | Generated billing settings | (b) review | `V13-BILLING-PROJECT-SETTINGS` | | | Application templates | (c) none | — | Generator output | | Starter migrations | (b) review | `V13-SQLITE-ONLY-SEED-TIME` | | | ERP starter access | (b) review | `V13-ERP-DASHBOARD-ACCESS` | | | LMS lesson progress | (b) review | `V13-LMS-PROGRESS-KEY` | | | LMS concurrent progress saves | (b) review | `V13-LMS-RECORD-PROGRESS` | | | LMS lesson media in Nexus | (b) review | `V13-LMS-MEDIA-KINDS` | | | Blog robots and sitemap | (b) review | `V13-BLOG-ROBOTS-SITEMAP` | | | Generated login and registration | (b) review | `V13-CREDENTIAL-RATE-LIMIT` | | | SaaS user JSON | (b) review | `V13-PASSWORD-HASH-HIDDEN` | | | Registration timestamps | (b) review | `V13-EMPTY-TIMESTAMPS` | | | Billing row timestamps | (b) review | `V13-EMPTY-TIMESTAMPS` | | | Registration lengths | (b) review | `V13-UTF16-LENGTHS` | | | Revocable login sessions | (c) none | — | Additive APIs and generator output | | Password change and reset | (c) none | — | Additive APIs and generator output | | VPS deploy proxy | (b) review | `V13-VPS-DEPLOY-PROXY` | | | Blank database status | (b) review | `V13-MODEL-ALL` | | | Portfolio profile | (b) review | `V13-PORTFOLIO-PROFILE` | | | Blog and Portfolio pages | (b) review | `V13-PAGE-CSP-NONCE` | | | Generated linker configuration | (b) review | `V13-LINKER-CONFIG` | | | Build files from the 12.0 CLI | (b) review | `V13-MSVC-FASTLINK`, `V13-CARGO-LOCK-IGNORED`, `V13-DOCKER-UNLOCKED-BUILD` | Projects generated by the 12.0 CLI | | ERP orders and stock | (b) review | `V13-ERP-STORE-ORDER` | | | Blog and ERP reads | (b) review | `V13-MODEL-ALL` | | | Blog and ERP page bounds | (b) review | `V13-PAGE-BOUNDS` | | | Docker build context | (b) review | `V13-DOCKER-CONTEXT` | | | Kubernetes Ingress | (b) review | `V13-K8S-INGRESS-TLS` | | | Starter health probes | (b) review | `V13-HEALTH-PROBES` | | | Kubernetes and Buildah names | (b) review | `V13-K8S-NAMES` | | | Pre-compressed static assets | (c) none | — | CLI | | Static asset caching | (c) none | — | Runtime default; not detectable in source | | Generated release profile | (c) none | — | Generator output for new projects | | Generated container runtime | (c) none | — | Generator output for new projects | | Interactive `new --api` | (c) none | — | CLI | | CLI `new` wizard | (c) none | — | CLI | | Minimal default features | (b) review | `V13-RULLST-DEFAULT-FEATURES`, `V13-ORM-DEFAULT-FEATURES` | Reports `rullst` and `rullst-orm` dependencies that keep their defaults and select no driver | | Generated `rullst-orm` dependency | (c) none | — | Generator output; such a dependency is reported under Minimal default features | | ORM adapter aggregate features | (a) must-change | `V13-POLYGLOT-FEATURE` | Dependency `features` and `[features]` entries such as `"rullst/orm-polyglot"` | | Omni and offline sync removed | (a) must-change, (b) review | `V13-OFFLINE-SYNC-REMOVED`, `V13-OMNI-REMOVED` | The feature in dependency `features` and `[features]` entries; the shell's `omni-app/tauri.conf.json` or `omni-app/src/lib.rs`, and `cargo rullst omni`/`make:omni` in workflows, root scripts, Makefile and justfile | | Starter page language | (b) review | `V13-RENDER-PAGE-LANGUAGE` | | | Repository examples | (c) none | — | Repository examples, not applications | | Windows paths in CLI and console output | (c) none | — | CLI output | | Consumer scaffolds under symlinked directories | (c) none | — | CLI | | Core HSTS default | (c) none | — | Runtime default; not detectable in source | | Artisan and `.env` | (c) none | — | `std::env::var` calls in migrations and seeders are not located | | Core WAF in development | (c) none | — | Development-only behaviour | | CSRF token on static-looking paths | (c) none | — | Only requests that previously received no token change | | Multipart CSRF refusal | (c) none | — | Only the refusal text changes; tests that compare it are not located | | Secure cookies over HTTPS | (c) none | — | Runtime behaviour; not detectable in source | | CSRF cookie on cacheable responses | (c) none | — | `Cache-Control` values set by handlers are not located | | Connect mock credentials | (c) none | — | Credentials come from configuration and are not located | | Connect mock authorization URL | (c) none | — | Mock mode only | | Account mail language and product name | (c) none | — | Generator output; existing projects keep their code | | Production log defaults | (c) none | — | Runtime defaults; not detectable in source | | Connect GitHub email | (b) review | `V13-CONNECT-GITHUB-EMAIL` | `GithubProvider` and `Connect::driver("github", ...)` | | Account mail link lifetime | (c) none | — | Only reset lifetimes that previously failed change; the link wording is not located | | Generated reset link lifetime | (c) none | — | Generator output; existing projects keep their code | | CLI deploy targets removed | (b) review | `V13-DEPLOY-TARGETS-REMOVED` | `fly.toml`, `render.yaml`, `railway.json`, `railway.toml`, and `deploy --platform fly\|railway\|render` in workflows, root scripts, Makefile and justfile; introduced in 13.0.0-alpha.4 | | SQLite database in the deployed image | (c) none | — | Generator output for new Dockerfiles; an existing Dockerfile without `DATABASE_URL` is not located, and `cargo rullst deploy` warns when it keeps one; introduced in 13.0.0-alpha.4 | | CLI Foundry, Nix, Buildah and gRPC generators removed | (b) review | `V13-DEPLOY-GENERATORS-REMOVED` | `Foundry.toml`, a `nixify` `flake.nix`, `build_buildah.sh`, `src/grpc/mod.rs`, and the removed commands in workflows, root scripts, Makefile and justfile; introduced in 13.0.0-alpha.4 | | CLI self-update removed | (b) review | `V13-CLI-SELF-UPDATE-REMOVED` | `rullst update check\|verify\|stage\|install`, or `update guided` with `--scope`/`--root`, in workflows, root scripts, Makefile and justfile; introduced in 13.0.0-alpha.4 | | CLI `ai` removed, `footprint` renamed `green` | (b) review | `V13-CLI-AI-REMOVED` | `rullst ai` or `rullst footprint` in workflows, root scripts, Makefile and justfile; introduced in 13.0.0-alpha.4 | | Core error console assistant panel | (c) none | — | Development-only page; `handle_autofix` response readers are not located; introduced in 13.0.0-alpha.4 | | CLI starters moved to the examples | (c) none | — | CLI behaviour; applications generated from the removed starters keep their code; introduced in 13.0.0-alpha.4 | ## Planned simpler update experience **Since 13.0.0-alpha.4** the CLI never downloads or replaces its own binary. Install a newer CLI with Cargo and its exact version (`cargo install cargo-rullst --version =VERSION --locked`). At most once a day, in an interactive terminal, the CLI prints that command when crates.io has a newer release of its major; see the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-update-check-1210-working-source-unreleased). The 12.1.0 to 13.0.0-alpha.3 self-updater (`update check`, `update verify`, `update stage` and `update install`) was removed; the project commands below remain. `cargo rullst update project prepare --project PATH --json` copies the Git working directory into private storage and edits only the candidate's versioned workspace dependencies. It preserves dirty and untracked source, tracked deletions and the root lockfile, including a legacy ignored lockfile. Compare `before/` and `candidate/` at the reported location and review `preparation.json`. Builds/tests are not executed and neither execution nor application is authorized. See the [preparation limits and exclusions](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-update-project-prepare-1210-working-source-unreleased). Use `update project verify --prepared PATH --dry-run` to inspect its validation commands. Only after reviewing the trusted project, use `--allow-project-code` to authorize lockfile resolution, locked checks and tests in a fresh private copy. This is not a sandbox; tests inherit your environment and can have external effects. See the [verification options and limits](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-update-project-verify-1210-working-source-unreleased). Then `update project review --verified PATH --json` revalidates the verification record and shows the full dependency diff with a review digest. Use the returned `verified_directory`; the digest grants no apply authority. Explicit `update project apply --verified PATH --approved-review SHA256` applies only the reviewed manifests/root lockfile. The matching `recover` command restores only that operation and refuses unrelated edits. The [safe-update priority](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#safe-update-experience) proposes one guided flow for project preparation, validation and approved application. `cargo rullst update guided --to VERSION --project PATH` composes those commands. Each approval defaults to no and follows its complete review. Version, directories and digests are carried between steps; separate prompts govern trusted project execution/network and original-file application. `--offline` keeps verification local. Since 13.0.0-alpha.4 the flow no longer installs a CLI (`--scope` and `--root` were removed): install the target CLI with Cargo first. See the [guided command](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-update-guided-1210-working-source-unreleased). File recovery does not replace database backups or application acceptance tests, and updating the CLI alone never updates a deployed application. The 12.1.0 update mechanism does not implement v13 migrations. The v13 CLI (13.0.0-alpha.4 prerelease) carries its own rule catalog ([v12 to v13: source findings](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#v12-to-v13-source-findings)); `update guided` offers a project update only within the installed CLI's major. ## Is this unique? No. Assisted upgrades are an established framework practice: Rails documents interactive `bin/rails app:update`, Angular provides `ng update`, and Dart offers preview/apply analysis fixes through `dart fix`. Microsoft's .NET Upgrade Assistant also analyzed and changed projects, although Microsoft now marks it deprecated in favor of its modernization tooling. Rullst's useful distinction is the bounded composition: Cargo-workspace-aware TOML edits, a version-selected framework rule catalog, human and JSON plans, controlled snapshots, default rollback, explicit recovery and Cargo gates in one CLI flow. This is a testable design choice, not evidence that Rullst is the first or universally the best updater. Official references: - [Upgrading Ruby on Rails](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html) - [Angular `ng update`](https://angular.dev/cli/update) - [Dart `dart fix`](https://dart.dev/tools/dart-fix) - [.NET Upgrade Assistant overview](https://learn.microsoft.com/en-us/dotnet/core/porting/upgrade-assistant-overview) --- # 37. Polyglot Persistence Source: https://rullst.github.io/Rullst/book/polyglot-persistence.html # Polyglot Persistence > [!IMPORTANT] > This page describes the v13 prerelease source, and its dependency examples > pin the published `13.0.0-alpha.4` exactly: Cargo never selects a prerelease > on its own, and a plain `"13.0.0-alpha.4"` would also accept later alphas > (see the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html)). The v12 line (`rullst@12`) > reached end of life on 16 October 2026; see the [release record](https://rullst.github.io/Rullst/book/v12.html). Commit `Cargo.lock` for > reproducible builds, and use path dependencies only for checkout-local review. Rullst keeps explicit relational and specialized persistence contracts. SQLx Active Record supports SQLite, PostgreSQL, MySQL and MariaDB. The Blank starter can instead use Turso/libSQL as its primary database, through a separate typed adapter. MongoDB is for documents, DuckDB for embedded OLAP/analytics, SurrealDB for documents plus bounded read-only graph queries, Qdrant for dense vector search and Redis for selected native structures. No optional adapter silently changes the application's primary database. Only the SQLx backends work with the `#[derive(Orm)]` query builder, `Schema`, `artisan!`, `Server::with_db`, Nexus and Studio's database pages. The [Database Support](https://rullst.github.io/Rullst/book/database-support.html) table lists, backend by backend, what works, which `cargo rullst new` starters accept it and which CI test runs it against a real server. ## Choose the smallest feature Using the umbrella crate, next to the primary database profile: ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["strict-postgres", "orm-mongodb"] } ``` Available umbrella features are `orm-turso`, `orm-mongodb`, `orm-duckdb`, `orm-surrealdb`, `orm-qdrant` and `orm-redis`. Direct `rullst-orm` users select `turso`, `mongodb`, `duckdb`, `surrealdb`, `qdrant` or `redis`. There is no feature that enables them all: 13.0 removed the `polyglot` and `orm-polyglot` aggregates, so list the adapters the application uses. The examples below use `rullst_orm::…` paths, as projects generated by `cargo rullst new` do: they depend on `rullst-orm` directly, which `#[derive(Orm)]` requires because it expands to `rullst_orm::…` paths. Code that does not derive models can use the same items through the umbrella crate as `rullst::orm::…`. Features are additive. Prefer one adapter unless the application genuinely uses several, and inspect the final dependency graph with: ```bash cargo tree -e features ``` The project wizard first asks for a primary backend. SQLite, PostgreSQL, MySQL and MariaDB use SQLx; Turso is offered as the primary database of the Blank starter only, and its choice states what it does not integrate with. The wizard then offers the side stores, each described as one. For a hybrid SQLx application, use one or more explicit flags: ```bash cargo rullst new edge_app --default --database mariadb --turso --mongodb \ --qdrant --skip-initial-migration ``` MariaDB shares the SQLx MySQL protocol implementation but has its own live container contract. `--turso` is additive; `--database turso` is the explicit primary selection and currently rejects the SQLx-specific SaaS starter. ## Turso / libSQL as the primary ORM Turso-primary is an alternative to SQLx, not another SQLx backend. It works through `TursoOrm`, `TursoModel`, `TursoRepository` and `TursoMigration`, and does not integrate with `Schema`, `artisan!`, `Server::with_db`, Nexus or Studio's database pages; `#[derive(Orm)]` generates the separate `TursoModel` API described below. ```bash cargo rullst new edge_app --default --api --database turso cd edge_app cargo rullst make:model Event --migration cargo rullst db:migrate ``` The generated manifest enables `orm-turso` without a direct SQLx dependency, and the environment contains `TURSO_DATABASE_URL`, `TURSO_AUTH_TOKEN`, and `TURSO_OFFLINE_PATH` rather than a fictitious `DATABASE_URL`. Models use the same public derive with an explicit backend: ```rust #[derive(Debug, Clone, rullst_orm::Orm)] #[orm(table = "events", backend = "turso")] struct Event { id: i64, label: String, active: bool, } ``` Initialize once before using the generated inherent methods: ```rust,no_run # #[derive(Debug, Clone, rullst_orm::Orm)] # #[orm(table = "events", backend = "turso")] # struct Event { # id: i64, # label: String, # active: bool, # } # async fn use_turso_model() -> Result<(), Box> { rullst_orm::polyglot::TursoOrm::init_from_env().await?; let mut event = Event { id: 0, label: "started".into(), active: true, }; event.save().await?; let current = Event::find(event.id).await?; # let _ = current; # Ok(()) # } ``` `TursoOrm::init_from_env` reads `TURSO_DATABASE_URL`, `TURSO_AUTH_TOKEN` and `TURSO_OFFLINE_PATH` from the process environment only. The Turso blank starter (`cargo rullst new --database turso`) instead reads them with `rullst::config::project_setting`, which falls back to `./.env` without loading it into the process, and passes them to `TursoConfig::new` and `with_offline_path`; since 13.0.0-alpha.2 it no longer calls `dotenvy::dotenv()` or depends on `dotenvy`. The typed contract includes CRUD, equality filters, ordering, bounded pagination/counts, app-assigned or generated primary keys, checksummed migrations, status, and rollback. An equality filter whose value encodes as `NULL` (such as `Option::None`) matches with `IS NULL`. `#[orm(encrypted)]` columns store randomized ciphertext, so filtering or ordering on them returns an error instead of silently matching nothing. Generated `make:model` and `make:migration` commands retain the Turso backend. It does not yet provide SQLx ORM relations, hooks, automatic timestamps, seed generation, schema auto-diff, or transparent embedded-replica synchronization. Those limits are why only the blank/API starter is currently advertised as Turso-primary. The derive rejects unsupported SQLx-specific model behaviors at compile time instead of silently ignoring their attributes. ## Turso / libSQL explicit edge SQL ```rust,no_run use rullst_orm::polyglot::{ TursoConfig, TursoMigration, TursoQueryLimit, TursoStatement, TursoStore, TursoValue, }; async fn use_edge_sql() -> Result<(), Box> { let config = TursoConfig::new( std::env::var("TURSO_DATABASE_URL").unwrap_or_default(), std::env::var("TURSO_AUTH_TOKEN").unwrap_or_default(), ); let edge = TursoStore::connect(config).await?; let create = TursoStatement::new( "CREATE TABLE events (id INTEGER PRIMARY KEY, label TEXT NOT NULL)", vec![], )?; edge.migrate(vec![TursoMigration::new("m20260829_events", vec![create])?]) .await?; edge.execute(TursoStatement::new( "INSERT INTO events VALUES (?1, ?2)", vec![TursoValue::Integer(1), TursoValue::Text("started".into())], )?).await?; let rows = edge.query( TursoStatement::new( "SELECT id, label FROM events WHERE id = ?1", vec![TursoValue::Integer(1)], )?, TursoQueryLimit::new(100)?, ).await?; let _ = rows; Ok(()) } ``` The live path speaks the official Hrana HTTP v3 protocol directly through the workspace's Rustls-backed HTTP client; it does not embed the native SQLite engine or the legacy remote SDK dependency chain. URLs must use `libsql://` or HTTPS; cleartext HTTP requires an explicit loopback-development opt-in. Requests reject redirects and have a 30-second deadline. Tokens and endpoint details are redacted from `Debug`; remote responses are capped at 16 MiB, result materialization at 1–10,000 rows, each statement at 1,024 positional parameters/8 MiB of parameter data, and each transaction at 1,024 statements/16 MiB of raw SQL plus parameters. SQL result cells can still be large, so applications must also constrain selected columns and data at the schema/query boundary. An empty or `mock_*` endpoint selects a single-connection SQLite in-memory fallback, persisted to a file with `TursoConfig::with_offline_path` (`TursoOrm::init_from_env` uses `TURSO_OFFLINE_PATH`, default `turso-development.db`). It executes real SQL and migrations deterministically but does not simulate remote replication, latency, failover, or Turso Cloud. The live CI contract runs the same API against the official `sqld` container and proves that a failed multi-statement batch rolls back its earlier writes. ## The portable document contract MongoDB, SurrealDB, and the deterministic offline store implement the same bounded document operations: ```rust use rullst_orm::polyglot::{ CollectionName, DocumentId, DocumentPage, DocumentRepository, }; # fn bounded_document_inputs() -> Result<(), Box> { let collection = CollectionName::new("audit_events")?; let id = DocumentId::new("event-2026-0001")?; let page = DocumentPage::new(0, 50)?; # let _ = (collection, id, page); # Ok(()) # } ``` Collection names use a portable ASCII identifier grammar, IDs are bounded to letters, digits, `_` and `-`, and every list requires a 1–500 row page. Models must serialize as objects and must not own the driver ID field (`_id` for MongoDB, `id` for SurrealDB); the portable `DocumentId` owns that concern. ### Encrypted export and crash-resumable restore MongoDB, SurrealDB and the deterministic store also implement `DocumentInventory`, which retains each validated identifier. That enables one deliberately conservative recovery path without pretending the engines share transactions: ```rust,no_run use rullst_orm::polyglot::{ CollectionName, DocumentRecoveryBinding, DocumentRecoveryKey, DocumentRecoveryPolicy, export_document_snapshot, restore_document_snapshot, }; # async fn recovery(source: &S, destination: &D) -> Result<(), Box> # where # S: rullst_orm::polyglot::DocumentInventory, # D: rullst_orm::polyglot::DocumentInventory, # Event: serde::Serialize + serde::de::DeserializeOwned + Send + Sync, # { let collection = CollectionName::new("audit_events")?; let binding = DocumentRecoveryBinding::try_new( "my_application.production", collection, )?; let key = DocumentRecoveryKey::try_new( "recovery-2026-09", [42_u8; 32], // load 32 random bytes from a secret manager )?; let policy = DocumentRecoveryPolicy::try_new(100, 10_000, 16 * 1024 * 1024)?; // Persist this opaque envelope in application-owned durable storage. let snapshot = export_document_snapshot(source, &binding, &key, policy).await?; let report = restore_document_snapshot( destination, &snapshot, &binding, &key, policy, ).await?; assert_eq!(report.verified(), report.inserted() + report.replayed()); # Ok(()) # } ``` The key uses AES-256-GCM and a fresh nonce; its authenticated data binds the rotation ID, trusted application namespace and exact collection. Key material passes through a zeroizing temporary and key/snapshot `Debug` output is redacted. The policy permits pages of 1–500, 1–100,000 documents and a 1 KiB–64 MiB plaintext ceiling. Models in this portability path must be JSON objects without either `id` or `_id`. Export scans twice and refuses to seal unequal observations. This detects ordinary concurrent changes, but it is not a database snapshot isolation primitive: pause writers or use a source-side transaction/export facility. Restore accepts only an empty destination or a matching subset left by an earlier attempt. It inserts without replacement, rejects different or extra rows before mutation and verifies an exact final inventory. A failed attempt can retain earlier successful inserts, so retry the same authenticated snapshot. Keep unrelated destination writers paused until verification. The destination database, namespace and collection/table must already be provisioned where the engine requires them; schema or permission failures stay visible and are never reclassified as an empty destination. The live release matrix exercises MongoDB → SurrealDB → MongoDB. It proves the bounded adapter contract on that runner, not backup retention, key custody, replication, point-in-time recovery, topology failover or vendor operations. ### MongoDB ```rust,no_run use rullst_orm::polyglot::{ CollectionName, DocumentId, DocumentRepository, MongoDbStore, }; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] struct AuditEvent { action: String, } async fn store_audit_event() -> Result<(), Box> { let store = MongoDbStore::::connect_or_mock( std::env::var("MONGODB_URL").unwrap_or_default(), "my_application", ).await?; store.create( &CollectionName::new("audit_events")?, &DocumentId::new("event-1")?, &AuditEvent { action: "login".into() }, ).await?; Ok(()) } ``` An empty URL or one beginning with `mock_`/`mock://` selects an in-process, deterministic `BTreeMap` store. A non-mock URL uses the official MongoDB Rust driver. The release contract includes an optional Testcontainers CRUD suite; availability, backups, indexes, topology and authorization remain deployment responsibilities. ### SurrealDB documents and graph reads ```rust,no_run use rullst_orm::polyglot::{ GraphQuery, GraphRepository, SurrealAuth, SurrealConfig, SurrealDbStore, }; async fn read_graph() -> Result<(), Box> { let config = SurrealConfig::new( std::env::var("SURREALDB_URL").unwrap_or_default(), "main", "application", SurrealAuth::bearer( std::env::var("SURREALDB_TOKEN").unwrap_or_default(), ), ); let store = SurrealDbStore::::connect_or_mock(config)?; let query = GraphQuery::read_only( "MATCH (person:person)-[knows:knows]->(friend:person) RETURN friend", 100, )?; let rows = store.query_graph(&query).await?; let _ = rows; Ok(()) } ``` The adapter uses SurrealDB's documented HTTP `/key`, `/sql`, `/rpc` and `/gql` protocol instead of embedding its BSL-licensed SDK. `replace` runs a typed `UPDATE type::record($table, $id)` through `/rpc` rather than `PUT /key`, which upserts: replacing a missing (for example concurrently deleted) document returns `PolyglotError::NotFound` instead of recreating it, as on MongoDB. This statement uses SurrealDB 3 function naming. It disables redirects, requires HTTPS outside loopback unless cleartext is explicitly enabled, redacts authentication in `Debug`, streams through a configurable 1 KiB–8 MiB memory ceiling, and sends namespace/database headers on every scoped request. Every `DocumentId` is stored as a string record key, including all-digit IDs such as `9` (sent as a quoted key; SurrealDB would otherwise parse them as numbers). Inventory pages therefore follow the portable string order, `10` before `9`, on SurrealDB as on MongoDB. Records written by earlier versions under an ID that SurrealDB parsed as a number, `NULL` or an exponent (for example `9`, `null` or `1e5`) keep that non-string key and are not found by these string IDs; rewrite them under their string key when upgrading. Graph queries must start with `MATCH`; semicolons, caller-provided `LIMIT`, and the `INSERT`, `SET`, `REMOVE`, and `DELETE` tokens are rejected. The adapter then appends its own 1–1,000 row limit. This is a conservative read boundary, not arbitrary SurrealQL execution or a graph mutation API. The `/gql` endpoint requires SurrealDB 3.2 or newer and explicit experimental enablement on the 3.2 release line; the live contract pins 3.2.4 and enables that capability. ## DuckDB analytics ```rust,no_run use rullst_orm::polyglot::{ AnalyticsRepository, AnalyticsValue, DuckDbStore, QueryLimit, }; async fn analyze_events() -> Result<(), Box> { let analytics = DuckDbStore::in_memory().await?; analytics.execute( "CREATE TABLE events (sequence BIGINT, label VARCHAR)", vec![], ).await?; analytics.execute( "INSERT INTO events VALUES (?, ?)", vec![ AnalyticsValue::Signed(1), AnalyticsValue::Text("started".into()), ], ).await?; let rows = analytics.query( "SELECT sequence, label FROM events WHERE sequence >= ?", vec![AnalyticsValue::Signed(1)], QueryLimit::new(500)?, ).await?; let _ = rows; Ok(()) } ``` DuckDB is bundled for a predictable optional build. Its connection is guarded and every native operation runs through Tokio's blocking worker pool. Dynamic values are prepared parameters; SQL text remains trusted application structure. Results must declare a 1–10,000 row materialization limit. Scalar, decimal, temporal, interval, geometry and binary values are preserved; unsupported complex result types fail with a typed error instead of being guessed. ## Specialized Qdrant and Redis stores Qdrant has a separate dense-vector contract rather than a document or Active Record facade. See [RAG Systems & Vector Search](https://rullst.github.io/Rullst/book/tutorials/22-rag-vector-search.html) for its bounded collection, upsert, delete and cosine-query API. The project wizard and deterministic CLI expose it as the additive `--qdrant` choice. Redis native structures are also explicit and namespaced: ```rust,no_run use rullst_orm::{ RedisDataConfig, RedisDataKey, RedisDataStore, RedisField, RedisMember, RedisScanLimit, RedisStructuresRepository, RedisValue, }; # async fn redis_example() -> Result<(), Box> { let config = RedisDataConfig::new( std::env::var("REDIS_URL").unwrap_or_default(), "my-application", std::env::var("REDIS_USERNAME").unwrap_or_default(), std::env::var("REDIS_PASSWORD").unwrap_or_default(), ); let data = RedisDataStore::connect_or_mock(config).await?; let account = RedisDataKey::new("account:42")?; data.hash_set( &account, &RedisField::new("display_name")?, &RedisValue::new("Ada")?, ).await?; data.set_add(&account, &RedisMember::new("reader")?).await?; let roles = data .set_scan(&account, RedisScanLimit::new(100)?) .await?; # let _ = roles; # Ok(()) # } ``` The adapter also supplies atomic signed hash increments, exact membership, Sorted Set add/top ranking and exact per-structure deletion. Empty or `mock_*` endpoint/ACL credentials choose the deterministic fallback. Live remote Redis requires `rediss://`; intentionally unauthenticated development uses the loopback-only constructor. Hash values are capped at 1 MiB, members at 4 KiB, and scans/ranges at 1–1,000 accepted rows. Redis may still allocate a protocol value before client-side validation, so untrusted writers require server ACLs, quotas and isolation. Lists, Streams, cluster/failover, eviction and durable Pub/Sub are not part of this datastore contract. ## What this feature does not promise - no cross-backend transaction, replication, migration, or consistency layer; - no claim that document databases implement SQL joins or Active Record; - no automatic choice of a datastore based on a model; - no managed backups, production credentials, provider homologation, indexes, cluster failover, or performance guarantee; - no universal “support for every database”. Run the application's exact feature set in addition to the full workspace gates. Remote production readiness still requires a disposable live environment, negative authorization tests, backup/restore rehearsal, and operational review. --- # 38. Transactional Outbox & Durable Effects Source: https://rullst.github.io/Rullst/book/tutorials/38-transactional-outbox.html # 38. Transactional Outbox & Durable Effects Use a transactional outbox when a committed database change must eventually produce an external effect even if the application process stops between the two operations. Typical examples are a webhook, an e-mail request, a search projection or a message published to another service. `after_commit` is the simpler choice for a best-effort, process-local effect. `Outbox` is the durable choice. It stores an event in the same relational transaction as the domain mutation, then lets an independent worker claim and deliver it later. ## 1. Register the schema as a migration Applications using the built-in SQLx migration runner can register the versioned migration supplied by the ORM: ```rust,no_run use rullst_orm::OutboxMigration; use rullst_orm::schema::migration::Migration; fn migrations() -> Vec> { vec![Box::new(OutboxMigration)] } ``` In a generated project, add `Box::new(rullst::db::OutboxMigration)` to the vector returned by `get_migrations()` in `src/migrations/mod.rs`, then run `cargo rullst db:migrate`. `Outbox::install()` applies the same idempotent DDL directly and is useful in tests or explicit local setup. It never runs automatically. A production team should review and track the migration through the same deployment process as its domain schema; do not create tables opportunistically while serving a request. The built-in contract supports the SQLx relational backends: SQLite, PostgreSQL, MySQL and MariaDB. It does not span MongoDB, DuckDB, Turso or SurrealDB transactions. ## 2. Commit domain state and event together Choose a stable `stream` boundary and a deterministic event key. A stream can represent an application or tenant, but it is only a namespace: the framework does not infer authorization from it. ```rust,no_run use rullst_orm::{Error, Orm, Outbox}; use serde_json::json; # async fn create_invoice() -> Result<(), Error> { Orm::transaction(|_| Box::pin(async move { // `?` placeholders are SQLite/MySQL syntax; PostgreSQL uses `$1, $2`. let insert = sqlx::query( "INSERT INTO invoices (id, status) VALUES (?, ?)" ) .bind(123_i64) .bind("issued"); rullst_orm::execute_query!(insert, execute, pool)?; Outbox::enqueue( "tenant-42", "invoice:123:issued:v1", "invoice.issued", &json!({"invoice_id": 123}), ).await?; Ok::<(), Error>(()) })).await?; # Ok(()) # } ``` If the transaction rolls back, neither row survives. This also holds when a helper wraps `enqueue` in its own `Orm::transaction` and is called from inside another one: the nested call joins the outer transaction through a savepoint, so its outbox row commits or rolls back with the outer transaction. Calling `enqueue` outside `Orm::transaction` fails instead of silently opening an unrelated transaction. When the application already owns a raw SQLx transaction, use `enqueue_with_tx(&mut transaction, ...)`. `(stream, event_key)` is unique. Repeating the same kind and an equal JSON payload returns the existing event ID with `inserted == false`, also when a concurrent transaction committed that key after your transaction's first read (MySQL/MariaDB read the row back with a locking read). Payloads are compared as JSON values, so an object serialized with its keys in another order (for example one built from a `HashMap`) is the same content; array order and every value still count. Reusing that key for different content is an error; it does not overwrite the original event, and the error does not echo the stream or key. Streams and event keys are case-sensitive on every backend: `order:aB3x` and `order:Ab3X` are different keys, and a worker for `tenant-a` does not claim events of `tenant-A`. MySQL/MariaDB declare the key columns with the binary `ascii_bin` collation for this. A MySQL/MariaDB table created by an earlier version used the server's case-insensitive default collation; run `Outbox::install()` once, for example from a new migration, to convert it in place. The conversion rebuilds the table, so schedule it like other DDL. ## 3. Claim, deliver and acknowledge Each worker supplies a stable identifier, lease duration and maximum number of claims: ```rust,no_run use rullst_orm::{Error, Outbox}; # async fn deliver() -> Result<(), Error> { if let Some(event) = Outbox::claim_next( "tenant-42", "webhook-worker-1", 30, // lease seconds 8, // maximum claims ).await? { let payload = event.payload()?; // Perform the external effect. Pass event.event_key to a provider that // supports idempotency, or deduplicate it in the receiving service. let delivered = payload.get("invoice_id").is_some(); if delivered { let owned = Outbox::acknowledge(event.id, event.claim_key).await?; if !owned { // The lease expired or another worker reclaimed the event. // Do not assume ownership or mutate the newer claim. } } else { Outbox::fail( event.id, event.claim_key, "provider temporarily unavailable", 8, // maximum attempts 15, // retry delay seconds ).await?; } } # Ok(()) # } ``` An ACK or failure transition succeeds only for the exact, unexpired random claim token. A failed delivery becomes pending after the bounded delay, or `dead_letter` when its attempt limit is reached. If a worker dies during its final claim, the next claim sweep moves that expired event to dead-letter instead of retrying forever. Claims, acknowledgements and failures commit on their own. Calling them inside `Orm::transaction` returns a validation error, because an acknowledgement there would already be durable if the handler's transaction later rolled back. Commit the handler's database work first, then acknowledge. ## 4. Understand the guarantee The delivery guarantee is **at least once**: 1. the worker claims an event; 2. the external provider accepts the effect; 3. the process stops before the database ACK; 4. the lease expires and the event is delivered again. No local database design can atomically commit an arbitrary remote HTTP side effect. Make the consumer idempotent using the stable stream/event key. Do not use a random key on every retry. Other explicit limits: - key fields are 1–128 characters from a bounded ASCII grammar; - JSON payloads are at most one MiB; - leases are 1–3,600 seconds, attempts are 1–100 and retry delays are 0–86,400 seconds; - a failure message is 1–512 bytes without control characters; - ordering is not guaranteed across retries or concurrent workers; - generated observers are not automatically converted into outbox events; - cleanup, retention, tenant authorization and the worker supervision loop belong to the application; - monitor retry and dead-letter state through your normal database operations. This separation keeps model saves predictable while giving applications a durable primitive where the event schema and operational policy are explicit. ## 5. Relay into `rullst-messaging` With the umbrella `messaging-orm-outbox` feature, `OrmOutboxRelay` maps one exact outbox stream to one topic on any static `MessageBroker`. It validates the claimed JSON, uses the stream-scoped `event_key` (the lowercase hex SHA-256 of the stream, `/`, then the event key) as the broker idempotency key, publishes and then acknowledges the exact ORM claim. Its executable crash-window test stops after the first publish, reclaims the expired event and observes an exact broker replay with only one retained message. That bridge improves composition; it does not change the guarantee. ORM commit, broker publish and ORM ACK are not one distributed atomic transaction. Operate the claim loop, retry/dead-letter policy, cleanup, authorization and final consumer deduplication explicitly. See [Bounded Brokered Messaging](https://rullst.github.io/Rullst/book/tutorials/49-brokered-messaging.html#6-relay-a-relational-outbox-after-commit) for the relay code. --- # 39. Scout Search Providers Source: https://rullst.github.io/Rullst/book/tutorials/39-scout-search.html # 39. Scout Search Providers > [!IMPORTANT] > Dependency examples pin the v13 prerelease `=13.0.0-alpha.4` exactly: Cargo > never selects a prerelease on its own, and the v12 line reached end of life > on 16 October 2026 (see the [release record](https://rullst.github.io/Rullst/book/v12.html)). Commit Cargo.lock > for reproducible builds. Use path dependencies only when intentionally > testing checkout-local changes. > The result ordering, SQL fallback, indexed-field, `restore()`/`force_delete()` > projection and hit-cap rules below describe the v13 prerelease source > (`13.0.0-alpha.4`). Scout connects a `#[orm(searchable)]` SQLx model to one search backend. The model database remains authoritative; the search index is a projection updated only after a successful managed commit. ## Enable the provider adapters With the umbrella crate, add `orm-scout` next to the application's primary database feature (SQLite here): ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["orm-scout", "strict-sqlite"] } ``` Or enable `scout-http` directly on `rullst-orm`. The feature supplies bounded HTTP adapters for Meilisearch, Elasticsearch and Algolia. The in-memory `MockSearchEngine` is available without the HTTP feature. The examples use `rullst_orm::…` paths: `#[derive(Orm)]` and `FromRow` expand to `rullst_orm::…` and `::sqlx::…` paths, so a crate that derives models depends on `rullst-orm` and `sqlx` directly. Projects generated by `cargo rullst new` already list both. ## Configure one engine Configuration is process-wide and fail-closed if code tries to replace an already installed engine: ```rust,no_run use rullst_orm::{MeilisearchEngine, set_search_engine}; # fn configure() -> Result<(), rullst_orm::Error> { let engine = MeilisearchEngine::new( std::env::var("MEILI_URL").unwrap_or_default(), std::env::var("MEILI_API_KEY").unwrap_or_default(), )?; set_search_engine(engine)?; # Ok(()) # } ``` Empty or `mock_*` credentials deliberately select the deterministic offline store. That makes local/test behavior explicit and prevents an accidental live call with missing secrets. A keyless Meilisearch or Elasticsearch development server must be selected with `MeilisearchEngine::local(...)` or `ElasticsearchEngine::local(...)`; those constructors accept only loopback HTTP. Every endpoint must be an origin URL (no credentials, path, query or fragment) using HTTPS, or HTTP on a loopback host, and redirects are disabled. Algolia normally derives its official API origin from the application ID: ```rust,no_run use rullst_orm::AlgoliaEngine; # fn engine() -> Result { AlgoliaEngine::new("APPLICATION_ID", "ADMIN_API_KEY") # } ``` `AlgoliaEngine::with_endpoint` exists for an explicitly configured compatible proxy and applies the same HTTPS/loopback URL policy. Never expose an indexing or admin key to browser code. ## Mark and query the model ```rust,no_run use rullst_orm::{FromRow, Orm}; #[derive(Clone, Debug, FromRow, Orm)] #[orm(table = "articles", searchable)] struct Article { id: i32, title: String, body: String, } # async fn find() -> Result, rullst_orm::Error> { let results = Article::search("transactional outbox").await.get().await?; # Ok(results) # } ``` With an engine, the results follow the provider's ranking (best hit first, also for `first()` and `paginate()`) unless you add `order_by(...)`. Without a configured engine, `Article::search(...)` falls back to a SQL `LIKE` over the model's persisted columns, excluding `#[orm(hidden)]`, `#[orm(encrypted)]`, `#[orm(masked)]` and `SecretString` fields. `%` and `_` in the query match literally rather than as wildcards. Generated save/update/delete operations, `restore()` (re-index) and `force_delete()` (removal) project only after the relational commit. The indexed document omits `#[orm(hidden)]`, `#[orm(encrypted)]` and `#[orm(masked)]` fields, so the provider never receives their values; documents indexed by earlier versions should be reindexed. Rollback produces no search write. Provider or search errors remain typed errors; they are not silently converted into an empty result. The shared adapter boundary enforces: - index names (the model's table) of at most 128 bytes that start with a lowercase ASCII letter and contain only lowercase letters, digits, `-` or `_`; - positive `i32` document IDs; - object-shaped JSON documents no larger than one MiB; - queries no larger than 1,024 bytes and without control characters; - at most 1,000 parsed hits and a four-MiB response body; - five-second connect and twenty-second request deadlines; - disabled redirects and no secret-bearing error bodies. The hit cap (`rullst_orm::scout::MAX_SEARCH_HITS`) applies to the whole shared index, before generated tenant, model-wide and soft-delete scopes. For a model with such a scope, an answer of 1,000 IDs is treated as truncated and `search()` answers from the SQL fallback instead. Meilisearch and Algolia asynchronous indexing tasks are awaited with a bounded poll loop. Elasticsearch uses `refresh=wait_for` for the adapter operations. ## Choose the durability level The generated Scout hook is process-local after commit. A provider outage is reported as `PostCommit`: the model row is already durable and must not be blindly inserted again. This path is suitable when the application can rebuild the index or explicitly retry. If every projection must survive a process crash, write a versioned search event through `Outbox::enqueue` in the domain transaction and run the selected engine from an idempotent outbox worker. Rullst does not invent a stable event key or serialize every model hook automatically. See [Transactional Outbox & Durable Effects](https://rullst.github.io/Rullst/book/tutorials/38-transactional-outbox.html). ## Evidence boundaries The repository runs a real update/search/delete lifecycle against a digest-pinned Meilisearch container. Elasticsearch and Algolia run deterministic offline tests plus local HTTP protocol fixtures that verify paths, headers, payloads, response bounds and ID parsing. Those fixtures do not prove a hosted Algolia account, every Elasticsearch version, cluster failover, ranking quality or production capacity. --- # 41. Tenant-Bound RAG Source: https://rullst.github.io/Rullst/book/tutorials/41-tenant-bound-rag.html # 41. Tenant-Bound RAG in One Typed Operation > [!IMPORTANT] > Dependency examples pin the v13 prerelease `=13.0.0-alpha.4` exactly: Cargo > never selects a prerelease on its own, and the v12 line reached end of life > on 16 October 2026 (see the [release record](https://rullst.github.io/Rullst/book/v12.html)). Commit Cargo.lock > for reproducible builds. Use path dependencies only when intentionally > testing checkout-local changes. Rullst's bounded RAG pipeline turns one authenticated question into a guarded embedding, authorized retrieval request, budgeted context, grounded model call, source metadata, and a terminal audit event. It deliberately does not hide the datastore or invent authorization. The host application still decides who may ask, which records that identity may read, how documents are ingested and deleted, and which embedding model and vector dimensions define an index. ## Add the feature ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["ai"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } ``` ## Run the complete offline contract An empty or `mock_*` OpenAI key selects Rullst's deterministic offline provider. Its fixture embeddings have 16 dimensions, so the local index below uses the same exact dimension. ```rust,no_run use rullst::ai::rag::{ InMemoryRagAuditTrail, InMemoryRagRetriever, RagDocument, RagPipeline, }; use rullst::ai::{providers::openai::OpenAiProvider, AiClient}; use rullst::security::TenantContext; use std::sync::Arc; #[tokio::main] async fn main() -> Result<(), Box> { // Build this only from verified authentication and membership claims. let tenant = TenantContext::try_new("tenant:acme")?; let client = AiClient::new(OpenAiProvider::new("mock_rag")); let retriever = InMemoryRagRetriever::try_new(1_000, 16)?; let content = "Rullst upgrades start with cargo rullst upgrade --dry-run."; let vector = client.embed(content).await?; let document = RagDocument::try_new( &tenant, "upgrade-guide", content, 0.0, // The retriever replaces this with cosine similarity. )?; retriever.upsert(&tenant, document, vector)?; let audit = Arc::new(InMemoryRagAuditTrail::new(1_000)?); let pipeline = RagPipeline::new(client, retriever, Arc::clone(&audit)); let response = pipeline .answer(&tenant, "How do I preview a framework upgrade?") .await?; println!("{}", response.answer()); for source in response.sources() { println!( "source={} score={} chars={} truncated={}", source.document_id(), source.score(), source.included_chars(), source.truncated() ); } let events = audit.entries()?; println!("recorded {} terminal RAG event(s)", events.len()); Ok(()) } ``` Saved as `src/main.rs`, `cargo run` prints the deterministic offline answer, one `source=upgrade-guide …` line and `recorded 1 terminal RAG event(s)`. The offline provider is a deterministic test fixture, not evidence of live model quality. In a real application, select a live provider explicitly and evaluate its configured model against a versioned domain corpus. ## What the pipeline enforces For each call, `RagPipeline`: 1. validates the bounded question and runs guarded embedding; 2. passes the trusted `TenantContext`, query vector, and bounded limit to the application retriever; 3. rejects over-returned or differently tagged documents; 4. runs prompt-injection checks and outbound PII masking on every passage; 5. truncates by per-document and total Unicode-scalar budgets; 6. refuses to generate when no safe context remains; 7. returns only source identifiers, scores, included character counts, and truncation state alongside the answer; 8. records exactly one terminal, secret-minimized audit event, or fails the operation if the required audit sink is unavailable. Tune budgets with `RagConfig::try_new(max_documents, max_document_chars, max_context_chars)`. The constructor accepts 1–32 documents, 1–32,768 characters per document and 1–131,072 context characters (characters are Unicode scalar values); `RagPipeline::with_config` accepts only an already validated configuration. ## Use an authoritative production retriever `InMemoryRagRetriever` is bounded and tenant-partitioned, but it is process local, nondurable, and uses an exact linear cosine scan. It is appropriate for tests, development, and small ephemeral datasets. Production applications implement the public async `RagRetriever` trait over their chosen store. A PostgreSQL implementation should put the tenant predicate inside the same parameterized pgvector query that performs similarity ordering. A Qdrant implementation should apply the deployment's reviewed tenant filter or separate-collection policy. It then creates each `RagDocument` from the same trusted context passed to `retrieve`. The essential shape is: ```rust,ignore async fn retrieve( &self, tenant: &TenantContext, query_embedding: &[f32], limit: usize, ) -> Result, RagRetrievalError> { let rows = self.store .similar_chunks_for_tenant(&tenant.tenant_id, query_embedding, limit) .await .map_err(redacted_retrieval_error)?; rows.into_iter() .map(|row| RagDocument::try_new(tenant, row.id, row.content, row.score)) .collect() } ``` The pipeline verifies the returned tenant tag as defense in depth. That check cannot detect a datastore adapter that incorrectly read another tenant's row and then falsely relabeled it. Authoritative tenant and ownership filtering must therefore happen in the datastore query itself. ## Supply durable, minimized audit evidence `InMemoryRagAuditTrail` is useful for local assertions. A single-process service can use the bounded built-in local file: ```rust,no_run use rullst::ai::rag::DurableRagAuditTrail; use std::sync::Arc; let audit = Arc::new(DurableRagAuditTrail::try_open( "storage/audit/rag.log", )?); # Ok::<(), Box>(()) ``` The trail synchronously appends a distinct versioned stream, validates every event/frame on restart and fails closed on corruption, unsafe file types, external length changes and quota exhaustion. The default ceilings are 16 MiB and 4,096 events; `try_open_with_max_bytes` may select a smaller byte quota. Its SHA-256 frame digest detects corruption but does not authenticate the writer. The host owns trusted directory permissions, exclusive writer access, rotation, retention, backup and export. Multi-instance deployments should implement `RagAuditSink` over an append-only, access-controlled destination. The built-in event contains the tenant ID, a SHA-256 digest of the original question, document counts, included context characters, and the terminal outcome. It omits raw questions, passages, vectors, model answers, and provider error bodies. A digest is not encryption and low-entropy questions may be guessable, so do not expose the audit stream as public data. ## Security and quality boundaries - Prompt-injection filtering is heuristic defense in depth, not proof that a passage or answer is safe. - Source metadata proves which identifiers entered the prompt; it does not prove that the model cited them faithfully or that their contents were true. - Apply normal output encoding, domain validation, and tool authorization after generation. Never treat model text as an authorized command. - Keep ingestion, updates, deletions, retention, embedding-model migrations, ANN tuning, backups, and disaster recovery explicit. - Test cross-tenant denial, empty retrieval, hostile passages, provider failure, datastore failure, audit failure, and context truncation in the host application. For the lower-level vector-store contracts, see [RAG Systems & Vector Search](https://rullst.github.io/Rullst/book/tutorials/22-rag-vector-search.html). For the framework's broader claims and limits, see the [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html). --- # 42. Server-Bound OAuth/OIDC Sessions Source: https://rullst.github.io/Rullst/book/tutorials/42-server-bound-oauth-sessions.html # 42. Server-Bound OAuth/OIDC Sessions Rullst Connect manages the browser-to-provider callback challenge: it generates state and PKCE for OAuth 2.0, adds a nonce for OpenID Connect, keeps those private values on the server and consumes them exactly once before validating the callback. It offers two places to keep them: - a **login challenge store** (v13, from 13.0.0-alpha.3) in the application's own database, with only an opaque id in a short-lived cookie. A Rullst application signs users in with its own encrypted session cookie, so this is the path for it, and `cargo rullst make:oauth google|github|microsoft|apple` generates it; - a **`tower-sessions` session** (`axum-session` feature), for an Axum application that already runs a `SessionManagerLayer`. Either way this removes security-sensitive plumbing from ordinary handlers. It does not choose the application's account-linking or authorization policy. ## Store-backed login ### Generate it In a project with the account model of `cargo rullst auth`, the SaaS starter or the LMS example (`examples/lms`): ```bash cargo rullst make:oauth google # or github, microsoft, apple cargo rullst db:migrate ``` The command adds `/auth/` and `/auth//callback` (behind the password login's per-client rate limit), a "Sign in with …" link on the login page, an `oauth_identities` table (provider and subject, unique) and the `oauth`, `oauth-` and application `oauth-mock` features. The generated callback signs the account in with `make_login_cookie_with_epoch`, like the password login. A provider email links or creates an account only when the provider verified it; an email that already belongs to an account is refused by default, because registration does not prove that the person who registered it owns the address. See [`cargo rullst make:oauth`](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-makeoauth-v13-prerelease-preview) for the settings and the linking policy. Run the command once per provider: the first one creates the shared flow (`src/controllers/oauth/mod.rs`), the table and its migration; each further provider adds only its module, routes, login link and settings, so Google and GitHub (or all four) share one `oauth_identities` table, one migration and the same linking policy. A provider identity signs in to one account; linking a second provider to an existing account follows `EXISTING_ACCOUNT_POLICY`. What each provider's identity is: | Provider | Protocol | Subject in `oauth_identities` | Email that may link or create an account | | :--- | :--- | :--- | :--- | | Google | OpenID Connect | ID token `sub` | ID token `email` with `email_verified: true` | | GitHub | OAuth 2.0 (not OIDC) | numeric account `id` of `GET /user`, never the login name, which users can rename | the `primary` address of `GET /user/emails`, only when GitHub marks it `verified` | | Microsoft | OpenID Connect, tenant chosen by `MICROSOFT_TENANT` | `:`, the directory and object IDs (`sub` is pairwise: it changes with the app registration) | the `email` claim only with the optional claim `xms_edov: true` | | Apple | OpenID Connect, callback by form POST | ID token `sub`, stable for the developer team | ID token `email` with `email_verified`; it may be a private relay address | **Microsoft.** `MICROSOFT_TENANT` is required with the credentials and has no default, because it decides who can create accounts: `common` (any Entra ID organization and personal Microsoft accounts), `organizations` (any organization), `consumers` (personal accounts) or one directory (tenant) ID. Rullst Connect verifies the ID token against the tenant's keys and requires an issuer `https://login.microsoftonline.com//v2.0` whose `tid` the tenant choice admits. Microsoft does not verify the `email` claim: an organization's administrators can give a user any address, including one of another company. Treating it as verified would let any tenant administrator sign in to an account here (the "nOAuth" pattern). The generated code therefore trusts it only with `xms_edov: true`, Microsoft's statement that the address's domain is verified for the user's organization (or that the account is a personal one). Add the optional ID-token claims `email` and `xms_edov` under **Token configuration** in the app registration; without them a Microsoft identity cannot create or link an account, and only identities linked before can sign in. Changing `EXISTING_ACCOUNT_POLICY` does not change this check. **Apple.** Apple returns the callback as a cross-site form POST (`response_mode=form_post`). Browsers send no `SameSite=Lax` cookie with it, so the generated start uses `ChallengeCookie::for_cross_site_post` (`SameSite=None; Secure`), and the command lists `/auth/apple/callback` in `csrf_signed_webhook_paths` of `Rullst.toml`: Apple cannot send this site's CSRF token, so instead the handler accepts the POST only with the `state` of the login challenge that this browser's challenge cookie names, which another site cannot know. The client secret is a five-minute ES256 JWT that Rullst Connect signs for each code exchange from `APPLE_TEAM_ID`, `APPLE_KEY_ID` and the `.p8` key in `APPLE_PRIVATE_KEY` (process environment or a secret store; newlines may be written as `\n`; the command never writes it to a file). `AppleProvider::verify_signing_key` stops the application at startup when the key cannot sign. Apple sends the user's name only on the first authorization, in the unsigned `user` form field; the generated code uses it as the new account's display name and for nothing else. An Apple email may be a private relay address (`…@privaterelay.appleid.com`): register the domain of `MAIL_FROM` with Apple's Private Email Relay service, or account emails never arrive. Apple accepts only HTTPS return URLs on a registered domain, never `localhost`, so develop with the mock credentials. To try a provider without registering an application, run with mock credentials and the offline provider, which sends the browser straight back to the callback (a same-site GET, Apple included) and signs in `mock@example.invalid`: ```bash GOOGLE_CLIENT_ID=mock_google GOOGLE_CLIENT_SECRET=mock_google \ cargo run --features oauth-mock ``` The command prints the mock settings of the provider it added, such as `MICROSOFT_TENANT=common MICROSOFT_CLIENT_ID=mock_microsoft MICROSOFT_CLIENT_SECRET=mock_microsoft`. Without `--features oauth-mock` the application stops at startup with an error that names the feature, and staging and production refuse mock credentials. ### Or write the two handlers The store implements `ChallengeStore`: `MemoryChallengeStore` for tests and one process, or `SqlChallengeStore` over the application's own SQLx pool with the `sqlite`, `postgres` or `mysql` feature of `rullst-connect` (`oauth-sqlite`, `oauth-postgres` or `oauth-mysql` on the `rullst` facade, where the module is `rullst::connect::challenge`). `SqlChallengeStore::open` creates its table, `rullst_connect_login_challenges`, when it is missing. ```rust use axum::http::{HeaderMap, HeaderValue, StatusCode, header}; use axum::response::{IntoResponse, Redirect, Response}; use rullst_connect::challenge::{ ChallengeCookie, LoginOptions, MemoryChallengeStore, begin_login, finish_login, }; use rullst_connect::extractors::AuthCallback; use rullst_connect::providers::GoogleProvider; /// A redirect that also sets `cookies`. fn redirect(target: &str, cookies: &[String]) -> Response { let mut response = Redirect::to(target).into_response(); for cookie in cookies { if let Ok(value) = HeaderValue::from_str(cookie) { response.headers_mut().append(header::SET_COOKIE, value); } } response } /// `GET /auth/google` async fn start( store: &MemoryChallengeStore, google: &GoogleProvider, cookie: &ChallengeCookie, ) -> Response { match begin_login(store, google, LoginOptions::oidc("google")).await { Ok(login) => redirect(login.url(), &[login.set_cookie(cookie)]), Err(_) => StatusCode::SERVICE_UNAVAILABLE.into_response(), } } /// `GET /auth/google/callback` async fn callback( store: &MemoryChallengeStore, google: &GoogleProvider, cookie: &ChallengeCookie, headers: &HeaderMap, params: &AuthCallback, ) -> Response { let id = ChallengeCookie::read( headers .get_all(header::COOKIE) .iter() .filter_map(|value| value.to_str().ok()), ); match finish_login(store, google, "google", id.as_deref(), params).await { Ok(login) if login.user().email_verified == Some(true) => { // Map `login.user().id` (the provider subject) to an account and // add the application's session cookie here. redirect("/dashboard", &[cookie.clear()]) } Ok(_) | Err(_) => StatusCode::BAD_REQUEST.into_response(), } } ``` `ChallengeCookie::new("/auth/google", secure)` scopes the cookie to the login routes; pass `secure` the way Rullst Core decides it, `Environment::requires_secure_defaults()`. The cookie is named `rullst_oauth_challenge`, so Core also adds `Secure` to it on every HTTPS request. ### Store contract - `begin_login` stores the provider name, state, PKCE verifier, nonce (OIDC only) and an optional same-site return page (`ReturnPath`: an absolute path, never `//host` or `/\host`), and returns a fresh `ChallengeId`: 256 random bits, 43 base64url characters. The challenge lives ten minutes, or less with `LoginOptions::with_lifetime`. - The browser holds only the id, in an `HttpOnly`, `SameSite=Lax` cookie of at most ten minutes. `SameSite=Lax` still sends it with the provider's top-level `GET` redirect. For a provider that POSTs the callback from its own site (Sign in with Apple), `ChallengeCookie::for_cross_site_post` makes it `SameSite=None; Secure`, the only cookie a browser attaches to that POST. Several cookies of that name with different values are refused rather than guessed. - Stores key rows by a SHA-256 digest of the id, never the id. Consumption is one atomic removal: `DELETE … RETURNING` on SQLite and PostgreSQL, `SELECT … FOR UPDATE` then `DELETE` in one transaction on MySQL, a mutex in memory. Of several concurrent callbacks with one id, exactly one finds it. - `finish_login` reports a missing cookie, then an unknown, altered, used or expired id, before it reads the callback; from there the challenge is consumed whatever follows. It compares the state in constant time before it trusts a provider `error`, then exchanges the code with the stored verifier and nonce. Errors are typed (`LoginError`, `ChallengeError`) and carry no state, code, verifier, nonce or id. - Each store has an entry limit: a save at the limit first deletes expired challenges, then refuses with `ChallengeError::CapacityExceeded` (`purge_expired` deletes them on demand). The limit is exact on SQLite and in memory; on PostgreSQL and MySQL concurrent saves can pass it by at most their number. Concurrent login tabs each have their own challenge, unlike the one active challenge of a `tower-sessions` session. ## Alternative: `tower-sessions` The rest of this tutorial covers the `axum-session` feature. ## Enable the session feature Pin the v13 prerelease exactly (Cargo never selects a prerelease on its own; the v12 line reached end of life, see the [release record](https://rullst.github.io/Rullst/book/v12.html)): ```toml [dependencies] rullst-connect = { version = "=13.0.0-alpha.4", features = ["axum-session"] } tower-sessions = "0.15" ``` Applications should use one immutable compatible version. Add a `SessionManagerLayer` to the Axum router. `MemoryStore` is useful for local examples and tests, but it is process-local and is not a production durability or horizontal-scaling strategy. ```rust,ignore use tower_sessions::{cookie::SameSite, MemoryStore, SessionManagerLayer}; let sessions = SessionManagerLayer::new(MemoryStore::default()) .with_http_only(true) .with_same_site(SameSite::Lax) .with_secure(true); let app = app.layer(sessions); ``` `SameSite::Lax` permits the ordinary top-level OAuth callback, a cross-site `GET` redirect, while reducing cross-site cookie exposure. It does not cover a provider that returns the callback as a cross-site `POST`; see [Sign in with Apple](https://rullst.github.io/Rullst/book/tutorials/42-server-bound-oauth-sessions.html#sign-in-with-apple-form-post-callback). Production still requires HTTPS, a durable shared store where multiple instances are used, bounded store retention, protected keys and an explicit reverse-proxy policy. ## Start OAuth 2.0 with state and PKCE Use this path for providers such as GitHub where the application is using an OAuth authorization-code flow without an ID token: ```rust use axum::response::Redirect; use rullst_connect::prelude::*; use tower_sessions::Session; async fn start_github( session: Session, github: &GithubProvider, ) -> Result { let authorization = begin_oauth_session(&session, github).await?; Ok(Redirect::temporary(authorization.url())) } ``` The returned URL contains the random state and the SHA-256 PKCE challenge. The 64-character verifier is serialized only in the server-side session record. `OAuthAuthorization` deliberately redacts its URL from `Debug` output. ## Start OpenID Connect with nonce Use the OIDC variant for Google or a discovered custom OIDC provider: ```rust,ignore let authorization = begin_oidc_session(&session, &oidc_provider).await?; Ok(Redirect::temporary(authorization.url())) ``` This stores another random value and sends it as `nonce`. The provider adapter receives that same expected nonce later and validates it against the signed ID token in the adapters whose documented contract includes ID-token validation. The query-string `AuthSession` callback does not support Apple; see the next section. ## Sign in with Apple: form POST callback `AppleProvider` always requests `response_mode=form_post`, which Apple requires when the `name` or `email` scope is requested. Apple then returns `code`, `state`, `id_token` and, on the first sign-in only, `user` in an `application/x-www-form-urlencoded` POST from `appleid.apple.com`. Two parts of the query-string flow do not fit that request: - `AuthSession` reads the callback from the query string only, so the POST has no state to compare and the extraction fails; - the POST is cross-site, and browsers send a cookie on a cross-site POST only when it is `SameSite=None; Secure`. The `SameSite::Lax` session cookie shown above is not sent, so the callback cannot find its stored challenge. Do not relax the application's authenticated session cookie to `SameSite=None` to work around this. Keep the Apple challenge in a dedicated, short-lived store reached through its own `SameSite=None; Secure; HttpOnly` cookie scoped to the Apple routes. Without the managed extractor of the next section, validate the posted form with the framework-neutral primitives: ```rust,ignore use rullst_connect::extractors::AuthCallback; use rullst_connect::pkce::{generate_oauth_state, generate_pkce}; use rullst_connect::prelude::*; use rullst_connect::provider::ExchangeParams; // Start: generate the same values the managed flow would store. let state = generate_oauth_state(); let nonce = generate_oauth_state(); let (code_verifier, code_challenge) = generate_pkce(); let mut url = url::Url::parse( &apple.redirect_url_with_pkce_and_state(&code_challenge, &state), )?; url.query_pairs_mut().append_pair("nonce", &nonce); // Application-provided: store the three values for ten minutes under a random // ID and set `apple_challenge=; Path=/auth/apple; Max-Age=600; Secure; // HttpOnly; SameSite=None` on the redirect response. store_apple_challenge(&state, &nonce, &code_verifier).await?; // Callback: `POST /auth/apple/callback` with a small body limit. async fn apple_callback( axum::Form(callback): axum::Form, ) -> Result { // Application-provided: atomically remove the challenge named by the // `apple_challenge` cookie, failing if it is missing or expired. let challenge = take_apple_challenge().await?; callback.verify_state(&challenge.state)?; if let Some(error) = &callback.error { return Err(ConnectError::Provider(format!("Apple returned {error}"))); } let code = callback .code .as_deref() .ok_or_else(|| ConnectError::Token("missing code".to_string()))?; apple .get_user(ExchangeParams { auth_code: code, code_verifier: Some(&challenge.code_verifier), expected_nonce: Some(&challenge.nonce), }) .await } ``` `AuthCallback` ignores the extra `id_token` and `user` fields. `get_user` redeems the code and verifies the returned ID token's signature, issuer, audience, expiry and nonce. The unsigned `user` JSON is the only place Apple sends the user's name; parse it separately if needed and treat it as unverified. After a successful callback, rotate or create the application's own authenticated session as usual. ### Managed form POST callback `AuthSessionForm` (added in 13.0.0-alpha.1) is the form POST counterpart of `AuthSession`. It reads an `application/x-www-form-urlencoded` `POST` body of at most 16 KiB, ignores `id_token` and `user`, and consumes the same challenge that `begin_oidc_session` stored, with the same expiry, constant-time state comparison and single use. A non-`POST` request, another content type or an oversized body is rejected before the challenge is touched. ```rust use rullst_connect::prelude::*; async fn apple_callback( callback: AuthSessionForm, apple: &AppleProvider, ) -> Result { apple.get_user(callback.exchange_params()?).await } ``` The cookie requirement does not change: the session that holds the challenge must be `SameSite=None; Secure`. Give the Apple start and callback routes their own session layer rather than relaxing the application session: ```rust,ignore use tower_sessions::{cookie::SameSite, MemoryStore, SessionManagerLayer}; let apple_challenges = SessionManagerLayer::new(MemoryStore::default()) .with_name("apple_oauth_challenge") .with_path("/auth/apple") .with_http_only(true) .with_secure(true) .with_same_site(SameSite::None); let apple_routes = Router::new() .route("/auth/apple/start", get(start_apple)) // begin_oidc_session .route("/auth/apple/callback", post(apple_callback)) // AuthSessionForm .layer(apple_challenges); ``` An inner session layer replaces the outer `Session` for those routes, so the callback cannot also write the application session. Hand the verified identity over with an application-owned one-time step, for example a random single-use handoff ID that expires within a minute and is redeemed by a same-site route under the application session layer. ## Consume the callback Mount `AuthSession` directly as an Axum extractor. Extraction parses the real query, removes and immediately saves the stored challenge, rejects expiry or a constant-time state mismatch, and makes a later sequential replay fail: ```rust use rullst_connect::prelude::*; use tower_sessions::Session; async fn github_callback( session: Session, callback: AuthSession, github: &GithubProvider, ) -> Result { let user = github.get_user(callback.exchange_params()?).await?; // Rotate the browser session before establishing authenticated identity. session .cycle_id() .await .map_err(|error| ConnectError::Session(error.to_string()))?; session .insert("authenticated_user_id", &user.id) .await .map_err(|error| ConnectError::Session(error.to_string()))?; Ok(user.universal_profile()) } ``` Do not serialize `ConnectUser` as a credential store. Its public serialization already omits provider tokens, while `UniversalProfile` is the narrower credential-free identity projection. If an application needs provider refresh tokens, place them in a dedicated encrypted store with explicit rotation and revocation policy. For a provider that returned both a refresh token and `expires_in`, construct a bounded process-local coordinator at the trusted time the token response was received: ```rust,no_run use rullst_connect::{AutoRefreshingSession, ConnectError, ConnectUser}; use rullst_connect::prelude::ExposeSecret as _; # async fn call_authorized_endpoint(_: &str) -> Result<(), ConnectError> { # Ok(()) # } async fn provider_request( github: &rullst_connect::providers::GithubProvider, user: &ConnectUser, token_received_at: u64, ) -> Result<(), ConnectError> { let tokens = AutoRefreshingSession::from_user_at( github, user, token_received_at, )?; let lease = tokens.access_token().await?; call_authorized_endpoint(lease.access_token().expose_secret()).await?; Ok(()) } ``` `AutoRefreshingSession

    ` checks a bounded early-expiration window and holds one async process-local refresh gate, so provider refresh calls cannot overlap and waiters reuse the first valid result. It keeps the old refresh credential if the provider does not rotate, adopts a valid rotation, requires the same provider user ID and changes state only after full validation. ## Persist refresh state without storing plaintext tokens Rullst supplies a storage-neutral authenticated envelope. The application still chooses its database/file/secret manager, but the stored token record need not invent its own cryptographic format: ```rust use rullst_connect::{ AutoRefreshingSession, EncryptedTokenSnapshot, Provider, TokenSnapshotBinding, TokenSnapshotError, TokenSnapshotKey, }; async fn seal_current_state( session: &AutoRefreshingSession<'_, P>, key_bytes: [u8; 32], local_account_id: &str, ) -> Result { let state = session.state_snapshot().await; let binding = TokenSnapshotBinding::try_new("github", local_account_id)?; let key = TokenSnapshotKey::try_new("oauth-primary-2026", key_bytes)?; EncryptedTokenSnapshot::seal(&state, &key, &binding) } ``` Write only `EncryptedTokenSnapshot::as_str()` to durable storage. On restart: 1. parse the stored string with `EncryptedTokenSnapshot::try_from_envelope`; 2. read its non-secret `key_id()` and select that 32-byte key in a secret manager; 3. rebuild the same binding from trusted provider and local-account state; 4. call `open`, then pass the restored state to `AutoRefreshingSession::new`. The AES-256-GCM authentication tag covers the envelope version, key ID, provider and local account, so a copied record fails for another owner. The payload is bounded and revalidated after decryption. Debug output is redacted, and the envelope itself does not implement `Display` or Serde. The application must still commit each new generation transactionally, rotate/retain keys, authorize the account and revoke local state. Multi-process deployments also need a distributed compare-and-set/lease; retry/backoff, reauthentication and replay of the original API request are deliberately not inferred. ## Lifecycle and failure semantics The managed contract is intentionally small: - a challenge expires ten minutes after it is created; - there is one active challenge per browser session; - starting a second flow replaces the first, so the older browser tab fails; - authorization URLs must use HTTPS or exact loopback HTTP, contain no URL credentials/fragment, and preserve exactly one generated state and S256 PKCE tuple without a preconfigured nonce; - the challenge is removed and saved before state, nonce or PKCE-dependent exchange; - missing, mismatched, expired and later sequential callbacks fail closed; - the form POST variant accepts only a `POST` with a form body of at most 16 KiB and leaves the challenge untouched when it rejects the request shape; - provider error text is bounded before it becomes a typed error; - callback codes, state, nonce, verifier and authorization URLs are redacted from the managed types' `Debug` output. One active challenge makes replay and lifecycle behavior unambiguous, but it is not the best UX for applications that intentionally support concurrent login tabs. Such an application should use the [store-backed login](https://rullst.github.io/Rullst/book/tutorials/42-server-bound-oauth-sessions.html#store-backed-login), which keys each attempt by its own opaque id with the same expiry, atomic consumption, constant-time comparison and redaction properties. The generic `tower-sessions` store interface does not expose a distributed compare-and-delete. Two requests that already loaded the same record can still race even though each removal is saved immediately. Provider authorization codes are themselves single-use, but account creation/linking and authenticated session establishment must still be idempotent. Deployments requiring a strict distributed callback claim should use the store-backed login, whose SQL stores consume a challenge atomically. ## What the application still must prove Before release, test the exact deployed provider and browser path: 1. The registered redirect URI exactly matches the application route and uses HTTPS outside an exact loopback development host. 2. The session cookie remains Secure and HttpOnly, uses an intentional SameSite policy, and is rotated after successful authentication. 3. Every application instance sees the same durable session store, or routing is deliberately constrained without pretending failover works. 4. Issuer, audience, signature, expiry and nonce checks pass and fail against the provider's real or restricted environment. 5. Account creation/linking cannot attach an attacker-controlled provider identity to an existing local account. 6. Native or mobile clients do not sign in by sending a provider access token to `get_user_from_token`: a userinfo response does not prove the token was issued to this application's `client_id`. Verify their ID token with `verify_id_token` (Google, `OidcProvider`) and a server-issued nonce. 7. Denial, timeout, provider outage and abandoned-login recovery have bounded user-visible behavior without logging credentials. The local Rullst regressions prove generation, round-trip, mismatch, missing state, expiry, replacement, replay, typed exchange parameters and redaction, and, for the store-backed login, concurrent single consumption on SQLite (and on owned PostgreSQL and MySQL servers in CI). They are not provider certification or deployment evidence. --- # 43. Installable Apps: PWA, Google Play and Microsoft Store Source: https://rullst.github.io/Rullst/book/tutorials/43-progressive-web-apps.html # 43. Installable Apps: PWA, Google Play and Microsoft Store A Rullst application is already a web application. `cargo rullst make:pwa` makes it installable: browsers on Android, ChromeOS, Windows, macOS and Linux offer to install it, iOS users add it to the home screen from Safari, and the same site can be published in Google Play and the Microsoft Store. There is one codebase, and the server stays the authority for every rule and every piece of data. `make:pwa` replaces the Omni shells (`make:omni`), which 13.0 removes; see the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html). ## Make the application installable Run the generator at the root of an application created from an HTML starter (Blank or SaaS, or an application generated from the former LMS, Blog, Portfolio or ERP starters): ```bash cargo rullst make:pwa --name "Field Notes" --short-name Notes cargo rullst dev ``` Every flag is optional; the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-makepwa-1300-alpha3) lists them with their defaults. The command adds these files: | File | Purpose | | :--- | :--- | | `static/manifest.webmanifest` | Name, colours, `start_url`, `scope`, `display: standalone`, icons. | | `static/icons/*.png` | Placeholder icons: 192 and 512 pixels, a maskable 512 and the 180-pixel apple-touch icon. | | `static/pwa.js` | Registers the service worker and shows the update prompt. | | `src/pwa.rs` | Serves `/sw.js` and `/offline`; `head()` returns the tags a layout needs. | | `src/pwa/sw.js` | The service worker. | | `PWA.md` | The checklist for the generated project. | It also edits the application in three recognized places: the router mounts `crate::pwa::router()` next to the health probes, every page layout gains the PWA tags, and the crate root declares `mod pwa`. The whole plan is checked before the first write. Running the command again changes nothing, and a generated file you have edited stops the run instead of being overwritten. The JSON API starter has no pages to install and is refused. Open `http://localhost:3000`: browsers allow service workers on `localhost` and on HTTPS origins only. Chrome's DevTools **Application** panel shows the manifest, the worker and the cache. Before release, replace the four icons with your own PNG files of the same sizes. Keep the subject of `maskable-512.png` inside the central circle of 80% of its width: launchers crop that icon to their own shape. ## A layout that make:pwa does not recognize The generator recognizes an `html!` document, a document written in a string and Core's `render_page_with_lang`. For any other layout, add the tags yourself, inside ``: ```rust,ignore pub fn layout(title: &str, body: Markup) -> Markup { rullst::html! { {title} {crate::pwa::head()} {body} } } ``` With Core's layout, `render_page_with_head` places the same tags at the end of the page's ``: ```rust,ignore render_page_with_head(&htmx, "en", "Dashboard", crate::pwa::head(), content) ``` And mount the routes once, outside any CSRF layer (they answer GET only): ```rust,ignore router.merge_axum(crate::pwa::router().into_axum()) ``` ## How it fits the security defaults **Content-Security-Policy.** The default policy is `default-src 'self'; script-src 'self' 'nonce-…'; … worker-src 'self' blob:`. `static/pwa.js` is a same-origin file, so `script-src 'self'` loads it without a nonce, and it styles the update prompt through the DOM rather than a `style` attribute, which `style-src` refuses. `/sw.js` is allowed by `worker-src 'self'`, and the manifest, which has no `manifest-src` directive, falls back to `default-src 'self'`. A policy with `'strict-dynamic'` ignores `'self'`; give the registration script the request's nonce in that case. **Cache-Control.** Rullst answers `Cache-Control: no-store` when a response sets no policy of its own, and the worker never stores such a response, nor a `private` one, a non-GET request or a response from another origin. Personal and authenticated pages therefore never stay on the device. Static files are served with `no-cache` (content-hashed names with a year-long `immutable` policy), `/offline` and `/sw.js` with `no-cache`. A page that is the same for every visitor, such as a landing page, can opt into storage with [`rullst::cache_control`](https://rullst.github.io/Rullst/book/tutorials/10-static-assets-and-compression.html#downloads-and-feeds); the worker then keeps a copy for offline visits. ## What the worker does - **Install:** stores `/offline`, the manifest, `pwa.js`, the 192 and 512 icons and the starter's `rullst.css`, htmx and logo when they exist. The install fails if `/offline` cannot be stored. - **Navigations:** network first. A storable page is kept; without network the worker answers with the stored copy or the offline page. - **Static files:** a name that carries a content hash (`app.3f9a2c1b.css`, the rule Rullst's static server uses for its year-long cache) is served from the cache first; other files go to the network first and fall back to the stored copy. - **Everything else** (forms, htmx fragments, JSON APIs, other origins) goes to the network untouched. - **Updates:** the cache is named after `[package].version`, which `src/pwa.rs` writes into the worker it serves. Release a new version and browsers install the new worker next to the old one; the page shows "A new version of this app is available" with a **Reload** button, which sends `SKIP_WAITING` to the waiting worker and reloads once it takes control. The old cache is then deleted. Until the served worker changes (a new version, or an edit to `src/pwa/sw.js`), browsers keep the installed one and do not fetch the offline page again; network-first pages and files still refresh their stored copy whenever they load online. ## Data while offline The worker never queues writes. When a page must accept changes without a connection, keep the rule the server already applies to every client: the server is the authority, and a queued change is only a proposal. Store each pending write in IndexedDB with an idempotency key and send it again when the connection returns: ```js // static/outbox.js: load it like pwa.js, as a same-origin script. const STORE = 'writes'; function database() { return new Promise((resolve, reject) => { const request = indexedDB.open('app-outbox', 1); request.onupgradeneeded = () => request.result.createObjectStore(STORE, { keyPath: 'key' }); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } function done(transaction) { return new Promise((resolve, reject) => { transaction.oncomplete = resolve; transaction.onerror = () => reject(transaction.error); }); } // Queue a write; the key travels with every attempt to send it. export async function queue(url, body) { const db = await database(); const write = { key: crypto.randomUUID(), url, body, queuedAt: Date.now() }; const transaction = db.transaction(STORE, 'readwrite'); transaction.objectStore(STORE).put(write); await done(transaction); return write.key; } // Send the queued writes in order; stop at the first network failure. export async function flush(csrfToken) { const db = await database(); const read = db.transaction(STORE).objectStore(STORE).getAll(); const writes = await new Promise((resolve, reject) => { read.onsuccess = () => resolve(read.result); read.onerror = () => reject(read.error); }); writes.sort((a, b) => a.queuedAt - b.queuedAt); for (const write of writes) { let response; try { response = await fetch(write.url, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': write.key, 'X-CSRF-Token': csrfToken, }, body: JSON.stringify(write.body), }); } catch (error) { return; // Still offline: try again on the next `online` event. } // Accepted, already applied or refused by the server: it is settled. if (response.ok || response.status === 409 || response.status === 422) { const transaction = db.transaction(STORE, 'readwrite'); transaction.objectStore(STORE).delete(write.key); await done(transaction); } else { return; // Sign-in expired or the server failed: keep it and stop. } } } ``` Call `flush` with the page's CSRF token (the `_token` value its forms carry) from an `online` listener and when the page loads. The outbox holds what the user typed on this device until it is sent: queue only what the page needs and clear the store when the user signs out. On the server, accept each key once. Validate it, store it with the result in the same transaction as the write (a unique index on the key), answer a repeated key with the stored result, and check permissions, ownership and revisions again on every replay. `IdempotencyKey` validates the header before any work: ```rust use rullst::client_contract::IdempotencyKey; use rullst::http::HeaderMap; /// The replay key of a queued write, or `None` when it is missing or malformed. fn replay_key(headers: &HeaderMap) -> Option { let value = headers.get("idempotency-key")?.to_str().ok()?; IdempotencyKey::new(value).ok() } let mut headers = HeaderMap::new(); headers.insert( "idempotency-key", "3f6c2a9e-8b1d-4c57-9a0e-2d4f6b8c1e7a".parse().unwrap(), ); assert!(replay_key(&headers).is_some()); headers.insert("idempotency-key", "short".parse().unwrap()); assert!(replay_key(&headers).is_none()); ``` Never let a queued write carry a role, a tenant, a price, a score or a timestamp the server trusts: the server derives them from the signed-in session and its own clock. ## Google Play through a Trusted Web Activity A Trusted Web Activity (TWA) is an Android app that opens your site in the user's browser engine, full screen, without browser UI. Google's [Bubblewrap](https://github.com/GoogleChromeLabs/bubblewrap) builds one from the web manifest; PWABuilder uses it for its Android packages. The app holds no copy of your pages: deploying the site updates it. ### 1. Generate the Bubblewrap configuration The site must already be served over HTTPS at its final host. Then: ```bash cargo rullst make:pwa --name "Field Notes" --short-name Notes \ --android --host notes.acme.dev --package-id dev.acme.notes ``` `--host` is the site's host name (no scheme or path) and `--package-id` the Android application ID, which you own and can never change once published (`com.example.*` is refused). `make:pwa --android` can run after a plain `make:pwa` when it repeats that run's flags, as here; it then adds only `android/twa-manifest.json` and `PWA-ANDROID.md`. Other values would change the files already written, so the run stops before writing anything. The configuration repeats the web manifest: name, short name as `launcherName`, colours, `display`, start URL, the 512 icon as `iconUrl` and the maskable one as `maskableIconUrl`, plus `webManifestUrl`, `fullScopeUrl`, `appVersion` `1`/`appVersionCode` `1` and `enableNotifications: false`. The `signingKey` entry (`/private/path/upload.keystore`, alias `upload`) is a placeholder. ### 2. Install Bubblewrap and create the upload key Bubblewrap needs Node.js 18 or later and JDK 17; on its first run it offers to download the JDK and the Android command-line tools: ```bash npm i -g @bubblewrap/cli ``` Android apps are signed, and every update must be signed with the same key. Create the upload key once with the JDK's `keytool`, outside the repository, and let it prompt for the passwords (never pass them as arguments, where they reach shell history and logs): ```bash keytool -genkeypair -keystore /private/path/upload.keystore \ -alias upload -keyalg RSA -keysize 3072 -validity 10000 ``` Restrict the file to your account, back it up with its passwords, and write its real path and alias into `signingKey` in `android/twa-manifest.json`. Keep `*.keystore` and `*.jks` out of version control and CI artifacts. ### 3. Build the bundle ```bash cd android bubblewrap update --skipVersionUpgrade bubblewrap build ``` `bubblewrap update` generates the Android project from `twa-manifest.json` (with `--skipVersionUpgrade` it keeps version 1), and `bubblewrap build` asks for the key passwords (or reads `BUBBLEWRAP_KEYSTORE_PASSWORD` and `BUBBLEWRAP_KEY_PASSWORD`) and writes `app-release-bundle.aab` and `app-release-signed.apk`. Bubblewrap's own quick start is interactive instead: `bubblewrap init --manifest https://notes.acme.dev/static/manifest.webmanifest` reads the same manifest, asks you to confirm each value (compare them with `android/twa-manifest.json`), creates a signing key and the project, and is followed by `bubblewrap build`. Run it in an empty directory. ### 4. Play App Signing and Digital Asset Links Upload the `.aab` in Play Console. With Play App Signing, which Play requires for new apps, Google signs what users install with its **app signing key**; your key only uploads. Android shows the app full screen only when the site publishes the fingerprint of the key that signed the installed app, so copy the SHA-256 certificate fingerprint of the app signing key from Play Console's App integrity page. Add the upload key's fingerprint too if you install builds signed locally (`keytool -list -v -keystore /private/path/upload.keystore` prints it). The generated `src/pwa.rs` serves `/.well-known/assetlinks.json` once the application is told which app to name. Set the variable in the environment or `.env`: ```bash RULLST_ANDROID_ASSET_LINKS="dev.acme.notes=," ``` or add the app to `Rullst.toml` (the variable wins when both are set): ```toml [[android.asset_links]] package_name = "dev.acme.notes" sha256_cert_fingerprints = [ "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5", ] ``` Separate several apps with `;` in the variable, or repeat the table. Each fingerprint must be 32 colon-separated hexadecimal bytes; an invalid value is reported when the application starts and the file is then not served. The route answers `application/json` with `Cache-Control: public, max-age=3600`. Code outside the generated module can build the same route: ```rust use rullst::pwa::{AndroidApp, AssetLinks}; let fingerprint = "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"; let links = AssetLinks::new().with_app(AndroidApp::new("dev.acme.notes", [fingerprint])?); assert!(links.to_json().contains("\"package_name\":\"dev.acme.notes\"")); let router = rullst::Router::new().merge_axum(links.router()); # let _ = router; # Ok::<(), rullst::pwa::AssetLinksError>(()) ``` Restart the application and check `https://notes.acme.dev/.well-known/assetlinks.json`; Google's [Statement List Generator and Tester](https://developers.google.com/digital-asset-links/tools/generator) verifies it against the package. If the address bar still shows in the installed app, the fingerprint does not match the key that signed it. ### 5. Updates Web changes reach installed apps immediately; a new bundle is needed only for Android-side changes (name, icon, colours, start URL, package version). Then run `bubblewrap update` (it raises `appVersionCode` by one and saves `twa-manifest.json`) and `bubblewrap build`, and upload the new bundle signed with the same upload key. ## Microsoft Store As documented by Microsoft in [Publish a PWA to the Microsoft Store](https://learn.microsoft.com/microsoft-edge/progressive-web-apps/how-to/microsoft-store), the same site is published without code changes: PWABuilder packages it as MSIX, and the package carries the manifest's metadata and icons but none of the application's code, which keeps loading from your server. 1. **Before packaging:** the site is deployed over HTTPS with the manifest `make:pwa` wrote (its `id`, name, colours and icons; replace the placeholder icons first, because the package copies them). 2. **Reserve the name:** with a personal Microsoft account enrolled in the Windows Developer Program, open [Partner Center](https://partner.microsoft.com/dashboard/home), choose **Apps and games → New product → MSIX or PWA app** and reserve the name. Under **Product management → Product Identity**, copy the **Package ID**, **Publisher ID** and **Publisher display name**. 3. **Package:** on [PWABuilder](https://www.pwabuilder.com), enter the site's URL. Its report card evaluates the site and manifest and lists action items; when it is store ready, choose **Package For Stores → Windows → Generate Package**, paste the three values and download the `.zip`, which holds an `.msixbundle` and a `.classic.appxbundle`. 4. **Submit:** in Partner Center, start a submission for the reserved app, answer the pricing and age-rating questions and upload both bundles. Microsoft reviews it before it appears in the Store. **Updates.** Microsoft's page states that changes to HTML, CSS, JavaScript or the service worker reach users from your server without a new package. Changing the manifest (name, icons, or members such as `share_target`) requires a new package and submission, because the manifest is copied into the Windows package. Generate it with PWABuilder again, with a higher app version in its Windows options, and choose **Update** on the app in Partner Center ([PWABuilder's update steps](https://blog.pwabuilder.com/docs/update-an-existing-app-in-the-microsoft-store)). Apps installed from the Store send `Referer: app-info://platform/microsoft-store` on their first navigation, which lets you count them. ## iPhone and iPad There is no App Store package. Apple's [App Store Review Guideline 4.2](https://developer.apple.com/app-store/review/guidelines/#minimum-functionality) asks for "features, content, and UI that elevate it beyond a repackaged website", so an app that only shows the site is usually rejected, and a native iOS app is a separate product. iOS users install the PWA from Safari instead: **Share → Add to Home Screen**. The home-screen app uses the `apple-touch-icon` and name that `make:pwa` provides and opens in its own window, and Safari runs the same service worker and offline page. --- # 45. Accessible Academy Media Source: https://rullst.github.io/Rullst/book/tutorials/45-accessible-academy-media.html # 45. Accessible Academy Media Rullst's LMS example (`examples/lms`) contains a bounded lesson-presentation foundation for video and audio. Authorization and progress remain server-owned, while the browser receives accessible media markup and an escaped transcript. ## Start from the LMS example Before 13.0.0-alpha.4, `cargo rullst new --blueprint lms` generated this application; it now lives in the Rullst repository as [`examples/lms`](https://github.com/Rullst/Rullst/tree/main/examples/lms). From a checkout of the repository: ```bash cd examples/lms cargo test -p rullst-lms-example ``` Copy the directory to start your own academy (its README shows how to run it and how to switch to the published crates). It includes the course catalog, modules, lessons, this accessible player, enrollment, progress, login and a Nexus admin. Earlier releases also generated a complete Academy (assessment, gamification, automation and notifications); v13 removed it in favor of this smaller application. ## Lesson media contract Lessons store these fields: | Field | Contract | |---|---| | `media_kind` | Closed renderer values: `video` or `audio`. | | `media_url` | HTTPS URL or absolute same-origin path; control characters and backslashes are rejected. | | `captions_url` | Required valid source for video; normally a same-origin `.vtt` path. | | `transcript` | Required and bounded; the HTML renderer escapes it. | | `language_tag` | Required bounded ASCII language tag such as `en` or `pt-BR`. | For production, prefer application-owned same-origin or signed media. Put a caption file at `static/media/lesson.pt-BR.vtt` and store the public source as `/static/media/lesson.pt-BR.vtt`; `Server` mounts the local `static/` directory at `/static`. ```text WEBVTT 00:00.000 --> 00:04.000 Bem-vindo à primeira atividade. ``` The example intentionally ships no media binary. Its four seeded development lessons point at remote sample video/audio files, and its two video lessons use the `static/media/memory-safety.en.vtt` and `static/media/first-project.en.vtt` captions. The default CSP (`default-src 'self'`) does not allow that remote media, so replace the seeded lessons in Nexus (`/nexus`) with your own content. Add your reviewed audio/video asset or application-specific object-storage delivery, then use a same-origin path such as `/static/media/lesson.webm`. If you choose a remote host, add only that reviewed origin to a `media-src` directive of the `csp` template under `[security]` in `Rullst.toml`; do not weaken the policy to arbitrary HTTPS. Core also defaults COEP to `require-corp`, so the remote media server must emit a compatible `Cross-Origin-Resource-Policy` response. When a reviewed server cannot do so, an application can explicitly set `coep = "credentialless"` under `[security]` in `Rullst.toml`; browsers then omit credentials for eligible cross-origin no-CORS requests. Prefer same-origin delivery, and test the exact CSP/COEP/media combination in the deployed browser rather than disabling isolation globally. ## What the player enforces - no autoplay; - native video/audio controls; - a caption track for every video; - an always-available transcript for video and audio; - visible keyboard focus and nonce-bound styles; - escaped title and transcript values; - fail-closed rendering for unknown kinds, insecure sources or invalid accessibility metadata. The protected lesson controller checks the authenticated learner's active enrollment before rendering the player. Progress submissions use CSRF and idempotency data and remain authoritative in the database. Each rendered player carries a fresh random key, scoped to the requested percentage on submission: resubmitting the same click replays the stored event, while a later save, or another button, records new progress. ## Evidence boundary The example's own tests, run in the repository's workspace suite, cover catalog query bounds, progress keys, the production security baseline and the learning service's owner boundary; no test renders the player or its rejected source/metadata cases. They do not prove codec support, buffering behavior, screen-reader quality, subtitle accuracy, microphone or speech recognition, physical mobile devices, CDN delivery or app store behavior. Run browser accessibility tests with your real content and deployment before making those claims. --- # 48. Signed Local OIDC Testing Source: https://rullst.github.io/Rullst/book/tutorials/48-local-oidc-testing.html # 48. Signed Local OIDC Testing > [!IMPORTANT] > Dependency examples pin the v13 prerelease `=13.0.0-alpha.4` exactly: Cargo > never selects a prerelease on its own, and the v12 line reached end of life > on 16 October 2026 (see the [release record](https://rullst.github.io/Rullst/book/v12.html)). Commit Cargo.lock > for reproducible builds. Use path dependencies only when intentionally > testing checkout-local changes. Rullst Connect includes an explicitly mounted local identity-provider fixture so an application can exercise a cryptographically verified OIDC flow without a third-party account. Unlike an in-process provider stub, this path traverses HTTP discovery, authorization, token exchange, JWKS retrieval and ID-token verification through the ordinary `OidcProvider` implementation. ## Enable and bind the fixture Enable the Axum feature in development: ```toml [dev-dependencies] rullst-connect = { version = "=13.0.0-alpha.4", features = ["axum"] } ``` A project that enables the `rullst` facade's `oauth` feature (as `cargo rullst make:oauth` does) already has this feature: the module is `rullst::connect::mock_idp`. Mount the router only on an exact loopback listener. Axum is reached through the `rullst::web::axum` re-export, because generated projects do not depend on Axum directly: ```rust,no_run use rullst_connect::mock_idp::{MockIdpConfig, mock_router_with_config}; # async fn run() -> Result<(), Box> { let issuer = "http://127.0.0.1:8080"; let callback = "http://127.0.0.1:3000/auth/callback"; let config = MockIdpConfig::try_new( issuer, "academy-local-client", "academy-local-secret", callback, )?; let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?; rullst::web::axum::serve(listener, mock_router_with_config(config)).await?; # Ok(()) # } ``` `MockIdpConfig` rejects non-HTTP or non-loopback issuers and callbacks, issuer paths, empty/oversized identifiers and control characters. The application must still bind the actual listener to loopback; possession of a router does not control how the host serves it. ## Configure the ordinary OIDC client Use non-placeholder local credentials so `OidcProvider` traverses the HTTP fixture: empty or `mock_*` credentials select the separate network-free credential fallback instead (and are rejected without the `mock` feature): ```rust,no_run use rullst_connect::providers::OidcProvider; # async fn provider() -> Result { OidcProvider::discover( "http://127.0.0.1:8080", "academy-local-client", "academy-local-secret", "http://127.0.0.1:3000/auth/callback", ).await # } ``` Compose this provider with the server-bound session ceremony from [Tutorial 42](https://rullst.github.io/Rullst/book/tutorials/42-server-bound-oauth-sessions.html). That ceremony generates and stores state, PKCE verifier and OIDC nonce; the fixture binds the challenge and nonce to its one-shot authorization grant. The callback then supplies the consumed values to `OidcProvider::get_user`. ## What the test proves The checked-in loopback regression proves that the current client can: - validate discovery metadata on the exact issuer origin; - preserve an exact registered client and callback; - exchange one expiring authorization code only once; - reject a missing, malformed or mismatched S256 PKCE verifier; - bind the requested nonce into a signed ID token; - select the Ed25519 public key by `kid` from JWKS; - validate EdDSA signature, issuer, audience, expiry and nonce; and - accept only an issued, unexpired bearer token at userinfo. Authorization grants and access-token digests are process-local and capped at 64 records. A restart erases them. This is intentional test behavior, not a durable identity service. ## Non-production boundary The signing seed, client credentials and identity are deterministic public fixtures. The router has no interactive login, consent, refresh-token, device, federation, account-management, key-rotation or administrative lifecycle. It has not passed an OIDC conformance suite. Never expose it publicly, reuse its key or credentials, or use a successful fixture test as evidence that a live provider/deployment is correctly configured. --- # 49. Bounded Brokered Messaging Source: https://rullst.github.io/Rullst/book/tutorials/49-brokered-messaging.html # 49. Bounded Brokered Messaging > [!IMPORTANT] > Dependency examples pin the v13 prerelease `=13.0.0-alpha.4` exactly: Cargo > never selects a prerelease on its own, and the v12 line reached end of life > on 16 October 2026 (see the [release record](https://rullst.github.io/Rullst/book/v12.html)). Commit Cargo.lock > for reproducible builds. Use path dependencies only when intentionally > testing checkout-local changes. This tutorial starts with `rullst-messaging`'s deterministic in-memory broker and then switches the same trait contract to the durable local SQLite adapter. Remote broker protocols remain separate adapters. ## 1. Enable the umbrella feature ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", features = ["messaging"] } ``` Use `messaging-sqlite` instead of `messaging` for durable local state. A direct dependency uses `rullst-messaging = { version = "=13.0.0-alpha.4", features = ["sqlite"] }`. ## 2. Create a bounded broker ```rust use rullst::messaging::{BrokerConfig, InMemoryBroker}; # fn create_broker() -> Result<(), rullst::messaging::MessagingError> { let config = BrokerConfig::try_new("billing")? .with_limits( 10_000, // retained messages 128, // consumer-group subscriptions 5, // delivery attempts 1024 * 1024, // payload bytes )?; let broker = InMemoryBroker::new(config); # let _ = broker; # Ok(()) # } ``` The hard ceilings prevent an accidental configuration from turning the local broker into unbounded memory state. Capacity exhaustion fails closed until terminal messages are explicitly purged. ### Persist the same contract locally ```rust,no_run use rullst::messaging::{BrokerConfig, SqliteBroker}; # async fn open() -> Result<(), rullst::messaging::MessagingError> { let config = BrokerConfig::try_new("billing")? .with_limits(10_000, 128, 5, 1024 * 1024)?; let broker = SqliteBroker::connect("sqlite://storage/messages.sqlite", config).await?; # let _ = broker; # Ok(()) # } ``` All mutations use serialized SQLite write transactions. A committed publish or ACK survives restart, while an uncommitted operation rolls back. In-flight leases also survive; after expiry the next broker operation requeues or dead-letters them. Multiple processes may share one file and namespace, but reopening it with different limits is rejected. This is local durability, not network replication, automatic failover or exactly-once side effects. ### Protect header values and payloads `SqliteBroker::connect` deliberately retains the compatible plaintext profile. For a new namespace/database, load a 32-byte high-entropy key from a secret manager and select the encrypted profile explicitly: ```rust,no_run use rullst::messaging::{ BrokerConfig, MessagingKeyring, MessagingStorageKey, SqliteBroker, }; # async fn open_encrypted( # secret_manager_key: [u8; 32], # ) -> Result<(), rullst::messaging::MessagingError> { let primary = MessagingStorageKey::try_new("messages-2026-09", secret_manager_key)?; let keyring = MessagingKeyring::new(primary); let broker = SqliteBroker::connect_encrypted( "sqlite://storage/messages.sqlite", BrokerConfig::try_new("billing")?, keyring, ).await?; # let _ = broker; # Ok(()) # } ``` To rotate, put the new primary first and append the old decryption key: ```rust use rullst::messaging::{MessagingKeyring, MessagingStorageKey}; # fn rotate(new_key: [u8; 32], old_key: [u8; 32]) # -> Result { let keys = MessagingKeyring::new(MessagingStorageKey::try_new( "messages-2026-10", new_key, )?) .with_decryption_key(MessagingStorageKey::try_new( "messages-2026-09", old_key, )?)?; # Ok(keys) # } ``` New publications use the primary. Old records are not silently rewritten and their keys remain mandatory until those messages become terminal and are purged. Plaintext/encrypted profiles cannot be mixed or changed in place; move through a new namespace/database with an application-reviewed republish procedure. Passwords are not AES keys, and source literals are unsuitable for production key custody. The encrypted profile protects header values and payloads. Topic, event and content type, message/key IDs, timestamps, idempotency key, fingerprint and delivery state remain visible metadata. Protect the database file and backups, control rollback, rehearse key recovery and authorize topic access separately. ## 3. Register a consumer group and publish ```rust use rullst::messaging::{ MessageBroker, PublishRequest, StartPosition, SubscriptionRequest, TraceContext, }; # async fn example( # broker: &rullst::messaging::InMemoryBroker, # ) -> Result<(), rullst::messaging::MessagingError> { broker .subscribe(SubscriptionRequest::try_new( "invoices", "receipt-mailers", StartPosition::Earliest, )?) .await?; let trace = TraceContext::try_with_state( "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "vendor=value", )?; let request = PublishRequest::try_new( "invoices", "invoice.paid", "invoice/2026-0042/paid/v1", br#"{"invoice_id":"2026-0042"}"#.to_vec(), )? .with_content_type("application/json")? .with_trace_context(&trace)?; let first = broker.publish(request.clone()).await?; let replay = broker.publish(request).await?; assert_eq!(first.id(), replay.id()); assert!(replay.is_duplicate()); # Ok(()) # } ``` Idempotency is scoped to the topic. The same topic/key with different content returns `MessagingError::IdempotencyConflict`; it never silently overwrites the original publication. Only validated W3C version-00 `traceparent` and a conservative `tracestate` subset are copied. `baggage` is deliberately excluded because arbitrary application metadata may contain credentials or personal data. Sampling, export, retention and tenant-aware correlation remain host responsibilities. ## 4. Receive, acknowledge, retry, or dead-letter ```rust use rullst::messaging::{ FailureCode, MessageBroker, ReceiveRequest, RetryDisposition, }; use std::time::Duration; # async fn consume( # broker: &rullst::messaging::InMemoryBroker, # ) -> Result<(), rullst::messaging::MessagingError> { let request = ReceiveRequest::try_new( "invoices", "receipt-mailers", "worker-1", 10, Duration::from_secs(30), )?; for delivery in broker.receive(request).await? { let effect_succeeded = true; if effect_succeeded { broker.ack(delivery.ack_token()).await?; } else { let disposition = broker .retry( delivery.ack_token(), Duration::from_secs(15), FailureCode::try_new("mail.transient")?, ) .await?; if disposition == RetryDisposition::DeadLettered { // Alert through an application-owned operational channel. } } } # Ok(()) # } ``` If an adapter needs a stable broker-neutral representation, encode the received envelope explicitly: ```rust use rullst::messaging::{BrokerConfig, MessageEnvelope, WireEnvelopeCodec}; # fn frame( # envelope: &MessageEnvelope, # ) -> Result<(), rullst::messaging::MessagingError> { let config = BrokerConfig::try_new("billing")?; let bytes = WireEnvelopeCodec::encode(envelope, &config)?; let decoded = WireEnvelopeCodec::decode(&bytes, &config)?; assert_eq!(&decoded, envelope); # Ok(()) # } ``` The v1 frame is canonical, bounded by `BrokerConfig`, namespace-bound and rejects unknown versions, truncation, trailing bytes and non-canonical fields. It carries an accepted envelope only. It does not retain the caller's publish idempotency key, connect to a broker, or map remote ACK/retention semantics, so it cannot be presented as a Kafka/NATS/RabbitMQ adapter by itself. An ACK token is an opaque, single-use capability bound to one group and one lease. Once it expires, the message is requeued or dead-lettered at the attempt ceiling. Do not serialize tokens into logs or application records. ## 5. Design for at-least-once delivery The consumer may complete the mail/payment/webhook effect and stop before the ACK reaches the broker. Therefore: 1. use `delivery.envelope().id()` as a stable deduplication key; 2. claim that key transactionally in the side-effect system where possible; 3. ACK only after the effect is durably accepted; 4. treat retries and dead letters as normal operational states; 5. authorize topics and tenant scope in the host application. The in-memory broker loses state on process exit. The SQLite adapter retains state; its default profile is plaintext, while the explicit encrypted profile protects header values and payloads but not routing/idempotency metadata. The host owns key custody, file permissions, protected backup/restore, rollback detection, retention, disk monitoring and topic/tenant authorization. A future remote adapter is supported only after it passes the shared contract plus its own protocol, restart, and fault matrix; an adapter name alone is not durability evidence. The optional v13 [Redis Streams candidate](https://rullst.github.io/Rullst/book/redis-messaging.html) (`messaging-redis`) is such a standalone transport, with explicit operational limits and Rullst-owned delivery indexes; it passed hosted source admission, and its final release admission remains separate. Other remote adapters remain future work. ## 6. Relay a relational outbox after commit Enable `messaging-orm-outbox` on the umbrella crate together with the ORM backend the application uses (`drivers-all` or one `strict-*` feature; the relay feature enables `orm` but no SQL driver). Commit the domain mutation and `rullst_orm::Outbox::enqueue` together as described in the [transactional outbox tutorial](https://rullst.github.io/Rullst/book/tutorials/38-transactional-outbox.html). Then bind one exact outbox stream to one broker topic: ```rust use rullst::messaging::{ InMemoryBroker, MessagingError, OrmOutboxRelay, }; fn configure_relay( broker: InMemoryBroker, ) -> Result, MessagingError> { OrmOutboxRelay::try_new("tenant-a", "domain-events", broker) } ``` A supervised worker claims and relays one event: ```rust,no_run use rullst::messaging::{InMemoryBroker, OrmOutboxRelay}; use rullst_orm::Outbox; async fn relay_one(relay: &OrmOutboxRelay) { let claim = match Outbox::claim_next("tenant-a", "worker-a", 30, 8).await { Ok(Some(claim)) => claim, Ok(None) | Err(_) => return, }; match relay.relay_and_ack(claim).await { Ok(receipt) if !receipt.outbox_acknowledged() => { // The lease expired; a later claim will replay the same event key. } Ok(_) => {} Err(error) => { // Record a secret-free operational failure and let the lease expire. let _accepted_before_ack_failure = error.accepted_publication(); } } } ``` The ordering is intentionally publish then ACK. A stop in between causes a new ORM claim to publish the same event and content under the same broker idempotency key; the broker returns its original ID as a duplicate. The v13 source derives that key from the stream and the event key (the lowercase hex SHA-256 of the stream, `/`, then the event key), so relays of different streams can share one topic. This is at-least-once relay with bounded idempotency, not an atomic transaction across two systems. Keep the worker supervised and make the final consumer idempotent too. --- # 50. Auditable Revisions Source: https://rullst.github.io/Rullst/book/tutorials/50-auditable-revisions.html # 50. Auditable Revisions Rullst SQLx models can bind each instance mutation to a validated principal and restore eligible update revisions as a new compensating mutation. The audit row and model write share a transaction; an audit failure rejects the write. This contract is opt-in and does not infer identity from request data. Install the principal and tenant contexts only after your authentication and authorization boundary has validated them. ## 1. Mark the model and install the table ```rust,no_run use rullst_orm::{FromRow, Orm}; #[derive(Debug, Clone, FromRow, rullst_orm::Orm)] #[orm(table = "projects", auditable, tenant_column = "account_id")] struct Project { id: i32, account_id: String, name: String, #[orm(masked)] api_token: String, } # async fn install() -> Result<(), rullst_orm::Error> { rullst_orm::audit::create_audit_table().await?; # Ok(()) # } ``` `create_audit_table` also adds the v2 columns to the legacy audit table. Legacy records retain version 1 and cannot be restored. On MySQL/MariaDB, new audit tables store `old_values`, `new_values` and `restore_patch` as `LONGTEXT`; `TEXT` holds only 64 KiB, so a larger audited change would fail the mutation in strict SQL mode (or be truncated otherwise). A table created by an earlier version keeps `TEXT` columns and `create_audit_table` only logs a warning. Apply this reviewed migration during a maintenance window (it rebuilds the table): ```sql ALTER TABLE rullst_audits MODIFY old_values LONGTEXT, MODIFY new_values LONGTEXT, MODIFY restore_patch LONGTEXT; ``` Fields whose names contain password, token, secret, API key, credential, cookie or similar markers must use `#[orm(masked)]` on auditable models. Their values are never retained in audit payloads or reverse patches. The same applies to such keys inside JSON fields: when an update adds or removes one, the reverse patch keeps only the key's presence and that revision cannot be restored. `#[orm(masked)]` and `#[orm(encrypted)]` fields appear as `"***"` in every generated audit payload, whatever their name. When an update changes one of them, the audit row still records the field (as `"***"` on both sides) so the change is visible without its value, and that revision cannot be restored. ## 2. Bind a mutation to trusted context ```rust,no_run # use rullst_orm::{FromRow, Orm}; # #[derive(Debug, Clone, FromRow, rullst_orm::Orm)] # #[orm(table = "projects", auditable, tenant_column = "account_id")] # struct Project { id: i32, account_id: String, name: String, #[orm(masked)] api_token: String } use rullst_orm::audit::{AuditContext, with_audit_context}; use rullst_orm::with_tenant; # async fn update(mut project: Project) -> Result<(), rullst_orm::Error> { let audit = AuditContext::user("user-42")? .with_correlation_id("request-01J...")?; with_tenant("account-a", with_audit_context(audit, async move { project.name = "New name".to_string(); project.save().await })) .await?; # Ok(()) # } ``` Background work can use `AuditContext::service("billing-worker")`; reviewed maintenance can use `AuditContext::system("migration-2026-08")`. Empty, padded, control-character, and oversized identifiers fail validation. An auditable mutation without an active context also fails closed. ## 3. Restore an eligible update ```rust,no_run # use rullst_orm::{FromRow, Orm}; # #[derive(Debug, Clone, FromRow, rullst_orm::Orm)] # #[orm(table = "projects", auditable, tenant_column = "account_id")] # struct Project { id: i32, account_id: String, name: String, #[orm(masked)] api_token: String } # use rullst_orm::audit::{AuditContext, with_audit_context}; # use rullst_orm::with_tenant; # async fn restore(project: Project, audit_id: i32) -> Result { let actor = AuditContext::user("admin-7")? .with_correlation_id("support-case-1042")?; let restored = with_tenant("account-a", with_audit_context(actor, async move { project .restore_revision(audit_id, "approved support rollback") .await })) .await?; # Ok(restored) # } ``` The returned model contains the restored state. Rullst verifies the exact model, ID, active tenant, patch version and current post-state before writing. PostgreSQL and MySQL/MariaDB lock the row during this check. Success creates a new `updated` audit entry with `reverted_audit_id` and the bounded reason; audit history is not rewritten or deleted. Use `restore_revision_with_tx` when a caller-owned SQLx transaction must include other relational writes. Prefer `Orm::transaction` when generated post-commit observers must wait for the final commit decision. ## Deliberate refusal cases Restoration fails for: - legacy v1, create, or delete entries; - a revision from another model, record, or tenant; - a row changed again after the selected revision; - a patch containing a redacted/sensitive change, including any change to an `#[orm(masked)]` or `#[orm(encrypted)]` field; - malformed, empty, too deep, too large, or excessively wide patches. A change whose reverse patch would be too deep, too large or too wide (for example two multi-megabyte values or thousands of changed JSON keys) is still saved and audited; the revision is simply recorded without a restore patch and refused on restoration. Those refusals prevent audit history from becoming an unsafe generic backup mechanism. Use reviewed database backups and restore drills for disaster recovery. Bulk update/delete builders do not invent per-row audit history, and cross-process export or notification should use the transactional outbox. --- # 51. Supervised Development Auto-Reload Source: https://rullst.github.io/Rullst/book/tutorials/51-authenticated-hot-reload.html # Supervised Development Auto-Reload `cargo rullst dev` and `cargo rullst dash` rebuild and restart a directly linked application when you save. This keeps Tokio, ORM pools, sessions and other process globals in one runtime. Supervised restart replaced the advertised DLL-swap profile in v12, after Windows/LMS testing exposed an uninitialized ORM in the loaded library, and 13.0 keeps it (see [the v13 decision](https://rullst.github.io/Rullst/book/tutorials/51-authenticated-hot-reload.html#existing-dll-scaffolds-and-the-v13-decision); the [v12 release audit](https://rullst.github.io/Rullst/book/v12-release-audit.html#development-reload-decision) records the original one). ## Run the development loop Generate a normal application and use either development command: ```bash cargo rullst new learning-app --default --blueprint blank \ --database sqlite --skip-initial-migration cd learning-app cargo rullst dev # Or, in an interactive terminal: cargo rullst dash ``` There is no hot-reload question in the wizard. Both commands supervise reloads automatically; use `cargo run` for ordinary execution. The dashboard also shows live requests/s, latency, errors and, when reported, ORM and queue figures, and `r` restarts the application without rebuilding (see the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-dash)). ## What a save does ```mermaid flowchart LR A[Save source or assets] --> B[Coalesce changes] B --> C[Compile executable] C -->|Failure| D[Keep current application and show diagnostic] C -->|Success| E[Snapshot new executable] E -->|Failure| D E -->|Success| F[Stop owned child and start replacement] F --> G[Verify process generation over HTTP] G --> H[Browser refresh] ``` The watcher observes source, static assets, templates and manifest/configuration files, including atomic editor renames. It excludes build outputs, Git data and runtime database/log files. Events while a build is running remain pending for a later rebuild. Saving CSS also triggers this conservative rebuild/restart path; no compilation-free claim is made. Initial migrations run before the first start when the generated migration directory exists. Later migrations are explicit: use the dashboard migration action or `cargo rullst db:migrate`. The dashboard queues migrations through the supervisor, using its current executable snapshot with bounded output and cancellation cleanup; the action is serialized with rebuilds. It does not compile an unsaved or unbuilt migration into that snapshot. Restarting a process does not apply arbitrary database-schema changes safely. ## Failure and state boundaries | Event | Behavior | | --- | --- | | Compiler error | Keep the current process running; retain bounded diagnostics. | | Snapshot copy/create error | Keep the current process running and report the failure before stopping anything. | | Successful build | Snapshot the executable, stop the owned child, start the replacement. | | Replacement cannot spawn | Attempt to restart the prior executable snapshot and report the error. | | Replacement exits during startup | Report the exit; save a correction to rebuild and retry. | | Readiness cannot be verified in 15 seconds | Report the limitation; do not claim successful readiness. | | Dashboard exit / Ctrl+C | Cancel owned build/migration work and terminate/reap the application, subject to the Windows descendant limitation below. | | Configured port changes | Restart the CLI so its dashboard/readiness target follows the new port. | The old binary is copied before execution so Windows does not lock Cargo's output during the next build. Cleanup targets only the snapshot created by this supervisor. On Unix the child receives a two-second shutdown interval, followed by termination of its owned process group if needed; group cleanup precedes reaping the leader. Windows uses best-effort tree termination: without Job Objects it cannot guarantee cleanup of orphan descendants after their parent has exited, and it does not promise graceful request drain. All process-local state resets, including in-memory sessions/queues/caches. Persist important state explicitly. In-flight requests may be interrupted. This is a local development loop, not zero-downtime production deployment. ## Browser and security boundary The debug/development server serves a local reload script and an opaque generation marker under `/_rullst/dev-*`. The script polls the same origin and refreshes only after a different valid generation responds. Readiness checks match the child generation rather than trusting any service on the port. The marker is not a secret or an authentication credential. The same development processes also serve `/_rullst/dev-telemetry` for the `cargo rullst dash` metrics, to loopback clients only (see the [telemetry guide](https://rullst.github.io/Rullst/book/telemetry-guide.html#development-dashboard-endpoint)). These endpoints cannot instruct the server to compile, launch a process, or swap a library. Eligible full-document, known-size, uncompressed HTML up to 10 MiB receives the script and a nonce seeded before inner header middleware. HTMX partial requests, streaming/larger/compressed responses pass through without injection. A browser singleton prevents duplicate pollers, and changed HTML is marked no-store. Polling has a request timeout and bounded retry delay. Browser refresh loses unsaved DOM-only state. Custom routers that do not use the Rullst Server development composition may need manual browser refresh. Production/release builds do not mount this browser surface. Keep development servers on loopback and do not override their host for an untrusted network. The current supervisor probes `127.0.0.1` and the configured port; custom `HOST` or `RULLST_HOST` bindings, including IPv6-only loopback, are not equivalent verified-readiness configurations. Keep the default host and restart the CLI after changing its port configuration. ## Existing DLL scaffolds and the v13 decision The CLI (since v12) rejects the old `--hot-reload` generation flag and removes `HOT_RELOAD` when it launches a child, so old generated projects use their directly linked router unless their application reloads that variable itself. Remove legacy `HOT_RELOAD` entries from `.env` as well: application-owned dotenv loading can otherwise restore them. A newly generated project has no DLL layout; already edited application code may need application-specific migration. The existing library loader is retained only as a legacy experimental boundary. Passing Rust, Axum, Tokio or SQLx objects across a library ABI does not provide a stable interoperability contract. Do not reintroduce cross-library ORM pools. 13.0 keeps supervised restart. DLL swapping would return only if its safety requirements can be established and it offers a measured practical benefit over supervised restart, compared with real starters and databases on Windows, Linux and macOS: cold/warm reload time, failed-build recovery, cancellation, memory growth, process cleanup and state ownership. ## Terminal accessibility Use `RULLST_REDUCED_MOTION=1 cargo rullst dash` for static rendering with colors, and add `NO_COLOR=1` to remove the colors as well (`NO_COLOR=1 RULLST_REDUCED_MOTION=1 cargo rullst dash`). Use `cargo rullst dev` with redirected/non-interactive output. --- # 52. Typed Server Functions Source: https://rullst.github.io/Rullst/book/tutorials/52-typed-server-functions.html # Tutorial 52: Typed Server Functions `#[server_function]` lets one concrete async Rust signature describe both the native server implementation and its Wasm caller. The transport is explicit: the macro also creates a `_rpc_router()` that you mount in the server. It does not discover routes through runtime reflection. ```rust,no_run use rullst::{Router, server_function}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] pub struct SumResponse { pub value: u32, } #[server_function(path = "/api/rpc/math/add")] pub async fn add(left: u32, right: u32) -> rullst::rpc::RpcResult { Ok(SumResponse { value: left.saturating_add(right), }) } pub fn rpc_routes() -> Router { add_rpc_router() } fn main() { let _router = rpc_routes(); } ``` When this function is compiled for the native server, its written body runs. When compiled for `wasm32`, calling `add(20, 22).await` serializes `(20, 22)` and returns the decoded `RpcResult`. Transport failures are machine-readable `RpcFailure` values; they never become a fabricated default application value. The caller exists only in a `wasm32-unknown-unknown` build of the crate, such as the island build in [Tutorial 18](https://rullst.github.io/Rullst/book/tutorials/18-wasm-islands-rust.html). ## Mount the generated route inside server policy Merge the generated router before applying the standard security baseline and your domain layers. `rullst::Server` applies that baseline to the router it serves, so there it is enough to merge the generated router into the application router; the example calls `apply_security_baseline` directly to show the composition: ```rust,no_run # use rullst::{Router, server_function}; # use serde::{Deserialize, Serialize}; # #[derive(Deserialize, Serialize)] # pub struct SumResponse { pub value: u32 } # #[server_function(path = "/api/rpc/math/add")] # pub async fn add(left: u32, right: u32) -> rullst::rpc::RpcResult { # Ok(SumResponse { value: left.saturating_add(right) }) # } fn secured_transport() -> Result { let app = Router::new() .merge_axum(add_rpc_router().into_axum()) .into_axum(); rullst::apply_security_baseline( app, rullst::SecurityConfig::default(), rullst::config::Environment::Production, ) } fn main() -> Result<(), rullst::SecurityBaselineError> { let _app = secured_transport()?; Ok(()) } ``` The production baseline verifies the double-submit CSRF cookie/header pair. The Wasm caller reads the bounded `rullst_csrf` cookie and forwards it as `X-CSRF-Token`. `Server` issues that cookie only in staging and production; without it (as in Development) the caller sends no token and lets the server decide, and a non-JSON `403` is reported as `rpc.csrf_token_missing`. Other non-JSON failures are `rpc.http_failure`; `retryable()` is `true` for `429`, `502`, `503` and `504` and for any response with `Retry-After`, which covers the framework's lifecycle-drain and Traffic Shield rejections. The application must still add session/authentication, trusted tenant resolution, object/role authorization and rate limiting in the order documented by `ProductionPreset`. Never accept role, owner or tenant authority from a function argument. ## Failure codes Application failures use the same lowercase dotted-code grammar as the shared client contract: ```rust fn capacity_failure() -> Result { rullst::rpc::RpcFailure::application("course.capacity_reached", false) } let failure = capacity_failure()?; assert_eq!(failure.code(), "course.capacity_reached"); # Ok::<(), rullst::client_contract::ClientContractError>(()) ``` Do not place provider bodies, database errors, PII, tokens or debug text in a failure code. Log sensitive diagnostics only through an approved server-side telemetry policy. When the server rejects an envelope that still carries a valid `request_id` (an unsupported version such as a cached older Wasm client, or a payload of the wrong shape), the failure echoes that identifier. The caller therefore receives `rpc.version_unsupported` or `rpc.request_invalid` rather than `rpc.correlation_mismatch`. Content-type and body-size rejections happen before the body is parsed and still use a server-generated identifier; the generated client applies the same limits before sending. ## Exact limits - zero to 16 simple identifier parameters; - owned parameter types and one owned output type implementing the needed Serde traits; - a concrete async free function with no generics, receiver, `unsafe`, extern ABI or variadic arguments; - `rullst::rpc::RpcResult` as the return type; - an optional same-origin path below `/api/rpc/`, using at most 128 ASCII bytes; - 256 KiB encoded request and response policy; - JSON POST transport with a versioned envelope and request-ID correlation. These bounds do not make a mutation exactly once. Put a stable idempotency key in the domain payload and enforce it transactionally on the server when replay would be harmful. Actual browser-engine compatibility, network availability and deployed identity policy must be tested by the application. --- # Architecture Choices: ORM & Frontend Source: https://rullst.github.io/Rullst/book/architecture-decisions.html # Architecture choices in generated projects `cargo rullst new` records product capabilities that materially change the generated application. Like 12.x, Rullst 13.0 generates one audited application architecture instead of presenting incomplete compatibility markers as equal implementations. The [framework specification](https://rullst.github.io/Rullst/book/spec.html) and [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html) remain authoritative. ## ORM style Database-backed projects are generated with Active Record. The CLI does not ask for a global ORM architecture and does not accept an `--orm` flag. The framework's repository APIs remain available when an application needs an explicit persistence boundary. | Architecture | In 13.0 | Trade-off | | --- | --- | --- | | Active Record | The only generated profile | Concise, but persistence concerns remain close to models. | | Data Mapper / Repository | Application-owned: the repository APIs exist, but no starter generates this profile | More explicit persistence boundary and more code to maintain. | | Hybrid | Not a global project profile | The two styles may naturally coexist per module, but the application must define that boundary. | Earlier v12 prerelease menus offered three labels, but Repository and Hybrid shared generation branches and support varied by blueprint. Removing those selectors does not remove ORM APIs; it prevents the generator from claiming equivalent end-to-end profiles before the generated routes, services and tests support them consistently. All database operations still need parameter binding, authorization and transaction design. For most CRUD-oriented applications, start with Active Record and introduce a repository around domains that genuinely need a separate persistence boundary. ## Database engines and capabilities The first database question (`--database`) chooses exactly one primary backend: | Selection | Boundary | | --- | --- | | SQLite | Local SQLx pool, migrations and relational ORM contract. | | PostgreSQL | SQLx PostgreSQL pool and live container CRUD/schema contract. | | MySQL | SQLx MySQL pool and live MySQL CRUD/schema contract. | | MariaDB | The same MySQL protocol implementation with a separate live MariaDB contract. | | Turso | Typed Turso/libSQL primary profile for the Blank and API starters, without Nexus, Studio, Schema or the SQLx ORM; the SQLx-specific SaaS starter does not offer it. | The Blank starter can instead have no primary database (`--no-database`, or **No database** in the wizard). A second multi-select adds zero or more independent capabilities and accepts `Enter` with no selection. Capabilities already selected by the primary profile or CLI flags are omitted. The same screen also offers AI, Redis and a Dockerfile (`--ai`, `--redis`, `--docker`); the side stores have their own flags (`--turso`, `--mongodb`, `--duckdb`, `--surrealdb`, `--qdrant`). Turso supplies explicit edge SQL, transactions and checked migrations; MongoDB supplies portable document CRUD; DuckDB supplies bounded analytics; SurrealDB supplies document CRUD and bounded read-only graph queries; Qdrant supplies bounded dense-vector operations. Selecting one adds the precise Cargo features and environment keys, but does not make every model portable between different database families. This split prevents a Turso, MongoDB, analytics, or graph label from generating an application whose SQL Active Record migrations cannot run. See the [Polyglot Persistence guide](https://rullst.github.io/Rullst/book/polyglot-persistence.html) for the APIs and limits. ## Frontend profile Projects are generated with server-rendered `html!` views and HTMX enhancement (`cargo rullst new` reports `Zero-Bundle HTMX (html! SSR)`); the CLI does not ask for a frontend engine or accept a `--frontend` flag. The five labels shown by earlier v12 prerelease wizards were not five complete, interchangeable renderers: | Earlier label | In 13.0 | | --- | --- | | HTMX + Tailwind SSR | The generated scaffold: server-rendered `html!` views with the starter's own stylesheet (`static/rullst.css`; no Tailwind build step). There is no project-local SPA bundle, but HTMX and styles are still browser assets that must be pinned and served. | | LiveView | Core supplies `LiveComponent`, `Live::mount` and `live_ws_handler`, and `make:live` scaffolds a component. The application registers routes and supplies the HTMX WebSocket extension, authentication and reconnect policy. | | Wasm Island | `#[island]`, `make:island` and `build:client` provide a dual-target hydration foundation. Asset delivery, CSP, state, routing and browser E2E remain application work. | | Pico.css | Not generated. The application adds, pins and serves Pico.css and validates the resulting pages. | | Tera | Not generated. The application adds the dependency and its own file-template renderer. | The intended direction is to model independent capabilities instead of one misleading global frontend selector: rendering (SSR or API), interaction (HTMX, LiveView or Wasm islands), styling (the bundled design or an explicitly materialized CSS system), and templating (`html!` or a fully generated file-template path). A combination should appear only after its assets, routes and browser behavior have equivalent tests; 13.0 generates none of them. ## Full-stack versus headless API - Full-stack starters (Blank, SaaS) include server-rendered routes and relevant local tool integration. - `--api` (the API starter) generates a JSON-oriented Blank project without HTML view rendering; the SaaS starter rejects it instead of silently retaining its HTML routes. Generated code is application code. Review its routes, auth boundaries, database schema and dependencies before deployment. ## Optional AI Selecting AI adds `rullst-ai` and its provider-agnostic client foundation. Providers still require explicit credentials or the documented deterministic mock mode. Ollama can keep prompt traffic on infrastructure you operate, but “local” alone does not establish an air gap. Measure compile time, binary size, latency, quality and cost for the chosen provider and model. ## Cache and queues Without Redis, Rullst offers process-local memory cache and local queue options. With Redis selected, the generated project can configure the feature-gated Redis adapters. There is deliberately no silent production fallback from Redis to memory: a distributed deployment that loses Redis must not pretend that independent process-local state is equivalent. Choose the backend explicitly and test its failure policy. Core realtime broadcast/presence remains in-process; Redis cache and queue support does not turn it into distributed WebSocket pub/sub. ## Studio and Nexus - Studio's supported launcher is debug-only, binds to loopback and verifies direct peer information. It shows only data supplied by real local probes. - Nexus uses a local-development policy, explicit release credentials or the application's own sign-in (`NexusAuthPolicy::application`; see the [Nexus guide](https://rullst.github.io/Rullst/book/4-rullst-nexus.html#using-the-applications-sign-in)). A dependency or generated route does not remove the need for application RBAC. Neither interface should be exposed publicly without an application-owned identity, authorization, TLS and network policy. ## Installable applications Rullst uses a **web-first** model. The web application is the canonical, universally reachable product; the server remains authoritative for domain rules, authentication, authorization, persistence, realtime and security. `cargo rullst make:pwa` makes that application installable as a Progressive Web App: a web app manifest, placeholder icons, `/sw.js` and an `/offline` page, wired into the starter's router and layouts. The service worker works under the default nonce Content-Security-Policy and never stores a non-GET request or a response marked `no-store` or `private`, which Rullst answers by default, so authenticated pages are not kept on the device. The same site reaches Google Play as a Trusted Web Activity (a Bubblewrap configuration and `/.well-known/assetlinks.json` from `rullst::pwa::AssetLinks`) and the Microsoft Store through PWABuilder; iOS users add it from Safari. See the [PWA guide](https://rullst.github.io/Rullst/book/tutorials/43-progressive-web-apps.html). Until 13.0 the Omni command packaged the site in Tauri shells for desktop, Android and iOS. Those shells deliberately exposed no native API to the site, so they gave the same result as an installed PWA while costing three release workflows, Tauri upgrades and per-platform signing. 13.0 removes `make:omni`, `cargo rullst omni` and the native-only `offline-sync` feature, whose only consumer was the shell. `rullst::client_contract` supplies one shared `rullst.client` v1 JSON boundary for browser, server and future platform-enhanced code. It negotiates positive versions, rejects unknown outer fields and oversized bodies, and carries typed payloads, correlation, optional mutation idempotency, server time and bounded failure codes. It deliberately has no user/tenant/role claim: authentication, authorization, domain validation and durable replay handling remain server work. A page that accepts writes offline queues them in IndexedDB with an idempotency key, as the PWA guide shows; the server accepts each key once. ## Engineering invariants Repository policy requires typed failures rather than production `panic!`, `unwrap()` or `expect()`, explicit SQL parameterization/sanitization, and the workspace test/Clippy/format gates. These are review and CI rules, not a claim that arbitrary application code or third-party dependencies cannot panic. Rullst exposes Axum/Tower integration so applications can add middleware and routes directly. That escape hatch also means the final security and observability composition belongs to the application. ## Practical starting points - Single-process CRUD application: use the generated Active Record, `html!` SSR and HTMX profile with memory cache. - Domain-heavy service: introduce repositories per module with explicit transaction boundaries; 13.0 generates no Data Mapper / Repository profile. - Multi-process application: a configured shared cache/queue plus deployment tests; do not assume realtime is distributed. - Rich browser interaction: opt into LiveView or Wasm foundations only with application-owned integration and browser tests; 13.0 generates neither as a complete profile. Revisit these choices as evidence changes. Do not infer performance, security or availability from a wizard label. --- # Rullst AI: Developing with Autonomous Agents Source: https://rullst.github.io/Rullst/book/2-rullst-ai.html # Rullst AI: Developing with Autonomous Agents Rullst is designed as an **AI-native** Rust framework. Here that term describes inspectable code-generation and explicit compile-time contracts; it is not a claim of historical priority or proof that an AI will produce correct code. Some framework designs use runtime reflection, string-based dependency injection, or conventions that are harder to discover through static source inspection. Rullst instead favors visible types and generated Rust, while still retaining ordinary runtime state and framework abstractions where appropriate. Rullst favors strong typing and compile-time diagnostics. The compiler catches type and ownership errors in generated code, but it cannot validate requirements, security intent, or business correctness; AI-generated changes still require review and tests. For runtime LLM integrations, consult the machine-readable and documented [provider capability matrix](https://rullst.github.io/Rullst/book/ai-provider-capabilities.html). Streaming, native tools, built-in deadlines/retries, and explicit cancellation are not implied by the existence of a provider adapter. Local Rust tools use a separate [guarded execution boundary](https://rullst.github.io/Rullst/book/ai-tool-security.html) with an exact allowlist, principal authorization, closed JSON schema, payload limits, call budget and mandatory audit sink. Destructive and financial calls also require a one-use approval bound to the exact payload. ## 1. Repository instructions for coding agents The Rullst repository maintains its own contributor `AGENTS.md`. The v13 CLI candidate creates separate application instructions during `cargo rullst new` and preserves any existing project instructions during regeneration. The framework repository policy is not copied wholesale into applications. `cargo rullst generate:ai-context` writes a bounded metadata inventory to `.llms.txt` and `.rullst/context-map.json`; `--check` verifies freshness without writing. Source bodies and configuration values are excluded. Read the [context guide](https://rullst.github.io/Rullst/book/project-context.html) for the scope, limits and legacy migration. The generated instructions and inventory do not establish feature readiness or replace the application's authentication, privacy or deployment decisions. Conventions to retain when adapting project instructions: ```markdown 1. **Static Dispatch over Dynamic**: Prefer static dispatch (`impl Trait` or generics) over `dyn Trait` to ensure explicit concrete types for AI context tracking and optimization. 2. **Explicit APIs**: Avoid hidden state. Every controller and middleware should be explicit in its arguments. 3. **HTML Macros**: Boolean attributes in the `html!` macro are bare (`required`) or conditional (`disabled?={is_locked}`); never `disabled="false"`, which still disables. 4. **No Panics**: Never use `unwrap()` or `expect()` in production routes. ``` ## 2. Rullst's AI-Friendly Patterns Rullst's API is designed to give coding tools more explicit source landmarks; this can reduce ambiguity but does not measure or guarantee hallucination rates: - **Explicit Routes:** The `routes![ ... ]` macro is visual and delimited. The AI knows exactly where to add a new route without having to search across scattered files. - **Rullst ORM:** `#[derive(Orm)]` generates typed Active Record methods and a fluent query builder over SQLx; a query the builder does not cover is written in SQL with bound values (`Orm::raw` or SQLx). See the [ORM page](https://rullst.github.io/Rullst/book/crates/orm.html). - **Explicit background workers:** a worker registers an async handler under a job name (`worker.register("email", ...)`) and is started explicitly; `cargo rullst make:worker` writes both parts. ## 3. How to Get the Best Results When instructing an AI to add a feature in Rullst: 1. Ask it to read the instruction files that actually exist in your application and consult the matching version of the Rullst documentation. 2. Point it at an existing file: "Create a new controller following the pattern in `src/controllers/auth_controller.rs`" (written by `cargo rullst auth`). A concrete example in the project gives the assistant a pattern to follow. 3. Use the generators! Ask the AI to use `cargo rullst make:controller` in the terminal (if it's an autonomous agent), ensuring the correct file structure. 4. Run `cargo rullst generate:ai-context` so the assistant in your editor or terminal reads the project's `AGENTS.md` and its `.llms.txt` inventory, and point it at the framework's published [`llms.txt`](https://rullst.github.io/Rullst/llms.txt). Review every edit it proposes. (`cargo rullst ai`, the terminal assistant of the earlier 13.0 prereleases, was removed in 13.0.0-alpha.4.) --- # Rullst Studio: Real-Time Monitoring Source: https://rullst.github.io/Rullst/book/3-rullst-studio.html # Rullst Studio: local development control room Rullst Studio is a developer-facing Axum dashboard. It can run as a standalone server bound to `127.0.0.1` (port `5555` by default) or its router can be mounted explicitly by an application. Studio's built-in boundary is deliberately limited to debug builds and verified loopback peers. It is not a shared-environment authentication system; do not expose raw subrouters publicly without application-level authentication, authorization, TLS, and network policy. Projects generated from the Blank, API and SaaS starters (and the LMS, ERP, Portfolio and Blog examples in `examples/`) start the standalone server only in debug builds and link to `http://127.0.0.1:5555`. Release builds do not start it; runtime `RULLST_ENV` or legacy `APP_ENV` values cannot override that compile-time boundary. To watch a deployed application, mount the read-only [production monitoring endpoint](https://rullst.github.io/Rullst/book/production-monitoring.html) instead (new in v13): it reports request rates, latency, errors, queues and N+1 findings behind a token or the application's own sign-in. The standalone `rullst-studio` binary (`cargo install rullst-studio --version =13.0.0-alpha.4 --features remote --locked`, then `rullst-studio` or `cargo rullst studio --remote`) reads it for several projects from your machine, with a project switcher and encrypted local tokens; see [Watch your projects from Studio](https://rullst.github.io/Rullst/book/production-monitoring.html#watch-your-projects-from-studio). That remote mode has none of the local pages below. ## Running Studio In a project generated from a starter, run the application in a debug build (`cargo rullst dev`) and open `http://127.0.0.1:5555/studio`: its `main` starts the full Studio next to the server. Studio stops with the application. `cargo rullst studio` is different: it runs the application with the `studio` argument (`cargo run -- studio`), and Core's Artisan handler then serves a smaller local compatibility UI (database, AI, telemetry, security, capital and span pages) on `127.0.0.1:5555` until it stops; it is not the `rullst-studio` router described below. Both use port 5555, so stop `cargo rullst dev` first. ```bash cargo rullst studio ``` In your own `main`, the `rullst-studio` library entry point starts the full Studio: ```rust,no_run #[tokio::main] async fn main() -> Result<(), Box> { rullst_studio::run_studio(5555_u16).await } ``` Studio uses the process-wide ORM pool that the application initialized (`Server`, Artisan or an explicit `Orm::init`). If none exists when a database view is requested, Studio initializes it once from the current directory with the resolver shared by `Server` and Artisan: the process `DATABASE_URL`, then `DATABASE_URL` from `./.env` (never overriding the process environment), then `[database].url` parsed from `Rullst.toml`. Without a configured database the views report that database tools are unavailable and nothing is created; the former `sqlite://db.sqlite` fallback is gone, and errors never echo configuration content. Applications that call `Orm::init` with their own URL should do so before Studio serves requests. `Studio::new().into_router(LocalStudioAccess::loopback_only())` builds the same debug-only router for explicit composition. The serving stack must preserve Axum `ConnectInfo` or requests fail closed. An IPv4-mapped peer (`::ffff:127.0.0.1`, reported by a dual-stack `::` listener) is checked as the IPv4 address it carries. Unsafe methods need a same-origin `Origin` header. Studio responses carry `Referrer-Policy: same-origin`, so browsers send the page's real origin on Studio's own form posts and no referrer to other origins. If a host layer replaces that policy with `no-referrer`, browsers send `Origin: null`; Studio then accepts the request only with `Sec-Fetch-Site: same-origin`, which page scripts cannot set. A bare `Origin: null`, or one from a `same-site` document such as another local port, is rejected. Optional OpenAPI and queue views are enabled with `with_openapi` and `with_horizon`. `with_cache` opts a supported cache into metadata-only local inspection, while `with_distributed_traces` supplies the bounded store shared with a separately mounted push-only ingestion router. ## Current views - `/studio`: data browser and dashboard shell. - `/studio/radar`: runtime probes and recorded spans. Process CPU sampling is implemented for Linux and Windows and the KPI cards refresh from `/api/radar` every two seconds. The first delta-based CPU sample, unsupported platforms, and disconnected probes display `Unavailable`; Studio does not synthesize a healthy value. - `/studio/security`: counters and events emitted by the in-process security store. Audit-chain integrity displays `Unavailable` until a verifier is connected. - `/studio/capital`: the in-process revenue view; it is not an accounting ledger. Its table lists the 20 most recent webhook events and its badge counts every event the process-local manager retains (at most 100). - `/studio/traces`: local spans plus authenticated attribute-free distributed records and explicit slow/repeated SQL-label heuristics. - `/studio/cache`: metadata-only view of an explicitly supplied Memory/Redis cache, using opaque identifiers and one-entry invalidation. - `/studio/migrations`, `/studio/ai`, `/studio/env`, `/studio/features`, and `/studio/er`: development tools for their corresponding subsystems. - `/studio/requests`: the request log, streamed over SSE from `/studio/requests/stream`. - `/studio/jobs` and `/studio/api`: the queue view and the API reference, mounted only when `with_horizon` or `with_openapi` supplies their source. `/studio/api` renders the supplied document's operations, parameters, request bodies and responses on the server (no script or remote asset); the document itself is served at `/studio/api/openapi.json`, and `/studio/api/` redirects to the page. The radar page polls the `/api/radar` JSON endpoint; `/api/revenue` and `/studio/security/stats` also return JSON, and `/metrics` serves the Prometheus text format. The request logger uses SSE. The current crate does not promise a separate WebSocket telemetry transport or zero runtime overhead. ## Tooling boundaries - The data browser reads, searches, and paginates allowlisted SQLx identifiers. A page shows at most 25 rows, ordered by the complete primary key (otherwise by every selected column); the database cuts each cell's text to 256 characters before it reaches Studio (key columns keep up to 16 KiB for row actions, and a longer key makes its row read-only), and search terms are limited to 256 bytes. Search matches the displayed columns (at most 256), and the record count uses the same predicate. A SQL NULL shows an italic `NULL` marker; a present value Studio cannot decode as text, a number or a Boolean (for example a BLOB that is not UTF-8) shows an amber `unreadable` marker instead, and a stored text `NULL` is shown as an ordinary value. Inside the verified debug-loopback/same-origin boundary, it may edit one primitive non-key value or delete one complete-primary-key-selected row. Inputs are bounded and parameterized; exact deletion confirmation is required and backend-specific types, including MySQL/MariaDB unsigned or zero-filled integers, remain read-only. A row offers only the actions whose form (for Edit, with a maximum-size value) fits the 64 KiB request limit, and an empty text key is addressable. A table is read-only when a primary-key column falls outside the ASCII identifier boundary or the 256-column cap or uses a floating-point type (its rendered text is rounded), and rows whose key is `NULL`, is not decodable as text or contains a line break or NUL offer no actions. Each write runs in a transaction that commits only when exactly one row changed; any other count is rolled back and fails (non-transactional engines such as MySQL MyISAM cannot roll back). SQLite, PostgreSQL, MySQL, and MariaDB run separate executable contracts. With the default `sqlx::Any` build, Studio renumbers bind markers to `$n` for PostgreSQL and reads information-schema identifiers as `VARCHAR`, so the table view, search, row actions and ER diagram also work there; the PostgreSQL contract runs under that build and under `strict-postgres`. PostgreSQL browsing covers the `public` schema, and every data statement names that schema so that `search_path` cannot select a same-named table elsewhere. This does not supply application tenant/RBAC, audit history, undo, or a shared-production database administrator. - The API reference appears only when the application supplies its `OpenApi` document with `Studio::with_openapi`; Studio does not reverse-engineer arbitrary Axum routes. It is read-only: it shows at most 500 operations and 512 characters of each text field, and it does not send requests to the application (use the JSON document with an external client for that). - The request SSE records method, URI, status and latency. It deliberately does not capture bodies or headers, which commonly contain credentials and PII. - The jobs view lists the 50 most recent records exposed by a supplied queue; its processing/failed/completed counts describe only that window, while the pending count and the purge actions cover the whole queue. Studio previews at most 256 payload and 512 error characters. It reads records through `Queue::list_job_previews` (new in v13), which cuts each payload and error to 2 KiB: the SQLite and Redis drivers cut them inside the store, while other drivers inherit a default that still loads complete records first. SQLite deletes successful rows by default; an application can explicitly select `Queue::sqlite_with_completed_history` for bounded, transactionally pruned completion history and can purge that history from Studio. Retained payloads require host-controlled access and retention policy. Other drivers expose only the inspection/history contract they implement. Retry and purge require the verified local marker, so the raw `jobs_monitor::router` returns `403` for them when mounted outside `Studio::into_router`. - The ER view inspects SQLite, PostgreSQL, MySQL, or MariaDB metadata with bound lookup values and normalizes Mermaid identifiers. Each column is listed once, and composite foreign keys pair their columns by key position. An unconfigured or unsupported source remains visibly unavailable. - The feature-flags page changes the database table used by `DbFeatureDriver`. Toggles require the verified local marker; the raw `feature_flags::router` returns `403` for them. Viewing the page never changes the schema: a missing `rullst_feature_flags` table is reported with the schema to add in an application migration, while connection, permission or column errors are reported as query failures without that guidance. A successful toggle invalidates already-warm drivers in the same process; other processes and direct writers converge by TTL unless the host distributes an invalidation signal. - The environment page redacts values by default, also redacts an allowlisted value that carries URL user information, a bearer token or a secret-named assignment (such as `?access_token=`), and adds only a safe projection of process-global `RullstConfig`; URLs, filesystem paths, secrets, cookies and credentials are omitted. - Cache inspection returns at most 100 UI rows containing an opaque keyed identifier, value byte length and remaining TTL. Values and exact logical keys are never rendered, bulk flush is absent, and individual invalidation requires the verified local mutation marker. Custom cache drivers remain unavailable until they implement Core's bounded metadata contract. ## Telemetry contract Studio reads runtime state exposed by `RadarSnapshot`, `SpanCollector`, the security store, queues, and configured database connections. A counter means only that the corresponding instrumentation path emitted it; it is not proof that all traffic passed through that control. Missing sources must remain visibly unavailable. Remote producers do not connect to the viewer. The application separately mounts `TraceIngestor::router`, distributes a 32–128-byte `TraceIngestionKey`, and uses `TraceBatchSigner` for the exact body and four headers. Each ingestor binds one exact producer name to one key; multiple producers use separate endpoints/keys over the same store. The route accepts 1–128 closed v1 spans under 128 KiB, verifies HMAC-SHA256 plus a 60-second timestamp and one-time nonce, then commits to a bounded process-local store idempotently. It contains no Studio read or admin route. TLS/network policy, key custody and rotation, producer authorization, clock synchronization, label redaction, durable storage and OTLP integration remain deployment work. ## Production boundary Studio is an optional crate. Its supported `run_studio` and `Studio::into_router` paths reject credential-free use in release builds, but consumers should still exclude it from production features unless they are implementing and testing a separate authenticated administrator boundary. Built-in shared production access remains roadmap work, not a password environment-variable promise. ## v12.1.0 browser composition fix This section is for applications still on 12.0.x; 12.1.0 and every 13.0 prerelease include the fix. The [Portfolio issue report](https://github.com/Rullst/examples/blob/deb147f1b3cc75a84a804ceef64d69716916970f/docs/portfolio-errors-found.md) identified two omissions in the published `12.0.0` raw data-browser router: its layout requested `/studio/assets/studio.css`, but only the full local `Studio` builder mounted the assets, and its Cache link had no handler. Applications nesting `data_browser::router()` under `/studio` therefore saw an unstyled page and a Cache 404. The stock Portfolio generator starts a separate debug-only Studio; the Azure showcase added its own embedded router. The 12.1.0 fix serves the embedded CSS/client and an honest, unconnected Cache page through both the raw and nested browser compositions. It does not enable public Studio administration, connect an application cache automatically, or weaken the local access checks. The full builder still requires `LocalStudioAccess` and an explicitly supplied `Studio::with_cache` for real cache inspection. The browser's unconnected page displays **Unavailable**, not zero entries or a fabricated hit rate, and exposes no cache mutation endpoint. ### Migrating an existing showcase 1. Update the application's Rullst dependencies and lockfile together to 12.1.0 or later. Installing a newer CLI does not rewrite previously generated application files or redeploy containers. 2. Remove the temporary `.route(...)` registrations for `/assets/studio.css`, `/studio/assets/studio.css`, `/assets/logger.js`, `/studio/assets/logger.js`, `/cache`, and `/studio/cache` added directly to `data_browser::router()`. They would conflict with the framework's new routes. Remove the now-unused copied assets and temporary handlers only after checking their other uses. 3. Preserve the application's authentication/administrator-authorization layer, trusted TLS boundary, and network restrictions around the entire router. The routing correction does not validate that custom production policy. 4. Rebuild and verify that an authorized `/studio` page loads its same-origin stylesheet with HTTP 200 and `text/css`, and that `/studio/cache` returns a styled **Unavailable** page unless a supported cache is explicitly wired. Unauthorized requests must still be denied. Check full visits and any application-managed HTMX partial requests, then redeploy deliberately. The showcase's temporary Cache page contains hard-coded counters (including `100.0%` hit rate); those are not framework telemetry. Remove that workaround instead of treating its numbers as production measurements. Existing projects do not gain this source correction solely from a dependency update. --- # Rullst Nexus: Explicit Admin CMS Source: https://rullst.github.io/Rullst/book/4-rullst-nexus.html # Rullst Nexus: Explicit Admin CMS **Rullst Nexus** is a server-rendered administrative CMS for explicitly registered Rullst models. `NexusModel` metadata defines the tables, fields, and widgets available in the panel. Rullst builds CRUD, search, pagination, and batch routes from that registration; it does not discover an arbitrary database schema automatically. ## Derive and register a model The `Nexus` derive generates `NexusModel` metadata for named-field structs. It infers booleans, integers (`FieldKind::Integer` bounded by the Rust type, with `u64`/`usize` capped at `i64::MAX`, so a fraction or out-of-range value is rejected), floating-point numbers, dates (`chrono` `NaiveDate`; `DateTime` and `NaiveDateTime` under any path spelling, optional or not) and ordinary text; semantic widgets that Rust's type alone cannot reveal are selected explicitly: ```rust use rullst::db::{FromRow, Nexus, Orm}; #[derive(Debug, Clone, FromRow, Orm, Nexus)] #[orm(table = "users")] #[nexus(label = "Users", icon = "👥")] pub struct User { pub id: i32, pub name: String, #[nexus(kind = "email")] pub email: String, #[nexus(kind = "textarea", label = "Biography")] pub bio: String, #[nexus(kind = "enum", options = "invited, active, suspended")] pub status: String, pub is_active: bool, } ``` The edit form sends only the fields you change, so an edit never rewrites a value its widget cannot show: NULL (shown as an empty `NULL` input), an enum value that is not a registered option (kept selected but disabled), a date-time with an offset (shown as text), a text, e-mail or URL value with line breaks or other control characters (shown read-only; declare `kind = "textarea"` to edit multi-line text) or a value that cannot be decoded. Emptying a number, relation, date, time, date-time, enum or JSON field stores NULL; emptying a text, textarea, e-mail or URL field stores an empty string. A database `NOT NULL` constraint therefore rejects clearing a required typed column. ### Dates and times Each date and time field is parsed by its kind and written as a typed value, so native `DATE`, `TIME`, `TIMESTAMP` and `TIMESTAMPTZ` columns accept it as well as text columns. A malformed value is a `422` validation error that names the field. | Kind (derived from) | Input | Accepted value | Stored in a text column | | :--- | :--- | :--- | :--- | | `date` (`NaiveDate`) | `date` | `YYYY-MM-DD`, year 0001 to 9999 | `YYYY-MM-DD` | | `time` (`NaiveTime`) | `time` | `HH:MM`, `HH:MM:SS` or with a fraction of up to 9 digits | `HH:MM:SS[.fraction]` | | `datetime` (`NaiveDateTime`) | `datetime-local` | a local `YYYY-MM-DDTHH:MM[:SS[.fraction]]` (or a space for `T`) | `YYYY-MM-DD HH:MM:SS[.fraction]` | | `datetime_utc` (`DateTime`) | `datetime-local`, labelled with the panel's time zone, `(UTC)` by default | as `datetime`, read as a wall time in the panel's time zone; `Z`, `±HH:MM` or `±HH` is converted to UTC | RFC 3339, `2026-10-15T09:30:00+00:00` | A `datetime-local` input has no time zone, so a `datetime_utc` field shows and accepts wall times in the panel's time zone, which is UTC unless the application selects another with `Nexus::with_time_zone` (v13): ```rust,ignore use rullst::nexus::{Nexus, NexusTimeZone}; // A fixed offset (no daylight-saving changes), in minutes east of UTC. let nexus = Nexus::new().with_time_zone(NexusTimeZone::fixed_offset(-180)?); // An IANA zone with its daylight-saving rules: the `nexus-tz` feature // (`rullst-nexus/tz`), which uses `jiff` like Core's `scheduler-tz`. let nexus = Nexus::new().with_time_zone(NexusTimeZone::named("America/Sao_Paulo")?); ``` The label names the zone (`Starts at (America/Sao_Paulo)`). An administrator in São Paulo enters 09:30 and Nexus stores `12:30:00+00:00`; the stored instant is shown converted to the zone whatever offset the database reports (PostgreSQL's `+00`, an RFC 3339 offset, or a MySQL/MariaDB `DATETIME` holding UTC). In a zone with daylight-saving changes, a wall time that a gap skips (clocks move forward) does not exist and answers `422` naming the zone, and a wall time that a fold repeats (clocks move back) is read as its first, earlier instant. A stored instant at the later occurrence of a repeated wall time is shown in a text input as its UTC value (`2026-11-01T06:30:00Z`), so saving the form unchanged keeps it; type an offset (`2026-11-01T01:30-05:00`) to choose that occurrence. A value either picker cannot hold exactly (finer than milliseconds, or a wall time outside the years 0001 to 9999) is shown in a text input. 13.0.0-alpha.2 always used UTC. The `datetime` form of a text column is the text `CURRENT_TIMESTAMP` and SQLx write, so text comparisons and ordering agree with application rows. A `datetime` value with an offset can only come from a text column and is stored as entered. In the list, a NULL or undecodable number, relation or Boolean shows a `NULL` or `unreadable` marker rather than `0` or `No`, and a row whose key is NULL or cannot be decoded exactly has no batch checkbox or edit/delete actions. Opening the edit form of a missing, other-tenant or misspelled key returns `404` (and a failed query `500`) instead of an empty editable form. The form reads only the registered visible, non-password columns. Other form values are bound as text. PostgreSQL has no assignment cast from text, so there Nexus writes `number` and `integer` values through `NUMERIC`, relation values that are canonical integers (or empty) through `BIGINT`, Booleans as untyped `'0'`/`'1'` literals, and dates and times (and their NULL) through `DATE`, `TIME`, `TIMESTAMP` or `TIMESTAMPTZ` casts: integer, numeric, floating-point, `BOOLEAN` and date/time columns, the `INTEGER` columns of `Blueprint::boolean` and text columns all accept them. MySQL/MariaDB receive a `datetime_utc` value as its UTC wall time, which their `DATETIME` columns hold. Other kinds are written as text, so keep editable JSON and enum values in text columns; native `JSONB`, `UUID` or enum columns are not supported by Nexus. Lists and edit forms read date and time fields as text (`CAST(... AS TEXT)` on PostgreSQL, `CAST(... AS CHAR)` on MySQL/MariaDB), so native columns, such as the `timestamps()` columns of a strict driver pool, display. The panel addresses records under `/nexus/table/{table}/record/{key}` (with `/edit` for the form), so a key named `new`, `search` or `batch` never collides with an action route. The older `/nexus/table/{table}/{key}` routes remain for other keys. Record keys follow the registered primary-key kind: a `number`, `integer` (or relation) key must be a canonical integer, so `+1`, `01` or `1e3` name no record, and any other kind is compared as text, even when it looks numeric. ### Composite primary keys A table keyed by several columns, such as `lesson_progress (user_id, lesson_id)`, lists them all, in key order: ```rust,ignore #[derive(Debug, Clone, FromRow, Orm, Nexus)] #[orm(table = "lesson_progress")] #[nexus(primary_key = "user_id, lesson_id")] pub struct LessonProgress { pub user_id: i32, pub lesson_id: i32, pub percent: i32, } ``` Annotating each key field with `#[nexus(primary_key)]` declares the same key (in field order); a manual `NexusModel` returns the columns from `nexus_pk_columns()`. A key has at most 8 columns, each a registered field other than the tenant column. Every key column is entered on create (even an integer one), read-only on edit, and part of every read, update, delete and batch predicate. A record is addressed by its key values in key order, each percent-encoded (every byte except ASCII letters, digits and `-_.~`) and joined by `,`: user 7 and lesson `intro` are `7,intro`, and a text value `a,b` becomes `a%2Cb`. The URL encodes that address once more (`/nexus/table/lesson_progress/record/7%2Cintro`), and the audit records it as the record key. Only the canonical spelling with one value per key column names a record; integer columns still need canonical integers. Search matches the typed text literally (`%` and `_` are not wildcards) in the visible text, textarea, e-mail and URL columns. It is case-insensitive on PostgreSQL (`ILIKE`), ASCII case-insensitive on SQLite and follows the column collation on MySQL/MariaDB. Live search keeps the current sort, starts again at page 1, rebuilds the sort and pagination links for the new query and records it in the URL, so saving a record refreshes the same view. A model without such a visible column shows no search box, and a search query sent to it lists no rows (with a note) instead of every row. The list sorts only by the record key and visible, non-`password` columns; a `sort_by` naming a hidden or `password` column orders by the key instead. A `password` field is never displayed: the list shows a fixed mask and the edit form an empty input, and leaving it empty keeps the stored value. Nexus writes a new value exactly as typed and does **not** hash it. Keep hash columns `readonly` (or `hidden`) in Nexus and change them through an application flow that hashes; a field named `password_hash` is hidden by the derive. On an ORM model the derive also follows the `#[derive(Orm)]` field markers: - `#[orm(skip)]` and `#[sqlx(skip)]` fields have no column and are omitted. - `#[orm(encrypted)]` and `SecretString` fields are hidden, read-only `password` fields: Nexus never lists, searches, sorts, renders or writes them, so an edit cannot store plaintext in an encrypted column. Any other `#[nexus(kind/options)]` on them is a compile error. - `#[orm(hidden)]` fields are hidden and read-only; only an explicit `#[nexus(kind = "password")]` exposes one, as a write-only `password` field. - `#[orm(masked)]` fields default to the `password` widget; an explicit `#[nexus(kind = ...)]` deliberately shows them. `id` is the default primary key. Use `#[nexus(primary_key)]` on a field or `#[nexus(primary_key = "uuid")]` on the struct for another key, and several annotated fields or a comma-separated list for a [composite key](https://rullst.github.io/Rullst/book/4-rullst-nexus.html#composite-primary-keys). An annotated field is the key even when the struct also has an `id` field, which then stays an ordinary column; annotations that differ from the struct-level list are a compile error. A single integer key is treated as database-generated, so it is hidden and read-only; any other key (a UUID or code) is listed and entered in the create form, and stays read-only on edit. Field options also include `label`, `hidden`, `readonly`, and the `text`, `textarea`, `email`, `url`, `number`, `boolean`, `date`, `datetime`, `time`, `datetime_utc`, `password`, `json`, and `enum` widget kinds. A `hidden` field is left out of the list, search, sorting and the create/edit forms, and a submitted value for it is rejected; `readonly` keeps a field visible but rejects submitted values. Implementing `NexusModel` manually remains available when an application needs metadata that cannot be derived. Then select an explicit access policy in your routing file (usually `src/lib.rs` or `src/main.rs`) and mount the resulting router: ```rust,ignore let nexus_auth = rullst::nexus::NexusAuthPolicy::local_development_or_basic_from_env()?; let nexus = rullst::nexus::Nexus::new() .with_auth_policy(nexus_auth) .with_brand("SaaS Admin") .register::() .try_build()?; // ... and add it to the final router: let router = router.nest_axum("/nexus", nexus); ``` The helper is intentionally asymmetric: debug builds allow only requests whose `ConnectInfo` peer is loopback (an IPv4-mapped peer such as `::ffff:127.0.0.1`, which a dual-stack `::` listener reports for IPv4 clients, counts as IPv4); release builds load and validate `NEXUS_ADMIN_USERNAME` and `NEXUS_ADMIN_PASSWORD` from the process environment, then the working directory's `.env`. Missing connection metadata is denied, and neither `RULLST_ENV` nor legacy `APP_ENV` can turn credential-free access on in a release binary. Applications can call `basic_from_env()` directly in debug when testing the production authentication flow. `Nexus::try_build()` returns a plain `axum::Router`. Mount it on a `rullst::Router` with `.nest_axum("/nexus", nexus)`, as above: `rullst::Router::nest` accepts only another `rullst::Router`. In a plain Axum application, `axum::Router::nest("/nexus", nexus)` is the equivalent. `rullst::Router` has no `with_state`; pass shared application state with `.layer(Extension(state))` (`rullst::web::axum::Extension`) and read it in handlers with the `Extension` extractor, or mount an `axum::Router` after its own `with_state`; see [sharing application state](https://rullst.github.io/Rullst/book/tutorials/08-routing-and-middlewares.html#step-3-share-application-state). #### Using the application's sign-in An application with its own login can let it decide instead, so an administrator who is already signed in is not asked to sign in again. Implement `NexusAuthorizer`: Nexus calls it on every request, with the request's method, URI, headers and extensions, before reading or rendering any data. ```rust,ignore use rullst::nexus::{NexusAccess, NexusAuthPolicy, NexusAuthorizer, NexusAuthorizerError}; use rullst::web::axum::http::request::Parts; struct Admins; impl NexusAuthorizer for Admins { async fn authorize(&self, request: &Parts) -> Result { // Look the session up as your auth middleware does. let Some(user) = current_user(&request.headers).await? else { return Ok(NexusAccess::SignIn); }; Ok(if user.is_admin { NexusAccess::allow(format!("user:{}", user.id)) } else { NexusAccess::Deny }) } } let nexus = rullst::nexus::Nexus::new() .with_auth_policy(NexusAuthPolicy::application(Admins)) .register::() .try_build()?; ``` | Answer | Response | | :--- | :--- | | `NexusAccess::allow(actor_id)` | the request proceeds; `actor_id` names the administrator in the mutation audit (1 to 255 bytes, no surrounding whitespace or control characters, else `503`) | | `NexusAccess::SignIn` | a page request (`GET`/`HEAD` without `HX-Request`) is redirected (`303`) to `/login`; fragments and other methods get `401` with `HX-Redirect: /login`, which htmx and Nexus's script follow | | `NexusAccess::Deny` | `403` | | `Err(NexusAuthorizerError::new(reason))` | `503`; Nexus logs the reason and never admits the request | Use `NexusAuthPolicy::Application(NexusApplicationAuth::new(Admins).with_login_path("/account/sign-in"))` for another login page; it must be a local path (one leading `/`, at most 512 visible ASCII characters, no `\`), or `try_build()` returns `NexusBuildError::InvalidLoginPath`. Because the check runs on every request, a signed-out or demoted administrator loses access on the next one. The policy does not need TLS evidence (your session cookie's `Secure` flag is the application's concern); CSRF protection, the administrator role layer, tenant scoping and the audit stay as with every policy. `NexusAuthPolicy::local_development_or_application(Admins)` keeps the credential-free loopback policy in debug builds. The SaaS starter and the LMS example use it. Their `src/middlewares/nexus_access.rs` admits the user of the epoch-checked login cookie whose `users.is_admin` column is true, so signing out everywhere or a password change also ends Nexus access. `is_admin` defaults to false and is declared `#[orm(db_default)]`: ORM writes never touch it, so neither a form nor `save()` grants it, while `User::query().where_eq("is_admin", true)` still filters on it. Grant the first administrator with your database client, for example `UPDATE users SET is_admin = TRUE WHERE email = 'you@example.com';`; that administrator can then grant others from the Users model in Nexus. The `users` table that `cargo rullst auth` generates has the same column, ready for `NexusAuthPolicy::application`. #### Serving the application without Nexus Because the helper fails closed, a release build without `NEXUS_ADMIN_USERNAME`/`NEXUS_ADMIN_PASSWORD` cannot build Nexus. The Portfolio and ERP examples then start without `/nexus` (the ERP, whose back office shares the Nexus policy, serves only `/health` and `/ready`) and print one line naming the variables; a weak, placeholder or unreadable credential still stops startup. In your own application, mount Nexus only when its policy builds: ```rust,no_run # #[cfg(feature = "nexus")] # fn router() -> Result> { use rullst::nexus::{Nexus, NexusAuthPolicy, NexusBuildError}; let router = rullst::Router::new(); // your application's routes let router = match NexusAuthPolicy::local_development_or_basic_from_env() { Ok(policy) => { let nexus = Nexus::new().with_auth_policy(policy).try_build()?; router.nest_axum("/nexus", nexus) } Err(NexusBuildError::MissingCredential { variable }) => { eprintln!( "Nexus is not mounted at /nexus: {variable} is not set; set \ NEXUS_ADMIN_USERNAME and NEXUS_ADMIN_PASSWORD to enable it" ); router } // A weak, placeholder or unreadable credential is still a startup error. Err(error) => return Err(error.into()), }; # Ok(router) # } ``` The debug policy also requires a local `Host` and, for unsafe methods, a same-origin `Origin`. Its responses carry `Referrer-Policy: same-origin` unless the application sets one; when a host layer imposes `no-referrer`, browsers send `Origin: null`, which is accepted only with a single `Sec-Fetch-Site: same-origin` header (page scripts cannot set it). ### Basic Auth failures and reverse proxies Nexus counts failed Basic credentials per client bucket: one IPv4 address or one IPv6 /64 of the `ConnectInfo` peer. Five failures in five minutes lock the bucket for fifteen minutes. The unauthenticated `401` challenge that every browser receives first is not a failure. A locked bucket gets `429` without any credential check, so the lockout cannot confirm a guessed password. The exception is a browser that already logged in during this process: it received the `HttpOnly`, `Secure` `rullst_nexus_known_client` cookie and keeps having its credentials checked. Behind a TLS-terminating reverse proxy every client shares the proxy's address, so one attacker can lock the bucket for everyone who has no known-client cookie (new browsers, or all browsers after a restart). Configure Core's trusted-proxy layer with the proxy's own network to give each client its own bucket: ```rust,ignore Server::new(router).trusted_proxies( TrustedProxyConfig::new(["10.0.0.0/8"])?.trust_forwarded_proto(true), ) ``` List only the networks your proxies connect from: any host inside a listed network can choose the client address. Forwarding headers from every other peer are ignored. With `trust_forwarded_proto(true)`, an `X-Forwarded-Proto: https` from that trusted peer also satisfies the Basic Auth TLS requirement, so a separate `NexusVerifiedTls` middleware is unnecessary; enable it only when the proxy overwrites that header. The same settings are available in `Rullst.toml` as `[security] trusted_proxies`, `trusted_proxy_header` and `trust_forwarded_proto`. See the [security architecture](https://rullst.github.io/Rullst/book/security-architecture.html#identity-and-network-trust) for the resolution rules. ## Tenant-scoped administration Use an explicit tenant column when a registered model contains tenant-owned rows. The column must be a text, non-primary-key field. The derive makes it hidden and read-only so browser form data cannot choose the tenant: ```rust use rullst::db::{FromRow, Nexus, Orm}; #[derive(Debug, Clone, FromRow, Orm, Nexus)] #[orm(table = "projects", tenant = "organization_id")] pub struct Project { pub id: i32, pub organization_id: String, pub name: String, pub active: bool, } ``` Authentication middleware must resolve membership and install a trusted `rullst::security::TenantContext`. Do not construct it directly from `X-Tenant-ID`, a query parameter or another client assertion. Nexus applies the exact scope to list/search/edit/create/update/delete and batch routes (as a binary-string comparison on MySQL/MariaDB, whose default collations ignore case); missing context denies a scoped model. A model without the attribute remains global by design. `#[derive(Nexus)]` reads only `table` (or its ORM alias `table_name`) and `tenant` from a shared `#[orm(...)]` attribute. Other ORM options, such as `tenant_column`, `policy`, `soft_delete(...)` or a relation's `foreign_key`, are skipped. ORM tenant isolation (`tenant_column`) and the Nexus admin scope (`tenant`) are separate options; declare both when both are wanted. Fields that declare an ORM relation (`has_many`, `belongs_to`, ...) are not table columns and do not appear in Nexus. ## Require transaction-coupled audit Install the fixed audit schema as an explicit deployment step, then enable the policy on the panel: ```rust,ignore rullst::nexus::create_nexus_audit_table().await?; let nexus = rullst::nexus::Nexus::new() .with_auth_policy(nexus_auth) .register::() .with_required_audit() .try_build()?; ``` Each successful mutation and its minimized `rullst_nexus_audits` row commit in one database transaction. Audit failure rolls the mutation back. The record contains actor, optional tenant, table/action, optional known key (for a create, the key the administrator entered; a database-generated key is absent), affected-row count, committed outcome, optional bounded request ID, timestamp and format version. A key that does not fit 1 to 256 bytes of unpadded text without control characters is recorded as absent instead of blocking the change. `verify_nexus_audit_table()` checks deployment readiness and `recent_nexus_audits()` reads at most 1,000 newest rows, optionally tenant filtered; the application must authorize that export separately. The table is neither append-only nor protected from a database administrator. It does not persist denied attempts, and automatically assigned create keys are not recovered uniformly across all supported SQL dialects. Protect database permissions and send records to an independently operated immutable sink when that property is required. ## 👤 Example: Dynamic Profile Settings Applications like the **Portfolio** example (`examples/portfolio`) use explicit Nexus metadata to expose single-row or multi-row site configuration settings (such as developer name, title, bio, email, personal website, avatar photo, and social links). ```rust use rullst::db::{Orm, FromRow, Nexus}; #[derive(Debug, Clone, FromRow, Orm, Nexus)] #[orm(table = "profile")] pub struct Profile { pub id: i32, pub name: String, pub title: String, pub subtitle: String, pub email: String, pub website: String, pub avatar_url: String, pub github_url: String, pub linkedin_url: String, } ``` When registered in Nexus: ```rust,ignore let nexus_auth = rullst::nexus::NexusAuthPolicy::local_development_or_basic_from_env()?; let nexus = rullst::nexus::Nexus::new() .with_auth_policy(nexus_auth) .with_brand("Portfolio Admin") .register::() .try_build()?; ``` Administrators can edit the registered profile fields at `/nexus`. An application that reads those fields on each request can show the persisted values without a code change or redeployment; cache policy remains application-owned. Batch deletion is available for every registered model. Batch deactivation is shown only when the model declares a writable (neither `hidden` nor `readonly`) Boolean `is_active` or `active` field; Nexus never guesses which arbitrary status value means inactive. ## Content Security Policy Nexus pages load only same-origin assets from `/nexus/assets/` (`nexus.css`, `nexus.js`, a vendored htmx 2.0.4 and the Rullst logo `rullst-logo.png`, used as the brand mark and favicon) and contain no inline scripts, styles, event-handler attributes or `hx-on` attributes. The default production CSP applies to the panel unchanged, so there is no reason to add `'unsafe-inline'`, `'unsafe-eval'` or a CDN to the application-wide `security.csp`. A custom policy must keep `'self'` for scripts, styles, images and `connect-src`. Nothing is requested from GitHub, unpkg or Google Fonts; the panel uses system fonts. htmx's history cache is disabled, so admin pages and open edit forms are never snapshotted into origin-wide `localStorage`; Back reloads the page from the server. ## Benefits of Nexus 1. **Small Front-end Surface:** Nexus renders responsive tables, forms and actions with server-side HTML and HTMX. 2. **Fail-closed Construction:** Nexus cannot build without a selected access policy. Being in the same binary is not itself a security guarantee; the application still owns TLS, trusted proxies, roles, ownership, field policy and database permissions. 3. **Server-side Field Policy:** `hidden` and `readonly` metadata improve the UI, while authorization and write restrictions are also enforced on the server. In a generated debug application, open `/nexus` from the same machine. In a release deployment, configure strong unique credentials and the verified TLS boundary before exposing the route. ## Mobile maintenance in 12.1.0 This section is for applications still on 12.0.x; 12.1.0 and every 13.0 prerelease include these changes. The [Portfolio report, issues 3 and 4](https://github.com/Rullst/examples/blob/0182464b68a5adaf89fca65af8dd00142d78ad49/docs/portfolio-errors-found.md) identified a drawer that could not be dismissed on small screens and a desktop-only Portfolio layout. The changes shipped in **12.1.0** and are absent from 12.0.0: - Nexus supplies a close button, click/touch backdrop, Escape and link dismissal, focus containment and return, and synchronized `aria-expanded` state. Closed mobile links are inert; the background is inert while the drawer is open. Desktop resizing clears that state. Without JavaScript, ordinary navigation stays visible. The drawer does not require HTMX or a CDN to dismiss. - The Portfolio generator (since 13.0.0-alpha.4 the [`examples/portfolio`](https://github.com/Rullst/Rullst/tree/main/examples/portfolio) application) stacks its sections below 900px, tightens spacing below 640px, wraps long content and keeps project cards inside the viewport. Reduced-motion preferences stop decorative animations. Update the application dependencies and lockfile to 12.1.0 or later, then remove the temporary `nexus_mobile_patch` middleware that buffers and rewrites Nexus HTML. Retaining it can introduce duplicate close controls, backdrops and event handlers. Do not remove authentication, authorization, TLS, CSRF or other security middleware along with that presentation-only patch. Existing Portfolio projects own their generated `src/pages/home.rs`: updating the CLI or dependency alone does **not** replace that file. Compare its styles with `examples/portfolio/src/pages/home.rs`, apply the responsive CSS while preserving your content/design, and remove conflicting old overrides. Verify narrow screens, long names/URLs, keyboard focus, touch dismissal, desktop resizing and reduced motion before redeployment. Other repositories and deployed Azure applications are not modified by these framework changes. --- # Rullst Capital: SaaS Billing Made Easy Source: https://rullst.github.io/Rullst/book/5-rullst-capital.html # Rullst Capital: billing boundaries > **Vision preserved:** capabilities removed from the usable-today contract are > still evaluated item by item in the [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html#capital-and-billing-vision), > including whether each one is worth implementing and why. `rullst-capital` provides provider-neutral billing abstractions, revenue metrics, verified webhook plumbing and two payment-provider adapters: Stripe (supported) and InfinitePay (experimental until validated against a live account). Provider capabilities are not uniform: consult the adapter API and its tests before depending on a particular checkout, refund or webhook operation. Other gateways are integrated in application code by implementing the provider traits; see [Writing your own payment provider](https://rullst.github.io/Rullst/book/capital-custom-provider.html). v13 removed the Paddle, Lemon Squeezy, Polar, Razorpay, Mercado Pago, Alipay, Coinbase Commerce, PicPay and Wise (payout) adapters and the NFS-e fiscal preparation module. NFS-e was never validated with a real municipality and may return as a separate product outside Rullst. See the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html) row "Capital providers and NFS-e removed". `RevenueDashboardManager` is a bounded process-local presentation source. Applications call `update_metrics` with values reconciled from their durable billing database and call `record_event` after a verified webhook path. Recording an event deliberately does not invent a plan price, fee, currency or subscriber count from its event name. ## Payment providers Initialize only the provider required by the application and treat credentials as deployment secrets. An empty or `mock_*` API credential selects the adapter's deterministic offline behavior instead of a live request where that adapter documents support for it, so verify that production credentials are present. An empty webhook secret is a configuration error; a `mock_*` webhook secret selects mock verification, which the production webhook middleware rejects. Every reviewed live method uses the same pooled outbound client with a five-second connect timeout, twenty-second whole-request timeout, disabled redirects and ambient proxy discovery, and one-MiB JSON limit. Returned checkout locations must be bounded absolute HTTPS URLs without credentials or fragments. `CapitalError::Provider` exposes redacted static provider/operation labels, failure kind, optional HTTP status and bounded numeric `Retry-After`. Its permanent/transient/rate-limited class is evidence for application policy, not permission to repeat a mutation. Only retry after proving that the exact operation forwards a persisted idempotency key and retaining reconciliation. ### Immediate charges without raw payment data `Billable::charge_with` accepts only a provider-tokenized customer and payment method. Amounts are integer minor units and every attempt needs an application-owned idempotency key. The reviewed live adapter is Stripe Payment Intents; other billing adapters fail with `UnsupportedOperation` instead of pretending to charge. ```rust,no_run use rullst_capital::{Billable as _, CapitalError, StripeProvider}; async fn collect_order( account: &Account, stripe: &StripeProvider, ) -> Result<(), CapitalError> where Account: rullst_capital::Billable + Sync, { let receipt = account .charge_with( stripe, 4_990, // BRL 49.90 in cents "BRL", "cus_provider_owned", "pm_provider_tokenized", "order_42-attempt_1", ) .await?; if receipt.is_succeeded() { // Reconcile durable order state; do not grant access from this alone. } Ok(()) } ``` The application must establish the customer's authority over both provider IDs, retain the key with its order, reconcile signed webhooks and handle mandate/SCA rules. Empty/`mock_*` credentials produce a deterministic receipt with the distinct non-success `ChargeStatus::Mock`; it must never grant access or be booked as revenue. This API has no raw-card field and never guesses currency. ## Provider-specific metered usage Use the static-dispatch `MeteredBillingProvider` boundary for new code. `StripeMeterEvent` requires the authoritative `cus_*` customer, configured meter event name, positive quantity, timestamp and a unique visible identifier; the adapter sends the default `stripe_customer_id`/`value` payload and binds all of those fields in the accepted response. The response is capped at one MiB and offline credentials return a deterministic `UsageStatus::Mock`. Stripe provides only rolling identifier deduplication. A custom adapter whose provider has no event-key field reports `UsageDeduplication::ApplicationOutboxRequired`; the application must then atomically claim `event_key()` in a durable outbox before sending. It must also reconcile invoices and derive quotas/entitlements from authoritative state. The old uniform `BillingProvider::report_usage` remains source-compatible for mocks but fails closed in live Stripe configurations instead of guessing required fields. ## Coupons and relative trial extensions `CouponCode` validates/redacts the provider coupon identifier, while a statically dispatched `SubscriptionHandle` applies it. Stripe uses the current expanded `discounts[0][coupon]` update and binds the response to the requested subscription/coupon. Adapters without reviewed subscription-discount contracts, including InfinitePay, return `UnsupportedOperation` instead of silently succeeding. `handle.extend_trial(15)` resolves a bounded 15-day expiration from the current UTC clock. Retryable workers should persist the command creation time and call `extend_trial_days_at(15, command_created_at)`; this emits the same absolute expiration on every attempt. `set_trial_end` is available for explicit reconciliation. Stripe has bounded protocol/response-binding tests for trial updates. Authorization, command serialization, billing-cycle policy, webhook reconciliation and live account acceptance remain host/release responsibilities. See the [billing tutorial](https://rullst.github.io/Rullst/book/tutorials/19-saas-billing-capital.html#7-use-a-bounded-subscription-handle-and-grace-period). ## Shared subscriptions and strict resource quotas One Team/Workspace model can own `Billable` and its tier policy while every authorized member uses the same bounded `BillingSubject`. Derive that subject from trusted `TenantContext`, never from a client-selected owner ID. `Billable::quota_request` derives the limit from `tier_limit`; `QuotaGate` reserves before invoking a create operation, suppresses an exact retry and compensates an ordinary callback error. `InMemoryQuotaStore` provides deterministic process-local behavior. Enable `quota-sql` directly or `capital-quota-sql` on the umbrella crate for a durable unique-claim and conditional-counter implementation on SQLite, PostgreSQL, MySQL and MariaDB. Use `reserve_with_transaction` and perform the domain insert in that same transaction whenever quota and creation must commit atomically. The [complete tutorial](https://rullst.github.io/Rullst/book/tutorials/19-saas-billing-capital.html#8-enforce-one-shared-workspace-quota-before-creation) shows this path and its replay semantics. The application still owns membership, authoritative tier/webhook state, migrations, reconciliation of abandoned standalone reservations and adapters for Turso or non-relational stores. Rullst does not intercept writes performed outside the explicit quota boundary. ## Payment-bound invoice PDF and delivery Enable the umbrella `capital-mail` feature (or Capital's `invoice-pdf` and Mail's `capital-invoice` features separately). `Invoice::bind_succeeded_charge` rejects non-final/mock receipts and any recipient, minor-unit total or currency mismatch. `PaidInvoiceDelivery::prepare` then creates escaped HTML, a bounded native PDF attachment and a message that has already passed Mail pre-flight. It can use the configured facade, a tenant route or an explicit static driver. The returned stable `delivery_key` is for a unique durable outbox record owned by the application. Rullst does not infer a webhook, claim that key atomically, guarantee attachment support at every provider or promise exactly-once delivery. See the [SaaS billing tutorial](https://rullst.github.io/Rullst/book/tutorials/19-saas-billing-capital.html#4-render-and-deliver-the-invoice-only-after-final-success). Webhook endpoints must use the Capital verification middleware. Its Axum and opt-in Actix adapters call the same canonical verifier. For supported protocols it performs cryptographic verification, timestamp freshness checks where the protocol signs a timestamp (Stripe), a two-megabyte body limit, and bounded replay protection before the application receives a normalized event. A webhook route may receive a narrowly scoped CSRF exemption only when this verifier remains mandatory on that exact route. See the [payment guide](https://rullst.github.io/Rullst/book/payment-gateways-guide.html#actix-web-adapter) for Actix setup. The default store is process-local. The opt-in `webhook-sql` feature (`capital-webhook-sql` on the umbrella crate) persists bounded provider-scoped payload digests or stable event IDs across SQLite, PostgreSQL, MySQL, and MariaDB processes. SQL-backed middleware is a replay firewall that claims before dispatch, not exactly-once delivery. For an atomic relational business transition, use the verified provider event ID with `check_and_record_event_key_with_transaction` in the domain transaction and do not pre-claim it through SQL middleware. Cross-system effects still require an outbox, idempotent consumers, and reconciliation. ```rust,no_run use axum::{routing::post, Router}; use rullst_capital::verify_webhook; async fn billing_webhook() { // Read the verified event inserted by the middleware in real handlers. } let router: Router = Router::new() .route("/webhooks/billing", post(billing_webhook)) .layer(axum::middleware::from_fn(verify_webhook)); ``` ## Operational checklist - Derive tenant identity from an authenticated context, not an arbitrary client header. - Keep webhook secrets non-empty, rotate them, and retain replay state according to the provider contract. - Reconcile provider events idempotently in the application database. - Treat Studio revenue panels as observability views, not an accounting ledger. - Verify every adapter capability in a sandbox before enabling live traffic. --- # Integrating AI into Rullst Source: https://rullst.github.io/Rullst/book/6-ai-integration-tutorial.html # Integrating AI into Rullst `rullst-ai` provides guarded adapters for OpenAI, Anthropic, Gemini, DeepSeek, Ollama, and explicitly configured OpenAI-compatible local/cloud endpoints. The high-level `AiClient` applies prompt-injection heuristics and PII masking before dispatch. Those controls reduce known risks; passing them does not prove that a prompt or model response is safe or correct. Start with the [provider capability matrix](https://rullst.github.io/Rullst/book/ai-provider-capabilities.html). It separates implemented transport paths from model-dependent behavior and lists unsupported streaming, tool, timeout, retry, and cancellation boundaries. ## 1. Enable the AI facade ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["ai"] } serde = { version = "1", features = ["derive"] } serde_json = "1" ``` Pin the v13 prerelease exactly: Cargo never selects a prerelease on its own, and the v12 line reached end of life on 16 October 2026. Keep the application's `Cargo.lock` under version control and review future dependency updates before deployment. ## 2. Create a guarded client ```rust use rullst::ai::{AiClient, providers::openai::OpenAiProvider}; fn ai_client() -> Result { let api_key = std::env::var("OPENAI_API_KEY")?; Ok(AiClient::new(OpenAiProvider::new(api_key))) } ``` Empty and `mock_*` keys intentionally select deterministic offline behavior. Requiring the environment variable, as above, prevents a live deployment from silently becoming a demo. Tests can construct `OpenAiProvider::new("")` explicitly when offline behavior is desired. Other built-in constructors are available under: - `providers::anthropic::AnthropicProvider`; - `providers::gemini::GeminiProvider`; - `providers::deepseek::DeepSeekProvider`; - `providers::ollama::OllamaProvider`. For a local server exposing an OpenAI-compatible endpoint, declare only the request shapes verified for the exact model: ```rust,no_run use rullst::ai::{ AiClient, providers::openai_compatible::{ OpenAiCompatibleCapabilities, OpenAiCompatibleProvider, }, }; fn local_client() -> Result { let provider = OpenAiCompatibleProvider::try_local( "http://127.0.0.1:8080/v1", "my-local-model", )? .with_capabilities( OpenAiCompatibleCapabilities::chat_only() .with_embeddings() .with_json_mode(), ) .try_with_embedding_model("my-embedding-model")?; Ok(AiClient::new(provider)) } ``` `try_local` accepts HTTP only on literal loopback IPs and sends no authorization header. Use `try_local_with_bearer` for an authenticated loopback server or `try_cloud(https_base_url, api_key, model)` for a Bearer-authenticated cloud endpoint. None of these constructors discovers model capabilities; false declarations become provider errors, while omitted capabilities fail locally with `UnsupportedCapability`. A non-compatible API implements the public `AiProvider` trait instead. ## 3. Call it from an Axum handler This bounded handler rejects oversized input before invoking the client and does not expose the provider's full error details to the HTTP caller: ```rust use rullst::{Server, ai::AiClient}; use rullst::web::axum::{ Json, Router, extract::State, http::StatusCode, routing::post, }; use serde::{Deserialize, Serialize}; #[derive(Deserialize)] struct ChatPrompt { prompt: String, } #[derive(Serialize)] struct ChatResponse { answer: String, } async fn chat( State(client): State, Json(body): Json, ) -> Result, (StatusCode, &'static str)> { if body.prompt.len() > 8_192 { return Err((StatusCode::PAYLOAD_TOO_LARGE, "prompt is too large")); } let answer = client .prompt(&body.prompt) .await .map_err(|_| (StatusCode::BAD_GATEWAY, "AI request failed"))?; Ok(Json(ChatResponse { answer })) } async fn serve(client: AiClient) -> Result<(), rullst::ServerError> { let app = Router::new() .route("/api/chat", post(chat)) .with_state(client); Server::new(app.into()).run(3000).await } ``` Production applications should authenticate and rate-limit this route, bind tenant identity from authenticated state, cap response sizes, and record an audit event without logging prompts or secrets verbatim. ## 4. Inspect capabilities before optional operations ```rust,no_run # use rullst::ai::{AiClient, EgressFetcher, EgressPolicy, LocalImagePolicy, providers::openai::OpenAiProvider}; # async fn example() -> Result<(), Box> { let client = AiClient::new(OpenAiProvider::new("mock_local")); let capabilities = client.capabilities(); if capabilities.vision { // Bytes have already crossed the application's own admission boundary. let png = b"\x89PNG\r\n\x1a\n\x00"; client.prompt_with_image("Describe this image", png).await?; // Local files must remain under an exact canonical allowlisted root. let local = LocalImagePolicy::new(["./private-uploads"])?; client .prompt_with_image_file("Describe this image", "./private-uploads/photo.png", &local) .await?; // URLs require an explicit HTTPS egress allowlist and resource budget. let remote = EgressPolicy::strict() .with_allowed_hosts(["images.example.com"])? .with_max_response_bytes(2 * 1_024 * 1_024)?; client .prompt_with_image_url( "Describe this image", "https://images.example.com/photo.png", &EgressFetcher::new(remote), ) .await?; } # Ok(()) # } ``` Capability inspection prevents avoidable requests but is not a substitute for handling `UnsupportedCapability` and upstream model errors. Configuration can select a model that supports less than its provider transport. File and URL helpers check capability and guard the text before I/O, cap images at 10 MiB, sniff JPEG/PNG/WebP/GIF bytes and require a supplied remote `Content-Type` to match. The host must authorize the source and protect allowed local directories; Rullst does not infer tenant ownership from a path or URL. ## 5. Request structured output `json_prompt` requests parseable JSON. It does not claim JSON Schema enforcement. Use `structured_prompt_with_schema` only when the reported provider capability and configured model support it: ```rust,ignore use rullst::ai::StructuredOutputSchema; let schema = StructuredOutputSchema::new( "answer", serde_json::json!({ "type": "object", "properties": {"answer": {"type": "string"}}, "required": ["answer"], "additionalProperties": false }), )?; let answer: serde_json::Value = client .structured_prompt_with_schema("Summarize the incident", &schema) .await?; ``` Validate business rules after deserialization. Schema-conforming model output can still be false, malicious, stale, or unauthorized. ## 6. Streaming and tools `StreamingAiClient` streams guarded, bounded and cancellable (`AiCancellation`) output from an OpenAI-compatible configuration that declares `with_streaming()` and (new in v13) from Anthropic and Gemini through their native SSE protocols; `StreamSummary::usage()` returns the token usage the provider reported. The OpenAI, DeepSeek and Ollama transports do not stream. Rullst can host ordinary Axum SSE responses, but an application that uses a third-party streaming SDK owns its authentication, guardrails, backpressure, deadlines, cancellation, error mapping, and dependency lifecycle. Do not present that escape hatch as native `rullst-ai` streaming. `ToolRegistry` stores local tools but is not wired to provider function calling. Its [guarded execution API](https://rullst.github.io/Rullst/book/ai-tool-security.html) requires an exact allowlist, principal authorization, closed JSON validation, size/call limits and an audit sink. `Destructive` and `Financial` tools additionally require a one-use human approval bound to the exact JSON payload. Treat model output as untrusted input; the application still authenticates the principal/approver, enforces domain ownership and supplies durable production auditing. ## 7. RAG boundary Rullst supplies prompt construction, an in-memory vector index and the tenant-bound `RagPipeline`, which guards every passage, rejects documents tagged for another tenant and records a minimized audit event (see the [tenant-bound RAG tutorial](https://rullst.github.io/Rullst/book/tutorials/41-tenant-bound-rag.html)). Applications must still enforce document authorization before retrieval, prevent SSRF in any fetcher, bound document and prompt sizes, identify tenant provenance, and avoid sending secrets to a provider. Similarity is a ranking signal, not an access control decision. ## 8. Durable chat memory Enable the dedicated SQL memory feature when the application wants the framework-owned fixed schema rather than generated models: ```toml rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["ai-sql-memory"] } ``` The same adapter supports SQLite, PostgreSQL, MySQL, and MariaDB URLs: ```rust,no_run use rullst::{ ai::{ AiClient, ChatMemoryConfig, ConversationId, SqlChatMemory, StatefulChat, providers::openai::OpenAiProvider, }, security::TenantContext, }; async fn chat_service( database_url: String, api_key: String, ) -> Result< (StatefulChat, TenantContext, ConversationId), Box, > { let memory = SqlChatMemory::connect(database_url, ChatMemoryConfig::default()).await?; memory.prepare_schema().await?; let service = StatefulChat::new( AiClient::new(OpenAiProvider::new(api_key)), memory, ); let tenant = TenantContext::try_new("tenant-42")?; let conversation = ConversationId::try_new("support:case-7")?; service.ensure_conversation(&tenant, &conversation).await?; Ok((service, tenant, conversation)) } ``` `service.send(&tenant, &conversation, text).await` loads only the configured recent message pairs, applies the ordinary AI guardrails, and stores the user and assistant messages in one transaction. If another process committed from the same observed revision, the slower `StatefulChat` call receives `StatefulChatError::Memory(ChatMemoryError::RevisionConflict)`. Decide explicitly whether the UI asks the user to retry; the library will not repeat a potentially billable provider call. The table stores raw message text. Production code must authenticate conversation ownership inside the selected tenant, decide encryption and key management, implement retention/erasure and backup policy, audit provider use without logging secrets, and manage schema changes through its release process. Tenant and conversation IDs are case-sensitive on every backend. MySQL/MariaDB tables created by an earlier release keep their case-insensitive default collation, because `prepare_schema` never alters an existing table; they fail closed for IDs that differ only by case until you apply the [documented migration](https://github.com/Rullst/Rullst/tree/main/rullst-ai#upgrading-mysqlmariadb-chat-memory-tables). Use `cargo rullst make:chat-session` instead when you need application-owned models/migrations or the Turso-primary profile. --- # Rullst AI Architecture & Local Models Source: https://rullst.github.io/Rullst/book/7-ai-token-economy-and-local-llms.html # AI-friendly architecture and local model endpoints Rullst favors explicit types, conventional file locations, bounded macros and generated `.llms.txt` context. These choices can make a project easier for a human or coding assistant to navigate, but the repository has no controlled evidence for a universal token-saving percentage. Prompt size depends on the task, tool, repository state and model. ## What “AI-native” means here - `routes!` and `html!` provide recognizable syntax boundaries. - Public APIs prefer concrete types and static dispatch where practical. - `cargo rullst make:*` generates conventional, inspectable source files. - `cargo rullst generate:ai-context` records a compact structural summary. - External provider integrations have deterministic offline behavior for empty or `mock_*` credentials. This does not guarantee that an assistant understands the application, chooses the right edit, uses fewer tokens or produces secure code. Keep source review, tests and application threat models authoritative. The [AI maintainability and project-building roadmap](https://rullst.github.io/Rullst/book/ai-maintainability-roadmap.html) records the measurable v13 work needed to improve generated agent instructions, bounded task-oriented context and reproducible model evaluations. Those planned evaluation profiles are not current compatibility guarantees. ## Ollama through `AiClient::auto` The high-level client recognizes `OLLAMA_HOST` and `OLLAMA_MODEL`: ```bash export OLLAMA_HOST="http://127.0.0.1:11434" export OLLAMA_MODEL="llama3" ``` `OLLAMA_HOST` is read the way Ollama reads it, so the scheme-less `127.0.0.1:11434` form that `ollama serve` uses also works: a value without `http://` or `https://` means `http`, and port 11434 unless it names one. ```rust use rullst_ai::ai::AiClient; # async fn example() -> Result<(), rullst_ai::ai::AiError> { let client = AiClient::auto()?; let response = client.prompt("Summarize this bounded input").await?; println!("{response}"); # Ok(()) # } ``` `AiClient::auto()` also recognizes the built-in cloud-provider API-key variables. With no configured provider it selects a deterministic offline mock; that fallback is not a live model. ## OpenAI-compatible endpoints An OpenAI-compatible server is configured explicitly rather than inferred from an arbitrary environment variable: ```rust use rullst_ai::ai::{ AiClient, providers::openai_compatible::{ OpenAiCompatibleCapabilities, OpenAiCompatibleProvider, }, }; let provider = OpenAiCompatibleProvider::try_local( "http://127.0.0.1:1234/v1", "configured-model-name", )? .with_capabilities(OpenAiCompatibleCapabilities::chat_only()); let client = AiClient::new(provider); # Ok::<(), rullst_ai::ai::AiError>(()) ``` The same adapter can be used with local runtimes such as llama.cpp server, LocalAI, LM Studio, or vLLM **when the exact installed version and configuration expose the request shapes declared in Rullst**. Those product names are examples, not compatibility certification; check the runtime's API and model documentation. A server with a different protocol implements the public `AiProvider` contract instead. Compatibility must be tested for the methods the application uses. An endpoint may implement chat while differing on embeddings, vision, JSON Schema, errors or streaming. Optional request shapes are disabled until explicitly declared. Use `try_local_with_bearer` for an authenticated loopback server and `try_cloud` for HTTPS/Bearer cloud endpoints. Consult the [provider capability matrix](https://rullst.github.io/Rullst/book/ai-provider-capabilities.html). ## Measuring token usage (v13) The v13 prerelease `rullst-ai` reports the token counts a provider returns instead of estimating them. `ChatBuilder::send_with_usage` (or `AiProvider::chat_with_usage`) returns a `ChatCompletion` whose optional `TokenUsage` carries input, output, total and cached-input counts, and `StreamSummary::usage()` returns the counts reported at the end of a stream: ```rust,no_run use rullst_ai::ai::AiClient; # async fn example() -> Result<(), rullst_ai::ai::AiError> { let client = AiClient::auto()?; let completion = client .chat() .user("Summarize this bounded input") .send_with_usage() .await?; if let Some(usage) = completion.usage() { println!("input: {:?}, output: {:?}", usage.input_tokens(), usage.output_tokens()); } # Ok(()) # } ``` A count the provider does not send stays `None`, offline mocks report none, and Rullst ships no price table. The [capability matrix](https://rullst.github.io/Rullst/book/ai-provider-capabilities.html#token-usage-v13) lists the response fields read for each provider. ## Privacy boundary Using a loopback endpoint can avoid sending model requests to a cloud provider, but it does not prove an air gap or zero leakage. The model runtime, host network, proxy variables, logs, tracing, crash dumps and application code still determine the real data path. The compatible adapter ignores ambient proxies and redirects, but that is only one transport boundary. The built-in prompt and PII checks are bounded heuristics, not authorization or a complete data-loss-prevention guarantee. --- # Redis Architecture, Caching & Distributed Queues Source: https://rullst.github.io/Rullst/book/redis-guide.html # Redis, Local Cache & Queue Drivers Rullst keeps cache and queue backends explicit. Enabling a Cargo feature only compiles the adapter; it does not inspect `REDIS_URL`, switch drivers, or silently fall back when Redis is unavailable. ## Cache choices `Cache::memory()` uses a process-local `DashMap`. Values disappear on restart and are not shared between replicas. A TTL too large for the monotonic clock to represent, such as `u64::MAX`, is stored as "never expires" instead of panicking: ```rust use rullst_core::cache::{Cache, CacheError}; use std::sync::Arc; async fn load_profile(cache: &Cache) -> Result, CacheError> { cache .remember("profile:42", 300, || async { Ok("serialized profile".to_string()) }) .await } # async fn read_profile() -> Result<(), CacheError> { let cache = Cache::memory(); let profile = load_profile(&cache).await?; # let _ = profile; # Ok(()) # } ``` For a shared Redis cache, enable `cache-redis` (or the umbrella `redis` feature) and construct the adapter explicitly: ```toml [dependencies] rullst-core = { version = "=13.0.0-alpha.4", features = ["cache-redis"] } ``` ```rust,no_run use rullst_core::cache::Cache; async fn cache_featured_catalog() -> Result<(), Box> { let redis_url = std::env::var("REDIS_URL")?; let cache = Cache::redis(redis_url)?; cache.put("catalog:featured", "[...]", Some(600)).await?; Ok(()) } ``` A zero TTL removes the key, and a TTL beyond Redis's expiry range is stored without expiry, as in the memory driver. Constructing the driver validates the Redis URL but does not establish a connection. The first operation opens one multiplexed async connection that later operations share; it is not reopened per command. When that connection breaks, the failing operation returns a typed `CacheError` and the next one reconnects. Operations also return `CacheError` if Redis is unavailable. Choose an application-specific policy: fail startup, retry with bounds, or explicitly select `Cache::memory()` for a documented single-instance development mode. The built-in Redis cache prefixes keys with `rullst:cache:`. `flush()` scans and unlinks keys under that prefix; use dedicated credentials/database boundaries when multiple applications share a Redis service. ## ORM `.remember(...)` queries The ORM has a separate opt-in query-cache contract behind its `redis` feature: ```toml [dependencies] rullst-orm = { version = "=13.0.0-alpha.4", features = ["redis"] } ``` The generated `.remember(...)`, cache invalidation, `orm:events:*` publications and Redis hash helpers follow this ORM feature (or the facade's `redis` / `orm-redis`). From 13.0 the application does not declare a `redis` feature of its own; earlier macro output checked the application's features instead. ```rust,no_run use rullst_orm::{FromRow, Orm}; #[derive(Debug, Clone, FromRow, Orm)] #[orm(table = "users")] struct User { id: i32, active: bool, } async fn load_recent_users() -> Result<(), Box> { let redis_url = std::env::var("REDIS_URL")?; Orm::init_redis_with_namespace(&redis_url, "academy-production").await?; let recent = User::query() .where_eq("active", true) .remember(30) .get() .await?; let _ = recent; Ok(()) } ``` Generated `save_to_redis`/`get_from_redis`/`increment_redis_field` model hashes use the same namespace, and tenant models bind the active tenant into their key and require `with_tenant(...)`. Hashes that 12.0 and 12.1 wrote under `orm::` are handled as described in [Model hashes stored by 12.0 and 12.1](https://rullst.github.io/Rullst/book/redis-guide.html#model-hashes-stored-by-120-and-121). Use a stable, unique namespace for every application that shares a Redis database. Query keys bind that namespace, the table, generated SQL and typed bindings and, for a model with a `tenant_column`, an opaque digest of the active tenant; other models share one global entry. They do not expose raw tenant identifiers. Entries live under `rullst:orm:cache:v4::` and model hashes under `rullst:orm:hash:v1::`. Cache entries written by earlier versions are never read again and expire through their TTL. The older `Orm::init_redis(url)` API remains available and uses `default`; only use it with a dedicated Redis database. The failure and consistency rules are explicit: - `remember(0)` is rejected. - Missing Redis initialization is a configuration error for a remembered query outside a transaction. - Redis command failures or corrupt JSON fall back to the authoritative database; a successful read is returned even if cache population fails. - Explicit and task-scoped ORM transactions always bypass query cache. - Generated model saves/deletes invalidate the table's global entries and, for a tenant-scoped model, those of the tenant active at the write, only after commit; rollback keeps existing cache entries. Each write follows the table's key index (at most 10,000 live keys), never a keyspace `SCAN`. Raw SQL, bulk builders and writes from another process cannot be inferred. Keep defensive TTLs and do not cache authorization or reads that require a stronger distributed consistency contract. The Core `Cache` facade and ORM query cache use different keyspaces and APIs; initializing one does not initialize the other. ### Model hashes stored by 12.0 and 12.1 Releases up to 12.1 stored every generated model hash under the shared key `orm:
    :`. For a model without a tenant scope, `get_from_redis` reads that key while the namespaced hash is missing, and the next `save_to_redis` or `increment_redis_field` moves it to the namespaced key in one Redis script (every field is kept; an existing namespaced hash wins and the stale legacy hash is removed). Nothing has to be run. Applications that share one Redis database also shared these keys, so the first one to write a hash takes it over. A 12.x hash of a model with `#[orm(encrypted)]` fields holds them in plaintext, so reading it fails closed until `save_to_redis()` rewrites it. Tenant models never read, move or delete the 12.x key, which every tenant shared, so until migrated `get_from_redis` returns `None` and `increment_redis_field` starts from zero. Migrate them once after deploying 13: 1. List the keys of each tenant model table: `redis-cli --scan --pattern 'orm:
    :*'`. 2. Read each hash with `HGETALL`; every value is the JSON of one field. 3. Check its tenant column against the tenant that owns row `` in the database, and skip mismatches: another tenant may have overwritten it. 4. Decode the hash into the model (for example with `serde_json` from the parsed values), or reload the row when the hash only cached it, and call `with_tenant(tenant, model.save_to_redis())`. 5. Remove the 12.x key with `UNLINK`. ## Queue choices Rullst provides explicit SQLite and Redis queue constructors: ```toml [dependencies] rullst-core = { version = "=13.0.0-alpha.4", features = ["queue-sqlite"] } serde_json = "1" ``` ```rust,no_run use rullst_core::queue::Queue; use serde_json::json; async fn enqueue_receipt() -> Result<(), Box> { let queue = Queue::sqlite("sqlite://jobs.sqlite?mode=rwc").await?; let job_id = queue .dispatch("send_receipt", json!({ "invoice_id": 42 })) .await?; println!("queued {job_id}"); Ok(()) } ``` With `queue-redis`, construct `Queue::redis(redis_url)` instead. The Redis driver uses atomic Lua transitions for pending, processing, failed, and dead-letter state. Failed jobs (with their payloads) and dead letters are each retained up to 10,000 entries; recording one more evicts the oldest in the same script. `RedisDriver::try_with_failure_retention(failed_jobs, dead_letters)` accepts 1–100,000 for each (pass the configured driver to `Queue::custom`). Failed jobs recorded before this bound was introduced (for example by 12.x workers) are not indexed, so they are neither counted, listed nor evicted; `retry_failed_job` still finds them by ID and `purge_failed_jobs` removes them. `list_all_jobs` returns at most 1,000 rows, failures and dead letters first, and `list_job_previews` (new in v13) returns the same rows with each payload and error cut to a byte budget inside one Lua script; `retry_failed_job` moves a failed job back to the pending list with its attempt counter intact; `purge_failed_jobs` deletes every failed job and dead letter. Like the cache, each driver shares one lazily opened multiplexed connection and reconnects after a failed operation. Production validation must still cover Redis persistence, eviction policy, credentials/TLS, failover, monitoring, and worker recovery in the target topology. There is no automatic interchange between the SQLite and Redis queues: they store independent state. Switching a live deployment requires an explicit drain/migration plan. ## Real-time boundary Core's current WebSocket broadcast/presence helpers are process-local. They release channels without subscribers and empty presence rooms, so the registry tracks live rooms rather than every name ever used. Redis Pub/Sub, Kafka, and RabbitMQ real-time transports remain roadmap work; do not describe the cache or queue adapter as cross-instance real-time sync. The v13 [Redis Streams messaging](https://rullst.github.io/Rullst/book/redis-messaging.html) candidate in `rullst-messaging` is at-least-once brokered delivery, not WebSocket fan-out. ## Deployment checklist - Choose the backend in application configuration and make fallback policy explicit. - Never commit Redis credentials; prefer TLS and least-privilege network access. - Namespace application/tenant keys above the built-in driver prefix where isolation is required. `TenantCache` supplies validated tenant namespaces as `tenants::`, where `%` and `:` in the tenant segment are written as `%25` and `%3A` so a key containing `:` cannot reach another tenant. - Test disconnects, timeouts, retries, eviction, restart, and worker recovery. - Benchmark the deployed service. Rullst does not claim universal cache latency, memory usage, or infrastructure cost. --- # Threat Radar & SOC Master Guide Source: https://rullst.github.io/Rullst/book/threat-radar-soc-guide.html # Threat Radar and security telemetry > **Vision preserved:** external intelligence, verified audit feeds, durable > telemetry, and SOC ambitions remain itemized with an implementation opinion in > the [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html#security-authentication-studio-and-nexus). Rullst exposes development dashboards for security events in Studio and Nexus. They are observability surfaces, not a managed SOC, a penetration test, or proof of compliance. The current `dispatch_siem_alert` compatibility facade records a bounded local event; it does not transmit or acknowledge delivery to an external SIEM. The dashboards therefore label this metric as local SIEM-candidate alerts. ## Surfaces and access - Studio mounts its security view under `/studio/security`. - Nexus exposes its administrative security view only inside the authenticated, authorized Nexus router. Both surfaces contain sensitive operational information. Bind Studio to a trusted interface and protect any deployed administrative route with TLS, authentication, role checks, and rate limiting. ## What the counters mean The dashboards read counters and bounded recent events from the in-process `SecurityStore`. A value increments only when the corresponding middleware or helper is installed and records an event. A zero means “nothing recorded by this process”, not “the application is proven attack-free”. Restarting a process may reset in-memory state unless the application exports it to durable telemetry. Typical sources include: - honeypot route hits and active TTL-limited bans; - sanitizer, DLP, CSRF/CSWSH, RBAC, rate-limit, and login-jail events; - prompt-injection filtering and PII masking events; - secure-header applications and timing-guard executions. Client identity comes from the trusted peer by default. Forwarded headers are usable only when the application has explicitly configured a trusted proxy boundary, such as Core's `Server::trusted_proxies` listing only the proxy networks. ## Audit-chain status `AuditChain` signs canonical, length-delimited records with HMAC and can verify record integrity and sequence continuity. It detects modifications only when the key is protected and the complete sequence is retained. A writer that restarts must resume from the newest persisted record with `AuditChain::try_resume` (new in v13); a fresh chain restarts at genesis and the retained trail no longer verifies as one sequence. One writer must own each persisted chain. The dashboard must display `Unavailable` until an audit source and continuity verifier are actually connected. It must never infer “verified” merely because an event was emitted. An HMAC chain is tamper-evident; it cannot prevent deletion, key compromise, or loss of the entire log. ## Security controls and limits | Control | What it provides | Important limit | | --- | --- | --- | | RASP/WAF inspection | Bounded heuristics for common malicious request patterns. | It is not a complete parser or substitute for parameterization and domain validation. | | Secure headers | A strict nonce-based CSP/header baseline. | The deployed page, proxy, browser, and policy determine scanner results; no A+ grade is guaranteed. | | Honeypots | Exact synthetic trap paths and temporary bans of direct requests. A load a page initiated (fetch metadata other than `Sec-Fetch-Site: none`, or `Origin`/`Referer` without it) is refused and recorded without a ban. | They do not identify every scanner or replace edge rate limiting. A scanner can avoid the ban by sending those headers, and a browser that sends neither fetch metadata nor a referrer cannot be told apart from a scanner. | | Login guard | Shared failure tracking, delay, cleanup, and jail policy. | Account and recovery policy remain application responsibilities. | | DLP and PII filters | Supported textual-response masking with content-type and size safeguards. | Binary, encoded, streaming, and unsupported responses must follow explicit fail-open/fail-closed policy. | | AI guardrails | Prompt checks and masking in the high-level AI client. | Heuristics cannot guarantee that every adversarial prompt or sensitive value is detected. | ## Local verification Use a test-only application instance and assert both the HTTP response and the recorded event. Do not probe a third-party or production target without written authorization. Recommended negative tests include: - exact honeypot paths versus innocent paths containing similar substrings; - spoofed forwarding headers from untrusted peers; - ban expiry and bounded-cardinality behavior; - valid, invalid, reordered, and truncated audit sequences; - JSON, HTML, binary, compressed, SSE, streaming, and oversized DLP responses; - CSP nonces that match the rendered response and differ between requests; - authorization checks for every Nexus CRUD and batch operation. ## Production checklist - Mount every security layer required by the application; availability in a crate does not install it automatically. - Configure trusted proxies explicitly and retain the direct peer address. - Export logs and metrics to durable, access-controlled storage. - Protect audit keys separately from audit records and rotate them with a documented verification procedure. - Test application-specific CSP, CSRF exceptions, tenant boundaries, and authorization rules. - Keep Studio private and verify Nexus authentication, TLS, rate limiting, and admin-role enforcement end to end. --- # Telemetry & Distributed Tracing Source: https://rullst.github.io/Rullst/book/telemetry-guide.html # Rullst Telemetry, Spans & Process Observability 📡 Rullst exposes three related but separate observability surfaces: - `RadarSnapshot` samples supported process and Tokio runtime data; - `radar_metrics_router()` exposes those samples in Prometheus text format; - `SpanCollector` is a bounded, process-local buffer for spans that application or framework code records explicitly. The optional `telemetry` Cargo feature also installs an OpenTelemetry tracing layer. None of these components proves a performance target or replaces a durable production observability backend. Rullst 13 (since 13.0.0-alpha.1) adds an explicit, minimized cross-process profile and repairs legacy OTLP transport initialization. See [distributed operation tracing](https://rullst.github.io/Rullst/book/distributed-tracing.html) for parent trust, approved labels, owned shutdown, actual collector tests and admission status. ## Process and Tokio observations `RadarSnapshot::collect_async()` measures one scheduler yield and samples the probes available on the current platform: ```rust use rullst_core::radar::RadarSnapshot; # async fn inspect_process() { let snapshot = RadarSnapshot::collect_async().await; println!("uptime: {}s", snapshot.uptime_seconds); println!("rss: {:?} MB", snapshot.memory_rss_mb); println!("cpu: {:?}%", snapshot.cpu_usage_percent); println!("tokio tasks: {:?}", snapshot.active_tokio_tasks); println!("yield observation: {:?} us", snapshot.tokio_latency_micros); # } ``` The option-valued fields are deliberately `None` when a real probe is not available. Linux and Windows provide the current RSS/CPU implementations; active-task data requires a Tokio runtime. A yield observation is not a complete event-loop latency distribution. ## Prometheus endpoint Mount the metrics router explicitly: ```rust use axum::Router; use rullst_core::radar::radar_metrics_router; let app = Router::new().merge(radar_metrics_router()); // GET /metrics ``` Only available metrics are emitted. The exporter formats a point-in-time local snapshot; authentication, network exposure, scraping, retention, dashboards, alerts, and multi-instance aggregation belong to the deployment. ## Bounded local span collector The global collector holds at most 500 `TraceSpan` records in memory. Recording is explicit; merely constructing a server does not instrument every HTTP, SQL, AI, mail, or security operation. ```rust use rullst_core::telemetry_spans::{TraceSpan, global_span_collector}; use std::time::{Instant, SystemTime, UNIX_EPOCH}; let started = Instant::now(); // Run the operation being observed. let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_secs()) .unwrap_or_default(); global_span_collector().record(TraceSpan { name: "catalog.refresh".to_string(), kind: "job".to_string(), duration_us: u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX), timestamp, }); ``` Studio's `/studio/traces` and `/studio/radar` pages display the records that are actually present in this process. The buffer is not distributed, persistent, or a parent/child tracing model. ## OpenTelemetry export The legacy initializer below uses the OTLP transport repaired in v13. Prefer the owned profile linked above when you need explicit metadata policy and flush/shutdown. Enable the feature with the exact prerelease version (Cargo never selects a prerelease on its own): ```toml [dependencies] rullst-core = { version = "=13.0.0-alpha.4", features = ["telemetry"] } ``` ```env OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://127.0.0.1:4318/v1/traces RUST_LOG=info ``` Then initialize the tracing subscriber once at startup: ```rust # fn initialize() -> Result<(), Box> { rullst_core::telemetry::init_telemetry()?; # Ok(()) # } ``` `Server::run` also attempts this initialization, but an application that needs to fail closed on telemetry configuration should initialize it explicitly and handle the returned error before starting the server. The current exporter uses OTLP over HTTP and the service resource name `rullst-app`. The generic `OTEL_EXPORTER_OTLP_ENDPOINT` instead denotes a base URL to which the initializer appends `/v1/traces`; do not put a gRPC endpoint in either value. Without `RUST_LOG` (or with one that does not parse), the subscriber that `Server::run` and `init_telemetry` install uses `info,sqlx=warn`: SQLx's own connection and statement events, such as `acquired connection`, appear only from WARN up. To see them again, set `RUST_LOG=info,sqlx=info` (or `sqlx=debug`); any `RUST_LOG` replaces the default as a whole. ### Health probes in the access log `Server` prints one `[HTTP] GET /orders -> 200 (1.20 ms)` line per request. In staging and production it skips `/health` and `/ready` requests that answer below 400, so orchestrator probes every few seconds do not fill the log; a failing probe (for example `/ready -> 503` while draining) is still printed. Set `RULLST_LOG_HEALTH_PROBES=true` in the process environment or `.env` to print every probe again, or `false` to skip successful probes in development too. `RedactPersonalDataLayer` detects a small list of sensitive *field names* and emits a warning. It cannot rewrite a tracing event already observed by another layer, so secrets must be removed or redacted at the call site. ## Development dashboard endpoint `cargo rullst dash` shows live request, latency, error, ORM and queue figures from `GET /_rullst/dev-telemetry` (v13). `Server` mounts this endpoint next to the development reload routes, so it exists only when all of these hold: - a debug build (`debug_assertions`); - the Development environment (`RULLST_ENV`, legacy `APP_ENV` or `[app].env`; see [Tutorial 09](https://rullst.github.io/Rullst/book/tutorials/09-environment-config.html)); - a valid `RULLST_DEV_GENERATION`, which the `cargo rullst dev`/`dash` supervisor sets for the process it starts. Release builds, Staging, Production and an application started with `cargo run` never mount it. The endpoint answers only a direct loopback peer that addresses the server with a loopback `Host` (`localhost`, `127.0.0.0/8` or `[::1]`) and, when present, a loopback `Origin`, over HTTP/1.1 or newer and without a forwarding header (`Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Forwarded-Server`, `X-Real-IP`, `Via`, `CF-Connecting-IP` or `True-Client-IP`); any other request receives an empty `404`. This rejects other machines, clients resolved through trusted proxies, DNS-rebinding pages and the common same-host reverse proxies and tunnels (Apache `mod_proxy`, Caddy, Traefik, ngrok, cloudflared and nginx's default HTTP/1.0 upstream), which connect from loopback and may rewrite `Host` to a loopback address. A same-host proxy that speaks HTTP/1.1, rewrites `Host` and adds none of these headers is indistinguishable from a local client, so do not publish a development server through one. This is a local development boundary, not authentication. Responses carry `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`. Like the reload poll, the dashboard's poll bypasses the rate limiter and Traffic Shield and is neither access-logged nor counted. The `rullst.dev-telemetry.v1` document contains: - `http`: request, 4xx and 5xx counters since the process started and the newest 64 requests (sequence number, method, path without query string, status, duration in microseconds). They are recorded by the outermost layer, so a handler panic that the development error console answers with `500` and the responses of the security baseline (for example a CSRF `403`), lifecycle admission, the rate limiter (`429`) and Traffic Shield are counted too. Like the access log, it skips the development polls and the files served from the framework's `/static` directory; - `database`: ORM operation counters and the newest 16 operations that took at least 100 ms, with their static model, table and operation labels, or `unavailable` with `subscriber_not_installed` or `orm_spans_filtered`. An observed database also reports possible N+1 queries (v13): `repeated_threshold` (3), `repeated_queries_total` and the newest 16 `recent_repeated` findings, each one ORM operation fingerprint that a single request ran at least that many times, with the request's method and matched route template (or its path when no route matched); - `queue`: the pending count of the queue passed to `Server::with_dev_queue`, `not_configured`, or `unavailable` with `timeout` (250 ms) or `driver_error`. ```json { "schema": "rullst.dev-telemetry.v1", "generation": "0123456789abcdef0123456789abcdef", "uptime_ms": 5120, "http": { "requests_total": 2, "client_errors_total": 0, "server_errors_total": 1, "recent": [ {"seq": 1, "method": "GET", "path": "/", "status": 200, "duration_us": 912}, {"seq": 2, "method": "POST", "path": "/orders", "status": 500, "duration_us": 48210} ] }, "database": { "state": "observed", "queries_total": 3, "slow_queries_total": 1, "slow_threshold_ms": 100, "recent_slow": [ {"seq": 1, "operation": "select_many", "model": "Order", "table": "orders", "duration_us": 152004} ], "repeated_threshold": 3, "repeated_queries_total": 1, "recent_repeated": [ {"seq": 1, "method": "GET", "route": "/orders/{id}", "fingerprint": "LineItem.find (line_items)", "occurrences": 12} ] }, "queue": {"state": "observed", "pending": 4} } ``` Request and response bodies, headers, cookies, query strings, SQL text, bindings and error messages are never recorded. Paths appear as the access log already prints them, so a path segment that carries a secret is visible here too. Recording starts only after the endpoint is mounted; until then each request costs one atomic load. Methods are cut to 16 bytes, paths to 256 bytes and labels to 64 bytes, and control characters are replaced. ORM figures come from the existing secret-free `rullst.orm.query` spans. `telemetry::init_telemetry` (which `Server::run` calls) adds a passive layer that times the outermost such span of each ORM operation, from creation until it closes. Since 13.0.0-alpha.3 the layer is installed in release builds too, for the [production monitoring endpoint](https://rullst.github.io/Rullst/book/production-monitoring.html); outside a recording scope it only compares the span's name; nested operations such as eager loads belong to their outer operation. The span of a `chunk`/`chunk_by_id` traversal also covers the application's handler, so it is neither counted nor treated as enclosing: each page it fetches and each operation the handler runs is counted on its own. An application that installs its own global subscriber first, or a `RUST_LOG` that disables `rullst_orm` INFO spans, receives the `unavailable` state instead of misleading zeros. Statements executed directly through SQLx are not observed. N+1 findings use the shared rule in `rullst_core::query_patterns`, which Studio's trace view also applies: the same fingerprint at least `N_PLUS_ONE_THRESHOLD` (3) times within one request. The recording layer runs each request in a task-local scope; an outermost ORM operation created on that task adds its fingerprint (`Model.operation (table)`, built from the static span labels, so no SQL text or literal value is involved). Operations without a model or table label, such as `Orm::raw` statements, are not fingerprinted because different statements share one label, and operations started on another task (for example inside `tokio::spawn`) are not attributed to the request. Both cases can only hide a repetition, never invent one. A finding is a heuristic: repetition can be intentional. Report a queue's pending count, read with a 250 ms limit on each poll: ```rust,no_run use rullst_core::{Queue, Server, routes, routing::get}; use std::sync::Arc; # async fn run(queue: Queue) -> Result<(), Box> { let queue = Arc::new(queue); // also shared with the application's workers Server::new(routes![get("/" => || async { "OK" })]) .with_dev_queue(queue.clone()) .run(3000) .await?; # Ok(()) # } ``` The setting has no effect outside a supervised debug Development process. ## Studio boundaries - Radar cards poll the local `/api/radar` endpoint and display `Unavailable` instead of fabricated values. - Local span pages reflect only the in-memory collector in the Studio process. - Studio binds to loopback by default and has no built-in shared-environment password mode. Do not expose it publicly without an authenticated boundary. - Measure collector/export overhead against the real application workload and release build; Rullst publishes no universal latency or memory number. --- # Production Monitoring Endpoint Source: https://rullst.github.io/Rullst/book/production-monitoring.html # Production monitoring endpoint Rullst Studio's local pages are a development tool: they run only in debug builds, on `127.0.0.1:5555`, and read the local process. To watch an application in staging or production, Rullst 13 (since 13.0.0-alpha.3) adds a read-only monitoring endpoint to the application itself: ```text GET /_rullst/monitor/v1 ``` It answers a JSON document, versioned as `rullst.monitor.v1`, with request rates and latency per route pattern, status classes, queue depth and worker activity, possible N+1 queries and the process radar. It never contains database rows, request data or configuration. It is phase 1 of the [Studio production monitor plan](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#delivered-in-1300-alpha3); phase 2 is the [standalone Studio](https://rullst.github.io/Rullst/book/production-monitoring.html#watch-your-projects-from-studio) on your machine, which reads this endpoint for several projects. Any HTTPS client (curl, a script, an uptime tool) can read it too. The endpoint is **off by default**. The threat model is [`TM-STUDIO-2`](https://rullst.github.io/Rullst/book/threat-models.html#tm-studio-2--production-monitoring-endpoint). ## Enable it Two things are needed: the application mounts the monitor, and the operator gives it a credential. ```rust,no_run use rullst::Server; use rullst::monitor::Monitor; use rullst::routes; use rullst::routing::get; #[tokio::main] async fn main() -> Result<(), Box> { Server::new(routes![get("/" => || async { "OK" })]) .with_monitor(Monitor::new().with_application( env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), )) .run(3000) .await?; Ok(()) } ``` Then generate a token of at least 32 random bytes and set it where the server reads its configuration, the process environment or `.env`: ```bash openssl rand -base64 32 # or: openssl rand -hex 32 ``` ```env RULLST_MONITOR_TOKENS= ``` When the server starts it validates every token: fewer than 43 base64/base64url or 64 hexadecimal characters, other characters, fewer than ten distinct characters or a placeholder such as `changeme` stop start-up with a configuration error that names the token's position, never its value. Without any token and without an application authorizer, `with_monitor` mounts nothing: `/_rullst/monitor/v1` answers `404` and requests are handled exactly as without the monitor. So the code can stay in place and the operator decides per deployment. `Monitor::with_token` adds a token from your own secret store, in addition to the environment's. Projects generated by `cargo rullst new` with the Blank, API or SaaS starter (and the LMS example) already carry these lines in `src/main.rs`, commented out in the server chain, and list `# RULLST_MONITOR_TOKENS=` in `.env` and `.env.example`, also commented out. To enable the monitor, uncomment the `.with_monitor(...)` call, generate a token and set `RULLST_MONITOR_TOKENS` in the deployment's environment (keep real tokens out of `.env.example`). Until then nothing is mounted. Projects generated earlier, and the ERP and Portfolio examples, add the call as shown above. Read it with the token in the `Authorization` header, never in the URL: ```bash curl -fsS -H "Authorization: Bearer $RULLST_MONITOR_TOKEN" \ https://shop.example/_rullst/monitor/v1 ``` ### Use the application's own sign-in An administrator who is signed in to the application can read the endpoint with their session instead of a token, the way Nexus accepts `NexusAuthPolicy::application`. Implement `MonitorAuthorizer`: ```rust,no_run use rullst::http::request::Parts; use rullst::monitor::{Monitor, MonitorAccess, MonitorAuthorizer, MonitorAuthorizerError}; /// Admits requests an outer session layer marked as coming from operators. struct Operators; impl MonitorAuthorizer for Operators { async fn authorize(&self, request: &Parts) -> Result { Ok(match request.headers.get("x-verified-operator") { Some(_) => MonitorAccess::Allow, None => MonitorAccess::SignIn, }) } } let monitor = Monitor::new().with_authorizer(Operators); ``` The authorizer is asked for every request without a valid token. `Allow` serves the document, `SignIn` answers `401`, `Deny` (a signed-in user without the permission) answers `403`, and an error answers `503` and logs the reason. Browsers cannot read the endpoint from another page; see below. ### Report queues ```rust,no_run # use rullst::monitor::Monitor; # use std::sync::Arc; # fn monitor(queue: Arc) -> Monitor { // The same Arc the application's workers use. let monitor = Monitor::new().with_queue("default", queue.clone()); # monitor # } ``` Up to 16 queues, named with 1 to 64 letters, digits, `.`, `_` or `-`. ## Put it behind your proxy or VPN In staging and production the endpoint refuses plain HTTP with `426 Upgrade Required`, before it looks at credentials. A request is accepted when: - a reverse proxy listed in `Server::trusted_proxies` (or `[security] trusted_proxies`) with `trust_forwarded_proto` reported `https`; - the application terminates TLS itself and inserts `MonitorVerifiedTls::from_trusted_tls_termination()` into the request's extensions from its TLS listener; or - `Monitor::allow_loopback_plaintext(true)` is set and the request comes directly from a loopback address, names a loopback `Host` and carries no forwarding header: an SSH tunnel (`ssh -L 9000:127.0.0.1:3000 server`) or a sidecar on the same host. A forwarded header from any other peer, or an absolute `https://` request target, is not evidence. If a token ever crossed a network in plain text, rotate it. Exposure is your choice. The safest setups keep the path off the public internet: allow it only from your VPN (WireGuard, Tailscale) or office ranges in the reverse proxy, for example with nginx: ```nginx location /_rullst/monitor/ { allow 100.64.0.0/10; # Tailscale deny all; proxy_pass http://127.0.0.1:3000; } ``` ## Rotate tokens `RULLST_MONITOR_TOKENS` holds up to eight comma-separated tokens, and any of them is accepted. To rotate without downtime: 1. add the new token: `RULLST_MONITOR_TOKENS=,`, and restart or redeploy (tokens are read at start-up); 2. switch your monitoring clients to the new token; 3. remove the old one and restart again. Give each client (each developer machine, each script) its own token, so you can revoke one by removing it. ## Responses | Status | When | | --- | --- | | `200` | Document served (`Content-Type: application/json`). | | `401` | No credential, or an invalid one: always the same empty answer, with `WWW-Authenticate: Bearer realm="rullst-monitor"`. | | `403` | The request carries an `Origin` header or a `Sec-Fetch-Site` of `cross-site`/`same-site` (a browser page), or the authorizer denied it. | | `404` | The monitor is not mounted. | | `405` | Any method other than `GET`/`HEAD`. | | `426` | Staging or production without TLS evidence. | | `429` | More than 30 requests in a burst (one more each second) from one client, with `Retry-After: 1`; or, after 10 invalid presented tokens, every request from that client until one more attempt is allowed every 90 seconds (`Retry-After: 90`), without evaluating credentials. | | `503` | The application authorizer failed. | Every answer of the mounted endpoint carries `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`; staging and production also add the usual security headers. The endpoint adds no CORS headers, and because it refuses any request with an `Origin`, an application CORS allowlist never releases it to a browser page. Clients are identified per IPv4 address or IPv6 /64, as resolved through trusted proxies: configure them, or every client behind your proxy shares one budget and one lockout. These limits are per process. The server's own rate limiter and Traffic Shield apply to the endpoint like any route, and the endpoint's own requests are not counted in its statistics. ## What it reports ```json { "schema": "rullst.monitor.v1", "generated_at": 1792648800, "application": { "name": "shop", "version": "1.4.0" }, "framework": { "name": "rullst", "version": "13.0.0-alpha.4" }, "uptime_seconds": 86400, "process": { "memory_rss_mb": 48.2, "cpu_usage_percent": 3.5, "active_tokio_tasks": 42, "tokio_latency_micros": 18 }, "http": { "window_seconds": 300, "route_limit": 256, "routes_overflowed": false, "requests_total": 120345, "status_total": { "1xx": 0, "2xx": 118000, "3xx": 1200, "4xx": 1100, "5xx": 45 }, "window": { "covered_seconds": 271, "requests": 1626, "requests_per_second": 6.0, "status": { "1xx": 0, "2xx": 1600, "3xx": 10, "4xx": 15, "5xx": 1 }, "latency_ms": { "p50": 2.5, "p90": 10.0, "p95": 25.0, "p99": 100.0, "max": 412.3 } }, "routes": [ { "method": "GET", "route": "/orders/{id}", "requests_total": 5400, "status_total": { "1xx": 0, "2xx": 5300, "3xx": 0, "4xx": 98, "5xx": 2 }, "window": { "covered_seconds": 271, "requests": 80, "requests_per_second": 0.295, "status": { "1xx": 0, "2xx": 79, "3xx": 0, "4xx": 1, "5xx": 0 }, "latency_ms": { "p50": 5.0, "p90": 10.0, "p95": 10.0, "p99": 25.0, "max": 19.8 } } } ] }, "database": { "state": "observed", "queries_total": 9876, "n_plus_one": { "threshold": 3, "findings_total": 12, "findings": [ { "method": "GET", "route": "/orders/{id}", "fingerprint": "LineItem.find (line_items)", "requests": 12, "max_occurrences": 25, "last_seen": 1792648790 } ] } }, "queues": [ { "name": "default", "pending": { "state": "observed", "count": 3 }, "in_flight": 1, "completed_total": 5000, "failed_total": 4, "retried_total": 2, "stalled_recovered_total": 0 } ] } ``` - **Routes** are keyed by the matched route pattern (`/orders/{id}`) and a normalized method (the standard methods, else `OTHER`); requests that matched no route are counted as `(unmatched)`. At most 256 method and pattern pairs are kept; later ones share `{"method": "*", "route": "(other)"}` and `routes_overflowed` becomes `true`. - **Totals** count since the process started. The **window** covers the last five minutes in one-minute slots; `covered_seconds` says how much of it has elapsed (less right after start-up), and `requests_per_second` is `requests / covered_seconds`, rounded to thousandths. - **Latency percentiles** come from a fixed histogram (bucket bounds from 0.1 ms to 10 s): each value is the upper bound of its bucket, capped at the window's maximum, and `latency_ms` is `null` without requests in the window. Durations run from the outermost framework layer to the response headers. - **Database** counts the outermost ORM operations started by requests and lists repeated operations as the ORM's static `Model.operation (table)` fingerprint, never SQL text or values (at most 64 entries, the least recently seen replaced first). It needs the framework's tracing subscriber (`Server` installs it) and a `RUST_LOG` that keeps `rullst_orm` at `info`; otherwise it reports `{"state": "unavailable", "reason": "subscriber_not_installed"}` or `"orm_spans_filtered"`. - **Queues**: `pending` is read from the queue store (all workers, with a 250 ms limit per queue; `timeout` or `driver_error` otherwise, never the driver's message). The other counters cover this process's workers since start: running handlers, completed, failed (error, panic, timeout, no handler), retried (requeued at shutdown, handed back for lack of a handler, or `Queue::retry_failed_job`) and stalled leases recovered. What it never contains: raw paths, query strings, headers, cookies, client addresses, user identifiers, request or response bodies, SQL text or bound values, error messages, environment variables or configuration values. Route patterns and queue names are chosen by your code, so keep personal data out of them. ## Watch your projects from Studio The standalone Studio runs on your machine, without an application process, and shows the document of each project you add: overview (radar, request rate, error rate, latency), routes, queues and N+1 findings, with a project switcher in the header. It only connects outwards to your projects; nothing new is collected or stored on the servers. ```bash cargo install rullst-studio --version =13.0.0-alpha.4 --features remote --locked rullst-studio # http://127.0.0.1:5556/studio # or, with the CLI installed: cargo rullst studio --remote # --port 7000 for another port ``` Pin the version: a plain `cargo install rullst-studio` selects the end-of-life 12.3.0, which has no `remote` feature. The `remote` feature is not enabled by default: applications that mount Studio's local pages in development do not build its HTTP client, encryption or SQLite history. It listens on `127.0.0.1` only, with the same protections as Studio's local pages: loopback peer and `Host`, same-origin forms, and a per-process form token. Its pages are the remote ones only: the data browser, `.env` viewer, AI and API playgrounds and migration pages do not exist in this mode. ### Add a project On the **Projects** page, enter a name, the project's base URL and one of the tokens of its `RULLST_MONITOR_TOKENS`; give each machine its own token, so you can revoke it alone. Studio reads `_rullst/monitor/v1`. - The base URL must use `https://`, or `http://` to a loopback address such as `http://127.0.0.1:9000` (an SSH tunnel). User information, query strings and fragments are refused. Certificates are always validated. - The token is sent only as `Authorization: Bearer` to that origin. Studio follows no redirect (a redirect is shown as a status), ignores `HTTP_PROXY`/`HTTPS_PROXY`, and reads at most 1 MiB within 5 seconds. Anything else (an unreachable host, `401`, `426`, an invalid document) is a status on the page, never the remote body. - Every value from a project is HTML-escaped: a hostile or compromised application cannot run script in Studio. ### Where your tokens are kept | File | Location (Linux and macOS / Windows) | Contents | | --- | --- | --- | | Project list | `$XDG_CONFIG_HOME/rullst/studio/projects.json` (default `~/.config/…`) / `%APPDATA%\rullst\studio\projects.json` | Names, base URLs, encrypted tokens | | Key | `$XDG_DATA_HOME/rullst/studio/projects.key` (default `~/.local/share/…`) / `%LOCALAPPDATA%\rullst\studio\projects.key` | 32 random bytes | | History | `$XDG_DATA_HOME/rullst/studio/history.sqlite` / `%LOCALAPPDATA%\rullst\studio\history.sqlite` | Aggregates of watched projects and fired alerts | Both are created owner-only (`0600` in `0700` directories on Unix) and never followed through a symlink; Studio warns when either is readable by other users. Each token is encrypted with AES-256-GCM and bound to its project name and base URL: if someone edits the URL in the file, the token no longer decrypts and is never sent to the new address (remove and add the project again). A copy of the project list alone (a dotfile repository, a backup of `~/.config`) reveals no token. Malware running as your user can still read both files; the operating system keychain is not used. ### History and alerts On the **Alerts** page, tick **Watch** for a project: while Studio runs, it reads that project every 60 seconds and keeps, per poll, only aggregates the document already contains: whether it answered, requests per second, window requests, 5xx rate, p95 latency, and the pending and failed totals of its queues. Routes, queue names and other labels are not stored. The **History** page charts them for the last hour, day or week. Samples older than seven days, beyond 10,080 per project (a week at one per minute) and alerts beyond the newest 500 are deleted on every write; removing a project deletes its history. Nothing new is collected on the server. Each project has alert rules, all off by default: | Rule | Fires when | | --- | --- | | Unreachable | the project cannot be read (any status on the overview page) | | 5xx rate | the window's 5xx share reaches the percentage, with at least 20 requests in the window | | p95 latency | the window's p95 reaches the milliseconds | | Pending jobs | the pending jobs of all its queues reach the count | | Queue failures | this many jobs failed between two polls (a server restart, which resets the counters, never fires it) | A rule fires again for the same project only after its cool-down (15 minutes by default, 1 to 1,440). Alerts appear on the Alerts page, with a badge until you acknowledge them. Optionally, each alert is posted once to a webhook you configure (Slack, Teams, a chat bot, your own receiver): ```json {"source": "rullst-studio", "project": "shop", "rule": "p95_latency", "message": "shop: p95 latency 412.3 ms (threshold 250 ms)", "at": 1792648800} ``` The webhook URL is a secret like a token: it is encrypted in the project list and never shown again. It must be `https://` without user information. The post follows no redirect, ignores proxy variables and gives up after five seconds; the outcome (`delivered`, `failed: HTTP 500`, `failed: timeout`…) is recorded on the alert, and it is not retried. Alert text is Studio's own wording with the project name and numbers, never labels from the remote document. Polling stops when Studio stops; for alerts around the clock, run `rullst-studio` on an always-on machine or keep using your existing uptime monitoring. ### Reaching private servers Studio does not open tunnels itself. Use the tool you already trust: - **SSH tunnel:** `ssh -N -L 9000:127.0.0.1:3000 deploy@server`, then add `http://127.0.0.1:9000`. On the server, call `Monitor::allow_loopback_plaintext(true)` so the application accepts the tunnel's plain HTTP from loopback in production. - **WireGuard or Tailscale:** the monitor still requires TLS evidence in production, so put the application's reverse proxy (with HTTPS) on the VPN address, or use `tailscale serve` to publish it over HTTPS, and add that `https://` URL. Restrict `/_rullst/monitor/` to the VPN range in the proxy. ## Limits - Each process reports itself. Behind a load balancer, each replica has its own counters, budgets and lockouts, and a poll reaches one of them; poll replicas individually if you need all of them. - Counters restart with the process. - The development hot-reload server does not mount the monitor. - A panic that aborts the connection is not counted. - The schema is versioned: fields are only added in a new version, served next to `v1` at a new path. ## Overhead Collection runs on every request once the monitor is mounted: it reads the matched route, takes a read lock on the route table, updates that route's counters under its own lock, and gives the request a scope in which ORM operations are counted. Measured on an 8-thread Linux machine in a release build (`cargo test --release -p rullst-core --lib monitor::overhead_tests -- --ignored --nocapture --test-threads=1`, from a clone of the Rullst repository): a trivial Axum route took 0.72 µs per request without the recorder and 1.32 µs with it, so about **0.6 µs per request**; the statistics update alone took about 96 ns, and 70 to 80 ns per call with 4 to 8 threads recording at once. Memory is bounded: about 1 KiB per route pattern (273 KiB for 257), 64 N+1 entries, and the endpoint's per-client limiters, which keep at most 100,000 clients each like Core's `RateLimiter`. --- # Payment Gateways & Financial Infrastructure Source: https://rullst.github.io/Rullst/book/payment-gateways-guide.html # 💳 Payment Gateways & Financial Infrastructure Guide Rullst Capital (`rullst-capital`) provides typed payment, subscription and webhook contracts with two built-in adapters. Unsupported operations return typed errors, and mock credentials select deterministic offline behavior. The adapters share common traits, but they do not all implement every operation. An adapter's presence is not a promise of geographic availability, tax treatment, settlement time, pricing, or regulatory suitability. --- ## 📊 Adapter inventory | Adapter | Status | Rullst contract | | :--- | :--- | :--- | | Stripe | Supported | Owner-bound customer and subscription checkout, bound portal, reconciliation reads, Payment Intents charges, Meter Events, coupons and trials; unsupported methods fail explicitly. | | InfinitePay | Experimental | Deterministic offline checkout, portal and webhook fixtures. Live plan-only checkout and live callbacks return `UnsupportedOperation` until a reviewed contract is validated against a live account. | | Your own adapter | Extension point | Implement `BillingProvider` (and optionally `MeteredBillingProvider`) in the application; see [Writing your own payment provider](https://rullst.github.io/Rullst/book/capital-custom-provider.html). | v13 removed the Paddle, Lemon Squeezy, Polar, Razorpay, Mercado Pago, Alipay, Coinbase Commerce, PicPay and Wise (payout) adapters; see the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html) row "Capital providers and NFS-e removed". Provider pricing and terms change. Check the provider's current official documentation and the concrete trait implementation before selecting an adapter. --- ## Failures, timeouts, and retry ownership Reviewed live methods use a single bounded egress contract: five-second connect and twenty-second whole-request timeouts, no redirects, no ambient proxy variables, and at most one MiB of JSON. Checkout responses additionally require a bounded credential-free HTTPS URL and provider-specific origin/identity checks. Stripe hosted Checkout preserves its documented opaque fragment; other checkout locations must not carry a fragment. These controls do not prove that a provider account, product, price, or operation is accepted live. `CapitalError::Provider` exposes only static provider/operation labels, a `ProviderFailureKind`, optional HTTP status, bounded numeric `Retry-After`, and one of three dispositions: permanent, transient, or rate-limited. It does not retain a raw URL, credential, response body, or `reqwest` diagnostic. Log those structured fields instead of formatting the original request. Rullst deliberately does not retry mutations. A transient classification means only that a later attempt may succeed. Before retrying, the application must prove that the concrete operation forwards the same persisted idempotency key; otherwise reconcile provider state first. Backoff, jitter, attempt budgets, dead-letter handling, and operator alerts remain explicit application policy. --- ## 🔍 Selection model Choose a provider only after checking which trait methods the Rullst adapter implements, the currencies and countries enabled on the actual merchant account, the current provider contract, webhook replay/idempotency requirements, and the application's legal and tax responsibilities. Merchant-of-record status and tax handling are external contractual properties, not guarantees made by Rullst. --- ## 💻 Rust Code Integration Examples ### 1. Choose the implemented operation The [Capital README](https://github.com/Rullst/Rullst/blob/main/rullst-capital/README.md) lists each adapter's current operations. Stripe has a generated durable subscription integration (`cargo rullst make:billing` and the SaaS starter). InfinitePay is experimental and only its offline fixtures succeed. For another gateway, write an application-owned adapter as described in [Writing your own payment provider](https://rullst.github.io/Rullst/book/capital-custom-provider.html). ### 2. Create an owner-bound Stripe checkout ```rust,no_run use rullst_capital::{CapitalError, StripeCheckoutRequest, StripeProvider}; async fn checkout() -> Result<(), CapitalError> { let provider = StripeProvider::new("mock_key", "mock_webhook"); // In the host: authorize this owner and persist the account/mode-scoped // customer binding and immutable attempt before dispatch. let attempt = StripeCheckoutRequest::new( "cus_existing", "price_monthly", "owner_opaque", "attempt_unique", "https://app.example/billing/success", "https://app.example/billing/cancel", )?; let session = provider.create_subscription_checkout(&attempt).await?; let _ = session; // Persist its ID before a 303 handoff; never infer payment. Ok(()) } ``` The account's recurring price allowlist is server-owned. A local user may never supply another customer's ID or choose a raw provider URL. For the complete SQLx/Turso application flow, use the SaaS starter or `cargo rullst make:billing` and follow the generated `BILLING.md`; see the [SaaS billing tutorial](https://rullst.github.io/Rullst/book/tutorials/19-saas-billing-capital.html). ### 3. Cryptographically Verified Webhook Endpoint Signature verification authenticates raw delivery bytes. It does not establish application ownership or settlement. Use the provider's typed verified envelope with the persisted owner/customer/checkout binding; do not grant or revoke access from `WebhookEvent.customer_email`, an event name alone or a browser return. For Stripe, the generated handlers verify signed Checkout/subscription events, read current provider state under a database revision fence and atomically commit the scoped event receipt with subscription state. Exact replays do not repeat changes; conflicting receipts, foreign customers and obsolete attempts are rejected. Unknown provisioning/checkout outcomes require bounded recovery. InfinitePay accepts only explicit `mock_*` webhook fixtures; a real secret returns `UnsupportedOperation` until callback authentication is validated. A custom adapter supplies its own durable orchestration, atomic event processing and reconciliation. Rullst also supplies canonical Axum/Actix webhook middleware for supported normalized events. The production entry points reject empty/`mock_*` secrets. A normalized `WebhookEvent` is a lower-level input, not a complete entitlement decision; preserve raw verified event identity and scope where needed. #### Actix Web adapter Enable `rullst-capital` with `default-features = false, features = ["actix"]`, or enable `rullst/capital-actix` through the umbrella crate, and add `actix-web` as a direct application dependency. An explicit provider-bound state avoids global provider configuration and makes the replay boundary visible: ```rust,no_run use actix_web::{App, HttpMessage, HttpRequest, HttpResponse, HttpServer, middleware, web}; use rullst_capital::{ InMemoryWebhookReplayStore, StripeProvider, WebhookEvent, WebhookMiddlewareState, verify_webhook_actix_with_state, }; use std::sync::Arc; async fn handle_billing_event(request: HttpRequest) -> HttpResponse { let Some(event) = request.extensions().get::().cloned() else { return HttpResponse::InternalServerError().finish(); }; // This example only demonstrates middleware extraction. Before any domain // mutation, bind verified identity and reconcile current state as above. let _ = event; HttpResponse::NoContent().finish() } async fn serve() -> std::io::Result<()> { let provider = Arc::new(StripeProvider::new( "sk_live_from_secret_store", "whsec_from_secret_store", )); let replay = Arc::new(InMemoryWebhookReplayStore::default()); let state = WebhookMiddlewareState::production_with_provider(provider, replay); HttpServer::new(move || { App::new() .app_data(web::Data::new(state.clone())) .wrap(middleware::from_fn(verify_webhook_actix_with_state)) .route("/webhooks/capital", web::post().to(handle_billing_event)) }) .bind(("127.0.0.1", 8080))? .run() .await } ``` The default in-memory replay store is atomic only inside one process. Enable `rullst-capital/webhook-sql` (or umbrella `rullst/capital-webhook-sql`) to share a bounded replay ledger across SQLite, PostgreSQL, MySQL, or MariaDB processes: ```rust,no_run use rullst_capital::{ SqlWebhookReplayStore, StripeProvider, WebhookMiddlewareState, }; use std::{sync::Arc, time::Duration}; async fn webhook_state( database_url: String, ) -> Result { let replay = Arc::new( SqlWebhookReplayStore::connect( database_url, 100_000, Duration::from_secs(24 * 60 * 60), ) .await?, ); replay.prepare_schema().await?; let provider = Arc::new(StripeProvider::new( "sk_live_from_secret_store", "whsec_from_secret_store", )); Ok(WebhookMiddlewareState::production_with_provider( provider, replay, )) } ``` Run equivalent reviewed DDL through deployment migrations instead of relying on request-time setup. Capacity/TTL are immutable for an existing ledger; drift, corruption, storage failure, and a full unexpired ledger fail closed. Only provider-scoped SHA-256 claims are stored, not raw payloads or event IDs. SQL-backed middleware claims the payload before handler dispatch. It prevents cross-process replay but cannot make handler delivery exactly once: a crash can still occur between admission and a business mutation. For an atomic relational path, verify the exact provider payload through its low-level contract, select the provider's stable event ID, and call `check_and_record_event_key_with_transaction` in the same transaction as the domain mutation. Do not also pre-claim that event through SQL middleware. External calls, e-mail, and queues still need an outbox, idempotent consumers, and reconciliation. ### 4. Provider-Specific Metered Usage Use `MeteredBillingProvider` with `StripeMeterEvent`. The Stripe request carries customer, configured event name, positive value, bounded timestamp and an identifier forwarded to the provider and HTTP idempotency header. Stripe's provider identifier has only a rolling uniqueness window, so keep a durable application record of submitted events and make reconciliation idempotent. A custom adapter whose provider has no deduplication key returns `UsageDeduplication::ApplicationOutboxRequired`; claim the event key durably before sending. Protocol fixtures verify request/response shape and bounds; they do not replace live provider-account testing. ### 5. Payment-Bound PDF Invoice Delivery With the umbrella `capital-mail` feature, bind an authoritative invoice to the final charge receipt and prepare a pipeline-validated HTML/PDF message through `rullst::mail::PaidInvoiceDelivery`. Non-final/mock receipts and mismatched recipient, minor-unit total or currency fail before delivery. Persist the stable delivery key under a unique constraint before calling `send`; the bridge is at-least-once and does not infer webhook reconciliation. The complete runnable shape and its outbox boundary are shown in [Tutorial 19](https://rullst.github.io/Rullst/book/tutorials/19-saas-billing-capital.html#4-render-and-deliver-the-invoice-only-after-final-success). ### 6. Adding another gateway Rullst no longer ships adapters for other gateways or payouts. Implement `BillingProvider` for the gateway in application code, declare its webhook verification mode, verify the provider's exact signed bytes inside `handle_webhook`, and mount it behind the same canonical middleware with `WebhookMiddlewareState::production_with_provider`. The [custom provider guide](https://rullst.github.io/Rullst/book/capital-custom-provider.html) walks through a compiling example. Payouts, transfers and fiscal documents are application or separate-product responsibilities. --- ## 🛡️ Security controls and boundaries 1. **Bounded verification:** webhook handlers should bound the body before parsing and reject a missing or malformed signature. Reading and parsing still allocate according to the concrete HTTP stack and payload. 2. **Cryptographic verification:** supported webhook adapters use HMAC or constant-time verification for the exact signed bytes. Each provider's timestamp/replay policy and deployed secret lifecycle still require review. The default replay store is process-local; multi-instance deployments need the opt-in `webhook-sql` ledger or another durable shared idempotency boundary. It holds at most 10,000 proofs for 24 hours each and answers 503 when full rather than evict an unexpired proof; size a store for `verify_webhook_with_state` when one process verifies more than about 10,000 deliveries per day. 3. **Typed parsing:** supported provider responses map into Rust enums and structs without runtime reflection. A typed response does not establish authorization, idempotency, or correctness of the upstream service. --- # Writing Your Own Payment Provider Source: https://rullst.github.io/Rullst/book/capital-custom-provider.html # Writing your own payment provider Rullst Capital ships two built-in adapters: **Stripe** (supported) and **InfinitePay** (experimental until it is validated against a live account). Every other gateway is integrated by the application through the same provider-neutral contracts the built-in adapters use. Version 13 removed the Paddle, Lemon Squeezy, Polar, Razorpay, Mercado Pago, Alipay, Coinbase Commerce, PicPay and Wise adapters; see the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#changes-from-the-published-1210-source) row "Capital providers and NFS-e removed". This guide walks through a complete adapter for a fictitious gateway, **AcmePay**. The code below is included from [`rullst-capital/examples/custom_provider.rs`](https://github.com/Rullst/Rullst/blob/main/rullst-capital/examples/custom_provider.rs), which CI compiles and which runs offline from a clone of the Rullst repository: ```text cargo run -p rullst-capital --example custom_provider ``` In an application, the same types are available through the facade as `rullst::capital::*` with the `capital` feature. `BillingProvider` is an `#[async_trait]` trait: use the `async-trait` crate, as the example does, or the facade's `rullst::async_trait`. The example's HMAC check uses `ring`, `hex` and `subtle`; add equivalent crates to your own `Cargo.toml`. ## What the framework does and what the adapter does | Responsibility | Owner | | :--- | :--- | | Bounding the webhook body (2 MiB), rejecting duplicate Standard Webhooks headers, refusing `mock_*` secrets in production state, replay protection, restoring the exact body and inserting the `WebhookEvent` | `verify_webhook_with_state` (Axum) or `verify_webhook_actix_with_state` (Actix) | | Declaring real or mock verification, authenticating the exact signed bytes, checking freshness, mapping only documented states | Your `BillingProvider` implementation | | Owner binding, plan allowlists, persistence, reconciliation and entitlements | Your application, with the Capital checkout, entitlement and quota contracts | The adapter is trusted server code. A common trait does not mean every gateway supports every operation: an operation the gateway does not offer, or that you have not reviewed, must return `CapitalError::UnsupportedOperation` instead of fabricating success. ## 1. Implement `BillingProvider` ```rust,ignore /// Largest clock difference accepted between AcmePay and this server. const WEBHOOK_TOLERANCE_SECONDS: i64 = 5 * 60; /// Adapter for the fictitious AcmePay gateway. pub struct AcmePayProvider { api_key: String, webhook_secret: String, } impl AcmePayProvider { pub fn new(api_key: impl Into, webhook_secret: impl Into) -> Self { Self { api_key: api_key.into(), webhook_secret: webhook_secret.into(), } } /// Empty or `mock_*` credentials select the deterministic offline fixture. fn is_offline(&self) -> bool { self.api_key.is_empty() || self.api_key.starts_with("mock_") } /// Checks `acme-signature: t=,v1=.")>`. fn verify_signature( &self, payload: &[u8], headers: &HashMap, now: i64, ) -> Result<(), CapitalError> { let header = headers .get("acme-signature") .ok_or_else(|| CapitalError::InvalidSignature("missing acme-signature".into()))?; if self.webhook_verification_mode()? == WebhookVerificationMode::Mock { // Local fixtures still require the configured `mock_*` secret. return match header .as_bytes() .ct_eq(self.webhook_secret.as_bytes()) .into() { true => Ok(()), false => Err(CapitalError::InvalidSignature("mock signature".into())), }; } let mut timestamp = None; let mut signature = None; for part in header.split(',') { match part.split_once('=') { Some(("t", value)) => timestamp = value.parse::().ok(), Some(("v1", value)) => signature = hex::decode(value).ok(), _ => {} } } let (Some(timestamp), Some(signature)) = (timestamp, signature) else { return Err(CapitalError::InvalidSignature( "malformed acme-signature".into(), )); }; if (now - timestamp).abs() > WEBHOOK_TOLERANCE_SECONDS { return Err(CapitalError::StaleWebhook("acmepay timestamp".into())); } let key = hmac::Key::new(hmac::HMAC_SHA256, self.webhook_secret.as_bytes()); let mut signed = format!("{timestamp}.").into_bytes(); signed.extend_from_slice(payload); // `ring::hmac::verify` compares in constant time. hmac::verify(&key, &signed, &signature) .map_err(|_| CapitalError::InvalidSignature("acmepay signature".into())) } } #[async_trait] impl BillingProvider for AcmePayProvider { fn name(&self) -> &'static str { "acmepay" } // Without this override the canonical middleware refuses the provider. fn webhook_verification_mode(&self) -> Result { match self.webhook_secret.trim() { "" => Err(CapitalError::ConfigurationError( "acmepay webhook secret cannot be empty".into(), )), secret if secret.starts_with("mock_") => Ok(WebhookVerificationMode::Mock), _ => Ok(WebhookVerificationMode::Real), } } async fn create_checkout_session( &self, _customer_email: &str, plan_id: &str, _redirect_url: &str, ) -> Result { if plan_id.is_empty() || !plan_id .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'_') { return Err(CapitalError::ConfigurationError("invalid plan ID".into())); } if self.is_offline() { // A reserved `.invalid` host never reaches a real gateway. return Ok(format!("https://mock.acmepay.invalid/checkout/{plan_id}")); } // Call the gateway here, bind the response to the request and return // only a validated HTTPS URL. Never report success you did not verify. Err(CapitalError::UnsupportedOperation( "acmepay live checkout is not implemented in this example".into(), )) } fn handle_webhook( &self, payload: &[u8], headers: &HashMap, ) -> Result { // Authenticate the exact bytes before parsing anything. self.verify_signature(payload, headers, chrono::Utc::now().timestamp())?; let json: serde_json::Value = serde_json::from_slice(payload) .map_err(|_| CapitalError::PayloadParseError("acmepay JSON".into()))?; let text = |field: &str| { json["data"][field] .as_str() .filter(|value| !value.is_empty()) .map(str::to_owned) .ok_or_else(|| CapitalError::PayloadParseError(format!("acmepay {field}"))) }; // Map only documented states; an unknown state is an error, not access. let status = match text("status")?.as_str() { "active" => SubscriptionStatus::Active, "trialing" => SubscriptionStatus::Trialing, "past_due" => SubscriptionStatus::PastDue, "canceled" => SubscriptionStatus::Canceled, _ => return Err(CapitalError::PayloadParseError("acmepay status".into())), }; Ok(WebhookEvent { subscription_id: text("subscription")?, customer_id: text("customer")?, customer_email: text("email")?, plan_id: text("plan")?, status, ends_at: json["data"]["current_period_end"].as_i64(), }) } async fn create_customer_portal( &self, _customer_email: &str, _return_url: &str, ) -> Result { if self.is_offline() { return Ok("https://mock.acmepay.invalid/portal".into()); } Err(CapitalError::UnsupportedOperation("acmepay portal".into())) } async fn cancel_subscription(&self, subscription_id: &str) -> Result<(), CapitalError> { if subscription_id.is_empty() { return Err(CapitalError::SubscriptionError( "empty subscription ID".into(), )); } if self.is_offline() { return Ok(()); } Err(CapitalError::UnsupportedOperation("acmepay cancel".into())) } // Operations the gateway does not offer fail explicitly. async fn pause_subscription(&self, _subscription_id: &str) -> Result<(), CapitalError> { Err(CapitalError::UnsupportedOperation("acmepay pause".into())) } async fn report_usage(&self, _: &str, _: &str, _: u64) -> Result<(), CapitalError> { Err(CapitalError::UnsupportedOperation("acmepay usage".into())) } async fn apply_coupon(&self, _: &str, _: &str) -> Result<(), CapitalError> { Err(CapitalError::UnsupportedOperation("acmepay coupons".into())) } async fn extend_trial(&self, _: &str, _: i64) -> Result<(), CapitalError> { Err(CapitalError::UnsupportedOperation("acmepay trials".into())) } } ``` Points that matter for security and testing: - **Declare the verification mode.** The default `webhook_verification_mode` returns a configuration error, so an adapter that forgets it can never be mounted behind the middleware. An empty secret is a configuration error; a `mock_*` secret selects `WebhookVerificationMode::Mock`. - **Authenticate before parsing.** `handle_webhook` verifies the exact body bytes with the gateway's documented algorithm and a constant-time comparison (`ring::hmac::verify` here), and checks the timestamp window when the protocol signs one. Mock fixtures still require the configured `mock_*` secret. - **Map only documented states.** An unknown status or a missing identity is a `PayloadParseError`, never `Active`. - **Offline fallback.** Empty or `mock_*` API keys return deterministic URLs on reserved `.invalid` hosts, so tests and local sandboxes never contact the gateway. Live paths must bind the provider response to the request before returning it. - **Optional contracts.** Override `BillingProvider::charge` only after forwarding the request's idempotency key and binding amount and currency in the response. Metered billing uses the separate `MeteredBillingProvider` trait; return `UsageDeduplication::ApplicationOutboxRequired` from `UsageReceipt::from_verified_provider_response` when the gateway offers no deduplication key. ## 2. Plug it into checkout and subscriptions ```rust,ignore /// The subscription owner, loaded from authenticated application state. struct Workspace { billing_email: String, subscription_id: Option, } #[async_trait] impl Billable for Workspace { fn email(&self) -> String { self.billing_email.clone() } fn subscription_id(&self) -> Option { self.subscription_id.clone() } } ``` ```rust,ignore let provider = Arc::new(AcmePayProvider::new("mock_api_key", "mock_webhook_secret")); let workspace = Workspace { billing_email: "owner@example.com".into(), subscription_id: Some("sub_42".into()), }; // Checkout through the trait object, exactly as for a built-in adapter. let checkout = provider .create_checkout_session( &workspace.email(), "plan_pro", "https://app.example/billing", ) .await?; assert_eq!(checkout, "https://mock.acmepay.invalid/checkout/plan_pro"); // Subscription operations with static dispatch on the explicit provider. workspace .subscription_with(provider.as_ref())? .cancel() .await?; let paused = workspace .subscription_with(provider.as_ref())? .pause() .await; assert!(matches!(paused, Err(CapitalError::UnsupportedOperation(_)))); // `charge_with` keeps the trait's fail-closed default for direct charges. let charge = workspace .charge_with(provider.as_ref(), 4_990, "BRL", "cus_1", "pm_1", "order_1") .await; assert!(matches!(charge, Err(CapitalError::UnsupportedOperation(_)))); ``` `subscription_with` and `charge_with` dispatch statically to the provider you pass. The compatibility helpers (`Billable::subscribe`, `Billable::billing_portal_url`, `Billable::cancel_subscription`) use the global provider; select it once at start-up with `rullst::capital::try_init_provider(Box::new(provider))`, which reports a second initialization instead of replacing the first provider. Derive the subscription ID and the plan from authenticated application state, and keep a server-owned plan allowlist. A checkout redirect is not proof of payment; grant access from verified, reconciled state, for example with the `entitlements::EntitlementGate`. ## 3. Verify webhooks with the canonical middleware ```rust,ignore async fn billing_webhook(Extension(event): Extension) -> StatusCode { // Only events that passed the provider check and replay claim arrive here. // Persist them against the owner you already know; never trust the email alone. println!( "verified {} event for {}", event.status.as_str(), event.subscription_id ); StatusCode::OK } fn billing_router(provider: Arc) -> Router { let replay_store = Arc::new(InMemoryWebhookReplayStore::default()); // Production uses `WebhookMiddlewareState::production_with_provider`, // which rejects `mock_*` secrets; this offline example opts in to mocks. let state = WebhookMiddlewareState::local_mock_with_provider(provider, replay_store); Router::new() .route("/billing/webhook", post(billing_webhook)) .layer(middleware::from_fn_with_state( state, verify_webhook_with_state, )) } ``` Mount `WebhookMiddlewareState::production_with_provider` in production. It rejects `mock_*` secrets with `MockWebhookNotAllowed`; `local_mock_with_provider` is for local fixtures only. The middleware maps failures to HTTP status codes: 401 for a bad signature or stale timestamp, 409 for a replay, 413 for an oversized body and 503 when the replay store is full or unavailable. The in-memory replay store is process-local. With several processes, use the `webhook-sql` feature: `SqlWebhookReplayStore` shares replay claims, and its `check_and_record_event_key_with_transaction` claims the gateway's stable event ID inside your own database transaction. Body-hash replay protection does not stop a gateway from resending the same event with a different body; prefer the stable event ID when the gateway provides one. ## 4. Test the adapter The example's `main` exercises the contract offline: - a signed mock delivery returns 200 and its identical replay returns 409; - a wrong signature returns 401 and never reaches the handler; - the live HMAC path accepts a fresh signature and rejects a stale timestamp; - unsupported operations and direct charges fail with `UnsupportedOperation`. Add the gateway's published webhook examples as fixtures, and validate the live adapter against a sandbox account before enabling it in production. Tests with mocks establish local contracts, not provider interoperability. --- # Writing Your Own Mail Transport Source: https://rullst.github.io/Rullst/book/mail-custom-transport.html # Writing your own mail transport Rullst Mail ships four delivery transports: **Resend**, **AWS SES**, **SendPulse** and **SMTP**, plus the `log`, `memory` and offline `mock_*` drivers for development and tests. Every other provider is integrated by the application through the same `MailDriver` trait the built-in transports implement. Version 13 removed the SendGrid, Postmark, Mailjet, Mailtrap and Azure Communication Services transports; see the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#changes-from-the-published-1210-source) row "Mail providers removed". This guide walks through a complete transport for a fictitious HTTP email API, **AcmeMail**. The code below is included from [`rullst-mail/examples/custom_transport.rs`](https://github.com/Rullst/Rullst/blob/main/rullst-mail/examples/custom_transport.rs), which CI compiles and which runs offline from a clone of the Rullst repository: ```text cargo run -p rullst-mail --example custom_transport ``` In an application, the same types are available through the facade as `rullst::mail::*` with the `mail` feature. The example also uses `reqwest`, `secrecy`, `serde_json`, `chrono` and `async-trait` (`MailDriver` is an `#[async_trait]` trait; the facade's `rullst::async_trait` works too); add equivalent crates to your own `Cargo.toml`. ## What the framework does and what the transport does | Responsibility | Owner | | :--- | :--- | | Header-injection, recipient, link and homograph checks, outbound secret redaction, and attachment count, size and CID bounds | `DeliveryPipeline::prepare`, which the transport calls first | | Default sender, queueing and scheduling, tenant routing, failover, suppression, attachment inspection and observations | The `Mail` facade, `TenantMailResolver`, `FailoverDriver`, `SuppressionGuard`, `AttachmentInspectionGuard` and `ObservedMailDriver`, which wrap any `MailDriver` | | Authenticating to the provider, mapping the message to its API, rejecting what the API cannot express, bounding and checking the receipt, and classifying failures | Your `MailDriver` implementation | | Sender-domain verification, provider limits, bounce and complaint handling, retention and the provider contract | Your application and provider account | The transport is trusted server code. A transport that cannot express part of a message (a schedule, an inline image, an unsubscribe header) must return an error instead of sending the message without it. ## 1. Implement `MailDriver` ```rust,ignore /// Stable low-cardinality label used in errors and telemetry. const PROVIDER: &str = "acmemail"; /// Largest delivery receipt read from AcmeMail. const MAX_RECEIPT_BYTES: usize = 16 * 1024; /// Messages the offline fixture captured instead of sending. pub type OfflineOutbox = Arc>>; /// Transport for the fictitious AcmeMail HTTP API. pub struct AcmeMailDriver { api_key: SecretString, endpoint: String, client: reqwest::Client, outbox: OfflineOutbox, } impl AcmeMailDriver { /// Creates the transport. Empty or `mock_*` keys select the offline fixture. pub fn try_new(api_key: impl Into) -> Result { let api_key = api_key.into(); if api_key.contains(['\r', '\n']) || api_key.len() > 4096 { return Err(MailError::ConfigError( "AcmeMail API key is malformed".into(), )); } // No redirects (they would forward the key and the message), and // deadlines for connecting and for the whole exchange. let client = reqwest::Client::builder() .redirect(reqwest::redirect::Policy::none()) .connect_timeout(Duration::from_secs(5)) .timeout(Duration::from_secs(30)) .build() .map_err(|_| MailError::ConfigError("cannot build the AcmeMail client".into()))?; Ok(Self { api_key: SecretString::from(api_key), endpoint: "https://api.acmemail.invalid/v1/messages".into(), client, outbox: OfflineOutbox::default(), }) } /// Uses another endpoint: HTTPS, or plain HTTP to a loopback test fixture. pub fn try_with_endpoint(mut self, endpoint: impl Into) -> Result { let endpoint = endpoint.into(); let loopback = endpoint.starts_with("http://127.0.0.1:"); if !endpoint.starts_with("https://") && !loopback { return Err(MailError::ConfigError( "AcmeMail endpoint must use HTTPS".into(), )); } self.endpoint = endpoint; Ok(self) } /// Whether the key selects the real API or the offline fixture. pub fn delivery_mode(&self) -> DeliveryMode { credential_mode(self.api_key.expose_secret()) } /// The offline fixture's captured messages, shared with every clone. pub fn offline_outbox(&self) -> OfflineOutbox { Arc::clone(&self.outbox) } /// The API request body. Content the API cannot express is an error, /// never silently dropped. fn payload(message: &Message) -> Result { let from = message.from.as_deref().ok_or_else(|| { MailError::ConfigError( "AcmeMail requires a verified sender: set `from`, or MAIL_FROM for the Mail facade" .into(), ) })?; if message .send_at .is_some_and(|send_at| send_at > chrono::Utc::now()) { return Err(MailError::ConfigError( "AcmeMail cannot schedule mail; use Mail::enqueue with a durable queue".into(), )); } if message.attachments.iter().any(|item| item.is_inline()) { return Err(MailError::ValidationError( "AcmeMail has no inline images; use a transport that supports CIDs".into(), )); } let mut headers = serde_json::Map::new(); if let Some(value) = message.list_unsubscribe_header() { headers.insert("List-Unsubscribe".into(), Value::String(value)); // RFC 8058 one-click unsubscription requires an HTTPS URI. if message .unsubscribe_url .as_deref() .is_some_and(|url| url.starts_with("https://")) { headers.insert( "List-Unsubscribe-Post".into(), Value::String("List-Unsubscribe=One-Click".into()), ); } } let attachments: Vec = message .attachments .iter() .map(|item| { json!({ "filename": item.filename, "content_type": item.mime_type, "content": item.to_base64(), }) }) .collect(); Ok(json!({ "from": from, "to": message.to, "subject": message.subject, "text": message.body_text, "html": message.body_html, "headers": headers, "attachments": attachments, })) } /// Reads at most `MAX_RECEIPT_BYTES` and requires a message ID. async fn receipt(mut response: reqwest::Response) -> Result<(), MailError> { let invalid = || MailError::SendError("AcmeMail returned an invalid receipt".into()); let mut body = Vec::new(); while let Some(chunk) = response .chunk() .await .map_err(|_| MailError::transport(PROVIDER, "receipt read failed"))? { if body.len() + chunk.len() > MAX_RECEIPT_BYTES { return Err(invalid()); } body.extend_from_slice(&chunk); } let receipt: Value = serde_json::from_slice(&body).map_err(|_| invalid())?; match receipt.get("id").and_then(Value::as_str) { Some(id) if !id.is_empty() && id.len() <= 128 => Ok(()), _ => Err(invalid()), } } } #[async_trait] impl MailDriver for AcmeMailDriver { async fn send(&self, message: &Message) -> Result<(), MailError> { // The shared pre-flight checks: header injection, recipients, // outbound secrets, links and attachment bounds. let prepared = DeliveryPipeline::prepare(message)?; let message = prepared.message(); let body = Self::payload(message)?; if self.delivery_mode() == DeliveryMode::OfflineMock { self.outbox .lock() .map_err(|_| MailError::DriverError("AcmeMail outbox is unavailable".into()))? .push(message.clone()); return Ok(()); } let response = self .client .post(&self.endpoint) .bearer_auth(self.api_key.expose_secret()) .json(&body) .send() .await .map_err(|_| MailError::transport(PROVIDER, "request failed before a response"))?; if response.status().is_success() { return Self::receipt(response).await; } let retry_after = response .headers() .get(reqwest::header::RETRY_AFTER) .and_then(|value| value.to_str().ok()) .and_then(|value| value.parse::().ok()) .map(|seconds| Duration::from_secs(seconds.min(86_400))); // Error bodies can echo addresses and content: keep only the status // and the retry delay. 429 becomes `RateLimited`, 5xx is transient. Err(MailError::from_provider_response( PROVIDER, response.status().as_u16(), "provider response body omitted", retry_after, )) } } ``` Points that matter for security and testing: - **Run the pipeline first.** `DeliveryPipeline::prepare` applies the same pre-flight checks as the built-in transports; send `prepared.message()`, never the original. - **Never invent a sender.** A message without `from` fails with `MailError::ConfigError` before any request. The `Mail` facade fills it from `MAIL_FROM` or `[mail] from` before the transport sees it. - **Reject what the API cannot express.** A future `send_at` without provider scheduling, inline CID images or headers the API has no field for are errors, not silent drops. Durable scheduling belongs to `Mail::enqueue`. - **Offline fallback.** An empty or `mock_*` key selects `DeliveryMode::OfflineMock` through `credential_mode`, and the transport captures the message without network I/O, as section 3.5 of Rullst's [`AGENTS.md`](https://github.com/Rullst/Rullst/blob/main/AGENTS.md) requires. Validate the payload before that check so the fixture enforces the live contract. - **Bound the HTTP exchange.** Disable redirects (they would forward the key and the message), set connect and request deadlines, require HTTPS except for a loopback test fixture and read at most a bounded receipt. - **Classify failures.** `MailError::from_provider_response` turns HTTP 429 into `RateLimited` with its `Retry-After` and 5xx into a transient error; `MailError::transport` marks a failure before any response. `FailoverDriver` moves only transient and rate-limited failures to a fallback. Keep provider error bodies out of the error: they can echo addresses and content. - **Accept only a real receipt.** Success requires the provider's message ID; a 2xx without one is a `SendError`. Acceptance is not proof of inbox delivery. - **Idempotency.** `send_with_delivery_id` defaults to at-least-once. Override it only when the provider deduplicates by a key you forward. ## 2. Install it ```rust,ignore // 4. Install the transport for the `Mail` facade (and its queue worker)... let transport = AcmeMailDriver::try_new("mock_acme")?; let facade_outbox = transport.offline_outbox(); Mail::set_driver(Box::new(transport)); Mail::send_now(welcome("grace@example.com")).await?; Mail::reset_driver(); // ...or for one tenant, with the global default for everyone else. let (default_driver, default_store) = MemoryDriver::isolated(); let resolver = TenantMailResolver::with_default(default_driver); resolver.register("tenant_acme", AcmeMailDriver::try_new("mock_tenant")?)?; resolver .send_for_tenant("tenant_acme", &welcome("linus@example.com")) .await?; resolver .send_for_tenant("tenant_other", &welcome("ken@example.com")) .await?; // Shared guards wrap a custom transport like a built-in one. let store = InMemorySuppressionStore::new(16, 16)?; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs(); store .record(SuppressionEvent::try_new( PROVIDER, "evt_bounce_1", "bounced@example.com", SuppressionReason::HardBounce, now, )?) .await?; let guarded = SuppressionGuard::new(AcmeMailDriver::try_new("mock_acme")?, store); assert!(matches!( guarded.send(&welcome("bounced@example.com")).await, Err(MailError::SuppressedRecipient { .. }) )); ``` `Mail::set_driver` replaces the facade's configured driver for every send and for the queue worker (`register_mail_handler`) in that process, so install it at start-up in every process that sends or works the mail queue. `Mail::reset_driver` returns to `MAIL_DRIVER`. A `TenantMailResolver` selects a transport per tenant, with a default for the others. Suppression, attachment inspection and observation wrap the transport with static dispatch, as they wrap the built-in ones. A `MAIL_DRIVER` value cannot name a custom transport: the facade builds only the kept drivers from configuration, and a removed driver name fails with `MailError::ConfigError` instead of falling back. ## 3. Test the transport The example's `main` exercises the contract offline: - a `mock_*` key captures the message and never reaches the network; - a message without a sender, and one with an inline image, fail before any request; - against a loopback HTTP fixture, a 202 receipt succeeds, a 429 becomes `RateLimited` with a seven-second `Retry-After`, and a 503 fails over to the fallback driver; - the facade, a tenant resolver and a suppression guard use the transport. Add the provider's documented request and response examples as fixtures, and validate the live transport against a sandbox account and a verified sender before enabling it in production. Tests with mocks establish local contracts, not provider interoperability. --- # Monorepo Examples & Reference Apps Source: https://rullst.github.io/Rullst/book/examples.html # Examples and generated starters The repository examples and CLI starters serve different purposes: | Artifact | Purpose | Trust boundary | | --- | --- | --- | | `examples/blog` | Workspace integration showcase with local data, interactive demos, and offline provider fixtures. | Development-only; not a production template or compliance proof. | | `examples/saas` | Reproducible CLI-generated SQLite SaaS with authenticated notes, tenant membership and the same HTTP journey used in CI. | Disposable Linux development example; membership is an explicit local operator action. | | `examples/webgpu` | Browser wave-interference lesson served by `rullst-core`, with WebGPU rendering and a Canvas fallback. | Public read-only demo on loopback; browser output establishes no server-owned result. | | `examples/lms`, `examples/erp`, `examples/portfolio` | Complete applications that were the LMS, ERP and Portfolio starters of `cargo rullst new` until 13.0.0-alpha.4, kept as workspace members with their own tests. | Examples to copy from, not supported starters: no generator or upgrade path behind them. | | CLI starters (Blank, API, SaaS) | Small starting structures generated into a new project. | Generated output must be reviewed, configured, formatted, checked, and tested by the application owner. | No example is expected to exercise 100% of workspace behavior. External provider paths use deterministic mock credentials so CI and local development do not make live purchases, send email, or call cloud LLMs. ## Blog showcase The blog package demonstrates: - server-rendered HTML and Active Record persistence; - a parameterized repository query; - a LiveView/WebSocket presentation example (Wasm islands are covered by `cargo rullst make:island` and the [islands tutorial](https://rullst.github.io/Rullst/book/tutorials/18-wasm-islands-rust.html), not by this showcase); - Pico CSS and a small embedded file-template fixture (`include_str!` plus fixed placeholder replacement; not Tera or another template engine); - `Billable` quota evaluation and offline Stripe/InfinitePay adapter fixtures; - bounded security-helper demonstrations and a local AI/vector fixture; - pages that render under the unrelaxed production Content Security Policy, with same-origin stylesheets, scripts and vendored HTMX/Pico.css files; - a debug-only standalone Studio and Nexus access that is loopback-only in debug builds and credential-protected in release builds. The complete, current route list is maintained in `examples/blog/README.md` alongside its configuration requirements. ## Small SaaS example The [SaaS example](https://github.com/Rullst/Rullst/tree/main/examples/saas) installs the source CLI, generates the starter and adds a small notes API using database-owned membership, parameterized SQL and owner authorization. Its interactive mode opens a disposable loopback application: ```sh python3 examples/saas/run.py --serve ``` Follow `examples/saas/README.md` to register a user, grant local membership and call the notes API. Without `--serve`, the same command runs its automated authentication, isolation, CSRF, persistence/restart and revocation journey. The script requires Linux, Python 3.11+, the pinned toolchain and 32 GiB of initial disk headroom. It removes generated data on exit and uses no real provider account. This directory holds the canonical example recipe and resource; it is not a duplicate checked-in copy of a generated application. ## LMS, ERP and Portfolio examples Until 13.0.0-alpha.4, `cargo rullst new --blueprint lms|erp|portfolio` generated these applications; the CLI now offers only the Blank, API and SaaS starters, and `--blueprint lms`, `erp`, `portfolio` or `blog` fails before writing anything with a report that names the example. Each was generated with that CLI (SQLite, `--default`), then adapted to the workspace: `publish = false`, workspace dependencies, a library with `router()` plus a thin binary, and the generated tests (security baseline, CSRF, rate limits, ownership checks) as the package's own tests. | Example | What it shows | | :--- | :--- | | [`examples/lms`](https://github.com/Rullst/Rullst/tree/main/examples/lms) | Course catalog, modules and lessons, an accessible player, enrollment, owner-bound progress, sign-in with password reset and a Nexus admin for signed-in administrators. Its owner-boundary test is the threat-model evidence for `ACADEMY-02`. | | [`examples/erp`](https://github.com/Rullst/Rullst/tree/main/examples/erp) | Products, stock movements and orders; the dashboard, forms and Nexus share one administrator policy. | | [`examples/portfolio`](https://github.com/Rullst/Rullst/tree/main/examples/portfolio) | A developer portfolio read from the database and edited in Nexus, checked in a mobile Chromium viewport in Linux CI. | Run one from its directory, so the server finds `.env`, `Rullst.toml` and its static files: ```bash cd examples/lms cp .env.example .env # then set APP_KEY to 32 random characters, e.g. `openssl rand -hex 16` cargo run -p rullst-lms-example -- db:migrate cargo run -p rullst-lms-example ``` The ERP and Portfolio examples work the same way with `rullst-erp-example` and `rullst-portfolio-example`. To start your own application from one, copy the directory and replace its `workspace = true` dependencies with the published crates (each README shows the line). Applications generated earlier from these blueprints keep working: their code is theirs. ## Browser graphics example The [WebGPU example](https://github.com/Rullst/Rullst/tree/main/examples/webgpu) serves embedded HTML, CSS and JavaScript modules behind Rullst's production CSRF/WAF/secure-header baseline. From the repository root: ```sh cargo run --locked -p rullst-webgpu-example ``` Then open `http://127.0.0.1:3007/webgpu/`. All computation stays in the browser; missing WebGPU support selects the Canvas fallback. Follow `examples/webgpu/README.md` for the reusable modules and browser tests. ## Blog tenant selection The blog example inserts a static test-only `TenantMembership` before the tenant layer. The `X-Tenant-ID` header can select only one of those fixed memberships. This models the separation between an untrusted selector and trusted authenticated claims. Handlers read the membership-checked `TenantContext` that `TenantLayer` inserts. The `Post` model declares `tenant_column`, so its generated queries fail closed outside `with_tenant(...)`; the hand-written repository SQL binds the same tenant itself. In a real application, authentication middleware must derive membership from a verified session or token. Never construct membership from the same client header used to select a tenant. ## Provider fixtures The `/pricing` page uses `mock_*` credentials and performs no live checkout. It lists only the built-in Stripe and InfinitePay (experimental) adapters. Mock URLs and sample provider metadata are test fixtures, not a promise of live capability, pricing, tax treatment, or regional availability. ## Running locally ```bash cd examples/blog cargo run -p rullst-blog-example ``` Before local startup, configure `APP_KEY` in `examples/blog/.env` as documented in the example README. The showcase selects its database like `Server` and the Artisan commands (`DATABASE_URL`, `.env`, then `Rullst.toml`), so `DATABASE_URL` is optional locally and honored in deployments. The debug build needs no Nexus password, but verifies the socket peer as loopback. A release build does not start Studio and refuses to construct Nexus without validated `NEXUS_ADMIN_USERNAME` and `NEXUS_ADMIN_PASSWORD` values. Studio is local developer tooling; keep it on a trusted interface. ## Verifying examples and starters For the checked-in examples, run package tests and the workspace trifecta. For CLI output, use a temporary directory and verify every generated project: ```bash cargo fmt --all -- --check cargo check --all-features cargo test --all-features ``` CI smoke or DAST workflows cover only the routes and assertions present in those files. Their logs, commit digest, toolchain, and skipped steps are the evidence; the example itself is not evidence of production readiness. --- # Axum/SQLx Interoperability Source: https://rullst.github.io/Rullst/book/axum-sqlx-migration.html # Axum and SQLx interoperability guide Rullst builds on Axum, Tokio, Tower and SQLx and preserves direct access to their APIs. This reduces coupling for HTTP and database code; it does not make every optional framework subsystem free to remove. ## Mount an existing Axum router `rullst::Router` supports conversion to and from `axum::Router`: ```rust use axum::{Router as AxumRouter, routing::get}; use rullst::Router; async fn existing_handler() -> &'static str { "existing Axum route" } let existing = AxumRouter::new().route("/existing", get(existing_handler)); let rullst_router: Router = existing.into(); let axum_router: AxumRouter = rullst_router.into(); # let _ = axum_router; ``` The standard Axum extractor and response types remain available, including through Rullst's documented re-exports. Generated projects do not depend on `axum` directly: add `axum = "0.8"` (the version Rullst uses) to run this example as written, or import the same types through `rullst::web::axum`. ## Keep raw SQLx where it is useful Application-owned SQL can live beside generated ORM queries. Bind every dynamic value and review structural SQL separately: ```rust use sqlx::PgPool; async fn active_names(pool: &PgPool) -> Result, sqlx::Error> { sqlx::query_scalar("SELECT name FROM users WHERE active = $1") .bind(true) .fetch_all(pool) .await } ``` ## Generate an escape-hatch template ```bash cargo rullst eject ``` The command writes a reviewable Axum/Tokio entry-point template to `src/ejected_main.rs` (`--output src/.rs` chooses another file below `src/`) and leaves `src/main.rs` unchanged. The template is a placeholder route behind the configured Rullst security baseline, compiled through the `rullst::web::axum` re-export. It does not copy the application's routes; ORM models, migrations, authentication policy, Studio/Nexus integration and other selected subsystems can still require deliberate migration work. `cargo rullst eject --force` writes the template over `src/main.rs` instead and keeps the original at `src/main.rs.rullst-backup` (it refuses to run while that backup exists). Use it only with a committed or backed-up worktree, move the module declarations and routes from the backup into `application_routes()`, then run `cargo check`. ## Practical migration sequence 1. Keep domain types and handlers independent of framework globals. 2. Convert router boundaries incrementally. 3. Replace generated ORM calls with raw SQLx only where that trade-off helps. 4. Inventory authentication, middleware order, jobs, cache and admin surfaces. 5. Run application-specific integration and authorization tests before removing dependencies. Interoperability is a maintained design goal. “Zero lock-in” or zero migration cost is not a framework guarantee. --- # GitHub CLI Installation & Safe Login Source: https://rullst.github.io/Rullst/book/gh-install.html # GitHub CLI installation and safe login The GitHub CLI (`gh`) lets a maintainer inspect workflow runs, pull requests, issues, Dependabot state, and Code Scanning alerts from a terminal. It does not gain access merely by being installed: access begins only after the maintainer completes GitHub's browser authorization flow. Official references: - [GitHub CLI installation](https://github.com/cli/cli#installation) - [`gh auth login` manual](https://cli.github.com/manual/gh_auth_login) - [Code Scanning API permissions](https://docs.github.com/en/rest/code-scanning/code-scanning) ## Current Rullst workstation On the maintainer's Linux workstation, `gh` is installed for the current user at `~/.local/bin/gh`. The downloaded GitHub release archive was verified against its official SHA-256 checksum. Confirm that the command is on the shell path: ```bash command -v gh gh --version ``` If the first command prints nothing, start a new terminal. For the current terminal only, this adds the user-local directory without changing system files: ```bash export PATH="${PATH}:$HOME/.local/bin" ``` For another machine, use GitHub's current official installation instructions. Common package-manager entry points are: ```bash # macOS with Homebrew brew install gh # Windows with WinGet winget install --id GitHub.cli ``` Linux repository commands vary by distribution and can change; copy them from the [official Linux installation guide](https://github.com/cli/cli/blob/trunk/docs/install_linux.md) instead of an old blog post. ## Browser login for Rullst maintenance Run this command yourself in the terminal: ```bash gh auth login --hostname github.com --git-protocol https --web --scopes security_events ``` GitHub will show a one-time code and open its authorization page. Verify that the browser is on `github.com`, sign in as the intended Rullst maintainer, read the requested permissions, and approve only if they match the task. The additional `security_events` scope allows the CLI to query Code Scanning data; GitHub CLI's web flow also maintains its documented baseline scopes. Do not paste an access token into chat, a repository file, shell history, an issue, or a commit. Do not use `--insecure-storage`. The browser flow asks the system credential store to keep the credential; if `gh` reports that no secure credential store is available, stop and configure one before continuing. Verify the active account without printing its token: ```bash gh auth status --hostname github.com --active ``` Never add `--show-token` to a command whose output may be shared. Once the status is healthy, a read-only check of Rullst's open Code Scanning alerts is: ```bash gh api 'repos/Rullst/Rullst/code-scanning/alerts?state=open&per_page=100' \ --jq '.[] | {number, rule: .rule.id, severity: .rule.security_severity_level, url: .html_url}' ``` Authentication makes inspection possible; it does not authorize dismissing an alert, merging a pull request, changing repository settings, publishing a release, or modifying secrets. Those actions still require an explicit task and an evidence-based review. ## Logout and revocation Remove the local GitHub CLI session with: ```bash gh auth logout --hostname github.com ``` The logout command removes the locally stored authentication entry but does not revoke the OAuth grant. To revoke it, open [GitHub authorized applications](https://github.com/settings/applications), select **GitHub CLI**, review the impact on other machines, and choose **Revoke Access**. After logout, confirm that this workstation no longer has an active session: ```bash gh auth status --hostname github.com ``` --- # Preparing Applications for v13 Source: https://rullst.github.io/Rullst/book/migration-v13.html # Preparing an application for v13 **The current source is `13.0.0-alpha.4`, a prerelease, not a stable v13 release.** The first prerelease, `13.0.0-alpha.1`, was published on October 11, 2026. The 12.x line, on the frozen `v12` branch, reached end of life on 16 October 2026 with 12.3.0 as its last release; `main` develops v13. The development packages use the same v13 version and internal requirements. `rullst-privacy`, `rullst-supervision`, `rullst-media` and `rullst-labs` join the candidate package inventory and were first published in 13.0.0-alpha.1. Privacy has explicit `privacy-*` umbrella features; Supervision, Media and Labs remain standalone opt-ins. All twenty packages are published together as each v13 prerelease; the current one is `13.0.0-alpha.4`. Cargo never selects a prerelease on its own, so `cargo add rullst` still selects the end-of-life 12.3.0: opt in with an exact requirement such as `rullst = "=13.0.0-alpha.4"`. A plain `"13.0.0-alpha.4"` is a caret requirement that also accepts later alphas, which may break the API. From 13.0.0-alpha.2 the CLI writes the `=` form; a project generated by the 13.0.0-alpha.1 CLI has the caret form, so add the `=` to its `rullst` and `rullst-orm` lines. Since 13.0.0-alpha.3 `rullst` and `rullst-orm` have no default features and compile no SQLx driver on their own, so a hand-written manifest selects the database profile it uses, as `cargo rullst new` does (see the Minimal default features row): ```toml rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["strict-postgres"] } ``` This inventory covers the `13.0.0-alpha.4` development source. Revisit it as the remaining [delivery priorities](https://rullst.github.io/Rullst/book/v13-delivery-plan.html) land. It does not promise that every existing application, provider account or deployment works without review. To find the rows that affect an application, run `cargo rullst upgrade --dry-run` with the v13 CLI: its `rullst-upgrade-rules-v4` catalog reports `MUST-CHANGE` and `REVIEW` findings at `file:line`, each naming the first-column title of its row below, with the guidance to apply by hand. The [assisted upgrade tutorial](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#v12-to-v13-source-findings) describes the workflow and [classifies every row](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#v12--v13-rule-classification). Rows without rules still need a reading. An application already on a 13.0 prerelease gets only the rules introduced after its version, and the plan says how many earlier rules it skipped; `--all-rules` applies every rule ([rules by release](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#rules-by-release)). An application that already runs 13.0.0-alpha.3 follows the shorter [alpha.3 to alpha.4 checklist](https://rullst.github.io/Rullst/book/migration-v13.html#upgrading-an-alpha3-application-to-alpha4); one on 13.0.0-alpha.2 follows the [alpha.2 to alpha.3 checklist](https://rullst.github.io/Rullst/book/migration-v13.html#upgrading-an-alpha2-application-to-alpha3) first. ## Changes from the published 12.1.0 source | Area | Application impact | | :--- | :--- | | Existing runtime APIs | Source preparation changes supported dependency requirements; it invents no Rust rewrites. Review the explicit account-session schema migration below even when existing Rust calls still compile. | | Account/session registry | Before serving requests with the new Auth recovery store, explicitly run `SqlRecoveryStore::migrate()` to add session details and the expiry index. Existing opaque sessions remain valid with unknown historical metadata. Configure expired-session maintenance and authorize new inventory/logout routes; see [session management](https://rullst.github.io/Rullst/book/session-management.html). The CLI manifest updater does not apply this SQL migration. | | Private object storage | The optional `storage-s3` candidate requires explicit signing credentials and production validation. Existing cloud constructors stay unconfigured; no bucket, IAM policy or credentials are inferred. See [private storage](https://rullst.github.io/Rullst/book/private-object-storage.html). | | R2 public URLs | `Storage::r2(...).url(key)` (and `TenantStorage::url` over R2) now returns `StorageError::Unsupported` instead of `https://.r2.cloudflarestorage.com//`. That S3 API endpoint requires a signature for every request, so the URL never loaded in a browser. Build public URLs from your `r2.dev` or custom domain in the application, or use a signed download with `storage-s3`. | | Tenant local storage | `TenantStorage` reads, metadata and deletes on local storage return `StorageError::Io` when the tenant directory lies below a regular file (for example a file named `tenants` in the base path), on every platform. On Windows, 12.x and 13.0.0-alpha.1 returned `NotFound` there, as if the object were missing. | | Recoverable Live UI | Existing `LiveComponent` and `make:live` output keep their original protocol. Opt in to `live::recovery` with explicit authenticated scope, periodic authorization, persistent revisions and atomic domain updates; mount its separate browser module. Full snapshots replace child DOM and never replay uncertain actions. See [Live recovery](https://rullst.github.io/Rullst/book/live-recovery.html). | | Distributed tracing | Opt in to `telemetry::distributed` with approved operation names, upstream trust, a collector and owned flush/shutdown. The legacy initializer now uses compatible threaded OTLP/HTTP export and defaults to `127.0.0.1:4318/v1/traces`; `OTEL_EXPORTER_OTLP_ENDPOINT` is a base URL, while `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is exact. Redirects and request bodies over 1 MiB are unsupported; the legacy initializer exports at most 64 spans per request (unless `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` is set), honours `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` and reports dropped batches on stderr, while the minimized profile ignores proxy variables. Review existing collector environment configuration; [the tracing guide](https://rullst.github.io/Rullst/book/distributed-tracing.html) distinguishes both metadata policies. | | Core queue | SQLite claims jobs in due-time order, so a due scheduled or handed-back job no longer waits behind later immediate jobs, and `retry_failed_job` keeps the attempt counter (it fences claims) instead of resetting it to 0. A job no worker has a handler for is failed with `HandlerNotFound` at its 720th claim attempt instead of being handed back forever. `dispatch`/`dispatch_at` reject empty names and names over 256 bytes, and `Worker::run` rejects `job_timeout(Duration::ZERO)`, all with `QueueError::InvalidConfiguration`. Redis queue commands wait up to 10 s for a reply instead of 500 ms. Stalled recovery of a claim made without a lease (SQLite `pop`) may now happen up to two seconds after `stalled_after`, never before. Failed jobs that a 12.x process records in a Redis queue, including 12.x workers during a rolling upgrade, are not in the new failed-job index: they are neither counted nor evicted by the 10,000-entry retention, so retry them with `retry_failed_job` or remove them with `purge_failed_jobs` once every worker runs 13. | | Core scheduler, cache and realtime | `Scheduler::start` returns the new `SchedulerError::InvalidConfiguration` for a zero task timeout; exhaustive matches in other crates are unaffected because the enum is `#[non_exhaustive]`. The Redis cache treats a zero TTL as an already-expired value (the key is removed) and a TTL beyond Redis's expiry range as no expiry, as the memory driver does. `PresenceTracker` counts connections: call `user_left` once per `user_joined`, since a user stays online until the last leave. `Channel::new` clamps the capacity to 65,536. The Traffic Shield database probe gives up at `max_db_latency` and records at least that latency. A Live recovery session no longer counts its own callback and send time as peer silence. `TenantCache` now writes `%` as `%25` and `:` as `%3A` in the tenant segment of its keys (`tenants:a%3Ab:`), so entries of tenant IDs containing those characters start cold, and while 12.x and 13 instances share the cache a `forget` on one version does not remove the other's entry for such a tenant; rely on TTLs or finish the rollout before depending on invalidation. | | Core memory cache sweeps | `Cache::memory()` sweeps expired entries after a number of operations equal to a quarter of the entries the previous sweep kept (at least 256), instead of every 256 operations, so the sweep cost per request no longer grows with the cache. Reads still never return an expired value; unread expired entries may stay in memory longer in a large cache. | | Studio queue monitor | Studio lists queue records with the new `QueueDriver::list_job_previews` (at most 2 KiB of each payload and error) instead of `list_all_jobs`. Custom drivers keep working through the default projection of `list_all_jobs`; override `list_job_previews` to cut values in the store. | | Studio API playground | `Studio::with_openapi` now takes a utoipa 6 `OpenApi`. Applications that build their document with utoipa 5 must upgrade to utoipa 6 (and `utoipa-axum` 0.3 when they use its router helpers). Studio no longer embeds Swagger UI (its `utoipa-swagger-ui` build dependency needs a `zip` release that crates.io has yanked): `/studio/api` is now a server-rendered, read-only reference of the operations, `/studio/api/` redirects to it and `/studio/api/openapi.json` still serves the document. There is no in-page "Try it out"; send requests with an external client. | | Studio local access and table view | `LocalStudioAccess::loopback_only()` now accepts an IPv4-mapped loopback peer (`::ffff:127.0.0.1`, reported by a dual-stack `HOST=::` listener) instead of returning 403; other mapped addresses stay rejected. The table view, and the public `get_any_value_as_string`, now report a present value that no text, integer, float or Boolean codec decodes (for example a non-UTF-8 BLOB) as `unreadable` instead of `NULL`, and a stored text `NULL` is no longer styled as SQL NULL. | | LMS blueprint | `--blueprint lms` now generates a small starter: catalog, modules, lessons, an accessible player, enrollment, owner-bound progress, login and a Nexus admin. The complete Academy scaffold (schools, roles, quizzes, activities, scores, automation, notifications, publication, privacy lifecycle) and `--lms-modules` were removed; existing generated applications keep their code. Registration no longer generates the school-membership hook, and the learner dashboard no longer claims a default demo school: it points to the course catalog. Since 13.0.0-alpha.4 this starter is the `examples/lms` application instead (see "CLI starters moved to the examples"). | | Consumer generators | `make:supervision` was removed with the complete Academy LMS it targeted; integrate `rullst-supervision` directly. `make:privacy` and `make:age-gate` now target only the SaaS starter (`--blueprint saas`, which requires `--tenant-ref`). | | `html!` event handlers | A dynamic value in an `on*`, `hx-on` or `hx-on-*` attribute (for example `onclick={handler}`) no longer compiles, because escaping cannot make data safe inside JavaScript. Static handler strings still compile; move data-driven handlers into a nonce'd script with `addEventListener` and pass values through `data-*` attributes. The default CSP already blocked such inline handlers. | | `html!` returns `Markup` | Since 13.0.0-alpha.2, `html!` returns `rullst::html::Markup` (escaped HTML) instead of `String`. Interpolating a `Markup` inserts it as HTML, so a fragment built by another `html!` call is no longer shown as escaped text and needs no `RawHtml`; `String`, `&str` and numbers are still escaped. A function declared `-> String` that returns `html!` no longer compiles: declare it `-> Markup`. `RawHtml(html! { .. })` no longer compiles: interpolate the `Markup` directly. Collect lists with `.collect::()` (collecting `Markup` into a `String` does not compile) and use `Option` for optional parts. `render_page`/`render_page_with_lang` take `impl Into` (wrap a `String` of trusted HTML with `Markup::from_trusted`), and `Live::mount` and native `#[island]` functions return `Markup`. A handler can return `Markup` (`text/html; charset=utf-8`), `Html(html! { .. })` still compiles, `Markup` compares with and reads like a `&str`, and `.into_string()` returns the `String`. See [`html!` returns `Markup`](https://rullst.github.io/Rullst/book/migration-v13.html#html-returns-markup). | | Core request validation | `ValidatedForm`/`ValidatedJson` return 413 for oversized bodies and 415 for a wrong content type (HTMX requests receive the same code in `X-Rullst-Validation-Status`); other read failures stay 400. Nested errors are keyed `address.zip` and `items[0].name`, and `ValidationError` formatting no longer includes submitted values. Update clients or tests that expected 400 for these cases. | | Core feature flags and scheduler | A user outside a declared A/B split now resolves to `"disabled"` instead of falling through to the next driver, and a negative database `rollout_percentage` means 0%. `Scheduler::task` uses POSIX weekday numbering (0 and 7 are Sunday) with the day-of-month OR day-of-week rule in UTC; review numeric weekdays in existing schedules. `RullstConfig::environment()` now reports the environment the Server enforces, including a value read from `.env`. Percentage rollouts and A/B variants of the Env, TOML, Memory and DB drivers use a versioned SHA-256 bucket hash instead of `std`'s `DefaultHasher`, so upgrading reassigns users to buckets once (percentages and variant weights are kept). End experiments whose assignment must not change before upgrading, and keep the period in which 12.x and 13 replicas both serve short, because they assign the same user differently. | | Memory feature-flag splits | A `MemoryFeatureDriver::override_variants` flag (including `FeatureManager::overrides()`) no longer reports `enabled`/`enabled_for` as true for everyone. Like the Env, TOML and DB drivers, `enabled_for` is true only for identifiers assigned a variant named `"enabled"`; like the Env and TOML drivers, `enabled` is false. Gate A/B code on `variant`. | | Database feature-flag splits | A `rullst_feature_flags` row whose `variants` column holds an A/B split now makes `DbFeatureDriver::enabled` (and therefore `feature::enabled` when the DB driver answers) false, as in the Env, TOML and Memory drivers. It was true when `rollout_percentage` was empty. `enabled_for` and `variant` are unchanged; gate A/B code on `variant`. | | Security headers | Core and both Security header layers preserve an endpoint's exact `Referrer-Policy: no-referrer`, including when it occurs among duplicate values. Other endpoint values still yield to `SecureHeadersLayer` and `CspSecurityLayer`; the Core baseline now keeps them (see Core security header precedence). Review handlers that intentionally set this more restrictive policy. | | Core WAF signatures | `waf_middleware`, which `Server` mounts in staging and production, now refuses injection structure instead of SQL and shell keywords. Ordinary text such as "Please select an option", "Delete my account" or "curl the API with your token" in a query, `Referer`, cookie or body is no longer rejected with 403. A quote followed by `or`/`and` and a comparison or by a SQL comment, a `;` followed by a SQL statement, `union [all] select`, SQL probe functions (`sleep(`, `waitfor delay`, `information_schema`…) and a shell metacharacter (`;`, `\|`, `&&`, a backtick, `$(`) followed by a command name are still refused, also when `/* */` comments separate keywords; XSS and traversal signatures are unchanged. JSON bodies are inspected key by key and string by string after decoding, so a `\u003c` escape no longer hides ``) are blocked; bracket-free constructs such as `Vec::` or `Box` inside `vec![...]` no longer make a label ambiguous. It decodes CommonMark backslash escapes and entity or numeric character references in destinations, so `![x](https://host/c)` is blocked like the literal URL; an unrecognized named reference (for example `é`) makes that destination untrusted. Images and definitions inside code spans and closed fenced or indented code blocks are ignored, as CommonMark never renders them (code whose extent depends on an ambiguous layout is still read); lines end at LF, CRLF or a bare CR and only spaces and tabs make a line blank, so a bare CR or a no-break-space line can no longer turn a rendered image into apparent indented code. A reference image whose ASCII label matches no definition, such as `vec![x]` or `routes![...]` in Rust code, renders as literal text and is not blocked even next to a remote URL; a non-ASCII label, a label with a backslash-escaped bracket (read with its escapes, as `[a\]b]`) or an unterminated label is still blocked when the text names a remote URL, and a reference image whose definitions are all local is not blocked by an unrelated link. An inline image whose destination and title are not certainly valid (such as `![ß](x y)`) is also judged by the shortcut reference CommonMark falls back to, matched with Unicode case folding. | | AI RAG tenant tags | `RagPipeline::answer` checks the tenant tag of every document the retriever returns, including those after the context budget is exhausted, and fails with `RagError::InvalidDocument` (audited as `ContextRejected`, no provider call) instead of answering when any of them belongs to another tenant. | | Anthropic provider output | `AnthropicProvider` requests up to 16,000 output tokens instead of a fixed 1,024, and a reply whose `stop_reason` is `max_tokens`, `model_context_window_exceeded` or `refusal` now fails with `AiError::ApiError` instead of returning the partial text as a complete answer (so `StatefulChat` does not store it and `RagPipeline` audits `GenerationFailed`). Choose another limit with the new `with_max_tokens`; a longer reply can take longer than the 30-second default request deadline, so raise it with `with_request_timeout`. | | OpenAI provider output | `OpenAiProvider` vision requests no longer send `max_tokens: 1024` (deprecated by OpenAI and rejected by reasoning models); like text requests they leave the output limit to the model. A chat, vision, JSON or structured-output reply whose `finish_reason` is `length` or `content_filter` now fails with `AiError::ApiError` instead of returning the partial text. DeepSeek and `OpenAiCompatibleProvider` are unchanged. | | Ollama host | `OllamaProvider::new` and `AiClient::auto()` read `OLLAMA_HOST` the way Ollama does: a scheme-less value such as `127.0.0.1:11434`, `localhost` or `[::1]` now means `http` with port 11434 unless a valid port is named (`ollama.com` alone means `https://ollama.com:443`), instead of producing a URL every request rejected. Values with `http://` or `https://` are used as before. | | AI chat memory keys on MySQL/MariaDB | `SqlChatMemory` tenant and conversation IDs compare case-sensitively on every backend; new MySQL/MariaDB tables use `ascii_bin` key columns, and `prepare_schema` never alters an existing table. On a table created by 12.x, an ID that differs from a stored one only by letter case fails closed (`ChatMemoryError::InvalidConfiguration`, `ConversationNotFound` or `RevisionConflict`). Stop chat-memory writers, back up both tables, review conversations that 12.x may have merged and apply the migration in the [AI README](https://github.com/Rullst/Rullst/blob/main/rullst-ai/README.md#upgrading-mysqlmariadb-chat-memory-tables). PostgreSQL and SQLite tables are unchanged. | | Core CSRF on HEAD | `csrf_middleware` handles `HEAD` like `GET`: it installs the request-scoped `CsrfToken` and, when the request carries no CSRF cookie, sends the same `Set-Cookie` that GET would. A `get` route that extracts `Extension` now answers HEAD (monitors, crawlers, `curl -I`) with its GET status instead of 500. | | Hot-reload machine endpoints | `Server::new_hot(...).with_machine_endpoints(policy)` now authenticates the policy's exact routes in the development hot-reload server, as the static server already did in every environment. Previously hot reload ignored the policy and those routes ran unauthenticated; development machine clients must now send the configured credentials. | | Core JSON PII masking | With `enable_pii_masking`, JSON string escapes are decoded before masking, so `\uXXXX` hex digits never join a card-number run (which produced invalid JSON such as `\u****`), while escaped digits and `@` are masked like their literal characters. A masked character is written as a literal `*`; unmasked escapes keep their original spelling. | | Core PII masking of range responses | With `enable_pii_masking`, a `206 Partial Content` response whose range would be masked now becomes `502` with `Cache-Control: no-store` instead of a rewritten 206 that kept its status but lost `Content-Range`. A range that needs no masking passes through unchanged. A `multipart/byteranges` response, previously passed through uninspected, is now checked part by part and withheld the same way when a textual part would be masked or when the body cannot be split exactly (invalid boundary or delimiter line, more than 256 parts, a part header block over 8 KiB, no close delimiter). | | Security DLP, RASP and honeypot | `DlpResponseLayer` now also masks XML (`application/xml`, `+xml`), YAML and `application/javascript` responses, as it already did for text and JSON; review clients that compare such bodies byte for byte. A `206 Partial Content` response whose range contains a masked secret now becomes `502` with `Cache-Control: no-store` instead of a rewritten 206 without `Content-Range`. A `multipart/byteranges` response, previously passed through uninspected, is now checked part by part and withheld the same way when a textual part contains a secret or when the body cannot be split exactly (at most 256 parts and 8 KiB of headers per part). A honeypot ban TTL beyond the monotonic clock (for example `Duration::MAX` in `HoneypotState::try_with_limits`) now bans until the furthest representable deadline instead of never banning, and telemetry lists a ban only when one was stored. `redact_secrets` no longer increments `dlp_secrets_masked` or emits `DLP_SECRET_LEAK_PREVENTED` events (it counts only `log_redactions`), so dashboards stop reporting log lines as blocked responses. `RaspSecurityLayer` now serves requests whose `User-Agent` carries the stock PowerShell product token (`PowerShell/7.4.1`, `WindowsPowerShell/5.1…`) instead of returning 403; `powershell` elsewhere, or as execution syntax in that header, still blocks. `HoneypotLayer` telemetry (`honeypot_route_hits`, ban reasons and live events) names the configured trap path instead of the request's letter case, so case variants share one route counter. | | Security audit log lines | `StdoutAuditLogger` writes `actor`, `action` and `resource` as quoted, escaped strings (`actor="bob" action="login"`) instead of raw text, so a value with spaces, `=` or a line break can no longer forge fields or lines. Update log parsers and alerts that match the unquoted 12.x form. | | Connect generic OIDC | `OidcProvider` no longer rejects an ID token or userinfo response without the optional `name` claim (`Missing name in id_token`/`userinfo`); `ConnectUser::name` falls back to `given_name`/`family_name`, `preferred_username`, `nickname` and may be empty. Applications that relied on a non-empty name must handle the empty case. An ID token without a `kid` header is now verified when the issuer's JWK Set holds exactly one key that fits the token's asymmetric `alg`, instead of failing with `Missing 'kid' header`; other sets still fail closed, and a kid-less symmetric token now reports the algorithm error before any JWKS request. | | Age assurance | Optional `rullst-privacy` APIs and `make:age-gate` add an authenticated native declaration journey. Existing apps acquire no age policy, verified age, guardian relationship, replay storage or camera capture by upgrading a dependency. | | Optional consent and export | `make:privacy` adds explicit versioned choices, effective withdrawal of the demonstrated optional greeting and an authenticated own-account name/email export. It requires reviewed application installation and explicit consent-store initialization. It is not a complete export of all application data or automatic worldwide legal compliance. | | Project context | New projects receive application instructions and a bounded metadata map. `generate:ai-context` replaces recognized legacy source-concatenation output, preserves existing `AGENTS.md`, and adds a non-writing `--check`. See the [context guide](https://rullst.github.io/Rullst/book/project-context.html) for size/path/configuration limits. | | Typed API generation | `generate:api` is an explicit additive OpenAPI profile with generated Rust codecs and a TypeScript client. Install/review the schema and application handlers; existing scanning generators and routes are not migrated automatically. See the [supported profile](https://rullst.github.io/Rullst/book/typed-api.html). | | CLI generator output | `make:migration:auto` reads `#[orm(table_name = "...")]`, qualified `#[derive(rullst_orm::Orm)]` and a `table` option written after other `#[orm(...)]` options, so it diffs the declared table instead of a guessed `s` table. `make:live` imports `rullst::async_trait` instead of the undeclared `async_trait` crate; edit components generated earlier the same way. `make:scalar` imports `rullst::web::axum::Router` instead of the undeclared `axum` crate. `eject` writes a template that compiles (Axum through `rullst::web::axum`) and wraps its placeholder routes in the configured security baseline; it still does not carry over application routes, so `--force` output needs the routes moved from `src/main.rs.rullst-backup`. `foundry:deploy` uploads the binary to `/opt/rullst//incoming/.upload` (created `0700` for the SSH user during provisioning) instead of `/tmp`, and the remote step requires `stat` and `sha256sum` to check its owner and digest. `foundry:init` creates a missing `.gitignore`, no longer treats a comment or `!Foundry.toml` as protection, and writes `Foundry.toml` with mode `0600`. `make:model` rejects names such as `Match` or `Type` whose module would be a Rust keyword, as `make:resource` already did. `make:migration` keeps leading `m` characters of the name (`modify_users` previously became `odify_users`) and rejects names that are not ASCII letters, digits, `_` or `-`; generators that refresh `src/migrations/mod.rs` now fail on an `m*.rs` file whose name is not a valid module name instead of writing an unparsable registry. `make:controller --api` and `make:resource --api` handlers extract `Json`/`Json` instead of `Form`, so clients must send `application/json`; controllers generated earlier keep `Form` until edited. `generate:ts` also scans `src/lib.rs` and turns `{id}`/`{*path}` captures into URL-encoded method arguments; regenerate `rullst-client.ts`, whose parameterized methods change signature. Re-run `hook:install` to refresh a managed `commit-msg` hook: it now accepts Git's merge, revert and `fixup!`/`squash!`/`amend!` subjects. `generate:openapi` takes an operation summary only from the handler with the exact name, not from an earlier function whose name starts with it (`show_archived` for `show`). `make:worker` handlers no longer print the full job payload; existing workers keep their logging until edited. Fixture-mode `make:billing` (and SaaS starter) checkout/portal handlers answer a database failure with a generic `Billing identity is unavailable` 500 and log the error server-side. `make:migration:auto` ignores the ORM `migrations` table and `rullst_*` tables, and prints destructive-only differences instead of writing and registering a comment-only `auto_sync` migration on every run. `hook:install` fails when `core.hooksPath` selects another directory, instead of reporting success for wrappers Git never runs. `inspect schema` prints the ORM model schema derived from `#[derive(Orm)]` structs when no project-provided `rullst-schema.json` exists, instead of asking for a file that nothing generates. | | CLI generator robustness | `generate:models` with `--driver postgres` or `--driver mysql` casts and aliases the `information_schema` columns it reads, so PostgreSQL 12+ and MySQL 8 schemas introspect instead of failing to decode `sql_identifier` or panicking on upper-case MySQL labels; unexpected metadata is now an `IntrospectionError` instead of a panic. `make:migration:auto` declares columns from the field's Rust type (`integer`, `big_integer`, `float`, `boolean` or `string`) instead of TEXT for every field, declares non-`Option` fields of a new table `NOT NULL`, adds a required column to an existing table as `NOT NULL DEFAULT 0`/`0.0`/`''`, and fails without writing when a field type has no mapping (for example `Vec`, `Uuid`, a tuple or an enum) or a required date/encrypted/JSON column would leave existing rows NULL; mark such fields `#[orm(skip)]` or add them with `make:migration`. `cargo rullst auth` and `make:mfa` refuse a project without a SQL migration runner (`pub mod migrations;` plus a `get_migrations()` call in `src/main.rs`/`src/lib.rs`), such as a `--no-database` starter, instead of writing a users or MFA migration nothing compiles or applies; the generated `User` model, users migration and auth controller name the ORM as `rullst::orm::Error` instead of the undeclared `rullst_orm` crate. `inspect routes` no longer follows symlinks under `src/` (a link cycle made it effectively hang, and a link out of the project listed routes from outside it); route files reached only through a symlink are no longer listed. `generate:ts` clients send the `rullst_csrf` cookie value as `X-CSRF-Token` on state-changing requests and set `credentials: 'same-origin'`, so they pass `csrf_middleware` instead of receiving 403; the constructor gains an optional `RullstClientOptions` argument (`csrfToken`, `credentials`). Regenerate `rullst-client.ts`. `make:live` now requires a Rullst project root, and `make:live`, `make:controller` and `make:middleware` reject names whose module or type is not a non-keyword Rust identifier (such as `../notes`, `self` or `Bad.Name`) instead of writing outside `src/live` or an unparsable `mod.rs`. New projects' `.gitignore` also lists `*.duckdb`, `*.duckdb.wal`, `*.sqlite-shm` and `*.sqlite-wal`, matching `.dockerignore`; add them to the `.gitignore` of an existing project, particularly one created with `--duckdb` (`DUCKDB_PATH=analytics.duckdb`). | | ORM database-managed columns | `#[orm(skip)]` means "not a database column for the ORM": generated filters, ordering, grouping and `select` reject it with a `Validation` error that now says so and names `#[orm(db_default)]`. A column the table has that ORM writes must not touch (such as `users.is_admin` and `users.session_epoch`) is declared `#[orm(db_default)]`: read, filterable (`where_eq("is_admin", true)`), never written by `INSERT`, `save()` or `update_partial()`, and included in `to_json()`, audit and event payloads and query-cache entries (where the skipped field previously came back as `Default`). New SaaS, LMS and `cargo rullst auth` `User` models use it; in an application generated by 13.0.0-alpha.2 replace `#[orm(skip)]` with `#[orm(db_default)]` on those two fields (`cargo rullst upgrade` reports `V13-ORM-SKIP-MANAGED-COLUMNS`). | | ORM dirty tracking | Opt-in: a model that declares `#[sqlx(skip)] pub snapshot: RowSnapshot` (`rullst_orm::RowSnapshot`, also `rullst::db::RowSnapshot`) makes `save()` of a loaded record write only the columns whose value changed and refresh the others from the stored row (`RETURNING *` on PostgreSQL and SQLite, a re-read on MySQL/MariaDB). An unchanged save runs no `UPDATE` and no `updating`/`updated`/`saved` observer, `after_save` hook, audit entry or event, and without a `before_save` hook, policy or observers opens no transaction. `update_partial()` on such a model writes only the patched columns that differ. Call `mark_all_dirty()` before `save()` where code relied on overwriting concurrent changes. `UPDATE OF ` triggers fire only for written columns, and a `before_save` hook that stamps `updated_at` on every call makes every save write (check `is_dirty()` first). Struct literals need `snapshot: RowSnapshot::default()`, and models deriving Serde need `#[serde(skip)]` on the field. `make:model` adds the field to new models; from 13.0.0-alpha.3 every SQLx model the CLI writes declares it (`generate:models`, `cargo rullst auth`, `make:billing`, `make:chat-session` and the Blank, Blog, ERP, Portfolio, SaaS and LMS starters), so new projects save only changed columns. Models without it, including those of projects generated earlier, are unchanged. | | ORM partial updates | `update_partial()` now merges into a freshly loaded row and runs full-save policy/hooks/audit/post-commit behavior. It performs full-row SQL and refreshes the caller object; review audit identity, triggers, unsaved local fields and outer rollback. New `save_with_tx` is explicit. See [the migration contract](https://rullst.github.io/Rullst/book/transactional-partial-updates.html). | | ORM derive checks | Some models that compiled on 12.x now fail at compile time instead of misbehaving at runtime: `belongs_to` without `foreign_key` defaults to `_id` (it previously used `_id`, joining on the wrong column); `local_key` on `belongs_to`/`morph_to` and `related_key` on `has_one`/`has_many`/`morph_one`/`morph_many` are rejected because those relations ignore them; and `searchable` models need a lowercase, Scout-valid table name. Add an explicit `foreign_key`, remove the ignored option or set `#[orm(table = "...")]`. | | ORM queries and cache | `count()`/`paginate()` totals count DISTINCT rows and GROUP BY groups; offset `chunk()` pages are ordered by primary key unless `order_by` is set; `SecretString` columns are rejected in filters, ordering, grouping and `pluck_*`. While a transactional `stream()` is open, other ORM calls on that transaction return `Validation` instead of waiting. Models without a `tenant_column` share one global query-cache partition, so cache keys built manually with `query_cache::query_key` inside `with_tenant` for such models no longer match generated keys. Scout-backed `search()` results follow the engine's ranking unless `order_by` is set. Generated writes now bind `#[sqlx(json)]` fields as SQLx `Json` (JSON on PostgreSQL/MySQL instead of the field's own SQL type), so such a field type needs `Serialize`; a type that implemented only `Encode` no longer compiles. | | ORM query-cache index | Generated `.remember(...)` keys move to the `rullst:orm:cache:v4:` prefix, and each table index becomes a Redis sorted set (`:index`) scored by entry expiry instead of the `:keys` set. Entries written under the previous prefix are not read again and expire through their TTL, so caches start cold after the upgrade; during a rolling upgrade a write on an instance of the other version does not invalidate the new entries before their TTL. Index members of expired entries are pruned on each cache write and no longer count toward the 10,000-key `PostCommit` invalidation cap. Tools that inspected the old `:keys` set must read the sorted set. | | ORM Redis model hashes | Generated `save_to_redis`/`get_from_redis`/`increment_redis_field` hashes move from the shared 12.0/12.1 key `orm:
    :` to namespaced keys (`rullst:orm:hash:v1:::table-:`), and tenant models require `with_tenant(...)`. For a model without a tenant scope nothing has to be run: `get_from_redis` still reads the 12.x hash while the namespaced one is missing, and the next `save_to_redis` or `increment_redis_field` moves it to the namespaced key in one Redis script (an existing namespaced hash wins and the stale 12.x hash is removed); a 12.x hash with `#[orm(encrypted)]` fields holds them in plaintext and fails closed until `save_to_redis()` rewrites it. Tenant models never read the 12.x key, which every tenant shared: until you run the one-time procedure in [Model hashes stored by 12.0 and 12.1](https://rullst.github.io/Rullst/book/redis-guide.html#model-hashes-stored-by-120-and-121), `get_from_redis` returns `None` and `increment_redis_field` starts from zero. A 12.x instance never reads a moved hash and a 13 instance ignores 12.x writes once the namespaced hash exists, so stop 12.x writers of these hashes before 13 instances write them instead of mixing versions. | | ORM outbox keys on MySQL/MariaDB | Outbox streams, event keys and claim tokens compare case-sensitively on every backend; new MySQL/MariaDB `rullst_outbox` tables declare those columns `ascii_bin`. A table created by 12.x keeps the server's case-insensitive collation without any error, so keys that differ only by letter case still share idempotency and claims. Run `Outbox::install()` once, for example from a new migration (an `OutboxMigration` that already ran is not run again), to convert the table in place; it rebuilds the table, so schedule it like other DDL. See the [transactional outbox tutorial](https://rullst.github.io/Rullst/book/tutorials/38-transactional-outbox.html). PostgreSQL and SQLite tables are unchanged. | | ORM audit payloads on MySQL/MariaDB | New MySQL/MariaDB `rullst_audits` tables store `old_values`, `new_values` and `restore_patch` as `LONGTEXT`. A table created by 12.x keeps `TEXT` (64 KiB): `create_audit_table` only logs a warning, and an audited change with a larger payload still fails in strict SQL mode (or is truncated otherwise). Apply `ALTER TABLE rullst_audits MODIFY old_values LONGTEXT, MODIFY new_values LONGTEXT, MODIFY restore_patch LONGTEXT` in a maintenance window, as described in [auditable revisions](https://rullst.github.io/Rullst/book/tutorials/50-auditable-revisions.html#1-mark-the-model-and-install-the-table). | | Turso migrations | `TursoStore::rollback_last` and `TursoOrm::rollback_last` now refuse, with the same drift error as `migrate`, a latest migration whose digest (its up and down statements) differs from the recorded one, instead of running the changed down statements and deleting the history row. Restore the applied definition to roll it back. | | Schema table names on PostgreSQL | `Schema::create` and `Schema::drop_if_exists` lower-case the quoted table name on PostgreSQL, matching the unquoted names generated model SQL uses, so `Schema::create("UserProfiles", ...)` now creates `userprofiles`. A table an earlier version created as `"UserProfiles"` is not dropped by `drop_if_exists("UserProfiles")` and stays unreachable by generated SQL; rename it with `ALTER TABLE "UserProfiles" RENAME TO userprofiles`. MySQL/MariaDB and SQLite are unchanged. | | Legacy `SecretString` ciphertext | Pre-v12 `SecretString` values, which carry no key ID, now decrypt with the current key and then each `RULLST_ENCRYPTION_KEYRING` key (`default` first), so they stay readable after a documented rotation. When the current key does not match, a malformed keyring now fails the read with `PrivacyError::EnvError` instead of `DecryptionFailed`. | | `SecretString` client input | `SecretString`'s `Deserialize` still decrypts any serde envelope under the configured key or keyring, and that envelope binds no model, field or owner. Review request types that deserialize a `SecretString` from client input, including models used directly as JSON bodies, and add `#[serde(deserialize_with = "rullst_orm::privacy::deserialize_plaintext_secret")]` (or `deserialize_optional_plaintext_secret`, v13 prerelease API), which rejects envelope-shaped values without decrypting them. | | ORM protected values and `SecretString` serialization | New audit rows and committed events (including Redis `orm:events:*`) write `#[orm(encrypted)]` and `#[orm(masked)]` values as `"***"`, Scout documents omit them, and `save_to_redis` stores encrypted fields as the SQL envelope; a change to such a field makes its audit revision non-restorable. `SecretString` serializes as an encrypted `RULLST:v2` envelope (`"***"` in generated projections) instead of its plaintext, and serializing it without `RULLST_ENCRYPTION_KEY` fails. Audit rows, search documents and events written by 12.x are not rewritten: purge or reindex them if they may contain plaintext. A 12.x process that deserializes a `SecretString` serialized by 13 (queue jobs, caches, JSON exchanged between services) takes the envelope as the plaintext value, so upgrade every reader before the writers and do not exchange such payloads between versions. | | Offline Redis mock | The offline `RedisDataStore` mock returns equal `sorted_set_top` scores in descending lexicographic member order, like Redis `ZREVRANGE`, instead of ascending order. Update tests that relied on the old tie order. | | SQLite DSN paths | The missing-file preparation before connecting percent-decodes the SQLite DSN path as SQLx does, so `sqlite://data%20dir/app.db` creates `data dir/app.db`, the file SQLx opens, instead of a stray `data%20dir/app.db`. Stray encoded-name files left by earlier versions can be removed. | | Auto-healing diagnostics | `SchemaErrorInterceptor::diagnose_sql_error` returns `None` for constraint violations, `already exists` errors, other messages that merely mention a relation, and names that are not plain identifiers. Missing-table suggestions use SQLite or MySQL DDL when the message comes from those drivers, and SQLite `no such column` and MySQL `Unknown column`/`Table ... doesn't exist` messages are recognized. | | ORM migrations | The `Migration` trait gains `within_transaction()` (default `false`, v13 prerelease API). Returning `true` makes the runner apply `up()`/`down()` and its `migrations` record in one `Orm::transaction`; only statements that join it (`Schema`, `Orm::raw(..).execute()`, `execute_query!`, generated model methods) are covered, and MySQL/MariaDB DDL still commits implicitly. The template written by the ORM artisan `make:migration` now opts in, so such a migration that sends queries to `Orm::pool()` directly must return `false`. Existing migrations keep the separate statements. `db:seed` now fails, without running any seeder, while a migration registered with `artisan!` is not recorded in the `migrations` table; run `db:migrate` first. | | ORM transactional migrations and the pool | Since 13.0.0-alpha.3, while the `up()`/`down()` of a migration that returns `true` from `within_transaction()` runs, `Orm::pool()`, `Orm::try_pool()`, `Orm::read_pool()`, `Orm::try_read_pool()` and `Orm::begin_transaction()` return `Error::Validation` naming the migration, instead of a connection outside the migration's transaction; the run then fails (`DatabaseError` with `Transaction failed: Validation error: ...`) and the migration rolls back unrecorded. Send that SQL with `Orm::raw(sql).execute()`, `execute_query!` or `Schema`, or return `false` from `within_transaction()`. Migrations that keep the default `false` are unaffected. | | ORM instance mutations | `save()` of a row deleted after it was loaded, and `delete()`/`force_delete()` of a missing row, now fail with `RecordNotFound` (tenant models keep their tenant-scope `Validation` error) instead of returning `Ok` and running observers, audit and post-commit effects. A second soft `delete()` of a trashed row fails the same way instead of re-stamping its deletion time, and `restore()` of a missing or live row is a no-op on every model. `cascade_soft_delete` now also trashes children that the child model's `global_scope` hides. A model whose only persisted field is `id` can now be inserted. An auditable `save()` locks the row it diffs (`FOR UPDATE` on PostgreSQL/MySQL), `deleted`/`force_deleted`/`restored` audit entries record the persisted row instead of the in-memory handle, and that read, revision restores, `restore()` re-reads and `belongs_to_many` pivot loads now fail with the configured query timeout instead of waiting indefinitely. Callers that deleted idempotently must accept `RecordNotFound`. | | ORM constraint errors | A unique or primary-key violation now returns `Error::UniqueViolation { constraint, .. }` and a foreign-key violation `Error::ForeignKeyViolation { constraint, .. }`, from generated writes, query builders and `Orm::raw`, instead of `Error::DatabaseError(String)`. Their message names only the constraint (PostgreSQL) instead of quoting the database's text, so code that matched `DatabaseError` or searched the message for `UNIQUE constraint failed`, `duplicate key` or `Duplicate entry` must match the variants instead. `Orm::transaction` returns them unchanged too (see ORM transaction errors). | | ORM transaction errors | `Orm::transaction` returns `Result`, where `E` is its closure's error type and must implement `From` and `Display`, instead of `Result` (13.0.0-alpha.3). After the rollback the closure's error reaches the caller unchanged instead of `Error::DatabaseError("Transaction failed: ...")`, so a `UniqueViolation`, `ForeignKeyViolation` or the application's own error keeps its type; begin, commit and post-commit failures arrive through `E::from`. A closure whose error type lacks `From` (such as `String`) no longer compiles: return `rullst_orm::Error` or an application error that wraps it. Code that matched `DatabaseError` on the result must match the closure's own error instead; `cargo rullst upgrade` reports `Orm::transaction` calls in files that mention `DatabaseError` (`V13-ORM-TRANSACTION-ERRORS`). | | ORM encryption key settings | `RULLST_ENCRYPTION_KEY`, `RULLST_ENCRYPTION_KEY_ID` and `RULLST_ENCRYPTION_KEYRING` are now also read from `.env` in the current working directory when the process environment does not set them, as `APP_KEY` already was; the process environment still wins and the file is never exported. A `.env` that sets them now takes effect for `#[orm(encrypted)]` fields and `SecretString`, so remove stale entries; a malformed `.env` now fails such a lookup when the process lacks the variable. | | ORM query values | `RullstValue` is now `#[non_exhaustive]` and gains `I64(i64)` and `Null`, so a `match` on it outside the ORM needs a wildcard arm. `i64` and `u32` values now convert to `I64` (bound as `BIGINT`) and `Option` to `T`'s value or `Null`, so `where_eq("lesson_id", Some(3))` and `BIGINT` filters compile. `Null` binds as a text-typed `NULL`; on PostgreSQL cast it for a column of another type, and filter missing values with `where_null`. Integer literals still convert to `Int` (`i32`). The `chrono` types `DateTime`, `NaiveDateTime`, `NaiveDate` and `NaiveTime` convert to the new `DateTime`, `NaiveDateTime`, `Date` and `Time` variants; strict driver pools bind them natively and the SQLx `Any` pool as ISO-8601 text. | | ORM date/time columns | With a strict driver feature (`strict-postgres`, `strict-mysql` or `strict-sqlite`, which generated projects use), `Blueprint::timestamps()` now creates `timestamp_tz()` columns instead of `TEXT DEFAULT CURRENT_TIMESTAMP`: `TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP` on PostgreSQL, `DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)` holding UTC on MySQL/MariaDB and RFC 3339 `TEXT` (`2026-10-15T09:30:00.123+00:00`) on SQLite. Read them as `rullst::orm::chrono::DateTime`, set with `Utc::now()`, or as `Option>` marked `#[orm(db_default)]` so inserts keep the database default; a `String` field no longer decodes them on PostgreSQL or MySQL/MariaDB (on SQLite it still does, but the default text changes from `YYYY-MM-DD HH:MM:SS` to the RFC 3339 form). Only tables created from now on change, including a fresh database built by an existing migration that calls `timestamps()`; existing `TEXT` columns stay until a reviewed migration converts them, for example `ALTER TABLE t ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at::timestamptz` on PostgreSQL. The SQLx `Any` pool (the `drivers-all` default without a strict feature) cannot read date/time values, so there `timestamps()` keeps the `TEXT` columns and the new `date()`, `time()`, `timestamp()` and `timestamp_tz()` columns fail on PostgreSQL and MySQL/MariaDB with `Error::Validation` before any DDL runs. `make:migration:auto` declares `chrono` fields with those helpers instead of `string()`. Nexus now reads `Date`/`DateTime` fields as text on PostgreSQL and MySQL/MariaDB so native columns display, and writes them as typed values (see "Nexus dates and times"). `cargo rullst upgrade` reports `String` `created_at`/`updated_at` fields of SQLx models. | | Schema builder constraints | `Column` gains the public field `is_unique` (set by `Column::unique()`), so building a `Column { .. }` with a struct literal needs it; use `Column::new`. `Schema::create` also runs the `CREATE INDEX` statements of `index()`/`unique_index()` on PostgreSQL and SQLite, and `Blueprint::build()` includes the table's unique, foreign-key and CHECK constraints. `Error` gains `UnsupportedSchemaChange { driver, change }`, returned by `Schema::table` for changes SQLite cannot make to an existing table; a `match` on `Error` outside the ORM already needs a wildcard arm. | | ORM sandbox tests | `#[rullst_orm::test]` keeps the function's declared return type and returns the body's value after the rollback, so a test returning `Err` now fails instead of passing. A test whose declared type does not match its body, or is not a `Termination` type such as `Result<(), E: Debug>`, no longer compiles; fix the signature. Post-commit effects of sandboxed writes (`committed` observers, cache invalidation, Redis events, Scout) no longer run, because the sandbox never commits; assert them in a test that commits through `Orm::transaction`. | | ORM model SQLx options | A struct-level `#[sqlx(...)]` option other than `default` on a `#[derive(Orm)]` model, such as `rename_all = "camelCase"`, no longer compiles: `FromRow` decoded the renamed columns while generated SQL used the field names, so writes or reads failed at runtime. Name the fields after their columns instead. | | ORM trashed scopes | `only_trashed()` on a model without soft deletes now fails with `Validation` instead of returning, counting or bulk-deleting every live row as if it were trashed (an "empty trash" `only_trashed().delete_all()` deleted the whole table). Remove the call, or give the model a `deleted_at` field or `#[orm(soft_delete)]`. | | ORM bulk soft deletes | On soft-delete models, `delete_all()` with `with_trashed()` or `only_trashed()` now fails with `Validation` instead of re-stamping the deletion time of rows that were already trashed, which kept age-based purges from ever seeing them expire. Bulk-delete live rows without these modifiers and purge trashed rows with `force_delete()`. | | ORM joined scopes | Generated tenant and soft-delete predicates and the `chunk_by_id` keyset are now qualified with the model's table (`orders.tenant_id = ?`, `orders.deleted_at IS NULL`, `ORDER BY orders.id ASC`), so a lazy `belongs_to_many` load or a join with a table that has the same columns no longer fails with an ambiguous column. Update tests that compare generated SQL text; Redis query-cache entries are keyed by the new SQL and are rebuilt on the next read. | | ORM chunk bounds | `chunk()`, `chunk_with_tx()`, `chunk_by_id()` and `chunk_by_id_with_tx()` now honour an explicit `limit(n)` as a cap on the total rows handed to the handler and an explicit `offset(k)` as the starting point, instead of silently walking every matching row from the first one. Remove `limit()`/`offset()` from chunked queries that relied on them being ignored. | | ORM eager pivot order | An eager `belongs_to_many` load now orders each parent's related rows like the related query, so an `order_by` in `with__constrained` applies, instead of following the order of the unordered pivot rows. Without an `order_by` the order remains unspecified. | | PersonalData reports | `compliance_schema()` now reports the table `#[derive(Orm)]` uses (`#[orm(table)]` or the `s` default, previously the lowercased struct name) and lists in `encrypted_fields`/`has_encrypted_data` only `#[orm(encrypted)]` and `SecretString` columns; `#[privacy]` fields, which are not encrypted by the derive, are listed by the new `ComplianceModel::personal_fields()`. | | Nexus derive | A field annotated `#[nexus(primary_key)]` is now the record key even when a later field is named `id` (previously the last of them won, and both were hidden and read-only). An annotation that contradicts the struct-level `#[nexus(primary_key = "...")]` no longer compiles; since alpha.2, several annotated fields declare a composite key (see "Nexus composite keys"). A non-integer derived key (a UUID or code) is now listed and entered in the create form instead of being hidden and read-only; mark it `#[nexus(readonly)]` or `#[nexus(hidden)]` to keep the previous behaviour for a database-generated key. Fields typed `chrono::DateTime`, `DateTime` or `NaiveDateTime` now get the validated date-time widget instead of free text. Rust integer fields (`i8` to `i64`, `u8` to `u64`, `isize`, `usize`, also under `Option` or `kind = "number"`) now derive `FieldKind::Integer { min, max }` bounded by their type (`u64`/`usize` capped at `i64::MAX`) instead of `Number`, so Nexus rejects fractions, exponents and out-of-range values with `422`; code comparing derived metadata with `FieldKind::Number` must expect `Integer`. | | Nexus composite keys | `NexusModel::nexus_pk_columns()` (default: `vec![nexus_pk()]`) declares a composite primary key, and `RegistryEntry` gains `pk_columns: Vec<&'static str>` (empty means the single `pk`): code that builds a `RegistryEntry` with a struct literal must add `pk_columns: Vec::new()`. In `#[derive(Nexus)]`, `#[nexus(primary_key = "a, b")]` or `#[nexus(primary_key)]` on several fields now declares a composite key (two annotated fields used to be a compile error); every column of a composite key is entered on create, even an integer one. A composite record is addressed by its values, each percent-encoded and joined by `,` (`7,intro`), in URLs, batch selections and the audit `record_key`. Registry validation rejects more than 8, repeated, unregistered or tenant key columns. | | Nexus dates and times | Date and time fields are written as typed values instead of text, so native PostgreSQL `DATE`/`TIME`/`TIMESTAMP`/`TIMESTAMPTZ` columns accept edits (PostgreSQL writes go through a cast; a text column still stores text). `#[derive(Nexus)]` now maps `chrono::DateTime` to the new `FieldKind::DateTimeUtc` (it was `DateTime`) and `NaiveTime` to the new `FieldKind::Time`; `kind = "datetime_utc"` and `kind = "time"` select them. A `DateTimeUtc` field's `datetime-local` input shows and accepts wall times in the panel's time zone, UTC unless `Nexus::with_time_zone` selects another `NexusTimeZone` (13.0.0-alpha.3; IANA zones need the `nexus-tz` feature): a value without an offset is a wall time in that zone, one with `Z`, `±HH:MM` or `±HH` is converted, and a text column stores RFC 3339 (`2026-10-15T09:30:00+00:00`). The handlers `nexus_new_form`, `nexus_edit_form`, `nexus_create_record` and `nexus_update_record` take an extra `Option>` argument, which only code calling them directly sees. Code comparing derived metadata with `FieldKind::DateTime` for a `DateTime` field must expect `DateTimeUtc`. A `Date` (and the date of a date-time) with year `0000` is now a `422`. A local date-time or time with a fraction is stored with chrono's 3, 6 or 9 digits (`10:00:00.5` becomes `10:00:00.500`). Hand-written metadata keeps working; switch `DateTime` fields to `FieldKind::DateTimeUtc` for UTC handling, as the starters now do for `created_at`/`updated_at`. | | Nexus access in the starters | New SaaS and LMS projects mount Nexus with `NexusAuthPolicy::local_development_or_application(middlewares::nexus_access::NexusAdmins)`: debug builds keep loopback access, and release builds admit the signed-in user whose new `users.is_admin` column (Boolean, default false, `#[orm(db_default)]` on `User`, editable in Nexus) is true, with no `NEXUS_ADMIN_*` credentials (their `.env` no longer lists them). Grant the first administrator with `UPDATE users SET is_admin = TRUE WHERE email = '…';`. To adopt this in an existing SaaS/LMS application, add a migration with `table.boolean("is_admin").not_null().default(ColumnDefault::Integer(0))` (through `Schema::table`), the `#[orm(db_default)] pub is_admin: bool` field, and copy `src/middlewares/nexus_access.rs` from a new project. `cargo rullst auth` users also get `is_admin`, and its controller now sets `is_admin: false` when registering: a regenerated controller needs the field in an older `User` model. New Blog, Portfolio and ERP projects without `NEXUS_ADMIN_*` in a release build now start without Nexus (the ERP without its back office, serving only `/health` and `/ready`) and print one line naming the variables, instead of refusing to start; a weak or placeholder credential still stops startup. | | Nexus credentials | `NexusAuthPolicy::basic_from_env` (and release builds of `local_development_or_basic_from_env`) reads `NEXUS_ADMIN_USERNAME`/`NEXUS_ADMIN_PASSWORD` from the process environment, then `.env` in the working directory only, through `rullst::config::project_setting`. It no longer loads `.env` (or a parent directory's `.env`) into the process environment, so code that relied on that side effect must read its own settings; a malformed or unreadable `.env` now fails with the new `NexusBuildError::InvalidDotenv` instead of being ignored. | | Nexus panel | A list `sort_by` naming a hidden column (for example a derived `password_hash`) now orders by the record key, as a `Password` column already did; bookmarked URLs that sorted by such a column show key order. A model without a visible text, textarea, e-mail or URL field shows no search box, and a `q` sent to it lists no rows instead of every row. With the debug loopback policy, an IPv4-mapped loopback peer (`::ffff:127.0.0.1`, reported by a dual-stack `HOST=::` listener) is now accepted as loopback instead of receiving 403; other mapped addresses stay rejected. That policy also accepts a mutation with `Origin: null` when it carries `Sec-Fetch-Site: same-origin` (what browsers send under `Referrer-Policy: no-referrer`) and stamps `Referrer-Policy: same-origin` on responses that set none. A create or update rejected by validation now answers `422` with a plain-text message (`Invalid
    .rs`/`*_create_
    _table.rs` migration already exists, because its rollback would drop the live table; add schema changes with `make:migration`. `generate:models`/`make:models-from-db` refuse to replace an existing `
    .rs` and append missing declarations to an existing `mod.rs` instead of rewriting it; move colliding files aside or use another `--output`. `make:k8s`, `dockerize`, `nixify` and `generate:buildah` no longer replace existing manifests, `Dockerfile`, `flake.nix`, `.envrc` or `build_buildah.sh`; move a file aside to regenerate it. `cargo rullst auth` rejects Turso-primary projects, refuses to replace existing user/auth files or to add a second users-table migration, enables the `orm` and `auth` features and registers its modules in the crate root. `make:mfa` now generates a server-side factor store (`user_mfa_factors` migration, replay-protected verification bound to the authenticated `Extension` user id) instead of a handler that verified a client-supplied secret; it enables `orm` and `security`, rejects Turso-primary projects and refuses to overwrite. Replace a previously generated `mfa.rs` that accepted a `secret` form field. | | CLI operations | `deploy --platform fly` and `--platform railway` now exit non-zero when the installed `flyctl deploy` or `railway up` fails, instead of printing a hint and exiting 0; a missing provider CLI is still advisory. Update scripts that relied on the old exit status. `update project verify` resolves the candidate with `cargo update --workspace` instead of `cargo generate-lockfile`, so unrelated `Cargo.lock` pins are kept; verifications recorded with the old command no longer pass `review`/`apply` and must be run again. Project-copy Cargo/rustc commands pin `RUSTUP_TOOLCHAIN` to the caller's toolchain (inherited value, otherwise rustup's default) and ignore the project's `rust-toolchain(.toml)`; set `RUSTUP_TOOLCHAIN` to verify with a project-pinned channel. `foundry:deploy` runs the service as a dedicated `rullst-` system account (it now requires `useradd`) in a sandboxed unit instead of root; existing `/opt/rullst//data` is re-owned, and the application can write only there, so move state written elsewhere (for example an absolute SQLite path) under that directory first. | | CLI diagnostics and side effects | `generate:diagram` writes a marker comment at the top of `diagram.md` and refuses to replace a `diagram.md` it did not generate; the refresh after `make:controller`, `make:model`, `make:middleware`, `make:worker`, `make:island`, `auth`, `make:billing`, `make:cors` and `make:jwt` keeps such a file and prints a notice. A diagram from an earlier release (a single unmarked Mermaid block) is still replaced. Move a hand-written `diagram.md` aside to regenerate it. `audit --compliance` and `audit --sbom` fail instead of writing through a symlinked `SECURITY_COMPLIANCE.md` or `sbom-cyclonedx.json`. The `audit` source scans and `generate:diagram` no longer follow symlinked files or directories, so code reachable only through a link is not scanned; a source tree deeper than 64 levels or larger than 250,000 entries makes the audit fail as incomplete. `audit --network` now exits non-zero, with an `ERROR` network row in the report, when the TCP listener inventory (`ss -ltnH`, from Linux iproute2) cannot run, which is always the case on macOS and Windows; it previously reported a clean scan. In `sbom-cyclonedx.json`, only crates.io packages keep the plain `pkg:cargo/@` purl: git and other-registry packages gain a `vcs_url` or `repository_url` qualifier, path and workspace packages lose their purl, and these components carry a `rullst:cargo:source` property. Update SBOM consumers that matched those purls. `pkg add` without a package name and an unknown `pkg` action now exit non-zero instead of printing an error and exiting 0. `deploy` rejects an unknown `--platform` before scaffolding a missing `Dockerfile` and `.dockerignore`. `make:k8s` and the `fly.toml` written by `deploy --platform fly` derive names from the `[package]` name as a lowercase RFC 1123 label (`my_app` becomes `my-app`, the image `my-app:latest`, the host `my-app.local`); manifests generated earlier keep their names, so rename the resources or the image tag if you regenerate them. `cargo install cargo-rullst` no longer installs the internal `sync-badges` repository tool: it requires the new `maintainer-tools` feature, which the repository's `cargo sync-badges`/`cargo sync` aliases enable. | | CLI home screen | `cargo rullst` without a subcommand shows the new opening (the RULLST wordmark and `v · SECURE, FAST AND AI-NATIVE RUST FRAMEWORK`) instead of the `RULLST v… // SECURE • FAST • …` tagline and the `COMMAND INTERFACE READY` line, followed by a summary of the detected project. Inside a project the main menu starts with dev, dash, scaffolding, database, `doctor` and deploy; the former `Already have a project?` operations remain under `All Project Operations`. From a project subdirectory, menu commands run at the project root. When standard input, output or error is not a terminal, `CI` is set or `TERM=dumb`, it prints the plain home and exits 0 instead of failing at the first prompt; scripts that matched the old banner text or relied on that failure must change. The opening animates on every run; since 13.0.0-alpha.4 `RULLST_OPENING=daily` limits it to the first run of a day (the date is stored under the user cache directory, `rullst-ui-v1`) and `RULLST_OPENING=static` never animates. See the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-no-arguments-home-screen). | | CLI security audit | Run from a package directory, `audit` now also scans the `src` of every workspace member below it (from `cargo metadata`) for unsafe code and parameterized routes instead of only `./src`. `audit --idor` and `audit --network` skip each top-level `#[cfg(test)]` item on its own instead of ignoring the rest of the file after the first one, so routes and listeners declared after an early `#[cfg(test)] mod tests;` are now scanned and can report new findings. `audit --idor` also checks every quoted path on a route line, not only the first, so the later routes of a one-line `routes!` list are reported, and a comment between `.route(` and its path literal no longer hides that path from the check (place the marker on that comment line). Guard names in comments, doc comments or `#[cfg(test)]` items no longer satisfy an `owner`, `role` or `admin` classification. Classify such routes with an adjacent `// rullst-access:` marker. | | CLI dashboard audit | The dashboard's security audit item adds `--geiger` only when `cargo geiger --version` succeeds, so it no longer fails on a machine without cargo-geiger. Run `cargo rullst audit --geiger` to require the dependency inventory. | | Core deferrable jobs | New in 13.0, opt-in: `Queue::dispatch_deferred` and `dispatch_deferred_with` place a job by a deadline, daily `TimeWindow`s and an optional application-provided `CarbonIntensitySource` (deterministic `FixedIntensitySource` for tests; no network source in Core), and `Scheduler::deferrable_task` defers recurring runs into windows. Placement reuses `dispatch_at`, so SQLite/Redis need no schema change and 12.x rows keep working; custom drivers without durable scheduling reject future placements with `QueueError::Unsupported`. Existing `dispatch`, `dispatch_at` and `Scheduler::task` are unchanged. See [deferrable jobs](https://rullst.github.io/Rullst/book/crates/core.html#deferrable-jobs-and-time-windows). | | Core scheduler time zones | New in 13.0.0-alpha.2, opt-in with the `scheduler-tz` feature: `Scheduler::task_in_tz(expression, "America/Sao_Paulo", handler)` matches the cron fields against that zone's wall clock, so a task written in UTC to stand for a local time (`0 10 * * *` for 07:00 in São Paulo) can state the local time instead. `Scheduler::task` is unchanged and stays in UTC. 13.0.0-alpha.3 adds `Scheduler::deferrable_task_in_tz`, the zoned form of `deferrable_task`, whose windows are wall times of the zone. `SchedulerError` gains `InvalidTimeZone`; matches are unaffected because the enum is `#[non_exhaustive]`. See [time zones for scheduled tasks](https://rullst.github.io/Rullst/book/crates/core.html#time-zones-for-scheduled-tasks). | | Dash N+1 warning | New in 13.0, opt-in by running `cargo rullst dash`: the development telemetry's observed `database` object gains `repeated_threshold`, `repeated_queries_total` and `recent_repeated` (ORM operation fingerprints a single request repeated at least three times, by matched route), and the dash shows a **POSSIBLE N+1 QUERIES** panel. The `rullst.dev-telemetry.v1` schema stays compatible: older dashboards ignore the fields, and a newer dashboard shows "N+1 detection needs request-correlated ORM telemetry" for an older application. Studio's `N_PLUS_ONE_THRESHOLD` now re-exports `rullst_core::query_patterns::N_PLUS_ONE_THRESHOLD` (still 3). See [N+1 query warning](https://rullst.github.io/Rullst/book/cli_reference.html#n1-query-warning). | | CLI security report | New in 13.0: `cargo rullst audit --report` (`md` by default, or `html` or `json`, with optional `--output `) writes `SECURITY_REPORT.`, an evidence report mapped to OWASP ASVS 5.0.0 Level 1 with a personal-data inventory, accessibility heuristics and a `NOT EVALUATED` list, and exits 1 when a check reports `FINDINGS` or `ERROR`. No change for existing commands: `audit` without `--report` keeps its checks, output, `rullst.cli-audit.v1` JSON and exit status. See the [security report guide](https://rullst.github.io/Rullst/book/security-report.html). | | Generated security tests | New projects get `src/security_tests.rs`, declared under `#[cfg(test)]` (`all(test, debug_assertions)` for Nexus starters) in `src/main.rs`, which now builds its router in `fn router()` that `main` passes to `Server::new`. `cargo test` runs the checks offline against that router wrapped in the staging/production baseline: security headers, CSRF, WAF, plus the SaaS/LMS sign-in rate limit, the LMS lesson owner check and the Blank JSON API bearer token. Existing projects can copy the file from a new project and extract their router the same way; nothing changes at runtime. | | Blank JSON API starter | The `--api` starter adds `POST /api/messages`, registered with `Server::with_machine_endpoints` as a bearer machine endpoint, and a random `API_TOKEN` in `.env` (empty in `.env.example`). JSON clients send `Authorization: Bearer `; requests with cookies are refused, and startup fails when `API_TOKEN` is unset or weaker than 32 characters. Previously the starter had no write route, and the first one added was refused with 403 for JSON clients in staging and production because the baseline requires CSRF. Existing API projects keep their behavior; to adopt it, register each JSON write route as shown in `machine_endpoints()`. | | CLI audit lockfile | `cargo rullst audit`, `audit --report` and `audit --sbom` in a workspace member, which has no `Cargo.lock`, now use the workspace root's lockfile found through `cargo metadata` (passed to `cargo audit` as `--file`) instead of failing the dependency check. The SBOM then lists every package of that lockfile. | | RASP shell signatures | `RaspSecurityLayer` and `RaspInspector` match `\| sh` only when `sh` ends at a word boundary, and `; cat` only when a path-like argument follows (a path, dotfile, `~`, `$`, option or glob). Prose such as "Add it to the cart \| shopping list" or "dogs; cat food" is no longer refused with 403; `; cat /etc/passwd` still is, while `; cat` followed by a bare word no longer is. | | CLI footprint report | New in 13.0.0-alpha.1, renamed `cargo rullst green` in 13.0.0-alpha.4 (see the CLI `ai` removed, `footprint` renamed `green` row): `cargo rullst footprint` builds the release binary (or measures a loopback `--url`), runs a bounded closed-loop load and reports requests/s, latency p50/p95/p99, errors, process CPU time, peak and idle RSS, binary and Docker image size, energy from readable RAPL counters or a labelled `--cpu-watts` estimate, and an SCI figure (ISO/IEC 21031:2024) only from a user-provided `--grid-intensity`; `--json` prints `rullst.cli-footprint.v1`. Unavailable values are `NOT MEASURED` with the reason. No change for existing commands. See the [green guide](https://rullst.github.io/Rullst/book/green.html). | | CLI `dev`/`dash` shutdown | SIGTERM and SIGHUP (and closing the console on Windows) now stop `dev` and `dash` like Ctrl+C: the supervised application and its process group are stopped and the `rullst-dev-*` snapshot removed. Previously the CLI died on the default signal action, leaving the application holding its port. Scripts that relied on the application outliving a terminated CLI must start it separately. | | CLI `dev --ts-sync` | The flag now regenerates `rullst-client.ts` after the initial build and after every successful rebuild, and reports a failed generation in the dev log. It previously generated the SDK once before startup, ignored errors, and never refreshed it. | | CLI `dash` live metrics | `dash` polls the application's loopback `GET /_rullst/dev-telemetry` once a second and shows requests/s, latency (p50/p95 and a sparkline), errors, recent requests and, when the application reports them, ORM query and queue figures. An application that does not serve the endpoint shows a "telemetry not available" panel with the steps to enable it instead of numbers, and figures from a process the dashboard did not start (another application on the same port) are never shown. `r` now restarts the application from the current build (refused while a migration or a rebuild runs) and `?` opens a help overlay that `Esc` closes before it quits the dashboard. The footer lists `r` and `?` and shows `d` and `Tab` only on terminals at least 120 columns wide. Both log panes now keep the newest line visible when earlier entries wrap; previously the newest rows could fall below the fold. `cargo rullst dev` prints one extra stderr line at startup suggesting `cargo rullst dash`; scripts that compare its exact output must allow it. See the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-dash). | | Core development telemetry | In a debug build running in Development under `cargo rullst dev`/`dash` (`RULLST_DEV_GENERATION` set), `Server` also mounts `GET /_rullst/dev-telemetry` for loopback peers with a loopback `Host` (HTTP/1.0 requests and requests with proxy forwarding headers receive `404`), records each request's method, path (no query string), status and duration in a bounded in-process recorder from an outermost layer (so panics answered by the development error console and the responses of the security, lifecycle and traffic layers are counted; development polls and `/static` files are not), and keeps that poll out of the access log, rate limiter and Traffic Shield. An application route at that path conflicts with it, as with the other `/_rullst/*` development routes. In debug builds `telemetry::init_telemetry` (called by `Server::run`) adds a passive layer that counts `rullst.orm.query` spans (a `chunk`/`chunk_by_id` traversal counts its page fetches and its handler's operations, not itself); it records nothing unless the endpoint is mounted. The new `Server::with_dev_queue(queue)` reports a queue's pending count to the dashboard. Release builds, Staging and Production are unchanged. See the [telemetry guide](https://rullst.github.io/Rullst/book/telemetry-guide.html#development-dashboard-endpoint). | | Core development error console | The development panic page (debug build, Development, loopback peer) replaces its AI panel with an "Ask your AI assistant" panel: a read-only prompt to copy into your own assistant (error message, location, the first project stack frames, request method and path) and, when the error is recorded, the path of its JSON context; the page no longer calls `/_rullst/explain` or `/_rullst/autofix` and makes no network request. Within `Server`'s development stack the console records each rendered panic (message, location, project backtrace frames, request method and path without query string; never headers, cookies or bodies) under a random 128-bit id, at most 32 entries for 30 minutes, in process memory, and `Server` mounts `GET /_rullst/errors/{id}` with the console, answered only to direct loopback requests with a loopback `Host` and no proxy forwarding headers (others receive `404`). `POST /_rullst/autofix` (`handle_autofix`, still exported) no longer validates a path or returns an AI-offline message: loopback peers receive `410 Gone` with `success: false`, a message that points to that panel and `context_url: "/_rullst/errors/"`, other peers `403`. An application route at `/_rullst/errors/{id}` conflicts with it, as with the other `/_rullst/*` development routes. The hot-reload server shows the panel without a context path. Release builds, Staging and Production are unchanged. | | CLI `ai fix` and `ai review` | Added by an earlier 13.0 prerelease and removed with `cargo rullst ai` in 13.0.0-alpha.4 (see the CLI `ai` removed, `footprint` renamed `green` row): `cargo rullst ai fix ` and `cargo rullst ai review` no longer exist. No 12.x command changes. | | CLI project updates on SELinux | `update project review` and `apply` no longer reject a manifest or staged file whose only extended attribute is the SELinux label (`security.selinux`), so they work on Fedora/RHEL hosts; the replacement receives the label the policy assigns to a new file in that directory. ACLs, file capabilities and other attributes still require manual handling. | | CLI `pkg` in a virtual workspace | In a `Cargo.toml` with `[workspace]` but no `[package]`, `pkg add` writes the dependency to `[workspace.dependencies]` instead of a top-level `[dependencies]` table that made every later Cargo command fail, and `pkg list` lists that table. Reference the entry from a member with `{ workspace = true }`. | | CLI `new` repository | Like `cargo new`, `cargo rullst new` now runs `git init` in the new project when Git is installed and the destination is not already inside a Git work tree, and writes a `.gitignore` keeping `.env` and `/target` out when none exists. Pass `--vcs none` to skip it, for example when a script copies the generated folder into another repository. Without Git it warns and keeps the project. | | CLI generated `validator` dependency | Generated manifests declare `validator = { version = "0.21", features = ["derive"] }`, because `#[derive(rullst::Validate)]` expands to `::validator` paths. In a project generated earlier, add that line before deriving `Validate` on a `ValidatedForm`/`ValidatedJson` DTO. | | CLI `make:model` imports | SQLx models now start with `use rullst::db::{FromRow, Orm};` instead of also importing the unused `RullstModel` and `sqlx`, so a new model compiles under `-D warnings`. Existing model files are not touched. | | CLI `dockerize` and `generate:buildah` | The binary path in the Dockerfile and the Buildah image name come from `[package].name` parsed as TOML, as in `deploy`, instead of the first line starting with `name = `. A `[lib]` or `[[bin]]` table before `[package]`, `name="x"` without spaces, single quotes or a trailing comment no longer produce a wrong name. Regenerate files written from such manifests. | | CLI errors and exit codes | Failures are printed on standard error by one report (`error: ` with `What happened`, `How to fix` and `Docs`) instead of `Error: <message>`; the original message stays in `What happened`, with passwords, secret-like values and terminal control characters masked. Match the message text or `error: ` instead of `Error: `. Project commands run outside a project root (`make:model`, `make:controller`, `make:resource`, `make:middleware`, `make:worker`, `make:migration`, `make:migration:auto`, `make:cors`, `make:jwt`, `make:mfa`, `auth`, `db:*`, `studio`, `build`, `generate:openapi`, `generate:ts`) report this on standard error and exit 1 instead of printing to standard output and calling `process::exit(1)`; the public `generators::*` functions behind them now return an `Err` instead of terminating the calling process. An unknown command prints the closest commands and exits 2 (clap's usage status) with a new message. A command run from the home menu or the command palette that fails is reported once, by the command itself, and the CLI exits with its status; previously a second `dashboard command ... failed` error followed and the exit status was always 1. A panic prints a short report unless `RUST_BACKTRACE` or the new global `-v`/`--verbose` flag is set. An argument that is not valid UTF-8 is a usage error (exit 2) instead of a panic (exit 101). When standard output is not a terminal, or with `NO_COLOR`, `CI` or `TERM=dumb`, generator output is now plain text without ANSI colours. See [errors and exit codes](https://rullst.github.io/Rullst/book/cli_reference.html#errors-and-exit-codes). | | CLI `doctor` | `cargo rullst doctor` prints grouped ✓/!/✗/· checks with a fix and docs link per problem instead of `[OK]`/`[WARNING]`/`[NOT FOUND]` lines and `N checks passed, M warnings`, and adds project, configuration, database reachability, migration, security-baseline and disk-space checks. It now exits 1 when a check fails (a missing or pre-1.96 `rustc`, missing Cargo, an unreachable configured database, an unknown `RULLST_ENV`, an invalid `Rullst.toml`, a Git-tracked `.env`, less than 1 GiB free); it previously always exited 0, and the public `generators::doctor::run_doctor` now returns an `Err` in that case. Missing optional tools (cargo-deny, cargo-geiger, cargo-mutants, Kani, cargo-llvm-cov, Docker) are informational; Kani is detected by its `cargo-kani` executable without running it. `--fix` still runs `rustup component add rustfmt clippy`; its result reads `installed by --fix`. `--json` prints `rullst.cli-doctor.v1`. See [doctor](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-doctor). | | CLI output additions | Successful generators end with a `Next steps` block of one to three commands on standard output (none for `new`, `auth`, `foundry:init`, `make:k8s` or `--json`). New `completions <shell>` and `info [--json]` (alias `version`) commands, and `--json` for `inspect routes` (`rullst.cli-routes.v1`) and `audit` (`rullst.cli-audit.v1`, progress on standard error). Both home menus gain `Search All Commands` before `View Help & Commands`, so the entries after it move down by one. `inspect routes` no longer appends the route call's closing parenthesis to the handler (`controllers::users::show` instead of `controllers::users::show)`). | | SaaS plan gates | The generated `/reports/billing` route uses authenticated identity, fresh revision-fenced Stripe reconciliation and an explicit `BILLING_REPORT_PLAN_IDS` allowlist. Production requires live state; offline fixtures cannot grant access. The additive `rullst-capital::entitlements` API supports other trusted adapters. Existing applications must review/install the generated module and route; no new billing schema is needed. | | Capital providers and NFS-e removed | `rullst-capital` keeps the provider-neutral contracts (`BillingProvider`, `MeteredBillingProvider`, checkout, subscription, entitlement, quota, charge, invoice and webhook verification/replay APIs), `StripeProvider` and the experimental `InfinitePayProvider`, whose live checkout and callbacks still fail closed. Removed: `PaddleProvider` with its `Paddle*` customer, checkout, subscription and portal types; `LemonSqueezyProvider` with `LemonSqueezyUsageRecord` and `LemonSqueezyUsageAction`; `PolarProvider` with `PolarCheckoutRequest` and `PolarCheckoutSession`; `RazorpayProvider`; `MercadoPagoProvider`; `AlipayProvider`; `CoinbaseCommerceProvider`/`CoinbaseProvider`; `PicPayProvider`; the Wise payout adapter (`WiseProvider`, `WiseTransferState`, `WiseTransferStateChange`); and the payout contracts (`PayoutProvider`, `PayoutStatus`, `PayoutEvent`, `init_payout_provider`, `try_init_payout_provider`, `payout_provider`). The NFS-e module is removed too: `rullst_capital::fiscal` and its crate-root re-exports, the `nfse` feature, the facade's `capital-nfse` feature, `Invoice::to_dps` and `CapitalError::FiscalError`. It was never validated with a real municipality and may return as a separate product outside Rullst. These adapters stay in the 12.x line and in Git history. To keep another gateway, implement `BillingProvider` in application code as described in [Writing your own payment provider](https://rullst.github.io/Rullst/book/capital-custom-provider.html). `make:billing` and the SaaS starter now generate Stripe only: `BILLING_PROVIDER` must be `stripe`, and the Paddle modules, the Lemon Squeezy fixture and the `BILLING_STORE_ID` and `BILLING_PADDLE_*` settings are gone; existing generated controllers are not rewritten. In one generated by 12.x (the SaaS starter or `make:billing`), delete the `lemonsqueezy` branches, `lemon_provider` and `BILLING_STORE_ID`, and keep `BILLING_PROVIDER=stripe`; the Stripe paths compile against v13 unchanged. `make:mail-invoice` now generates a receipt-only `PaymentReceiptEmail` that enables only `mailer`; delete `from_nfse_response` and the NFS-e variants from a mailable generated earlier. | | Capital quota keys on MySQL/MariaDB | `SqlQuotaStore` keys (subject kind and ID, feature, event key) are case-sensitive on every backend. New MySQL/MariaDB tables use `ascii_bin` key columns. A quota table created by an earlier release, whose key columns fold case, makes `prepare_schema` and every reserve, release and usage call return `QuotaError::StorageUnavailable` until you run the `ALTER TABLE` migration in the [Capital README](https://github.com/Rullst/Rullst/blob/main/rullst-capital/README.md#upgrading-mysqlmariadb-quota-tables). Rows that earlier releases merged for keys differing only by case stay merged. PostgreSQL and SQLite tables are unchanged. | | Capital zero tier limit | `Billable::quota_request` for a feature whose `tier_limit` is `Some(0)` now returns `QuotaError::LimitExceeded { used: 0, requested, limit: 0 }` instead of `QuotaError::InvalidRequest`, matching `check_quota`. Map it like any other quota denial (for example 402/403 with an upgrade prompt) rather than a 400/500. | | Capital provider subscription IDs | Subscription IDs made only of dots (`.`, `..`) now fail with `CapitalError::SubscriptionError` before any provider request. A live adapter put them into the URL path, where they are dot segments that send the authenticated request to another provider endpoint. | | Capital invoice amounts | `Invoice::validate`, `total_minor`, `try_generate_html`, the PDF renderers and `bind_succeeded_charge` now reject an item amount or total that rounds to zero minor units (for example `0.0000000005` USD or `0.0000001` JPY) with `CapitalError::InvalidInvoice`, as the error message already stated. Such an item used to pass as 0 minor units. Amounts of at least one minor unit are unchanged. | | Messaging outbox relay key | `OrmOutboxRelay` publishes with the broker idempotency key `<lowercase hex SHA-256 of the stream>/<event_key>` instead of the bare `event_key`, so streams that share a topic no longer deduplicate (identical content) or conflict (different content) with each other. A claim published before the upgrade and not acknowledged is republished once under the new key, producing a second broker message; drain or acknowledge in-flight outbox claims before upgrading, or rely on consumer idempotency. | | Messaging encrypted SQLite startup | `SqliteBroker::connect_encrypted` now opens one retained record per referenced storage key ID. A prior key with the right ID but wrong bytes, or a tampered record, fails at startup with `MessagingError::StorageAuthenticationFailed` (or `CorruptStorage` for damaged row metadata) instead of starting and later failing every receive whose oldest pending record uses it. Correct keyrings are unaffected. | | Messaging Redis Streams candidate | `RedisBroker` stores a new state format (v2) so that messages available at the same time, such as every retained message of a new earliest group, are claimed in publication order instead of text order (sequence 10 before 9). A namespace provisioned by an earlier v13 development build fails with `MessagingError::ConfigurationConflict`; drain it with that build, then provision a new namespace or generation. No 12.x release contains this adapter. | | Mail providers removed | `rullst-mail` keeps Resend (`ResendDriver`), AWS SES (`AwsSesDriver`), SendPulse (`SendPulseDriver`) and SMTP (`SmtpDriver`), the development drivers (`LogDriver`, `MemoryDriver` and the offline `mock_*` fixtures) and the shared failover, tenant resolver, pipeline, attachment inspection, suppression, observation, feedback and queue support. Removed: `SendGridDriver`, `PostmarkDriver`, `MailjetDriver`, `MailtrapDriver` and the Azure Communication Services transport (`AzureCommunicationDriver`, `AzureMailCredential`, `AzureMailAccessToken`, `AzureManagedIdentity`, `StaticAzureMailCredential`), their `MAIL_DRIVER` values `sendgrid`, `postmark`, `mailjet`, `mailjet-sandbox`, `mailtrap`, `mailtrap-sandbox` and `azure-acs`, and the settings only they read (`SENDGRID_API_KEY`, `POSTMARK_SERVER_TOKEN`, `POSTMARK_API_KEY`, `POSTMARK_MESSAGE_STREAM`, `MAILJET_API_KEY`, `MAILJET_SECRET_KEY`, `MAILTRAP_API_TOKEN`, `MAILTRAP_SANDBOX_ID`, `AZURE_COMMUNICATION_EMAIL_ENDPOINT`, and the managed-identity `IDENTITY_ENDPOINT`, `IDENTITY_HEADER` and `AZURE_CLIENT_ID`). Cargo features are unchanged. A `MAIL_DRIVER` or `[mail] driver` that names a removed driver now fails whenever the `Mail` facade resolves its driver (each send and each queued job) with `MailError::ConfigError` naming the driver and this row, whatever credentials are set; it never falls back to `log`, `memory` or an offline mock. Queued `rullst_mail_send` jobs written by 12.x name no provider: a 13 worker delivers them through the driver it is configured with, and a worker still set to a removed driver fails each job with that error, so the job stays failed until `Queue::retry_failed_job` after the driver is changed. Suppression rows and delivery observations keep their provider labels (for example `sendgrid`) and stay readable. Select a kept driver, or implement `MailDriver` in application code for another provider and install it with `Mail::set_driver` or `TenantMailResolver`, as described in [Writing your own mail transport](https://rullst.github.io/Rullst/book/mail-custom-transport.html). These adapters stay in the 12.x line and in Git history. | | Mail driver default | With neither `MAIL_DRIVER` nor `[mail] driver` set, the `Mail` facade no longer falls back to the metadata-only `log` driver in staging or production (`RULLST_ENV` or `APP_ENV` from the environment or `.env`, or `[app] env`, as `Server` resolves them); it returns `MailError::ConfigError`. Configure the real driver, or set `MAIL_DRIVER=log` explicitly. Development and test keep the fallback. | | Mail sender | Resend, SES and SMTP no longer substitute `noreply@rullst.dev` when a message has no `from`. Configure a default sender your provider account has verified with `MAIL_FROM` (or `from` in the `[mail]` section of `Rullst.toml`), for example `MAIL_FROM="Acme <no-reply@acme.example>"`: `Mail` facade sends, including generated mailables, use it for messages without a `from`, while an explicit `from` still wins. With neither, real delivery returns `MailError::ConfigError` before any request, as the 12.1 REST transports already did. New projects list an empty `MAIL_FROM` and a commented `MAIL_DRIVER` in `.env` and `.env.example`; the facade now reads its `MAIL_*` and provider settings from `.env` after the process environment, like `DATABASE_URL`. Offline mock delivery is unchanged. | | Mail attachment inspection | `LocalAttachmentInspector` classifies markup by its root element and namespaces, as browsers do, so XML whose root is a prefixed `svg`/`html`, follows a DOCTYPE or processing instruction, binds the SVG or XHTML namespace (also through character references) or declares DTD entities is now rejected as SVG (every policy) or active markup (`strict()`); UTF-16 markup is read by its byte-order mark. Review XML attachments that embed these namespaces or entity declarations. | | Mail link checks | The pre-flight homograph check now resolves each link as a browser does (WHATWG URL parsing through the `url` crate): tabs and newlines are removed, `\` counts as `/` for HTTP(S), `https:host`, `https:\\host` and scheme-relative `\\host` name a host, and percent-encoded or A-label (`xn--`) hosts are inspected in Unicode form. Messages whose links spell a mixed-script host this way, which were previously sent, now fail with `MailError::SendError`. Click tracking checks each destination as one link and no longer wraps one that contains whitespace or control characters, so the pipeline inspects (and may reject) the original `href`. | | Mail facade settings | A `MAIL_PORT` that is not an integer from 1 to 65535 (for example `587000` or `smtp`) now makes `MAIL_DRIVER=smtp` facade delivery fail with `MailError::ConfigError` instead of silently connecting to port 25. An unset or empty `MAIL_PORT` still means 25. The facade now reuses the driver built from its settings across messages and queue jobs until a setting changes (or `Mail::reset_driver()` runs), so connection pools and the native SES client are shared; `MAIL_DRIVER=memory` is still rebuilt per message. | | Mail queued attachments | Queued mail (`Mail::send` after `Mail::init_queue`, `Mail::enqueue` and `Mail::enqueue_for_tenant`) stores each attachment's bytes in the `rullst_mail_send` job as one standard base64 string instead of a JSON array with one number per byte. A 13 worker (`register_mail_handler`) still reads jobs written by 12.x, but a 12.x worker fails every job with an attachment written by 13. In a rolling deployment, upgrade every process that runs the mail worker before any process that enqueues mail; once 13 workers run, retry jobs that a 12.x worker failed with `Queue::retry_failed_job`. Before rolling back to 12.x, stop 13 producers and let 13 workers drain the queue. Jobs without attachments are unchanged. | | Mail SES addresses | Native and bearer-proxy SES requests now send 7-bit ASCII addresses, as SES requires: internationalized domains as IDNA A-labels and non-ASCII sender display names as RFC 2047 encoded-words, instead of verbatim Unicode that SES rejected with a permanent `BadRequest`. A non-ASCII local part now fails before the request with `MailError::ValidationError`. A trusted proxy receives the encoded `FromEmailAddress` and `ToAddresses`. | | Mail Resend scheduling | A direct (non-queued) `ResendDriver` send scheduled more than 30 days ahead, Resend's documented limit, now fails before the request with `MailError::ConfigError` instead of a permanent provider rejection. Schedule such mail through a durable Rullst queue (`Mail::enqueue`). The offline Resend fixture still accepts it. | | Mail tracking recipient | `Message::try_with_open_tracking`/`try_with_click_tracking` (and their deprecated variants) now sign the bare recipient address, so `OpenEvent.email` and `ClickEvent.email` are `alice@example.com` rather than `Alice <alice@example.com>`. Handlers that parsed the display-name form can compare the address directly. | | Mail plain-text fallback | The text part that `Message::html` derives when no text is set (and `strip_html_to_plain_text`) now appends each absolute HTTP(S) link target after its label as `label <URL>`, unless the label already is the URL, so text-only readers keep call-to-action links. Tests that compared the whole derived text of linked HTML need the new form; set `text` explicitly to control it. | | Labs runner | The unpublished `rullst-labs-runner` candidate (Linux Rust-to-Wasm/Wasmi executor) was removed from the v13 workspace and is not published; its source remains in git history. `rullst-labs` APIs are unchanged and never execute learner code. Applications that evaluated the candidate deploy their own isolated runner against the [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html); the non-executing `byo_runner_controller` example shows the controller flow. | | Education candidates | These v13 crates changed during source evaluation, before their first publication in 13.0.0-alpha.1. `rullst-supervision` returns `Capacity` for an observation once one learner (tenant and subject) holds 4096 unexpired events across all sessions, or the store-wide event limit when lower; ended sessions keep their events until event retention ends. The new `Limits::subject_events` selects another bound. The `rullst-media` upload module accepts a grant expiring up to 3600 seconds plus 300 seconds of clock skew after the browser clock, instead of refusing a maximum-length grant when the browser clock is behind the server. Its SQLite store returns `MediaError::Busy` instead of `Storage` when a lock or connection wait exceeds 3 seconds; retry it with backoff rather than treating it as an integrity incident. `rullst-labs` adds the optional `StoreConfig::tenant_exercises` quota for stores shared by several tenants; without it, `max_exercises` stays a single store-wide budget. | | Labs package | New in 13.0: `rullst-labs` is in the release inventory and was first published in `13.0.0-alpha.1` (previously `publish = false`). Depend on it directly; it has no umbrella feature and no default features. It packages exercise, authorization, job and grading contracts only and never executes learner code; applications still deploy their own runner against the [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html). No API changed. | | Media S3 storage | New in 13.0, opt-in: the `rullst-media` `s3` feature adds `s3::S3Storage` for AWS S3, Cloudflare R2 and MinIO. It stores and serves the original object without transcoding: upload grants come from the new `MediaService::upload_declared` with an `UploadDeclaration` (presigned `PUT`), playback uses the new `PlaybackKind::Original` (presigned `GET`), and grants expire within 900 seconds. `UploadGrant` gains `protocol` (Bunny grants serialize `"protocol":"Tus"`), `content_type` and `content_length`; `VideoProvider` gains `upload_declared`, whose default returns `Unsupported`. Code that matches `PlaybackKind` exhaustively needs an `Original` arm; Bunny returns `Unsupported` for it. Experimental; validated against the offline mock only; provider interoperability not yet validated. See [S3-compatible object storage](https://rullst.github.io/Rullst/book/managed-video.html#s3-compatible-object-storage-s3-r2-minio). | | Generated billing settings | New `make:billing`/SaaS controllers read every `BILLING_*` key with `rullst::config::project_setting` (process environment, then `.env`) instead of the process environment only, and an unreadable or malformed `.env` makes billing unavailable. Their production check now uses `RullstConfig::global().environment()`, so `RULLST_ENV`/`APP_ENV` from the `.env` a started `Server` read and `[app].env` count, and an invalid value is treated as production. Existing generated controllers are not rewritten; apply the same change by hand to read `.env` values. | | Application templates | SaaS and `make:billing` add the report module; privacy generators remain explicit additions. Updating a dependency does not replace generated controllers or application customizations. | | Starter migrations | New Blog, ERP and Portfolio seeds no longer call SQLite-only `datetime('now')`; seeds with explicit ids advance their PostgreSQL sequences; and SaaS, LMS, `cargo rullst auth` and SQLx `make:billing` create their indexed email, status, event-key and subscription-id columns as `VARCHAR`, with the LMS audit index ordered by `id`, so these migrations also run on PostgreSQL, MySQL and MariaDB. Already-applied migrations are not re-run: an existing PostgreSQL deployment that seeded rows should run `SELECT setval(pg_get_serial_sequence('<table>', 'id'), (SELECT MAX(id) FROM <table>))` per seeded table. | | ERP starter access | The dashboard at `/` now sits behind the same `NexusAuthPolicy` as Nexus and the inventory forms (loopback-only in debug, Basic Auth over verified TLS in release), because it lists customers and revenue. In existing ERP applications, move `get("/" => …index)` into the routes passed to `admin_access.protect_router`. | | LMS lesson progress | The player renders a fresh random idempotency key and the controller scopes it to the requested percentage, so later saves no longer return 409. Keys are unique per learner (the replay lookup and the unique index cover `(subject_user_id, event_key)`), so one learner can no longer claim another learner's key and block their saves. Existing LMS applications keep the fixed `progress:{user}:{lesson}:next` key until they copy the new `new_progress_key`/`progress_event_key` helpers; to scope keys per learner, add `subject_user_id` to the replay query and replace `lesson_progress_events_key_unique` with a unique index on `(subject_user_id, event_key)`. | | LMS concurrent progress saves | `record_progress` now claims the idempotency key by inserting the progress event as the transaction's first write (its `previous_percent` comes from a subquery) and treats a unique violation as a replay, so a double-submitted save returns the stored receipt instead of `503`. Existing applications must copy the service's `replay` helper and `record_progress`. | | LMS lesson media in Nexus | The Lesson Nexus fields `media_url` and `captions_url` are `FieldKind::Text` instead of `Url`, so lessons with the seeded same-origin `/static/media/*.vtt` captions can be edited again (the player still accepts only HTTPS or root-relative sources), and `media_kind` is an `Enum` of `video`/`audio`. A lesson the player cannot render now returns a logged `500` instead of a silent `503`. Existing applications can make the same changes in `src/models/lesson.rs` and the learning controller. | | Blog robots and sitemap | The Blog starter's `robots.txt` and `sitemap.xml` use absolute URLs from `RULLST_PUBLIC_ORIGIN` (an HTTPS origin without a path, read from the process environment, then `.env`); the sitemap lists the home page and the 1,000 newest posts. Unset, `robots.txt` has no `Sitemap:` line and the sitemap is empty instead of advertising the relative `/` and `/sitemap.xml` that crawlers reject. New Blog projects list the variable in `.env`/`.env.example`. Existing applications must copy the controller's `public_origin`, `path_segment`, `robots_txt` and `sitemap_xml`. | | Generated login and registration | SaaS, LMS and `cargo rullst auth` controllers run Argon2id through at most four bounded permits (503 after a two-second wait) and export `credential_rate_limit` (ten submissions per peer per minute, then 429), which the SaaS/LMS routers apply to `POST /login` and `/register`. Behind a reverse proxy, configure `[security] trusted_proxies`. Existing applications must copy the controller and route changes. | | SaaS user JSON | The SaaS starter's `User.password_hash` is now `#[orm(hidden)]`, as in the LMS starter, so `to_json()`, audit rows and post-commit Redis model events no longer carry the Argon2 hash. In an existing SaaS application, add `#[orm(hidden)]` to that field. | | Registration timestamps | SaaS, LMS and `cargo rullst auth` registration now stores the current UTC time in `created_at`/`updated_at`; the generated `User` model reads them as `chrono::DateTime<Utc>` (see ORM date/time columns), and `utc_timestamp()`, `User::change_password` and `password_reset::redeem` take that type instead of `YYYY-MM-DD HH:MM:SS` text. Registration previously stored empty strings, which replaced the column default because the ORM inserts every field. Existing applications can copy `utc_timestamp` from a new controller; rows created earlier keep their empty timestamps until updated. | | Billing row timestamps | SaaS and `make:billing` controllers now store the current UTC time in `created_at`/`updated_at` of the billing customer they create on a first checkout and of a new subscription (SQLx), instead of empty strings that replaced the column default because the ORM inserts every field. SQLx billing models read those columns as `chrono::DateTime<Utc>`, and the billing stores write `CURRENT_TIMESTAMP` in the column's own type instead of casting it to text; Turso billing keeps text. Existing applications can copy `utc_timestamp` from a new controller. | | Registration lengths | SaaS, LMS and `cargo rullst auth` registration count the 120-character name limit and the 12-character password minimum in UTF-16 code units, as the forms' `minlength`/`maxlength` do, instead of UTF-8 bytes. The password maximum stays at the 72 bytes that `rullst::auth::hash_password` accepts, and the error message now says so. Long non-ASCII names are no longer rejected, while a non-ASCII password shorter than 12 characters (for example six Cyrillic letters, 12 bytes) is now refused. Existing sign-ins are unaffected. Existing applications can copy `form_length` and `valid_password`. | | Revocable login sessions | Additive: `rullst-auth` adds `make_login_cookie_with_epoch(user_id, epoch, max_age)`, `encrypt_session_with_epoch`, `decrypt_session_claims`, `SessionClaims` (`verify_epoch`), `DEFAULT_SESSION_MAX_AGE`, `MAX_SESSION_MAX_AGE` and `AuthError::SessionRevoked`. `make_login_cookie` cookies keep their format, 30-day lifetime and validity; `decrypt_session` rejects the new `v2.` tokens. New SaaS, LMS and `cargo rullst auth` projects store `users.session_epoch` (`BIGINT NOT NULL DEFAULT 0`, `#[orm(db_default)]` on the model), issue epoch cookies, refuse a cookie with another epoch in `auth_middleware` (the `cargo rullst auth` middleware now loads the user on every request) and mount `POST /logout/everywhere`, which increments the epoch. To adopt in an existing application, add the column with a migration and the `#[orm(db_default)] pub session_epoch: i64` field, issue cookies with `make_login_cookie_with_epoch`, decode them with `decrypt_session_claims` and `verify_epoch`, and increment the column in one `UPDATE` for sign-out everywhere, password changes and resets, and deactivation. Existing cookies decode as epoch 0 and keep working until the first increment. `make:age-gate` and `make:privacy` accept only authentication files equal to this release's SaaS starter, so a SaaS application generated by 13.0.0-alpha.1 must adopt the new `auth_middleware.rs` and `auth_controller.rs` first. See [encrypted sessions](https://rullst.github.io/Rullst/book/crates/auth.html#revocable-cookies-the-session-epoch). | | Password change and reset | Additive: `rullst-auth` adds `PasswordResetToken` (`generate`, `expose`, `digest`), `password_reset_token_digest` and `password_reset_digests_match`. New SaaS, LMS and `cargo rullst auth` projects get `src/controllers/password_controller.rs` (`/password/change` behind `auth_middleware`, `/password/forgot` and `/password/reset`, every submission behind CSRF and `credential_rate_limit`), `src/models/password_reset.rs`, a `password_reset_tokens` migration, `User::change_password`, the `forgot_password_page`, `reset_password_page` and `change_password_page` views, a `notice` argument on `login_page` (shown on `/login?reset=done`) and the `mail` feature; the SaaS and LMS `.env` list `RULLST_PUBLIC_ORIGIN`. A change keeps only the asking browser signed in; a reset signs out every device and signs nobody in. To add the flows to an existing application that already uses session epochs, copy those three files and the migration (named after your users migration) from a new project of the same starter, add `User::change_password` to the user model, the three views and the login `notice` to `src/pages/auth.rs`, register the modules and routes as the new router does, make the `auth_controller` helpers that `password_controller` imports `pub(crate)`, enable the `mail` feature, and set `RULLST_PUBLIC_ORIGIN`, `MAIL_DRIVER` and `MAIL_FROM` for staging and production. See [Tutorial 11](https://rullst.github.io/Rullst/book/tutorials/11-authentication-system.html#what-the-scaffold-currently-enforces). | | VPS deploy proxy | `deploy --platform vps` now attaches both services to an `edge` compose network pinned to `172.31.250.0/24`, gives Caddy `172.31.250.10`, and adds that address to `[security] trusted_proxies` in `Rullst.toml` (creating it if needed) before a missing `Dockerfile` is scaffolded. Clients behind Caddy no longer share one rate-limit bucket, such as the starters' login/registration budget. Existing `docker-compose.prod.yml` files are kept: add the network and address yourself, or move the file aside and rerun the command. An existing `trusted_proxies` list is not changed. Fly.io, Railway and Render print a notice while no trusted proxy is configured. | | Blank database status | The Blank starter's public home page (HTML and JSON API, SQLx and Turso-primary) probes the database with a one-row `User::query().first()` instead of `User::all()`, which read up to 1,000 SQLx or 10,000 Turso rows per request. Existing applications can make the same one-line change. | | Portfolio profile | The Portfolio page shows the first profile by id instead of `Profile::find(1)`, returns `404` when none exists instead of a hard-coded placeholder person with `200`, and returns a logged `503` when a profile, project, experience or skill query fails instead of empty sections. Repository-mode `ProfileRepository::get` now returns `Result<Option<Profile>, rullst_orm::Error>` and the other repositories' `all` return `Result<Vec<_>, _>`. The seeded profile's e-mail is `hello@example.com`. Existing applications must copy the controller (and repositories) to adopt this. | | Blog and Portfolio pages | `index_page`, `detail_page` and the Portfolio `render` take a `csp_nonce` argument from the request's `CspNonce`; style attributes became classes, the pages use system fonts and the default avatar is `/static/rullst.png`, so styling survives the production CSP. Remote avatars need a same-origin copy or a reviewed `img-src` change. | | Generated linker configuration | `.cargo/config.toml` (mold/lld selection of the generating host) is now listed in the generated `.gitignore` and `.dockerignore`. In an existing project, add `.cargo/config.toml` to both files or delete its `-fuse-ld` flags before building in CI or Docker. | | Build files from the 12.0 CLI | Only projects generated by the 12.0 CLI; 12.1 generators no longer write these. `.cargo/config.toml` passes the unsupported MSVC linker flag `/DEBUG:FASTLINK`: delete that `[target.x86_64-pc-windows-msvc]` block. `.gitignore` lists `/Cargo.lock`: remove the line and commit the lockfile, so builds and deployments use the versions you tested. A Dockerfile written by `--docker` or `dockerize` runs `cargo build --release`: add `--locked` once the lockfile is committed. See [Projects generated by the 12.0 CLI](https://rullst.github.io/Rullst/book/migration-v13.html#projects-generated-by-the-120-cli). | | ERP orders and stock | `store_order` reserves stock with a conditional `UPDATE ... WHERE stock >= ?` and inserts the order in the same transaction; `add_stock` increments in SQL. Quantities outside 1–10,000, prices outside 0–1,000,000, stock outside 0–1,000,000 and names, SKUs or customer names over 255 characters (UTF-16 code units, as the forms' `maxlength` counts them) return `422`, an unknown product `404`, insufficient stock `409` and database errors `503`, instead of redirecting or rendering `0 Units`. Existing ERP applications keep the read-modify-write handlers until they copy the new controller. | | Blog and ERP reads | Blog `index` pages through posts newest first (`?page=N`, 20 per page) and `show` queries by slug; ERP `index` pages through products (`?page=N`, 50 per page), lists the 20 newest orders and computes revenue, order count and low-stock alerts in SQL. Neither depends on `Model::all()`, which stops at the ORM's 1,000-row cap without an `ORDER BY`. Database errors now return `503` instead of an empty page or a `404`. `blog::index_page` takes a `PaginationResult<Post>` and `erp::dashboard_page` a `PaginationResult<Product>` plus a `DashboardSummary`; existing applications keep their controllers until they copy the new ones. | | Blog and ERP page bounds | The Blog index and the ERP dashboard serve `?page=` 1–100,000 and answer a larger page with `404` instead of passing it to `paginate`, whose overflowing or out-of-range OFFSET was reported as a `503` database failure. Existing applications can add the same `MAX_PAGE` check. | | Docker build context | The `.dockerignore` written by `--docker`/`dockerize` now also excludes `.env.*` (keeping `.env.example`), `Foundry.toml`, `*.sqlite`, `*.sqlite3`, `*.duckdb` and their journal files, so the development database and secret files no longer reach the builder stage, a remote builder or its cache. An existing `.dockerignore` is not rewritten: add these patterns to it. | | Kubernetes Ingress | `make:k8s` now emits `ingressClassName: nginx` instead of the deprecated `kubernetes.io/ingress.class` annotation and a `spec.tls` section (`<project>-tls` Secret) for the placeholder host, so cert-manager issues a certificate. Previously generated Ingresses served the production app over plain HTTP; add a `tls` block for the real host to them. | | Starter health probes | Every starter now merges `rullst::health::health_router()`, so the `/health` and `/ready` probes written by `make:k8s`, `deploy` (Fly, Railway, Render) and `foundry:deploy` return `200` instead of `404`, which kept Kubernetes pods unready, restarted them and failed Fly releases. Existing applications should add `.merge_axum(rullst::health::health_router())` (or the lifecycle-aware `health_router_with_lifecycle`) outside any authentication layer. | | Kubernetes and Buildah names | `make:k8s` and the `--buildah`/`generate:buildah` script derive their object, container, Service and image names from the package name lowercased with each run of other characters replaced by `-` (at most 55 characters), so `my_startup` or `MyApp` no longer produce manifests and tags that Kubernetes and OCI registries reject. Manifests or scripts generated for a lowercase hyphenated name are unchanged; for other names, regenerate them or rename the objects and image. | | Pre-compressed static assets | `cargo rullst dev` deletes `static/**/*.br` and `*.zst` siblings of `html`, `css`, `js`, `json`, `svg`, `wasm`, `xml` and `txt` assets when they are not newer than their source, at startup and before each rebuild, and no longer restarts the application when only such a sibling changes. Siblings without a source file are kept. Rerun `cargo rullst build` to regenerate them; outside `dev`, rerun it after editing assets so stale siblings are not shipped. | | Static asset caching | Files served by the standard `/static` mount no longer fall back to the production baseline's `Cache-Control: no-store`. `2xx` and `304` responses for a content-hashed name (a `.`/`-`-separated segment of 8–64 lowercase hex characters with a digit and a letter, such as `app.3f9a2c1b.css`) get `public, max-age=31536000, immutable`; other static files get `no-cache` and are revalidated with the `ETag`/`Last-Modified` that `ServeDir` already sent (`304` on a match). Missing files and dynamic responses keep `no-store`. Give an asset a new hashed name whenever its content changes, and review proxy/CDN rules that relied on `no-store` for `/static`. See [static assets](https://rullst.github.io/Rullst/book/tutorials/10-static-assets-and-compression.html#step-3-cache-headers). | | Generated release profile | New projects get `[profile.release]` with `lto = "thin"`, `codegen-units = 1` and `strip = "symbols"`; `panic` stays `unwind`. Since 13.0.0-alpha.4 they also get a `[profile.dev]` that keeps line tables for the application only ([Build times](https://rullst.github.io/Rullst/book/build-times.html)). Existing manifests are not edited: copy the sections to adopt them. Release builds take longer and release backtraces lose symbol names (use `strip = "debuginfo"` to keep them). Measurements: [release defaults](https://rullst.github.io/Rullst/book/green-software-roadmap.html#measured-release-defaults). | | Generated container runtime | The `Dockerfile` written by `--docker`, `dockerize` or a first `deploy` now uses `gcr.io/distroless/cc-debian12:nonroot` for the runtime stage instead of `debian:bookworm-slim`: no shell, package manager or `HEALTHCHECK`, still UID/GID 10001 with `HOME=/app` and a writable `/app/data`. New projects only; an existing `Dockerfile` is never replaced. Probe `/health` over HTTP and run `/app/<name> db:migrate` directly instead of through a shell. | | Interactive `new --api` | Without `--default`, `cargo rullst new <name> --api` no longer asks for a blueprint or for the build type, whose Full-Stack default discarded the flag and generated an HTML/HTMX app; it generates the Blank JSON API profile. Scripts that answered those two prompts must drop the answers. | | CLI `new` wizard | In a terminal, `cargo rullst new` without `--default` runs a new wizard: a blueprint screen with descriptions and file-tree previews, a build-type screen (Blank only), one database list that includes *No database* for Blank (replacing the separate "need a database?" confirmation), one optional-features checklist (AI, Redis, Dockerfile, Nix flake and the storage add-ons, replacing the separate AI and Redis questions) and a review with *Create the project*, *Back* and *Cancel*; Esc goes back. An invalid project name given on the command line is refused before the first question. Scripts that typed answers into the old prompts must use flags instead. `--blueprint`, `--database`, `--no-database`, `--ai` and `--redis` no longer require `--default` and skip their questions (the `cargo_rullst::cli::Cli` parser used directly still requires it). Without an interactive terminal (or with `CI`/`TERM=dumb`) `new` without `--default` now fails immediately with a hint instead of failing at the first prompt; nothing is created. The `How to run:` block after creation became numbered `Next steps` ending on the welcome-page URL. New `--dry-run` prints the plan and writes nothing. Generated files are unchanged for the same choices. See the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-new-name). | | Minimal default features | Breaking (13.0.0-alpha.3): `rullst` has no default feature (it had `orm`, `drivers-all` and `queue-sqlite`) and `rullst-orm` has none (it had `drivers-all`), so `cargo add rullst` compiles no SQLx driver. A dependency that relied on those defaults now builds without the ORM, a database driver or Core's SQLite queue: code that uses `rullst::db`/`rullst::orm` stops compiling, and `Orm::init` of a driver-less `rullst-orm` returns `Error::Validation` naming the feature to enable. List what the application uses, for example `rullst = { version = "...", features = ["strict-sqlite", "queue-sqlite"] }` or `rullst-orm = { version = "...", features = ["strict-postgres"] }`; `drivers-all` keeps all three drivers. Projects generated by `cargo rullst new` already disable the defaults and list their features. `cargo rullst upgrade` reports such dependencies (`V13-RULLST-DEFAULT-FEATURES`, `V13-ORM-DEFAULT-FEATURES`). | | Generated `rullst-orm` dependency | Projects with a SQLite, PostgreSQL, MySQL or MariaDB primary now declare `rullst-orm = { ..., default-features = false, features = ["strict-..."] }`, so the ORM's default `drivers-all` no longer adds the other SQLx drivers (and the bundled SQLite C library) to a single-backend build. Existing projects can add `default-features = false` to that line; code that relied on another driver through it must enable that driver explicitly. Since 13.0.0-alpha.3 `rullst-orm` has no default driver at all (see Minimal default features). | | ORM adapter aggregate features | The `orm-polyglot` feature of `rullst` and the `polyglot` feature of `rullst-orm` were removed. They enabled every optional storage adapter at once, including DuckDB's bundled C++ build, which made the first build much slower. Cargo now refuses a manifest that names them: replace `orm-polyglot` with the adapters you use (`orm-turso`, `orm-mongodb`, `orm-duckdb`, `orm-surrealdb`, `orm-qdrant`), or `polyglot` with `turso`, `mongodb`, `duckdb`, `surrealdb` or `qdrant`. Each adapter feature is unchanged, and `--all-features` builds are not affected. `cargo rullst upgrade` reports a dependency or `[features]` entry that still names them. | | Omni and offline sync removed | 13.0 removes `cargo rullst make:omni`, `cargo rullst omni` (including `omni android --release`) and the native-only `offline-sync` feature with `rullst::offline_sync`; the Tauri shells exposed no native API, so an installed Progressive Web App gives the same result. Run `cargo rullst make:pwa` to make the application installable, add `--android --host <host> --package-id <id>` for a Google Play package (Trusted Web Activity built with Bubblewrap, with `/.well-known/assetlinks.json` from `rullst::pwa::AssetLinks`), and package the same site for the Microsoft Store with PWABuilder ([PWA guide](https://rullst.github.io/Rullst/book/tutorials/43-progressive-web-apps.html)). An existing `omni-app/` keeps building with its own Tauri toolchain but receives no further fixes; retire it once the PWA replaces it. Remove `offline-sync` from `rullst`/`rullst-core` features (Cargo refuses it) and queue browser writes in IndexedDB with idempotency keys, keeping the server authoritative. `cargo rullst upgrade` reports an Omni shell or a script that runs the removed commands (`V13-OMNI-REMOVED`) and a manifest that enables the feature (`V13-OFFLINE-SYNC-REMOVED`). | | Starter page language | The Blank starter renders with `render_page_with_lang(&htmx, "en", ...)` and the ERP page declares `<html lang="en">`, matching their English copy; `render_page` and the earlier ERP page declared `pt-BR`. Existing applications can make the same one-line changes. | | Repository examples | The blog showcase selects its database like `Server` and Artisan (`DATABASE_URL`, `.env`, then `Rullst.toml`, now `sqlite://blog.db?mode=rwc`) and should be started from `examples/blog`. `Post` uses fail-closed `tenant_column` with the request's `TenantContext`, and `/posts/repository` lists only the selected tenant. Its Nexus registration now declares `nexus_tenant_column`, so `/nexus` lists, searches, creates and changes posts only in the request tenant (another tenant's record is `404`) and hides `tenant_id`, which a form can no longer submit (`422`). `/pricing`, `/billing` and `/checkout` read provider credential presence (process environment, then `.env`) once at start-up instead of re-reading `.env` on every request, so a credential added later appears after a restart. Stories are bounded (120/4,000 characters, 100 per tenant, 64 KiB forms; 422, 403 or 413), listings show the newest 20, startup seeding no longer deletes posts that resemble old seeds, and pages render under the unrelaxed production CSP with same-origin assets, and the `/omni` guide page was removed with Omni. The non-working Wasm island page, the Tauri 1.5 shell and the Dioxus `omni-app` were removed; use `cargo rullst make:island`/`make:pwa`. In the SaaS example, note bodies with control characters receive 422 instead of 503, and `membership.py` rejects tenants made only of dots. | | Windows paths in CLI and console output | Generator refusals (`make:k8s`, `dockerize`, `introspect` and the other no-clobber outputs), `audit` IDOR, unsafe-code and listener findings (and the compliance report built from them) and the development panic console now name files with `/` separators on Windows as well (`k8s/deployment.yaml`, not `k8s\deployment.yaml`). Adjust scripts that matched the backslash spelling. | | Consumer scaffolds under symlinked directories | `make:age-gate` and `make:privacy` now inspect only the entries below the project directory for symlinks, so a project reached through a symlinked parent (macOS `/var` → `/private/var`, a Windows junction) is accepted. A symlink inside the project is still refused, as is a planned path outside it. | | Core HSTS default | From 13.0.0-alpha.2 the staging and production header baseline sends `Strict-Transport-Security: max-age=31536000; includeSubDomains` instead of `max-age=63072000; includeSubDomains; preload`, and `rullst_security::SecureHeadersConfig::default()` uses the same value. `preload` consents to the browsers' preload list, so it is now opt-in: set `[security]` `hsts_preload = true` (with `hsts_max_age_seconds` of at least `31536000` and `hsts_include_subdomains = true`, or the server refuses to start) to keep sending it. `hsts_max_age_seconds = 63072000` restores the two-year max-age. | | Artisan and `.env` | From 13.0.0-alpha.2 `db:migrate`, `db:rollback`, `db:status`, `db:seed` and `studio` (through `rullst::artisan!`, `check_and_run_artisan` or `Server::run`) no longer load `.env` into the process environment, matching `Server`. `DATABASE_URL` is still read from `.env`. A migration or seeder that called `std::env::var` for a value that only `.env` defines must call `rullst::config::project_setting` instead, which reads the process environment and then `./.env`. | | Core WAF in development | From 13.0.0-alpha.2 `Server` in development (and `apply_security_baseline` with `Environment::Development`) runs the WAF rules in report-only mode: no request is refused, but each one staging and production would refuse logs a `warn` event (target `rullst::security::waf`) with the rule and path, and textual request bodies are buffered (bodies over 1 MiB are replayed in full). The new `[security]` `waf_body_exempt_paths` exempts the bodies of exact paths from inspection in every environment. | | CSRF token on static-looking paths | From 13.0.0-alpha.2 a `GET` or `HEAD` for a path such as `/report.json` or `/static/page` receives `CsrfToken` when the browser sends a valid `rullst_csrf` cookie; it still never mints a token or sets the cookie there. | | Multipart CSRF refusal | From 13.0.0-alpha.2 a `multipart/form-data` request without a `_token` field the CSRF middleware can read (its first `_token` must end within the first 64 KiB, so before any file input) is still refused with `403`, but the body is `Missing CSRF token: a multipart/form-data form must send its _token field before any file field (the CSRF check reads only the first 64 KiB of the body), or send the X-CSRF-Token header` instead of `Invalid or missing CSRF token`. A readable but wrong token keeps the old text. Update tests or clients that compare that body. Forms read with the new `rullst::uploads::Multipart` get the same refusal in development; see [form uploads](https://rullst.github.io/Rullst/book/form-uploads.html#csrf). | | Secure cookies over HTTPS | From 13.0.0-alpha.2 the `rullst_csrf` cookie, and every `rullst_*` cookie (such as `rullst_session`) passing through `Server`'s security baseline, carries `Secure` in any environment when the request arrived over HTTPS: an `https` request URI, or `X-Forwarded-Proto: https` from a proxy listed in `[security] trusted_proxies` with `trust_forwarded_proto = true`. Staging and production already set it. A development or test deployment reached over HTTPS no longer needs `RULLST_ENV=staging` for this, and its browser then sends those cookies only over HTTPS. | | CSRF cookie on cacheable responses | From 13.0.0-alpha.2 a `GET` or `HEAD` response that shared caches may store (its `Cache-Control` has `public` or `s-maxage` and neither `private` nor `no-store`) no longer sets the `rullst_csrf` cookie, so a cache cannot hand one visitor's token to another. A page that renders a form must not be publicly cacheable; give it `private` or no cache policy. The new `rullst::cache_control::public`/`private` layers label dynamic routes, such as downloads and feeds, that may be cached. | | Connect mock credentials | From 13.0.0-alpha.3, outside `rullst-connect`'s own unit tests, empty or `mock_*` provider credentials need the `mock` feature (`oauth-mock` on the `rullst` facade). Without it `try_new`, `Connect::driver` and `OidcProvider::discover` return `ConnectError::InvalidConfiguration { field: "credentials", .. }` instead of building a provider whose code exchange later failed with `ConnectError::Offline`, and the deprecated `new` constructors return a disabled provider (`CredentialMode::Invalid`, redirect `about:blank`). Set real credentials, build the provider only when they are configured, or enable the feature in development and test builds. | | Connect mock authorization URL | From 13.0.0-alpha.3 a provider in mock mode returns the configured `redirect_url` with `code=mock_authorization_code`, the `state` and any PKCE challenge, instead of `https://example.invalid/rullst-connect/mock?provider=...`, which a browser could not open. Code that matched the old URL, or that redirected to its own callback to work around it, can follow the returned URL directly. | | Account mail language and product name | From 13.0.0-alpha.3 the password controller generated for SaaS, LMS and `cargo rullst auth` projects writes account emails in the request's language (the `rullst::i18n` `Locale` when the router has `LOCALES.layer()`, else `Accept-Language`, matched to English, Brazilian Portuguese or Spanish, else `DEFAULT_MAIL_LOCALE`) instead of always `MailLocale::En`, and names the product `APP_NAME` (environment, then `.env`) before the `APPLICATION_NAME` constant. Existing projects are not changed; to adopt it, pass a `MailLocale` from `MailLocale::from_preference` to `AccountMail::new` in `send_account_mail`. | | Production log defaults | From 13.0.0-alpha.3, in staging and production `Server` no longer prints `[HTTP]` lines for `/health` and `/ready` requests that answer below 400 (failing probes still appear); set `RULLST_LOG_HEALTH_PROBES=true` (process environment or `.env`) to restore them, and note that an invalid value now stops startup. Without `RUST_LOG`, the default tracing filter is `info,sqlx=warn` instead of `info`, which drops SQLx's own INFO events such as `acquired connection`; set `RUST_LOG=info` (or `info,sqlx=info`) to keep the previous output. | | Connect GitHub email | From 13.0.0-alpha.3 `GithubProvider` also calls `GET /user/emails` after `GET /user`: `ConnectUser::email` is the address GitHub marks `primary` (which may differ from the public profile address) and `email_verified` is that entry's `verified` flag (`Some(true)` or `Some(false)`) instead of always `None`. When the call fails (for example a token without the `user:email` scope) or lists no primary address, `email` stays the public profile address and `email_verified` stays `None`. Code that linked or created accounts on the GitHub email should now require `email_verified == Some(true)`; transports that answered every GET with the `/user` body keep the old result. `cargo rullst upgrade` reports `GithubProvider` and `Connect::driver("github", ...)` (`V13-CONNECT-GITHUB-EMAIL`). | | Account mail link lifetime | From 13.0.0-alpha.3 `AccountEvent::PasswordReset` accepts a remaining lifetime of 1–1440 minutes (24 hours) instead of 1–30; the verification and email-change links keep 1–30. Every account link now states whole hours as hours ("This link expires in 2 hours.", "1 hora") and one minute in the singular ("1 minute"); other values are unchanged ("30 minutes"). | | Generated reset link lifetime | From 13.0.0-alpha.3 new SaaS, LMS and `cargo rullst auth` projects bound `RESET_TOKEN_LIFETIME_MINUTES` (`src/models/password_reset.rs`) to 10–1440 with a compile-time assertion, and the forgot-password answer states that value instead of a hard-coded "30 minutes". In an alpha.2 project a value above 30 failed at send time (`AccountMail` refused it, so no email went out) and the answer still said 30 minutes: to change the lifetime, copy `reset_requested_notice` and `lifetime_words` from a new project's `password_controller.rs` and the assertion from its `password_reset.rs`. | | CLI deploy targets removed | **Breaking**, from 13.0.0-alpha.4: `cargo rullst deploy` no longer writes `fly.toml`, `railway.json` or `render.yaml` or runs `flyctl`/`railway`; `--platform fly\|railway\|render` fails before writing anything. It writes, when absent, the Dockerfile (with its dependency-cache stage), `compose.yaml` (the app and Caddy with automatic HTTPS; read-only root filesystems, every capability dropped except Caddy's `NET_BIND_SERVICE`, no new privileges, a `/health` check; works with `docker compose` and `podman compose`) and the `Caddyfile` (it checks the app's `/health` every 5 seconds and holds requests for up to 15 seconds while the app restarts), and trusts Caddy in `Rullst.toml` as `deploy --platform vps` did; `--platform vps` is still accepted. `compose.yaml` replaces `docker-compose.prod.yml`, which stays yours. It refuses a `Cargo.toml` package name that is not a valid Cargo name. Provider files you already have keep working with the provider's CLI; other targets are in [Advanced deployment](https://rullst.github.io/Rullst/book/advanced-deployment.html). `cargo rullst upgrade` reports `fly.toml`, `render.yaml`, `railway.json`, `railway.toml` and scripts that run the removed targets (`V13-DEPLOY-TARGETS-REMOVED`). For library users, `cargo_rullst::blueprints::deploy::{FLY_TOML, RAILWAY_JSON, RENDER_YAML, DOCKER_COMPOSE_PROD}` were removed (`COMPOSE_YAML` is new). | | SQLite database in the deployed image | From 13.0.0-alpha.4, the `Dockerfile` that `cargo rullst deploy` and `cargo rullst dockerize` write for a project whose `Rullst.toml` `[database] url` (or, without one, `.env.example` `DATABASE_URL`) is SQLite sets `ENV DATABASE_URL=sqlite:///app/data/rullst.db?mode=rwc`, as `new --docker` already did, so the database lives in the `/app/data` volume. Earlier `dockerize` and `deploy --platform vps` left it out: the starters' relative `sqlite://db.sqlite` then pointed into the image's read-only `/app`, and the app stopped at startup under a read-only root filesystem. An existing `Dockerfile` is kept, and `deploy` warns when it lacks the line: add it to the last stage, or set `DATABASE_URL` in `.env.production`. | | CLI Foundry, Nix, Buildah and gRPC generators removed | **Breaking**, from 13.0.0-alpha.4: `cargo rullst foundry:init`, `foundry:deploy`, `nixify`, `generate:buildah`, `make:grpc` and the `new --nix` and `new --buildah` flags were removed, with the wizard's "Nix flake" choice. Files they generated stay yours and are no longer regenerated; [Advanced deployment](https://rullst.github.io/Rullst/book/advanced-deployment.html) shows Nix, Buildah, Podman Quadlet and Kubernetes setups, and gRPC code keeps working with your own `tonic`/`tonic-build` setup. `cargo rullst upgrade` reports `Foundry.toml`, a `flake.nix` written by `nixify`, `build_buildah.sh`, `src/grpc/mod.rs` and scripts that run these commands (`V13-DEPLOY-GENERATORS-REMOVED`). For library users, `cargo_rullst::cli::Commands::{FoundryInit, FoundryDeploy, GenerateBuildah, Nixify, MakeGrpc}` and the `buildah`/`nix` fields of `Commands::New` and `ProjectScaffoldOptions` were removed, with the `generators::foundry` and `generators::grpc` modules, `generators::project::{generate_buildah_script, generate_nix_files}` and the deprecated `generators::project::create_new_project`. | | CLI self-update removed | **Breaking**, from 13.0.0-alpha.4: the CLI no longer downloads, verifies or replaces its own binary. `cargo rullst update check`, `update verify`, `update stage` and `update install review\|apply\|recover` were removed, and `update guided` lost `--scope`, `--root`, `--allow-major` and `--prerelease` (it now runs only the project flow, for a `--to` in the CLI's major). Install or update the CLI with `cargo install cargo-rullst --version =<version> --locked`; the native executables stay attached to each GitHub release for manual verification. At most once a day, in an interactive terminal, the CLI prints that command when crates.io has a newer release of its major (a newer prerelease too when the CLI is a prerelease); `RULLST_DISABLE_UPDATE_CHECK`, `CARGO_NET_OFFLINE`, `CI` and non-terminal streams silence it. `update project prepare\|verify\|review\|apply\|recover` are unchanged. `cargo rullst upgrade` reports scripts and workflows that run the removed commands (`V13-CLI-SELF-UPDATE-REMOVED`). | | CLI `ai` removed, `footprint` renamed `green` | **Breaking**, from 13.0.0-alpha.4: `cargo rullst ai` (the terminal assistant of the earlier 13.0 prereleases, with `ai connect`, `ai fix`, `ai review` and `ai upgrade`) was removed; `cargo rullst ai …` now prints what replaces it and exits with status 1. Use your editor's or terminal's AI assistant with the project's `AGENTS.md` and `.llms.txt` (`cargo rullst generate:ai-context` writes and checks them) and the published [`llms.txt`](https://rullst.github.io/Rullst/llms.txt). Keys saved by `ai connect` stay in `~/.config/rullst/credentials.toml` (`$XDG_CONFIG_HOME/rullst/`, or `%APPDATA%\rullst\` on Windows) and its git checkpoints under `refs/rullst/ai-checkpoints/` until you delete them. The `rullst-ai` crate is unchanged. `cargo rullst footprint` is now `cargo rullst green`, an Experimental command of the same `cargo-rullst` package (no separate install): it reports only measured figures (binary and `--image` size, idle RSS, `Cargo.lock` dependency count and, with `--bench`, CPU time per request) and an SCI estimate labelled `estimate` from your `--cpu-watts` and `--grid-intensity`; it drops requests/s, latency, peak RSS and RAPL energy, and its JSON is `rullst.green.v1` instead of `rullst.cli-footprint.v1`. `cargo rullst upgrade` reports scripts, workflows, Makefiles and justfiles that run `rullst ai` or `rullst footprint` (`V13-CLI-AI-REMOVED`). See the [green guide](https://rullst.github.io/Rullst/book/green.html). | | Core error console assistant panel | From 13.0.0-alpha.4 the development error page's "Fix with cargo rullst ai" panel (`cargo rullst ai fix <error-id>`) is an "Ask your AI assistant" panel: a read-only prompt to copy into your editor's or terminal's assistant, with the error message, its location, the first five project stack frames and the request method and path (no query string, headers, cookies or body), a copy button that only the page's nonce-carrying script shows, and, when `Server` records the error, the path `/_rullst/errors/{id}` of its JSON context. The retired `POST /_rullst/autofix` still answers loopback peers `410 Gone` with `success: false` and never edits files, but its message points to that panel and its `command` field is replaced by `context_url: "/_rullst/errors/<error-id>"`; a client that read `command` must read `context_url`. `catch_panic_middleware` records a panic only inside `Server`'s debug Development stack, which serves `/_rullst/errors/{id}`; the hot-reload router and an application's own router no longer record panics that nothing serves. No 12.x API changes. | | CLI starters moved to the examples | **Breaking**, from 13.0.0-alpha.4: `cargo rullst new` offers three starters: Blank, API (`--blueprint api`, the same as `--api`) and SaaS. The LMS, Blog, Portfolio and ERP blueprints were removed: the wizard no longer lists them or asks a separate web/API question, and `--blueprint lms\|blog\|portfolio\|erp` fails before writing anything with a report naming the example and its GitHub URL (exit status 1). LMS, ERP and Portfolio are complete applications in the repository's `examples/lms`, `examples/erp` and `examples/portfolio`, tested in its workspace; the blog showcase is `examples/blog`. Copy one to start from it. Applications generated earlier from these blueprints keep working: their code is theirs, so no upgrade rule applies. Library users of the `cargo_rullst` crate: the `blueprints::{lms, blog, portfolio, erp}` modules and `LMS_BLUEPRINT_ID`, `BLOG_BLUEPRINT_ID`, `PORTFOLIO_BLUEPRINT_ID` and `ERP_BLUEPRINT_ID` were removed (IDs 1, 3, 4 and 5 are never reused), and `BlueprintChoice::id()` became `starter()`, which returns the moved example (`MovedBlueprint`) as its error. | The [12.0→12.1 guide](https://rullst.github.io/Rullst/book/migration-v12-1.html) still applies to applications that have not adopted the 12.1 account, payment and deployment changes. Skipping directly to 13 does not perform those application/database migrations. ### `html!` returns `Markup` `cargo rullst upgrade` reports `V13-HTML-MARKUP-RETURN` for a function that declares a `String` (`String`, `Html<String>`, `Result<String, _>`) and returns `html!`, and `V13-HTML-RAW-MARKUP` for `RawHtml` around `html!` output. Before: ```rust,ignore use rullst::html; use rullst::html::RawHtml; fn badge(label: &str) -> String { html! { <span class="badge">{label}</span> } } fn rows(names: &[&str]) -> String { let rows: String = names .iter() .map(|name| html! { <tr><td>{name}</td><td>{RawHtml(badge("new"))}</td></tr> }) .collect(); html! { <table>{RawHtml(rows)}</table> } } ``` After: ```rust use rullst::html; use rullst::html::Markup; fn badge(label: &str) -> Markup { html! { <span class="badge">{label}</span> } } fn rows(names: &[&str]) -> Markup { let rows: Markup = names .iter() .map(|name| html! { <tr><td>{name}</td><td>{badge("new")}</td></tr> }) .collect(); html! { <table>{rows}</table> } } assert_eq!( rows(&["<Ana>"]), "<table><tr><td><Ana></td><td><span class=\"badge\">new</span></td></tr></table>" ); ``` Keep `RawHtml` (or `Markup::from_trusted`) only for trusted HTML produced outside `html!`, such as a sanitizer's output, and never for request data. ### Projects generated by the 12.0 CLI The 12.0.0 and 12.1.2 CLIs generate the same Rust sources, `Cargo.toml`, `Rullst.toml` and `.env` for the Blank and Blog starters, so the rows above and their findings apply unchanged. Three generated files differ; `cargo rullst upgrade` reports each under the "Build files from the 12.0 CLI" row: - `.cargo/config.toml` contains a `[target.x86_64-pc-windows-msvc]` block whose `rustflags` pass the unsupported MSVC linker flag `/DEBUG:FASTLINK` (`V13-MSVC-FASTLINK`). Delete that block; 12.1 no longer generates it. - `.gitignore` lists `/Cargo.lock` (`V13-CARGO-LOCK-IGNORED`). Remove that line and commit the lockfile, so builds and deployments use the versions you tested. The upgrade itself works without a committed lockfile. - A Dockerfile written by `--docker` or `dockerize` runs `cargo build --release` (`V13-DOCKER-UNLOCKED-BUILD`). Add `--locked` once the lockfile is committed. The starter's `rullst-orm` dependency relies on default features; since 13.0.0-alpha.3 `rullst-orm` has none, so after the upgrade it has no SQLx driver until the backend is named. The `V13-ORM-DEFAULT-FEATURES` finding gives the fix. ## Upgrading an alpha.2 application to alpha.3 This checklist moves an application from 13.0.0-alpha.2 to 13.0.0-alpha.3. The rows above that name alpha.3, and the ORM database-managed columns, ORM dirty tracking and Omni rows, are the changes in between. The [rehearsal on generated starters](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#1300-alpha2-to-1300-alpha3) shows the expected result. 1. **Prepare.** Work on a branch with a clean tree and a committed `Cargo.lock`, record the current `cargo test` result, and back up every database (check that the backup restores). 2. **Install the alpha.3 CLI.** `cargo install cargo-rullst --version =13.0.0-alpha.3 --locked --force`, then `rullst --version` must print `13.0.0-alpha.3`. 3. **Name the features.** `rullst` (which enabled `orm`, `drivers-all` and `queue-sqlite`) and `rullst-orm` (`drivers-all`) have no default features any more. A dependency that relied on them lists what the application uses, for example `rullst = { version = "=13.0.0-alpha.3", default-features = false, features = ["orm", "strict-sqlite", "queue-sqlite"] }` (`drivers-all` keeps the three SQLx drivers). Remove `offline-sync` from `rullst` and `rullst-core` features. Projects generated by `cargo rullst new` already disable the defaults and list their features. 4. **Inspect.** Run `cargo rullst upgrade --dry-run` and fix every `MUST-CHANGE` finding first (`V13-OFFLINE-SYNC-REMOVED` is the one alpha.3 adds). These findings concern alpha.3 changes and usually need an edit: - `V13-ORM-SKIP-MANAGED-COLUMNS`: replace `#[orm(skip)]` with `#[orm(db_default)]` on `User::is_admin` and `User::session_epoch` (SaaS, LMS and `cargo rullst auth` models generated by alpha.2) and on any other column the database manages. No schema change is needed. - `V13-ORM-TRANSACTION-ERRORS`: `Orm::transaction` returns its closure's own error type, so match that error instead of `DatabaseError`. A closure whose error type lacks `From<rullst_orm::Error>` (such as `String`) no longer compiles, which `cargo check` reports; return `rullst_orm::Error` or an application error that wraps it. - `V13-RULLST-DEFAULT-FEATURES` and `V13-ORM-DEFAULT-FEATURES`: step 3. - `V13-CONNECT-GITHUB-EMAIL`: the GitHub provider now returns the primary address and GitHub's verified flag; link or create accounts on it only when `email_verified == Some(true)`. - `V13-OMNI-REMOVED` and `V13-OFFLINE-SYNC-REMOVED`: replace the Omni shell with `cargo rullst make:pwa`, as the Omni row describes. The plan reads the application's version from its manifests and `Cargo.lock` and applies only the rules introduced after 13.0.0-alpha.2, which are the ones above; its `Rules:` line says how many earlier rules it skipped. If it names 13.0.0-alpha.1 instead, a requirement still has the caret form of the alpha.1 CLI (`"13.0.0-alpha.1"`), and the alpha.2 rules are listed too. `--all-rules` also lists the rules for the 12.x to 13 changes that alpha.2 already contained, such as `V13-REFERRER-NO-REFERRER`, `V13-MAIL-FACADE-CONFIG`, `V13-LMS-RECORD-PROGRESS` and `V13-HOT-RELOAD-MACHINE-ENDPOINTS`; an alpha.2 application already runs with those changes, so they need no edit for this step. 5. **Apply.** `cargo rullst upgrade` pins every Rullst dependency, including direct `rullst-auth`, `rullst-security` or `rullst-connect` lines, to `=13.0.0-alpha.3`, runs `cargo fix` and `cargo check --locked`, and restores the files if a step fails (`--keep-on-failure` keeps the failed state for diagnosis). Make the edits from step 4, then update the dependencies of the changelog's Dependency security entries that the lockfile contains: `cargo update -p ammonia -p rustls-webpki -p webpki-roots -p webpki-root-certs`. 6. **Read the alpha.3 rows that no rule locates** and adjust the application where it depends on them: ORM transactional migrations and the pool (no `Orm::pool()` inside a `within_transaction()` migration), Connect mock credentials and Connect mock authorization URL (mock credentials need the `mock` feature, `oauth-mock` on the facade), Production log defaults (health probes are no longer logged, SQLx INFO events are hidden, and an invalid `RULLST_LOG_HEALTH_PROBES` stops startup), and the opt-in ORM dirty tracking, Nexus time zone (`Nexus::with_time_zone`, in the Nexus dates and times row) and Account mail language and product name. Workarounds for alpha.2 defects that the changelog's "Fixes from alpha.2 dogfooding" lists, such as raw SQL to filter on `is_admin`, can then go. 7. **Verify** with the production feature set: `cargo build`, `cargo test` and `cargo clippy --all-targets -- -D warnings`; `db:migrate` against a restored copy of the production database; then sign-in, "Sign out everywhere", Nexus access for an `is_admin` account, each configured provider and the health probes on staging before production. ## Upgrading an alpha.3 application to alpha.4 This checklist moves an application from 13.0.0-alpha.3 to 13.0.0-alpha.4. alpha.4 changes the CLI and the files it generates, not the APIs of the framework crates: the rows above that name alpha.4 are the changes in between. 1. **Prepare** as for alpha.3: a branch with a clean tree and a committed `Cargo.lock`, the current `cargo test` result and a database backup. 2. **Install the alpha.4 CLI.** `cargo install cargo-rullst --version =13.0.0-alpha.4 --locked --force`, then `rullst --version` must print `13.0.0-alpha.4`. `cargo rullst green` (the former `footprint`) is part of this package; nothing else to install. `cargo rullst ai` was removed: use your editor's or terminal's AI assistant with the project's `AGENTS.md` (`cargo rullst generate:ai-context`) and the published [`llms.txt`](https://rullst.github.io/Rullst/llms.txt). 3. **Inspect.** `cargo rullst upgrade --dry-run` applies only the rules introduced after 13.0.0-alpha.3. All four are `REVIEW` findings about files and scripts, not Rust code: - `V13-DEPLOY-TARGETS-REMOVED`: `fly.toml`, `render.yaml`, `railway.json` or `railway.toml`, or a script that runs `deploy --platform` with Fly.io, Railway or Render. - `V13-DEPLOY-GENERATORS-REMOVED`: `Foundry.toml`, a `nixify` `flake.nix`, `build_buildah.sh`, `src/grpc/mod.rs`, or a script that runs a removed generator. - `V13-CLI-SELF-UPDATE-REMOVED`: a script that runs `update check|verify|stage|install` or `update guided --scope|--root`. - `V13-CLI-AI-REMOVED`: a script that runs the removed `rullst ai` or `rullst footprint` (now `green`). Generated files stay yours and keep working with their own tools; keep or delete them. Scripts that run a removed command need the replacement the finding names. 4. **Apply.** `cargo rullst upgrade` pins every Rullst dependency to `=13.0.0-alpha.4`, runs `cargo fix` and `cargo check --locked`, and restores the files if a step fails. 5. **Optionally** copy the `[profile.dev]` sections that new projects get ([Build times](https://rullst.github.io/Rullst/book/build-times.html)). A SaaS application whose code does not import `rullst_connect` can remove that dependency. 6. **Verify** with the production feature set: `cargo build`, `cargo test` and `cargo clippy --all-targets -- -D warnings`. ## Why the candidate uses a new major Adding optional privacy APIs, a new crate or more features does not by itself require a major version. A fully compatible increment would belong in a minor release under the [compatibility policy](https://rullst.github.io/Rullst/book/compatibility-policy.html). The current candidate does change a supported CLI/configuration contract: 13.0 removes commands and public items that 12.x applications can use (the rows marked Breaking above list them), for example `cargo rullst make:omni` and `cargo rullst omni`, the `offline-sync` feature with `rullst::offline_sync`, and the `Commands::MakeOmni`/`Commands::Omni` variants and `OmniPlatformChoice` of the `cargo-rullst` library. This intentional change supports retaining v13 for the synchronized release train; it does not mean every application's Rust code needs rewriting. The Omni row above documents the replacement. A green version-aware SemVer job alone cannot establish that this candidate could be renamed to 12.2. The current command derives the release type from the manifest; the 12.1-to-13 comparison permits major changes. The hosted comparison at `4773171a` reported **0 executed checks and 254 skipped checks** for compared libraries. Treat that as no Rust API compatibility result, not as proof that breaking changes are absent. Any reconsideration as a minor release requires an explicit minor-level API comparison plus review and regression tests for CLI, configuration, serialized contracts and generated consumers. Resolve the known Android compatibility change before making that claim. ## Use the target-major CLI A v12 CLI cannot learn v13 migration rules just by downloading another binary: install the v13 CLI, then start a **new invocation of that v13 CLI** for the project migration. To evaluate the published prerelease, install its CLI from crates.io. Cargo never selects a prerelease on its own (`cargo install cargo-rullst` installs the end-of-life 12.x CLI), so name the exact version: ```bash cargo install cargo-rullst --version =13.0.0-alpha.4 --locked rullst --version ``` The output must identify `13.0.0-alpha.4`. The projects it generates pin the framework packages with `=13.0.0-alpha.4`, as described at the top of this guide. Reinstall with `--force` to go back to 12.x (`--version '^12'`) or to move to a later prerelease. To evaluate unreleased `main` changes instead, build the CLI from the reviewed checkout: ```bash cargo build --locked -p cargo-rullst ./target/debug/rullst --version ``` Use that explicit executable path instead of assuming a `rullst` already on `PATH` is the same binary. This is a local source build, not authenticated published distribution evidence. Generated projects then refer to the matching local framework packages; keep that checkout available and review those paths. The final package rehearsal must independently prove registry-based consumers. ## Prepare, verify, review and apply Keep a restorable application and database backup. Quiesce writers during file application/recovery and review the application's ignored files, toolchain and Cargo configuration. The Git working directory's tracked and non-ignored files are copied into private preparation storage; the root `Cargo.lock` is included even if ignored. Typical ignored secret files and build outputs are excluded. Using the selected v13 executable, the structured sequence is: ```text rullst update project prepare --project /path/to/app --to 13.0.0-alpha.4 --json rullst update project verify --prepared PREPARED_DIRECTORY --dry-run --json rullst update project verify --prepared PREPARED_DIRECTORY --allow-project-code --json rullst update project review --verified VERIFIED_DIRECTORY --json rullst update project apply --verified VERIFIED_DIRECTORY --approved-review REVIEW_SHA256 --json ``` Use the directories returned by the preceding commands. Inspect the complete review before supplying its digest. Verification executes trusted build scripts, procedural macros and tests, and is offline by default; the private copy is not a sandbox. Supply `--allow-network` only after reviewing that separate need. Select the application's actual feature policy (`--features`, `--all-features` or `--no-default-features`) instead of assuming default features cover production. Preparation accepts supported versioned dependency declarations and preserves their TOML structure. Unversioned path/git dependencies, ambiguous ranges, unsupported source majors, stale inputs and unresolved source findings require manual review. Verification requires every managed framework package in the candidate lockfile to resolve to the selected exact target. It runs locked workspace checks and tests before review can authorize manifest/lockfile edits. After application, source-file recovery uses the same reviewed operation: ```text rullst update project recover --verified VERIFIED_DIRECTORY --approved-review REVIEW_SHA256 --json ``` Recovery rejects later conflicting edits. It restores the recorded manifest and lockfile states; it cannot undo database writes, network calls, provider effects or other external effects of application tests. It does not deploy either state. ## Privacy remains an application decision The preview [privacy guide](https://rullst.github.io/Rullst/book/privacy-age-assurance-roadmap.html) documents the explicit local source selection and authenticated SaaS installation contracts. Do not enable an age restriction merely because the framework offers one. Select a server-owned policy for the actual action and accept declarations only where that policy permits their assurance level. Production rejects unsupported stronger methods and process-local replay storage. Consent-store files, replay databases, encryption/signing keys, subject/tenant binding and retention/restore procedures need their own deployment plan. Shared-local SQLite consent is not a multi-host consensus store; the optional `consent-postgres` store shares consent through one authoritative PostgreSQL database. The profile export remains available independently of optional-consent configuration, but it only exports the authenticated account fields supported by its adapter. ## Evidence and remaining acceptance `update_project_cli` exercises distinct 12.1.0 and 13 packages through real Cargo resolution, prepare/verify/review/apply/recover, changed inputs and wrong approval digests. Those intentionally tiny packages prove the updater protocol, not the framework's runtime API compatibility. The separate generated-starter suites compile actual framework source and execute application/authentication tests; the age/privacy suites also exercise both opt-in installation orders. Final release admission still requires the combined exact-commit workspace and feature/consumer matrix, strict Clippy and formatting, package/native artifact rehearsals, the required security/fuzz workflows, and this inventory updated for all admitted changes. Application sandbox/provider journeys and deployment smoke tests remain distinct from framework CI. See the [SST](https://rullst.github.io/Rullst/book/spec.html#-12-assisted-framework-upgrade-contract) and [delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html). --- # Generated Project Instructions & Context Source: https://rullst.github.io/Rullst/book/project-context.html # Generated project instructions and context Since v13, `cargo rullst new` and `cargo rullst generate:ai-context` write a bounded project inventory and create an application `AGENTS.md` when none exists; existing instructions are preserved. Point your own coding assistant at `AGENTS.md` and `.llms.txt`. This is a source-navigation aid; the map does not establish that a feature is implemented, a test passed, a provider is configured or a deployment is secure. ```bash cargo rullst generate:ai-context cargo rullst generate:ai-context --check ``` The first command writes `.llms.txt` and `.rullst/context-map.json`. The second recomputes their expected content without writing and returns a nonzero exit status if either output is missing, modified or stale. New projects receive the same inventory and instructions. `make:controller`, `make:model`, `make:middleware`, `make:worker`, `make:island`, `auth`, `make:billing`, `make:cors`, `make:jwt`, `make:privacy`, `make:age-gate` and `make:oauth` refresh the map after they write; a refresh failure is reported while preserving the completed scaffold. The documentation site also publishes `llms.txt` and `llms-full.txt` at <https://rullst.github.io/Rullst/llms.txt> and <https://rullst.github.io/Rullst/llms-full.txt>: an index of the framework's book and its full text, for assistants that need framework documentation rather than your application's inventory. The versioned `rullst.project-context.v1` map contains the project and dependency names, normalized dependency requirements, requested dependency features, declared Cargo feature names, configuration key names, source-file paths/roles/sizes and an input SHA-256. Dependency requirements inherited from a workspace or omitted for path/git sources are identified explicitly. It does not resolve Cargo's complete feature graph or inspect external workspace members. It scans the selected Cargo root and its `src` directory; generate a separate map at each application root. The fingerprint covers Rust source bytes and the metadata actually inventoried. Configuration **values** and dependency URLs are excluded even from this digest. Changing only those values therefore does not make the inventory stale. The fingerprint is an aid to detecting source changes, not a signed attestation. ## Boundaries | Input or output | Limit or behavior | | :--- | :--- | | `Cargo.toml`, optional `Rullst.toml`, optional `.env.example` | At most 256 KiB each; errors do not echo their contents. | | `.env.example` | Single-line assignments, including quoted values and `export`; only key names are recorded. Multiline declarations fail before continuation content can become a key. No variable interpolation occurs. | | Rust sources | Regular `.rs` files below `src`; at most 512 files, 1 MiB each and 8 MiB combined. | | Traversal | At most 4,096 observed entries and 16 nested directories; linked/special files, hidden descendants, `target`, `vendor` and `node_modules` are excluded. A linked source root or configuration file fails. | | Names | Bounded ASCII names and relative paths; unsupported names fail rather than being silently renamed. | | Outputs | At most 256 KiB each; linked or unrecognized output is rejected before replacement. | | Project instructions | Existing `AGENTS.md` is neither read into the map nor replaced. Edit it to add application-specific requirements. | Source bodies, configuration values, credentials, live `.env` files, database files, dependency URLs and absolute workstation paths are not copied into the outputs. The map does not infer route authorization, model relationships or provider readiness from filenames. Read the source before changing behavior. The workspace must be trusted against concurrent adversarial filesystem edits; ordinary replacement uses the CLI's preflight and atomic rollback helpers. ## Existing applications Regeneration recognizes the prior CLI's generated `.llms.txt` header and replaces that source-concatenation format with the inventory. A legacy file larger than the output budget or an unrecognized manually maintained file is refused; move it aside yourself after reviewing what it contains. Regeneration updates only recognized generated outputs and never replaces project instructions. Current local fixtures exercise deterministic output, stale checks, malformed configuration, private-value omission, source limits, links, output conflicts, legacy migration and real project/scaffold commands. Combined hosted and installed-archive acceptance passed in [PR #221](https://github.com/Rullst/Rullst/pull/221); final release evidence is tracked separately in the [v13 delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html). --- # Schema-First Rust & TypeScript APIs Source: https://rullst.github.io/Rullst/book/typed-api.html # Schema-first Rust and TypeScript APIs `cargo rullst generate:api` is new in v13 (first published in 13.0.0-alpha.1). It consumes one explicit OpenAPI 3.1 JSON document and generates Rust DTOs/operation codecs, a TypeScript client and a canonical OpenAPI copy. It does not infer request or response types from a route handler, install dependencies or mount routes. The earlier `generate:openapi` and `generate:ts` scanning commands remain separate discovery helpers with placeholder/unchecked types. ## Generate and check Use the v13 CLI (`cargo install cargo-rullst --version =13.0.0-alpha.4 --locked`; see [Getting started](https://rullst.github.io/Rullst/book/1-getting-started.html)). From the project root: ```bash cargo rullst generate:api --schema api/profile.json --output src/profile_api cargo rullst generate:api --schema api/profile.json --output src/profile_api --check ``` The output directory contains `contract.rs`, `client.ts` and `openapi.json`. `--check` compares all three without writing anything. A missing, changed or stale output fails. Regeneration preflights all files, preserves unrelated files and refuses linked, special or unrecognized existing outputs. Keep application handlers outside these generated files. The schema digest records input identity; it does not authenticate the schema or prove deployment compatibility. Generation assumes a trusted project directory without concurrent adversarial filesystem edits. This minimal input uses the explicit `rullst.api.v1` profile: ```json { "openapi": "3.1.1", "x-rullst-profile": "rullst.api.v1", "info": {"title": "Profile API", "version": "1"}, "security": [{"bearerAuth": []}], "components": { "securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer"}}, "schemas": { "Profile": { "type": "object", "additionalProperties": false, "required": ["display_name", "note"], "properties": { "display_name": {"type": "string", "minLength": 1, "maxLength": 80}, "note": {"type": ["string", "null"], "maxLength": 200} } }, "Failure": { "type": "object", "additionalProperties": false, "required": ["code"], "properties": {"code": {"type": "string", "maxLength": 80}} } } }, "paths": { "/profiles/{owner}": { "get": { "operationId": "read_profile", "parameters": [{ "name": "owner", "in": "path", "required": true, "schema": {"type": "string", "minLength": 1, "maxLength": 80} }], "responses": { "200": {"description": "Own profile", "content": { "application/json": {"schema": {"$ref": "#/components/schemas/Profile"}} }}, "401": {"description": "Unauthenticated", "content": { "application/json": {"schema": {"$ref": "#/components/schemas/Failure"}} }}, "403": {"description": "Forbidden", "content": { "application/json": {"schema": {"$ref": "#/components/schemas/Failure"}} }} } } } } } ``` ## Server wiring and authorization Add direct `serde` (with `derive`), `serde_json` and `rullst-security` dependencies; pin `rullst-security` to the same exact version as `rullst` (`rullst-security = "=13.0.0-alpha.4"`), because `contract.rs` uses its `schema_guard::JsonSchemaPolicy`. Import `contract.rs` as an application module and construct `Contract::new()` once during startup; handle configuration errors before serving traffic. Each `op_<operationId>` module provides `METHOD`, `PATH`, a typed `Params`, `decode_params` and typed response variants plus `encode_response`. Body operations also expose `Request` and `decode_request`. Authenticate the bearer token and check tenant/resource ownership in the handler. Pass decoded path/query pairs to `decode_params`, retaining duplicate parameters so the codec can reject them. Path fields use `p_` and query fields `q_` prefixes. For a body operation, enforce `application/json` and the 64 KiB transport limit before reading, then pass the bytes to `decode_request`. Encode every response through the operation codec, set its returned HTTP status and JSON content type, and retain the application's secure headers, WAF, CSRF policy and rate limits. Never derive the authenticated user from a submitted path/body owner value. Serde derives are useful for application composition, but using them directly bypasses schema, presence and size checks. The generated codecs are the wire boundary. An optional field is omitted when its `Option` is `None`; a required nullable field serializes `None` as JSON `null`. Missing required nullable fields and null optional non-nullable fields are rejected before deserialization. ## Client use Compile `client.ts` with TypeScript's `--strict --exactOptionalPropertyTypes`, `--target ES2022` and `--lib ES2022,DOM`. The client uses Fetch, Web Streams, TextEncoder/TextDecoder and AbortSignal.any; this increment is exercised on Node 24, not a claim of acceptance on every browser or native WebView. ```typescript const client = new ContractClient('https://api.example.com', () => sessionToken); const response = await client.call_read_profile({p_owner: authenticatedUserId}); if (response.status === 200) { console.log(response.body.display_name); } else { console.log(response.status, response.body.code); } ``` The base URL must be an HTTP(S) origin without credentials, path, query or fragment. Supply the current application-owned token through the callback; it is not persisted by the client. Pass an optional AbortSignal to each operation. The client encodes path/query values, refuses redirects, omits cookies, requests no-store/no-referrer behavior and bounds each operation to ten seconds including credential lookup. Callbacks still own cancellation of their own background work. Declared error statuses return their typed body union. Unknown statuses, malformed JSON, duplicate keys, invalid UTF-8, oversized responses and schema mismatches raise a fixed `ContractError` without tokens, upstream bodies or server diagnostics. ## Supported profile and limits The profile follows the [OpenAPI 3.1 schema model](https://spec.openapis.org/oas/v3.1.1.html) and preserves [required versus optional object properties](https://json-schema.org/understanding-json-schema/reference/object). Enable [strict null checking](https://www.typescriptlang.org/tsconfig/strictNullChecks.html) so those distinctions remain visible to the consumer compiler. | Area | Initial supported contract | | --- | --- | | Document | JSON; OpenAPI 3.1.0/3.1.1/3.1.2; explicit `rullst.api.v1`; global HTTP bearer scheme. No external files/network resolution. | | Shapes | Closed named objects, local acyclic object references, bounded strings/arrays, booleans and safe integers. Unknown keywords fail. | | Presence | Required nullable scalar/array fields and optional non-nullable fields. Optional nullable fields and nullable references are unsupported. | | Strings/arrays | Required `maxLength` ≤ 4096 code points or `maxItems` ≤ 128; optional valid minimum. Invalid Unicode surrogates fail. | | Integers | Explicit `minimum`/`maximum` within ±9,007,199,254,740,991. Numeric tokens ≤128 characters and exponent within ±128; exact decimal/exponent forms must represent an integer, without fractional rounding. | | Parameters | Explicit path strings with `minLength` ≥1 and query strings/integers/booleans; explicit `required`. Empty/dot path segments, unknown/duplicate parameters, noncanonical query integers and null query values fail. | | Operations | GET/DELETE without body; POST/PUT/PATCH with required JSON object body; explicit JSON success, 401 and 403 responses. No wildcard statuses, redirects, empty-body 204/205 or streaming. | | Names | Components: uppercase-first ASCII alphanumeric, ≤64 characters. Fields/operation IDs: lowercase-first ASCII letters, digits and underscores, ≤40; reserved collision names fail. | | Generation | 128 KiB source, depth24/8192 source nodes, 32 schemas, 32 paths/operations, 32 properties/object, 16 parameters/responses, reference depth8; 512 KiB per output. | | Wire | 64 KiB per received/sent JSON body, depth24 and 8192 value nodes. Exact integer normalization has a separate bounded scratch buffer. | Formats, patterns, defaults, enums, open maps, inline objects, recursive schemas, polymorphism, arbitrary numeric wire values, file uploads, headers/cookie parameters and authentication schemes beyond this profile are rejected. Extend the source profile and its compiled consumer tests before adding another supported shape. Local acceptance compiles generated Rust with strict Clippy and TypeScript 5.9.3 from a pinned lockfile, then executes the client against a disposable Rust HTTP server. It covers Unicode, optional/null semantics, input/output validation, query duplicates, cross-owner/unauthenticated denial and transport faults. The test's fixed identities are fixtures, not a production authentication service. The combined source passed hosted and installed-archive acceptance in [PR #221](https://github.com/Rullst/Rullst/pull/221). The final release campaign and deployment-specific browser acceptance remain separate requirements. --- # Translations and Locales Source: https://rullst.github.io/Rullst/book/i18n.html # Translations and Locales Since 13.0.0-alpha.2, `rullst::i18n` translates an application's interface with catalogs checked at compile time, chooses each visitor's locale from a cookie or `Accept-Language`, and switches locale with a CSRF-protected form. It is part of Core and needs no Cargo feature. - **Catalogs:** one TOML file per locale, loaded by `rullst::i18n::catalog!` when the crate compiles. A key missing from any locale, a key used in code that no catalog has, or placeholders that differ between locales fail the build. - **`t!`:** returns plain text that `html!` escapes. Only keys ending in `_html` are trusted HTML. - **Plurals:** CLDR's cardinal rules for 18 languages (Portuguese has separate Brazilian and European rules), with every form (`zero`, `one`, `two`, `few`, `many`, `other`) the language needs. - **Negotiation:** the `rullst_locale` cookie, then `Accept-Language`, then your default; the chosen locale also feeds `<html lang>`. - **Formatting:** numbers, percentages, money and dates in the locale's conventions, with CLDR data for 20 locales. There is no `make:i18n` generator yet: the setup below is a few small files. ## Set up ### 1. Write one catalog per locale Create `locales/` next to `Cargo.toml`, with one file per locale named by its tag (`language[-Script][-REGION]` in canonical case: `en`, `pt-BR`, `zh-Hant`, `es-419`). Nested tables form dotted keys. ```toml # locales/en.toml [nav] home = "Home" language = "Language" [dashboard] title = "Dashboard" greeting = "Hello, {name}!" [inbox.unread] one = "You have one unread message" other = "You have {count} unread messages" [legal] terms_html = "I accept the <a href=\"/terms\">terms</a>, {name}" ``` ```toml # locales/pt-BR.toml [nav] home = "Início" language = "Idioma" [dashboard] title = "Painel" greeting = "Olá, {name}!" [inbox.unread] one = "Você tem {count} mensagem não lida" other = "Você tem {count} mensagens não lidas" [legal] terms_html = "Aceito os <a href=\"/terms\">termos</a>, {name}" ``` ### 2. Load them in a module ```rust,ignore // src/i18n.rs rullst::i18n::catalog!(default = "en"); ``` Declare it with `mod i18n;` in `main.rs`. `catalog!` defines two items in that module: `LOCALES`, the supported locales, and the `t!` macro, which other modules import with `use crate::i18n::t;`. Use `dir = "path"` for another directory (relative to `Cargo.toml`). `catalog!` tracks the files it read, so editing one rebuilds the crate. Stable Rust gives a macro no way to watch a directory, so add a `build.rs` next to `Cargo.toml` to rebuild when a locale file is added or removed: ```rust,ignore // build.rs fn main() { // Use the same directory as `catalog!(dir = …)`. println!("cargo::rerun-if-changed=locales"); } ``` Cargo rescans the directory before each build. Without this file, a new locale appears only after another change to the crate. ### 3. Negotiate, translate and switch ```rust,ignore use crate::i18n::{LOCALES, t}; use rullst::i18n::Locale; use rullst::security::CsrfToken; use rullst::server::Extension; use rullst::{HtmxRequest, html, render_page_with_lang}; async fn dashboard( locale: Locale, htmx: HtmxRequest, Extension(csrf): Extension<CsrfToken>, ) -> rullst::response::Html<String> { let switcher = html! { <form method="post" action="/locale"> <input type="hidden" name="_token" value={csrf.as_str()} /> <input type="hidden" name="next" value="/" /> <label>{t!(locale, "nav.language")} <select name="locale"> @for option in LOCALES.iter() { <option value={option.tag()} selected?={option == locale}>{option.tag()}</option> } </select> </label> <button>"OK"</button> </form> }; let body = html! { {switcher} <h1>{t!(locale, "dashboard.greeting", name = "Ana")}</h1> <p>{t!(locale, "inbox.unread", count = 3)}</p> }; render_page_with_lang(&htmx, locale.tag(), &t!(locale, "dashboard.title"), body) } let app = rullst::Router::new() .route("/", rullst::web::axum::routing::get(dashboard)) .route("/locale", LOCALES.switch_route()) .layer(LOCALES.layer()) // `Server` adds the CSRF middleware only in staging and production. .layer(rullst::server::from_fn(rullst::security::csrf_middleware)); ``` `LOCALES.layer()` negotiates every request's locale and the `Locale` extractor reads it; a handler that extracts `Locale` on a route without the layer answers `500` naming the missing layer. Pass `locale.tag()` to `render_page_with_lang` so `<html lang>` matches the text. The switch route is a form `POST`, so the CSRF middleware checks its `_token` field like any other form. `Server` installs that middleware only in staging and production; compose `csrf_middleware` on the router as above, as the starters do, or `Extension<CsrfToken>` answers `500` in development. `rullst-core/tests/i18n_catalog.rs` compiles this handler's language menu and runs catalogs like the ones above end to end, including the switch behind the CSRF middleware; the `rullst::i18n` API documentation runs the rest as a doctest. ## Catalog format | Form | Meaning | | :--- | :--- | | `key = "text"` | A message. Keys use ASCII letters, digits, `_` and `-`; tables nest them with `.`. | | `{name}` | A placeholder: `t!(locale, "key", name = value)`. Any `Display` value. Names are Rust identifiers that are not keywords. | | `{{` and `}}` | A literal `{` or `}`. | | a table of CLDR categories with `other` | A plural message, called with `count = n` (any integer type or a `Decimal`; the absolute value is used). | | a key ending in `_html` | Trusted HTML from the catalog. `t!` returns `Markup`, and its arguments are HTML-escaped. | `t!` returns `Cow<'static, str>`: a message without placeholders is borrowed from the binary, others are formatted. Text from a catalog is never trusted as HTML unless its key ends in `_html`, so `"a < b"` renders as `a < b`. ### Plural rules Rullst selects the form with CLDR's cardinal plural rules (the same rules ICU and browsers use, checked against CLDR's sample numbers). A plural message defines exactly the forms its locale's language uses: | Languages | Forms | Examples | | :--- | :--- | :--- | | `ja`, `ko`, `zh` | `other` | every count | | `de`, `en`, `nl` | `one`, `other` | `one`: 1 (but not `1.0`) | | `tr` | `one`, `other` | `one`: 1 and `1.0` | | `es`, `it`, `pt-PT` | `one`, `other`, optional `many` | `one`: 1; `many`: 1000000, 2000000 | | `fr`, `pt`, `pt-BR` | `one`, `other`, optional `many` | `one`: 0, 1, `1.5`; `many`: 1000000 | | `cs` | `one`, `few`, `many`, `other` | `few`: 2–4; `many`: decimals | | `pl` | `one`, `few`, `many`, `other` | `few`: 2–4, 22–24; `many`: 0, 5–21, 25 | | `ru`, `uk` | `one`, `few`, `many`, `other` | `one`: 1, 21; `few`: 2–4, 22; `many`: 0, 5–20; `other`: decimals | | `ro` | `one`, `few`, `other` | `few`: 0, 2–19, 101; `other`: 20, 100 | | `he` | `one`, `two`, `other` | `two`: 2 | | `ar` | `zero`, `one`, `two`, `few`, `many`, `other` | `few`: 3–10, 103; `many`: 11–99 | Regions and scripts use their language's rule (`fr-CA`, `es-419`, `zh-Hant`), except Portuguese: `pt-PT` has its own rule and other Portuguese regions are not supported. `many` in Spanish, French, Italian and Portuguese only applies to exact millions ("1 million de messages"); when a message leaves it out, `other` is used. A missing form is a compile error that names it: ```text error: locales/ru.toml: plural message `inbox.unread` needs a `few` form for `ru`, which uses `one`, `few`, `many` and `other` ``` ```toml # locales/ru.toml [inbox.unread] one = "У вас {count} непрочитанное сообщение" few = "У вас {count} непрочитанных сообщения" many = "У вас {count} непрочитанных сообщений" other = "У вас {count} непрочитанного сообщения" ``` Visible fraction digits count, as in CLDR: pass a `rullst::i18n::Decimal` (`Decimal::new(15, 1)` is 1.5, `"1.0".parse()` keeps its zero) to select the form of a decimal. `{count}` displays the `Decimal` as plain ASCII (`1.5`); pass a separately formatted value for display when the locale writes decimals differently. Without a catalog, `locale.plural_category(n)` returns the `PluralCategory`. ```rust use rullst::i18n::{Decimal, Locales, PluralCategory}; let locales = Locales::new(&["pl", "ar", "en"], "en")?; let pl = locales.get("pl").unwrap(); assert_eq!(pl.plural_category(22), Some(PluralCategory::Few)); assert_eq!(pl.plural_category(25), Some(PluralCategory::Many)); assert_eq!(pl.plural_category(Decimal::new(25, 1)), Some(PluralCategory::Other)); let ar = locales.get("ar").unwrap(); assert_eq!(ar.plural_category(0), Some(PluralCategory::Zero)); assert_eq!(ar.plural_category(111), Some(PluralCategory::Many)); # Ok::<(), rullst::i18n::LocaleError>(()) ``` ## Format numbers, money and dates A `Locale` formats values with CLDR's conventions (CLDR 48 data compiled into Rullst, no runtime files or network): decimal and grouping separators, the minus sign, percent and currency placement, and the locale's date and time patterns with its month and weekday names. ```rust use chrono::NaiveDate; use rullst::i18n::{Currency, DateStyle, Decimal, Locales, TimeStyle}; let locales = Locales::new(&["en-US", "pt-BR", "de-DE", "fr-FR"], "en-US")?; let [en, pt, de, fr] = ["en-US", "pt-BR", "de-DE", "fr-FR"].map(|tag| locales.get(tag).unwrap()); // Money is given in integer minor units (cents), never as a float. assert_eq!(pt.format_currency(123_456, Currency::BRL), "R$\u{a0}1.234,56"); assert_eq!(en.format_currency(123_456, Currency::USD), "$1,234.56"); assert_eq!(de.format_currency(-123_456, Currency::EUR), "-1.234,56\u{a0}€"); assert_eq!(en.format_currency(1_234, Currency::JPY), "¥1,234"); let franc = Currency::new("CHF", 2).unwrap(); assert_eq!(de.format_currency(1_250, franc), "12,50\u{a0}CHF"); // Numbers show every digit of a `Decimal`; round explicitly. assert_eq!(fr.format_number(1_234_567), "1\u{202f}234\u{202f}567"); assert_eq!(pt.format_number(Decimal::new(-12_345, 1)), "-1.234,5"); let ratio = Decimal::new(256, 3); // 0.256 assert_eq!(en.format_percent(ratio), "25.6%"); assert_eq!(en.format_percent(ratio.round_half_even(2)), "26%"); assert_eq!(fr.format_percent(ratio.round_half_even(2)), "26\u{a0}%"); // Dates and times, from any chrono date or time. let at = NaiveDate::from_ymd_opt(2026, 10, 22) .and_then(|date| date.and_hms_opt(15, 45, 0)) .unwrap(); assert_eq!(pt.format_date(&at, DateStyle::Short), "22/10/2026"); assert_eq!(en.format_date(&at, DateStyle::Short), "10/22/26"); assert_eq!(en.format_date(&at, DateStyle::Numeric), "10/22/2026"); assert_eq!(de.format_date(&at, DateStyle::Medium), "22.10.2026"); assert_eq!(pt.format_date(&at, DateStyle::Full), "quinta-feira, 22 de outubro de 2026"); assert_eq!( en.format_datetime(&at, DateStyle::Long, TimeStyle::Short), "October 22, 2026 at 3:45\u{202f}PM" ); assert_eq!(pt.format_time(&at, TimeStyle::Medium), "15:45:00"); # Ok::<(), rullst::i18n::LocaleError>(()) ``` In a view, interpolate the result like any other text: `<td>{locale.format_currency(order.total_cents, Currency::BRL)}</td>`. | Method | Input | Example (`pt-BR`) | | :--- | :--- | :--- | | `format_number(value)` | any integer or a `Decimal` | `1.234,5` | | `format_percent(ratio)` | a ratio (`0.25` is 25%) | `25%` | | `format_currency(minor_units, currency)` | `i64` minor units and a `Currency` | `R$ 1.234,56` | | `format_date(&date, style)` | any `chrono::Datelike` | `22/10/2026` | | `format_time(&time, style)` | any `chrono::Timelike` | `15:45` | | `format_datetime(&value, date_style, time_style)` | both | `22/10/2026, 15:45` | - **Locales with data:** `ar`, `cs`, `de`, `en` (United States), `en-GB`, `es` (Spain), `fr`, `he`, `it`, `ja`, `ko`, `nl`, `pl`, `pt` (Brazil), `pt-PT`, `ro`, `ru`, `tr`, `uk` and `zh` (Simplified). Another tag uses its language's data (`es-MX` writes like `es`, `fr-CA` like `fr`, `zh-Hant` like `zh`), and a language without data uses `en`'s; `locale.format_data_tag()` tells which. All of them write Latin digits and group by thousands (Indian lakh grouping is not covered; `hi` has no data). - **Styles:** `DateStyle::Short`, `Medium`, `Long` and `Full` are CLDR's date formats (`Short` may use a two-digit year, as `en`'s `10/22/26` does); `DateStyle::Numeric` is CLDR's day-month-year in digits, what browsers show by default. `TimeStyle::Short` and `Medium` omit the time zone, so format a `DateTime` already converted to the visitor's zone. - **Money:** `Currency::BRL`, `USD`, `EUR`, `GBP` and `JPY` use each locale's symbol (`R$`, `US$`, `€`, `£`, `¥`) and spacing; `Currency::new(code, minor_digits)` writes any other ISO code as letters. Amounts are integer minor units (`i64`), so no floating-point rounding happens. - **Decimals:** a `Decimal` keeps its fraction digits (`Decimal::new(150, 2)` is `1.50`; `"1.50".parse()` also works). `round_half_even` (CLDR and ICU's default) and `round_half_up` (what browsers do by default) choose the digits shown. - **Spaces:** CLDR writes U+202F NARROW NO-BREAK SPACE in places such as `fr` grouping and before `en`'s `AM`/`PM`, and U+00A0 NO-BREAK SPACE in others such as `pt-BR`'s `R$ 1.234,56`. Browsers write a plain space in dates and times where CLDR has U+202F. ## What fails the build - A key present in one locale and missing from another (both directions). - A key whose placeholders differ between locales, or that is plural in one locale and single in another. - An unbalanced `{` or `}`, or an invalid placeholder name. - A plural message without a form its language needs (the error names it), with a form the language never selects, or in a language without a rule. - A file name that is not a canonical tag (`pt-br.toml` asks for `pt-BR.toml`), and a `default` without a file. - In `t!`: an unknown key (with a suggestion for a near miss), a missing or unexpected argument, a repeated argument, and a key that is not a string literal. ```text error: unknown translation key `nav.hone`; did you mean `nav.home`? --> src/pages.rs:19:20 | 19 | let _ = t!(locale, "nav.hone"); | ^^^^^^^^^^ ``` Keys must be literals so they can be checked. To translate a value chosen at runtime (such as a status stored in the database), `match` it to keys. Editing a catalog file rebuilds the crate, and with the `build.rs` from step 2 adding or removing a locale file does too. ## Negotiation `Locales::negotiate` (used by the layer) picks the first of: 1. the `rullst_locale` cookie, when it names a supported locale (ignoring case); 2. the best match in the first `Accept-Language` header: ranges sorted by their `q` weight (RFC 9110), keeping the header order on ties; a range matches a supported tag exactly or by language (`pt-PT` and `pt` match `pt-BR`, preferring the default locale when it shares the language); `*` matches the default; `q=0` and malformed weights are skipped. Values over 4 KiB are ignored, and only the first 32 ranges (each up to 35 bytes) are read; 3. the default locale. `LOCALES.get(tag)`, `LOCALES.iter()` and `LOCALES.default_locale()` give the supported locales, for example to build a language menu. ## The switch route `LOCALES.switch_route()` accepts a URL-encoded form with `locale` and an optional `next`: - an unsupported `locale` or a malformed form answers `400`, a body over 4 KiB answers `413`, and the cookie is not changed; - otherwise it sets `rullst_locale=<tag>; Path=/; Max-Age=31536000; SameSite=Lax; HttpOnly`, plus `Secure` in staging and production (the same rule as the CSRF cookie); - it redirects with `303 See Other` to `next` when `next` is a local path (it starts with a single `/`, has no `\` and only printable ASCII, up to 2 KiB), and to `/` otherwise, so it cannot redirect to another site; - an htmx request (`HX-Request: true`) gets `204` with `HX-Redirect` instead, so the whole page reloads in the new language. ## Without a catalog An application that keeps its own message tables can still use the negotiation, the layer and the switch route with `Locales::new`: ```rust use rullst::i18n::Locales; use std::sync::OnceLock; static LOCALES: OnceLock<Locales> = OnceLock::new(); let locales = Locales::new(&["en", "pt-BR"], "en")?; assert_eq!( locales.accept_language("pt-PT, en;q=0.8").map(|locale| locale.tag()), Some("pt-BR") ); // `layer()` and `switch_route()` borrow the list for the whole program; a // `static` holds it without leaking memory. let locales: &'static Locales = LOCALES.get_or_init(|| locales); let _layer = locales.layer(); # Ok::<(), rullst::i18n::LocaleError>(()) ``` Account emails from `rullst-mail` keep their own languages; choose one from the request with `MailLocale::from_preference(locale.tag())`. ## Limits - Cardinal plural rules only for the languages above; no ordinal rules ("1st", "2nd"). - Language matching ignores scripts and regions (`zh-Hant` matches `zh-Hans` when no better tag exists). - Formatting covers the 20 locales above, Latin digits and the Gregorian calendar; no compact (`1.2K`), ordinal or spelled-out numbers, time zone names or relative times ("3 days ago"). - Catalogs are compiled in; there is no runtime catalog loading. --- # Active Sessions and Remote Logout Source: https://rullst.github.io/Rullst/book/session-management.html # Active sessions and remote logout The v13 candidate extends `rullst-auth::recovery::SqlRecoveryStore` with a bounded session inventory, selective sibling logout and logout of all other sessions. It uses the existing authoritative account registry and opaque bearer sessions; it does not add another crate. Hosted workspace/platform and extracted-package source admission passed in [PR #236](https://rullst.github.io/Rullst/book/v13-delivery-plan.html#six-increment-source-admission-on-september-21). Final release admission remains separate. Choose `recovery-sqlite` or `recovery-postgres` on Auth. The facade exposes `auth-sessions-sqlite` and `auth-sessions-postgres` without enabling Mail or the general ORM. Existing `account-mail-*` consumers gain the same APIs after their explicit schema migration. ## Adoption and isolation Follow the [account-registry adoption contract](https://rullst.github.io/Rullst/book/account-mail-v12-1.html) for keys, password verification, cookies and account migration. Run `migrate()` during a controlled deployment before using the new library: it adds a session-details table and an expiry index. Existing opaque sessions continue working and report unknown creation/label values if they predate those details. Encrypted-only `rullst_session` cookies are revoked through their own per-user [session epoch](https://rullst.github.io/Rullst/book/crates/auth.html#revocable-cookies-the-session-epoch), which ends all of a user's cookies at once, and independently issued JWTs through their own revocation store. Applications generated by `cargo rullst auth` and the SaaS starter (and the LMS example) use those epoch cookies. Their password change stores the new hash and increments the epoch in one `UPDATE`, then gives the browser that made the change a cookie with the new epoch, so only that browser stays signed in. Their password reset increments it in the transaction that consumes the single-use reset token and signs in nobody, so every device must sign in again. See [Tutorial 11](https://rullst.github.io/Rullst/book/tutorials/11-authentication-system.html#what-the-scaffold-currently-enforces). Stores are application/tenant boundaries selected by trusted server configuration, with separate databases and keys. A browser cannot choose a database, account subject or tenant by supplying an identifier. Every protected request calls `verify_session` against that same authoritative store. PostgreSQL can serve multiple hosts; SQLite requires processes sharing the same local file and does not provide multi-host replication. `SqlRecoveryStore::connect` requires certificate- and hostname-verified TLS (`sslmode=verify-full`) for every remote PostgreSQL host and accepts only connection/TLS query options, as email login and API tokens do. Management methods authenticate the supplied current session again inside a serialized SQL transaction. They never accept a target account. The host still checks current tenant membership and protects its routes with CSRF, the security baseline, ingress limits and `Cache-Control: no-store`. Use trusted server time, never browser timestamps, and maintain a reliable deployment clock. ## Inventory and logout Each account has at most 20 sessions. `active_sessions` returns only unexpired sessions matching the current account version, with a management ID, expiry, optional creation time, optional display label and a current-session marker. The ID is a purpose-separated keyed digest and cannot authenticate as the session. Debug output hides IDs and labels. ```rust,no_run # #[cfg(feature = "auth-sessions-sqlite")] async fn sign_out_other_devices( store: &rullst::auth::recovery::SqlRecoveryStore, current_cookie: &str, trusted_now: u64, ) -> Result<usize, rullst::auth::recovery::RecoveryError> { // Host middleware has already authorized this account/tenant and checked CSRF. store.revoke_other_sessions(current_cookie, trusted_now).await } ``` `revoke_other_session(current_cookie, &SessionId, trusted_now)` removes one active sibling. A missing, expired, foreign-account or current-session target is rejected. `revoke_other_sessions` removes all siblings and advances the account session version, preserving the current bearer token. A password-authentication proof obtained before that operation can no longer mint a session; authenticate again before creating a later session. Repeating logout of siblings is safe. Use the existing `revoke_session(current_cookie)` for current-device logout. `create_session_with_label` accepts a `SessionLabel` of 1–80 UTF-8 bytes without control characters or surrounding whitespace. Labels are explicit display text, not verified device identity. Escape them in HTML. No IP addresses, raw user agents, fingerprints or activity history are collected. The ordinary `create_session` records creation time without a label. ## Expiration, retention and failures Expired sessions are rejected at their exclusive expiry boundary. Time passing does not physically delete rows. Creating a session prunes that account's expired sessions; schedule the operator-owned `purge_expired_sessions(trusted_now, limit)` to remove 1–100 expired sessions per transaction, including inactive accounts. Metadata is deleted atomically with sessions during retention, logout and password reset. Operator database/backups/encryption and retention scheduling remain deployment responsibilities; do not expose this maintenance API as a public endpoint. Session creation, verification, management and retention operations have a ten-second database-operation deadline. Storage failures remain errors rather than authentication success; inventory also rejects out-of-bounds metadata. A timeout or lost commit acknowledgement does not prove that a write rolled back: query authoritative state before reporting its outcome. Already-authorized in-flight requests are not recalled by logout; long-lived WebSockets and other identity systems need their own revalidation policy. ## Automated acceptance Tests exercise account/store separation, metadata migration, expiry, password recovery, selective/all-other logout, transaction rollback, concurrent logout, retention and denied access after storage failure. Real HTTP fixtures use two independent application processes, the Core security baseline and tenant checks; they prove that a completed logout is visible on the other process's next request and persists after application restart. The digest-pinned disposable PostgreSQL journey additionally restarts the actual database and checks retained/revoked sessions afterwards. A held database lock tests the operation deadline. The archive consumer exercises the public facade from extracted packages. These are automated contract checks, not evidence of a particular production identity migration, failover topology or human-tested UI. --- # Email login Source: https://rullst.github.io/Rullst/book/email-login.html # Optional email login (v13 candidate) `rullst-auth` features `email-login-sqlite` and `email-login-postgres` expose `recovery::email_login::EmailLoginService`. The umbrella features are `auth-email-login-sqlite` and `auth-email-login-postgres`. They are opt-in and reuse the authoritative `SqlRecoveryStore` account and opaque-session registry. They do not enable JWT, OAuth or email login on existing accounts automatically. Combined hosted source/package/coverage admission passed in [PR #239](https://rullst.github.io/Rullst/book/v13-delivery-plan.html#additional-priorities-approved-on-21-september); final release admission remains separate. ## Account and application boundaries Create an immutable `EmailLoginConfig` with a server-owned application/tenant namespace, HTTPS landing URL, fixed local destination and capacity (1–100,000 account policies per namespace). Query/fragment/user-info in the landing URL and encoded/authority/traversal syntax in the destination are rejected. The explicit development constructor allows HTTP only on localhost/loopback. Use `initialize` during deployment and `connect` during normal startup. Access `accounts()` to register/authenticate accounts in this registry. Enable a policy with `set_account_enabled` only after recent password authentication plus the application's tenant and MFA policy. The proof must come from this service's account registry instance and current account epoch. Disabled accounts retain their policy/revision and count toward capacity. An administrator-provided namespace is a storage boundary, not proof that a caller belongs to a tenant. Resolve membership, roles and resource ownership on each authorized request. Email access is a single factor. Do not enable this route as an alternative that bypasses required MFA, passkeys or a stronger identity assurance policy. This increment does not implement enrollment UX, account linking, invitation-based registration, step-up MFA, verified age or guardian authorization. ## Request, delivery and deliberate confirmation 1. Generate an independent `BrowserBinding` on the server. Keep it in a dedicated `Secure; HttpOnly; SameSite=Lax` cookie, scoped to the application, with a maximum age of 15 minutes. Protect the request endpoint with CSRF and ingress limits. Never accept a browser binding chosen through a link or request body. 2. Call `request_login` with the email, binding and trusted server clock. Known, unknown, disabled, suppressed and throttled accounts receive the same `LoginRequestAccepted`. Each namespace allows 120 requests per minute and each account three per 15 minutes. A 250 ms minimum acknowledgement delay mitigates trivial enumeration; it is not a constant-time guarantee under database contention or faults. Rate-limit at ingress independently. 3. Issuance creates a distinct random 256-bit emailed token, stores domain-bound HMAC digests of both secrets, binds the account epoch and policy revision, and atomically queues an AES-256-GCM encrypted notice. A new accepted request replaces the prior link and pending/leased notice. Links expire after exactly 15 minutes. Account opt-out, password recovery or another account-epoch change invalidates the pending link. An account has one pending link and one request budget, whichever browser asks. Anyone who knows an enrolled address can therefore replace the owner's pending link (the new link only works in the requester's browser) and spend the three requests of each window, blocking email login for that account at about 12 requests per hour. This bounded budget is what prevents mail flooding; a per-browser budget would not, because browser bindings are free to mint. Email login is an optional route: keep password login and recovery available, apply per-client and per-target ingress limits, and let users disable email login if it is abused against them. 4. A trusted worker calls `claim_notice`, constructs Mail `AccountEvent::EmailLoginAt` with `ActionLink`, the server-configured origin and `expires_at`, then sends with the stable `delivery_id`. Use the recorded locale, with explicit fallback. Call `complete_notice` or `fail_notice` with a redacted delivery outcome. Claims last at most 60 seconds, allow six attempts and use bounded backoff. Delivery is at-least-once; transports without idempotency may send duplicates. A bounce or complaint suppresses the account's future transactional delivery. 5. The landing **GET/HEAD only renders confirmation**. It never consumes the token or creates a session. An explicit CSRF-protected POST passes the emailed token and cookie binding to `redeem`. The initiating browser is required; opening the email on another device requires starting a new request there. Never redeem automatically in JavaScript or on page load. Only a failed token lookup (malformed, unknown, expired, replayed or wrong-browser) is charged, to the presented browser binding. After 60 such failures within 60 seconds, that binding receives `RecoveryError::Throttled` until its window passes, before any database work, even for a valid link. Other bindings and valid redemptions are never charged or blocked, so one client cannot lock other users out of redemption (request replacement and the per-account request budget are described in step 3). The budget is process-local, keyed by an HMAC digest of the binding and bounded to 10,000 recent bindings. It is not a global brute-force bound: an attacker can rotate bindings, and a multi-host deployment keeps one budget per service instance, so ingress limits remain required. Map `Throttled` to a generic "try again later" response. 6. Redemption atomically consumes the link and creates a one-hour opaque session using the existing account epoch, 20-session limit and inventory/revocation machinery. Store it in a `Secure; HttpOnly; SameSite` cookie, clear the browser binding, and redirect only to `EmailLoginSession::destination()`. Verify the session and authorization on every request. Logout and account revocation remain authoritative across instances sharing the database. Use the framework security baseline (CSRF, WAF and secure headers), bounded request bodies, `Cache-Control: no-store` and `Referrer-Policy: no-referrer` on all landing and authentication responses, including errors. Do not load third-party scripts, analytics, fonts or images on the landing page. Redact query strings, cookies and mail bodies before request logging or tracing. Never serialize the returned session/delivery capabilities into application events. The test-only `/fixture/mail` route in the HTTP acceptance harness substitutes an inbox; it must never be mounted in a deployed application. The Mail template has deterministic English, Brazilian Portuguese and Spanish HTML/text, absolute expiry, a same-browser confirmation instruction and no tracking pixel. It neither generates tokens nor changes authentication state. ## Storage, cancellation and operations SQLite uses one private connection per instance, WAL, FULL synchronization and one shared local file. It is not a multi-host database. The deployment owns the trusted directory, file permissions, encryption-at-rest and backup. PostgreSQL uses a bounded four-connection pool, verified remote TLS, five-second SQL/lock limits, permanent tables, `fsync`/`full_page_writes` and synchronous commit on a writable primary. Public methods have a ten-second operation timeout (plus the minimum request delay). All state mutations serialize through the account registry's write lock and recheck clock observations. The namespace keeps a monotonic clock high-water mark: a host whose clock trails it by at most five seconds, as synchronized hosts do around each whole-second boundary, adopts the recorded time, while a larger backward step fails with `InvalidAction`. A lock wait or skew adoption cannot extend a link's expiry. An uncertain commit or cancellation never returns a session. PostgreSQL bootstrap serializes explicit initializers. Normal startup performs no DDL or missing-namespace repair. Runtime needs SELECT/INSERT/UPDATE/DELETE on the account/session/outbox and email-login tables in the trusted `public` schema; reserve CREATE/ALTER for the deployment role and revoke schema CREATE from untrusted roles. Use one authoritative writable database, synchronized clocks, replication fencing and a bounded number of server-configured namespaces. Configuration/key drift and non-durable PostgreSQL tables fail closed. Delivered/terminal email ciphertext is deleted immediately. Run `purge_expired` from a trusted retention worker even during idle periods; it removes expired links and notices. Invalidated encrypted notices that were already claimed may remain until expiry, and in-flight transport work cannot be recalled. Such links fail redemption. No recipient activity history is recorded by this component. A stale database restore can revive consumed links, account epochs and revoked sessions. Quiesce authentication, invalidate restored login challenges/outbox and session state, reconcile account epochs and policies, then resume. This adapter does not provide backup anti-rollback, high availability, secret rotation or a production mail delivery SLA. ## Automated evidence The candidate includes SQLite/PostgreSQL independent-pool races, purpose and namespace isolation, browser mismatch, replacement, opt-out/epoch invalidation, expiry/clock rollback, locked-database expiry, cancellation, SQL failure atomicity, bounded retries and suppression. Process tests use fresh executables; the owned PostgreSQL wrapper also restarts the database service. The Chromium HTTP fixture uses the actual framework security baseline and proves scanner GET/HEAD, CSRF rejection, deliberate POST, secure cookies, tenant authorization, replay rejection and logout across separate database pools. Run the ordinary SQLite contracts with `cargo test -p rullst-auth --features email-login-sqlite`. Run the real browser/service contracts with `RULLST_EMAIL_LOGIN_BROWSER_TESTS=1 python3 .github/check-auth-recovery-postgres.py`. The wrapper owns a disposable loopback Docker database; it does not use external accounts. Packaged-distribution acceptance exercises extracted archives through the facade. These automated fixtures do not establish live mail-provider or owner-application acceptance. --- # Scoped API tokens Source: https://rullst.github.io/Rullst/book/api-tokens.html # Scoped application API tokens (v13 candidate) Auth features `api-tokens-sqlite` and `api-tokens-postgres` expose `recovery::api_tokens::ApiTokenService`. Facade features are `auth-api-tokens-sqlite` and `auth-api-tokens-postgres`. They reuse authoritative recovery accounts and do not enable email login, JWT or OAuth. This increment is an implementation candidate; combined hosted and extracted-package source admission passed in [PR #239](https://rullst.github.io/Rullst/book/v13-delivery-plan.html#additional-priorities-approved-on-21-september), and final release admission remains separate. ## Create and manage credentials Configure an immutable server-owned `ApiTokenConfig`: namespace, exact scope allowlist, capacity (1–100,000 rows) and maximum lifetime (1 second–30 days). Scopes are explicit, case-sensitive names such as `orders:read`: 1–32 distinct names, each 1–64 lowercase ASCII letters, digits or `:._-`, starting with a letter. Wildcards and implied parent scopes are unsupported. Use `initialize` as a deployment task and `connect` for normal startup without DDL or repair. 1. Authenticate the account through `service.accounts().authenticate`. The resulting proof belongs to this exact registry instance and account epoch. The application must additionally require recent authentication, its MFA policy and current tenant/domain permissions. Intersect requested scopes with those permissions before calling `issue`; the configured allowlist alone is not an account authorization policy. 2. `issue` accepts the proof, approved scopes, a bounded `SessionLabel`, lifetime and trusted `SystemAuthClock`. Return `IssuedApiToken::expose_bearer` once over HTTPS with `Cache-Control: no-store`. Store it on the integration side in a secret store, never in URLs, analytics, browser local storage or application logs. Management endpoints retain CSRF, secure headers and ingress limits. 3. `inventory` returns this account's active metadata and opaque management IDs, never secrets or digests. Each account has at most 20 active credentials per namespace, within the deployment capacity. No IP, user-agent or usage history is collected. Run `purge_expired` even when issuance is idle. 4. `rotate` requires the exact current revision. It keeps ID/scopes/creation time, replaces the secret, increments the revision and optionally renews the expiry within policy. Only one concurrent rotation succeeds. Revoked or expired credentials cannot be rotated. A lost/uncertain rotation response requires authenticated inventory/revocation and a new rotation or separately approved issuance; the service cannot recover the secret. 5. `revoke` is owner-scoped and idempotent; foreign and absent IDs return false. `revoke_all` affects this account in this namespace. Recovery/account-epoch invalidation rejects all older API credentials and management proofs. Each wire credential contains a versioned `rlt1_` prefix, random 256-bit management ID and independent random 256-bit secret. Only a purpose/namespace-bound HMAC is persisted. A management ID, provider key or browser-session token cannot be used as this bearer. Credential/principal Debug output is redacted. ## Authenticate each request Call `verify` for every request with the route's required `ApiScopes` and trusted server clock. It reads current SQL state, compares the HMAC in constant time, checks scope inclusion, account epoch and expiry, and fails on storage faults. Resolve current tenant membership, account roles and resource ownership after verification. An `ApiTokenPrincipal` is a snapshot, not an authorization cache or a credential for issuing more tokens. For exact mutating machine routes, compose `service.machine_verifier(required_scopes)` with `MachineEndpoint::verified_bearer(Method::POST, "/api/orders", verifier)` and the Core security baseline. The verifier requires one `Authorization: Bearer` header and inserts the principal as an Axum extension. It rejects a request that carries `Cookie`, `Origin` or `Sec-Fetch-Site`, so ambient browser credentials cannot substitute for machine authentication. Only the verified exact method/path receives the machine CSRF exception; ordinary browser routes retain CSRF, and WAF/headers remain active. Apply bounded bodies, ingress rate limits and no-store responses. GET/read routes can call `verify` from their own authenticated handler; this constructor is deliberately limited to the existing mutating machine-route contract. The executable `api_token_http` fixture composes two independent database pools, real HTTP servers, the actual security baseline, a required write scope and tenant/owner authorization before a side effect. It proves body preservation, missing/duplicate/ambient credentials, wrong scope/tenant/account, route confusion, rotation, revocation and storage-outage rejection. ## Storage and operations SQLite uses one private connection, WAL and FULL synchronization on a trusted local file; it is not a multi-host store. PostgreSQL uses one authoritative writable database, verified remote TLS, four connections and five-second SQL deadlines. Public operations have ten-second timeouts. Runtime requires USAGE on the trusted `public` schema and SELECT/INSERT/UPDATE/DELETE on the recovery and API-token tables; reserve DDL for the deployment role. The adapter checks permanent tables and durable PostgreSQL settings on every operation. Token operations serialize with recovery/account changes through the existing registry write lock. This favors explicit revocation ordering over unlimited parallel throughput; size and load-test this shared registry for the application. Expiry and persisted clock observations are rechecked after database waits and before a credential or principal is returned. The namespace keeps a monotonic clock high-water mark: a host whose clock trails it by at most five seconds, as synchronized hosts do around each whole-second boundary, adopts the recorded time, so no lifetime is extended; a larger backward step fails with `InvalidAction`. Cancellation/uncertain commit never returns a new bearer. Revocation has a database ordering point; work already authorized before it cannot be recalled or made atomic with a later external side effect. Never replace SQL checks with positive local caching. The deployment owns synchronized clocks, key storage, database encryption, replication fencing and a bounded number of configured namespaces. Rotating a configuration/key requires a deliberate migration; mismatched instances fail closed. A stale backup can revive deleted tokens and old account epochs. Quiesce authentication, invalidate restored tokens and reconcile epochs before resuming. The adapter does not implement backup anti-rollback or automatic failover. Run `cargo test -p rullst-auth --features api-tokens-sqlite` for ordinary local contracts and `python3 .github/check-auth-recovery-postgres.py --suite api-tokens` for the owned disposable PostgreSQL, HTTP, restricted-role, durability and process/server-restart contracts. The package campaign repeats the lifecycle through extracted facade/Auth/Core archives. No real provider account is needed. --- # Shared Passkey Ceremony Contract Source: https://rullst.github.io/Rullst/book/shared-passkey-ceremonies.html # Shared passkey ceremonies This v13 implementation, published in the 13.0.0-alpha.1 prerelease, passed real-PostgreSQL and Chromium contracts, hosted workspace checks and isolated-archive acceptance in [PR #223](https://github.com/Rullst/Rullst/pull/223). The final release campaign remains separate; this is not a WebAuthn conformance claim. The existing synchronous `PasskeyAuth` API retains its process-local, single-use challenges. The existing SQLite credential registry persists credentials and counter CAS, not ceremonies. The optional path belongs in `rullst-auth`. Enable `passkey-postgres`, or `auth-passkey-postgres` through the umbrella package. `SharedPasskeyAuth<S>` composes a concrete `PasskeyCeremonyStore` through static dispatch and the existing ES256/`none` verification engine. The first real adapter is opt-in PostgreSQL, because it serves separate application hosts. A shared SQLite ceremony adapter, additional algorithms/attestation formats and discoverable-account login are separate work. It rewrites no starter and adds no public crate. ## Identity and API boundary A server-created `PasskeyBinding` carries bounded opaque tenant, subject and session references, plus a stable application-owned opaque user handle of 1–64 bytes. Constructing it validates shape; it does not authenticate a caller, establish account ownership or authorize registration. Registration requires an authenticated account or the application's separately authorized enrollment flow. For account-first login, bind the selected authoritative account and a random pre-authentication session; the challenge alone must never choose that account. Protect both flows with CSRF/origin and abuse controls, and refresh authorization before persisting a credential or creating a session. Each challenge binds a framed digest of that context, the normalized RP ID and origin, user-verification requirement and deployment epoch. Registration options use the supplied user handle, not an email or numeric account ID. Account-first authentication requires 1–32 current, host-owned credentials; a digest of each credential ID, public key and current counter is stored with the challenge. Registration and authentication are distinct purposes. Finishing cannot change the account, tenant, session, RP configuration, user handle or credential set. The initial response subset does not implement discoverable account selection. Forward the authenticator's optional `userHandle` to `finish_authenticate`; when present it must match the bound account handle. Options use fresh 256-bit random challenges. Responses have explicit size limits before decoding. The private crypto validation helpers are shared with the existing API; no public stateless verification bypass is introduced. A finish checks client challenge/origin/type, atomically consumes the bound store entry, then validates the authenticator response and checks expiration again before returning. Failed verification after consumption requires a new challenge. Successful assertion verification returns the updated public credential. The host must atomically persist its counter against the previous value and check current revocation/ownership before granting a session; this increment does not invent a distributed transaction with the application's credential repository. ## Durable store boundary `PasskeyCeremonyStore` is a trusted implementation contract. It exposes bounded issue, atomic bound consume and completion-expiry validation, a stable persisted configuration and explicit durability. `SharedPasskeyAuth` rejects a store that declares process-local durability. A declaration by a custom implementation is not an independent durability certification. Configuration supplies a separately retained opaque deployment epoch, a maximum of 1–100,000 pending challenges and a 1–600 second lifetime. PostgreSQL owns one fixed private schema with metadata and pending rows. All operations validate configuration and a persistent clock high-water mark under a transaction-level metadata lock. Clocks are trusted server clocks; hosts must synchronize them. Hosts cross each whole-second boundary at slightly different instants, so a host whose clock trails the recorded high-water mark by at most five seconds adopts that mark: shared time never moves backwards and no lifetime is extended. A larger backward step fails closed with `Configuration`, not `Corrupt`. Challenge and identity references are digested; rows contain purpose, bounded credential fingerprints and issued/expiry times, not passwords, private keys, display names, raw sessions or response bodies. Initialization is explicit and never repairs missing runtime state. Connections use a private bounded pool, primary durable tables, synchronous commits, a fixed search path and bounded acquisition, lock and statement waits. A twelve-second whole-operation deadline also bounds startup and issue/consume/confirm when a transport stops returning replies; timeout reports an uncertain outcome. Non-loopback TCP requires TLS certificate/hostname verification. Issue serializes quota checks, bounded expired-row cleanup and insertion. Consume compares all bindings and purpose, validates the bounded stored row and deletes it in the same transaction. A mismatched context cannot remove another ceremony. There is one winner across pools and processes. No unexpired challenge is evicted to make room. Cancellation before commit rolls back. An uncertain commit or a deadline crossed while waiting/committing returns an error; a consumed challenge may remain spent. The host retries with a new ceremony, never a local fallback. Expired rows may be removed logically; backups and physical erasure remain deployment concerns. Operators own schema permissions, synchronized clocks, TLS trust, encryption, backups and failover fencing. A stale restore with the same epoch can resurrect challenges; quiesce authentication and rotate the independently held epoch with fresh state before resuming. This adapter cannot detect physical rollback alone. ## Configuration example Initialize `PostgresCeremonyStore` once with a deployment role. Application startup uses `connect` and cannot recreate missing state. Keep the same epoch, capacity and lifetime on every participating instance. `SharedPasskeyAuth::new` also requires the `PasskeyConfig` challenge lifetime and pending-challenge maximum (defaults 300 and 10,000) to equal the store's lifetime and capacity: ```rust,no_run use rullst_auth::passkey::{PasskeyConfig, shared::{ CeremonyStoreConfig, PostgresCeremonyStore, SharedPasskeyAuth, }}; async fn configure(url: String) -> Result< SharedPasskeyAuth<PostgresCeremonyStore>, Box<dyn std::error::Error> > { let policy = CeremonyStoreConfig::new("independently-held-epoch", 10_000, 300)?; let store = PostgresCeremonyStore::connect(url, policy).await?; let rp = PasskeyConfig::new("Example", "example.com", "https://example.com"); Ok(SharedPasskeyAuth::new(&rp, store)?) } ``` The current option DTOs retain the existing Rust JSON field conventions. Map binary challenges/user handles/credential IDs and browser dictionary names at the HTTP boundary; the tested browser fixture shows this conversion. The fixture is not a production identity service or a generated authentication controller. ## Acceptance When PR #223 admitted it, local evidence included 85 normal all-feature Auth tests, strict all-target Clippy and production zero-panic checks. The explicit PostgreSQL fixture passes independent managers/pools, actual ES256 registration/assertion, wrong bindings, credential snapshots, parallel completion, post-consumption expiry, quotas, corruption, lock-wait expiry, cancellation, stalled transport, a separate process and server restart. Chromium's virtual authenticator completes registration and authentication through different HTTP managers and a PostgreSQL credential-counter CAS; missing cookie/CSRF and assertion replay are rejected. These checks do not demonstrate physical authenticator/device-fleet interoperability or failover safety. A focused real-database mutation sample detected all 21 selected changes to binding fingerprints, allowed credentials, issuance, consumption and expiry. This is a bounded sample, not a whole-Auth mutation score. Changes to this path keep these acceptance and maintenance requirements: Use real PostgreSQL with two independently constructed managers and pools, then a new process and database restart. Cover registration/assertion with actual ES256 signatures; replay, parallel completion, account/tenant/session/RP/purpose confusion, empty or changed credential sets, quotas, exact expiry, lock-wait expiry, cancellation, corrupt/oversized rows, missing metadata and configuration mismatch. Keep the existing synchronous and SQLite device-registry regressions. A minimal HTTP/browser consumer must demonstrate beginning on one manager and finishing on another without relying on shared application memory. Generated applications and an external authenticator/device fleet are separate acceptance. Run strict production lints, feature isolation and affected mutations; full workspace, archive and hosted release gates remain mandatory. The protocol boundary follows the W3C's registration and assertion verification procedures and their challenge/account binding requirements. This implementation still supports a deliberately bounded subset. See [WebAuthn Level 3 registration](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential) and [assertion verification](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion). --- # Form File Uploads Source: https://rullst.github.io/Rullst/book/form-uploads.html # Form file uploads `rullst::uploads::Multipart` reads a `multipart/form-data` request, the body a browser sends for a form with a file input, under explicit limits. It is new in 13.0.0-alpha.2. Text fields stay in memory; each file streams to memory while it is small and then to a private temporary file, so a large upload never sits in memory as a whole. Use it for ordinary form uploads that pass through the application. For large files that the application forwards to S3 or R2 in resumable parts, see [resumable private multipart uploads](https://rullst.github.io/Rullst/book/private-multipart-uploads.html) instead. ## A handler ```rust,no_run use rullst::uploads::{Multipart, MultipartError, MultipartPolicy, UploadKind}; async fn upload_document(multipart: Multipart) -> Result<String, MultipartError> { let policy = MultipartPolicy::default() .max_file_bytes(50 * 1024 * 1024) .max_total_bytes(51 * 1024 * 1024) .max_files(1) .allowed_kinds([UploadKind::Pdf]) .temp_dir("storage/tmp"); let mut form = multipart.read(&policy).await?; let title = form.field("title").unwrap_or("Untitled").to_owned(); let Some(document) = form.take_file("document") else { return Ok("Choose a PDF to upload.".to_string()); }; // Content-addressed: the same PDF uploaded twice is the same file. let target = format!("storage/documents/{}.pdf", document.sha256_hex()); match document.persist_to(&target).await { Ok(()) | Err(MultipartError::PersistTargetExists) => {} Err(error) => return Err(error), } Ok(format!("{title} was saved.")) } ``` Route it like any handler, for example `post("/documents" => upload_document)` in `routes!`. `Multipart` must be the handler's last argument, because it takes the request body. `MultipartError` is a response: a handler can return it with `?`, as above, or map it to a page of its own. The form renders `_token` first (see [CSRF](https://rullst.github.io/Rullst/book/form-uploads.html#csrf)): ```html <form method="post" action="/documents" enctype="multipart/form-data"> <input type="hidden" name="_token" value="…"> <input name="title"> <input type="file" name="document" accept="application/pdf"> <button>Upload</button> </form> ``` ## Limits Every limit has a safe default and a builder method. Exceeding one stops reading at once: the rest of the body is never read, files already received are dropped and their temporary files removed. | Method | Default | Error | Status | | :--- | :--- | :--- | :--- | | `max_file_bytes` | 10 MiB | `FileTooLarge` | 413 | | `max_total_bytes` (whole body; a larger `Content-Length` is refused before reading) | 16 MiB | `TotalTooLarge` | 413 | | `max_files` | 4 | `TooManyFiles` | 413 | | `max_fields` (text fields, `_token` included) | 32 | `TooManyFields` | 413 | | `max_field_bytes` | 64 KiB | `FieldTooLarge` | 413 | | `max_field_name_bytes` | 128 | `FieldNameTooLong` | 400 | | `idle_timeout` (wait for the next piece of the body) | 30 s | `Timeout` | 408 | | part header block (fixed) | 8 KiB | `PartHeadersTooLarge` | 400 | A file input left empty arrives as a part with `filename=""` and no content; it is skipped and does not count against `max_files`. The extractor ignores axum's `DefaultBodyLimit`: `max_total_bytes` bounds the body instead. A body that does not follow the form-data format (a preamble before the first boundary, a part without `Content-Disposition`, a nested `multipart/mixed` part, a body that ends before the closing boundary) is `Malformed` (400), and a text field that is not UTF-8 is `FieldNotUtf8` (400). Error responses are plain text with `Cache-Control: no-store` and never echo what the client sent. ## Temporary files A file stays in memory while it is at most `memory_threshold` bytes (32 KiB by default, at most 1 MiB; `0` writes every non-empty file to disk). A larger file streams to a file named `.rullst-upload-<random>.part` in `temp_dir`, which defaults to `std::env::temp_dir()` (`TMPDIR` on Unix). The file is created with `create_new` and, on Unix, mode `0600`, and it is removed when its `UploadedFile` is dropped, including when reading fails. A process that crashes, or is killed, while it holds such a file cannot remove it. Since 13.0.0-alpha.3 the first upload that a process writes to a `temp_dir`, and the first one after each further 24 hours, first sweeps that directory: it removes the regular files named exactly `.rullst-upload-<32 lowercase hex digits>.part` (the names Rullst creates) that were last modified 24 hours ago or earlier. It never follows or removes a symbolic link or a directory, never touches other names, and examines at most 10,000 entries and removes at most 1,000 files per sweep. The sweep runs on the blocking thread pool before that upload's file is created; if the directory cannot be read or a file cannot be removed, it logs a `rullst::uploads` warning and the upload continues. `UploadedFile::persist_to(target)` keeps the content at `target`: - it never overwrites: an existing `target`, a symbolic link included, is `PersistTargetExists`; - `target` must name a file in an existing directory and must not contain `..`; otherwise it is `InvalidPersistTarget`; - a temporary file is hard-linked into place when `temp_dir` is on the same file system, and copied otherwise; the new file has mode `0600` on Unix and is synced to disk. Choose a `temp_dir` on the same file system as the final directory, and build `target` from trusted values, such as an id or the SHA-256 digest, never from the client's file name. `bytes()` returns the content of a file kept in memory, `temp_path()` the temporary file of a larger one (for a scanner that reads files, for example), and `read_to_vec()` either. ## What the client says is not trusted - **Content type.** `declared_content_type()` is what the browser claimed. The checks look at the bytes: `allowed_kinds` admits only the `UploadKind`s (PNG, JPEG, PDF, MP4, WebM, plain text) that the in-memory `UploadPolicy` recognizes, with the same rules, and `expect_signature(b"%PDF-")` admits only files starting with one of the given byte strings (1 to 64 bytes each). Both run while the file streams, so a mismatch stops the upload early with `ContentDenied` (415). `kind()` reports what was recognized either way. - **File name.** `display_name()` is for display only: the last `/` or `\` segment of the client's name, without control or invisible format characters (such as the right-to-left override that disguises `fdp.png` as `png.pdf`), trimmed and cut to 255 bytes. Escape it when rendering, as `html!` does. For a quarantine and malware-scanner flow, pass the content to `UploadPolicy::admit` and `UploadScanner` (`read_to_vec()` gives the bytes). ## CSRF The CSRF middleware reads a multipart body only until its first `_token` field ends, within the first 64 KiB, so that it never buffers a whole upload. Render `_token` as the form's first field, before any file input, or send the `X-CSRF-Token` header (HTMX: `hx-headers`). Otherwise staging and production answer `403` with: ```text Missing CSRF token: a multipart/form-data form must send its _token field before any file field (the CSRF check reads only the first 64 KiB of the body), or send the X-CSRF-Token header ``` Development and test do not mount the CSRF middleware, so `Multipart::read` applies the same rule in every environment: a first `_token` field that ends after the first 64 KiB, without the header, is `CsrfTokenTooLate`, answered with the same `403` and text. The mistake therefore shows up before a deployment. ## WAF The WAF does not read multipart bodies. Where it runs, it marks the request, and `Multipart::read` inspects the name and value of each text field with the same signatures as a URL-encoded form body: staging and production refuse a match with the WAF's `403` (`WafRefused`), development logs it at `warn` level (target `rullst::security::waf`, rule `multipart-field-signature`, the path only). Paths listed in `[security] waf_body_exempt_paths` are not inspected. File contents and file names are not inspected. ## What it does not do It does not parse documents, strip metadata, transcode media or scan for malware, and it does not resume interrupted uploads. Leftover temporary files are swept only from a directory this process writes uploads to, and only once they are 24 hours old (see [Temporary files](https://rullst.github.io/Rullst/book/form-uploads.html#temporary-files)). --- # Private S3/R2 Object Storage Source: https://rullst.github.io/Rullst/book/private-object-storage.html # Private object storage The v13 `storage-s3` candidate adds optional AWS S3 and Cloudflare R2 operations to `rullst-core::Storage` and the `rullst` facade. It supports bounded object upload, download, metadata, deletion and signed GET URLs. Bare Core does not enable this feature or its HTTP/signing dependencies. Protocol tests and a digest-pinned disposable Garage service exercise signature rejection and restart persistence. Hosted workspace/platform and extracted-package source admission passed in [PR #236](https://rullst.github.io/Rullst/book/v13-delivery-plan.html#six-increment-source-admission-on-september-21). Final release admission remains separate. No AWS/R2 owner account has been exercised; bucket policy and provider interoperability remain deployment responsibilities. ## Explicit configuration Enable `storage-s3` in Core or in the facade. To use the authorization helpers below, also enable the facade's `security` feature. ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["storage-s3", "security"] } ``` The version above is the 13.0.0-alpha.4 prerelease, which `cargo add rullst` does not select; name it explicitly. Final release admission remains separate. ```rust,no_run # #[cfg(all(feature = "storage-s3", feature = "security"))] # fn configure() -> Result<(), Box<dyn std::error::Error>> { use rullst::Storage; use rullst::storage::cloud::{CloudCredentials, CloudStorageConfig}; let credentials = CloudCredentials::new( std::env::var("R2_ACCESS_KEY_ID")?, std::env::var("R2_SECRET_ACCESS_KEY")?, )?; let config = CloudStorageConfig::new(credentials).require_production()?; let storage = Storage::r2("private-files", std::env::var("R2_ACCOUNT_ID")?) .with_cloud_config(config)?; # let _ = storage; # Ok(()) # } ``` For AWS, replace `Storage::r2` with `Storage::s3(bucket, region)`. Attaching the configuration validates the provider parameters without making a network call. Credentials are explicit: the library never searches environment variables, profiles, instance metadata or ambient proxies. Known temporary credentials can include a session token and expiration through `with_session_token`. Default limits are **16 MiB per object** and **30 seconds per request**. A bounded download additionally has a total operation deadline. `with_limits` accepts 1 byte–256 MiB and 1 millisecond–120 seconds. Requests use HTTPS, reject redirects and disable transparent content decompression. The caller owns concurrency limits; the per-object ceiling is not a process-wide memory quota. Uploads are buffered and sent once. Errors do not trigger automatic retries or imply that a timed-out write was never committed; reconcile state before retrying an ambiguous write, especially with provider versioning enabled. ## Authorize before accessing or sharing Load account membership and file ownership from authenticated, authoritative application state. Check the tenant boundary and then the owner/role policy before obtaining `TenantStorage` or issuing a signed URL. A tenant prefix does not authorize every member to access every object in that tenant. `TenantStorage` applies `tenants/<tenant>/` to upload, download, metadata, deletion and download grants, and fails with `StorageError::PathTraversal` when the tenant identifier is not exactly one normal path segment (empty, containing `/` or `\`, or only dots such as `.`). Cloud keys reject empty/parent/dot components, backslashes, control characters and more than 1,024 UTF-8 bytes. Request paths are the SigV4 canonical URI: the bucket and each key segment are percent-encoded once with AWS `UriEncode`, so only `A-Z a-z 0-9 - . _ ~` and the `/` separators stay literal. Keys and tenant identifiers containing spaces, Unicode, `%`, `:`, `=`, `+`, `(`, `)` or other reserved characters therefore sign and resolve as the provider expects. The [archive consumer](https://github.com/Rullst/Rullst/blob/main/.github/fixtures/storage-facade.rs) demonstrates owner/tenant enforcement, including denial of an administrator from another tenant, using the same facade API shipped in the archive. Use the existing upload admission/quarantine contract before persisting or sharing untrusted files. The storage transport is not a malware scanner. Uploads use `application/octet-stream`; custom browser rendering and media transforms are outside this increment. ## Private downloads and deletion `signed_download(key, lifetime)` issues a GET grant valid for **1–900 whole seconds**, bounded by any known credential expiration. It does not query object existence. `SignedDownload::expose_url()` deliberately exposes the bearer secret; its Debug output is redacted. Keep that value out of logs, analytics and public pages. The configured private backend rejects the older unsigned `url()` helper. Anyone holding a valid URL can reuse it until expiry or provider invalidation. It is not single-use, does not recheck the application's current permissions on each download and cannot promise individual immediate revocation. Use an authenticated application download endpoint when every read must consult current access policy. `expires_at()` describes the requested deadline; the provider can invalidate access earlier. `metadata` returns the byte length and an optional opaque ETag. ETags are not a portable content hash. Remote `delete` is idempotent for a missing key; a versioned bucket may retain older object versions according to provider policy. The adapter does not administer lifecycle rules, backups, bucket ACLs or IAM. ## Deterministic offline mode and validation Two empty credential values or two `mock_*` values select an in-memory store, shared by clones of the configured `Storage`. Mixed live/mock or partially empty credentials fail. Offline capacity is 256 objects and 64 MiB total, in addition to the configured per-object limit. It cannot issue a real provider URL. `require_production()` rejects this fallback and development endpoints. `with_loopback_test_endpoint` accepts only literal-loopback HTTP(S) endpoints without user information, path prefixes, query strings or fragments. It cannot modify a configuration already checked for production. The automated checks cover tenant/owner denial, canonical keys, credential and grant expiry, HTTP failures, declared/chunked response bounds and timeouts. The independent S3 fixture runs with an immutable image digest, loopback-only port, resource limits and an owned disposable data directory. It rejects unsigned, altered and expired URLs and verifies data after service restart. This verifies the implemented S3 protocol journey; it does not establish AWS/R2 account configuration, all S3 extensions or multi-region failover. Server-mediated resumable multipart uploads are the separate opt-in [`storage-multipart`](https://rullst.github.io/Rullst/book/private-multipart-uploads.html) candidate. Direct presigned uploads, image processing and additional S3 operations remain future increments. --- # Resumable private multipart uploads Source: https://rullst.github.io/Rullst/book/private-multipart-uploads.html # Resumable private multipart uploads The v13 prerelease (13.0.0-alpha.1) adds Core/facade `storage-multipart`. It extends the existing private S3/R2 adapter with server-mediated parts and recovery after an interrupted connection or process restart. It does not change default features. The application sends bounded parts through its authenticated server; provider credentials remain there. Direct browser presigned uploads are outside this API. ## Bind the object and policy Resolve the current account, tenant membership and object permission before each operation. `TenantStorage::multipart` confines the object to that authenticated tenant. Raw `Storage::multipart` is an operator-level API for an already-approved complete object key. A checkpoint does not authenticate its presenter. ```rust,no_run # #[cfg(feature = "storage-multipart")] # mod example { use rullst::{TenantStorage, storage::cloud::multipart::{ MultipartKey, MultipartLimits, MultipartStorage, }}; use std::time::Duration; fn uploader(tenant: &TenantStorage, independent_key: String) -> Result<MultipartStorage, Box<dyn std::error::Error>> { let key = MultipartKey::new(independent_key)?; let limits = MultipartLimits::new( 1024 * 1024 * 1024, 8 * 1024 * 1024, Duration::from_secs(86400), )?; Ok(tenant.multipart("quarantine/server-allocated-unique-name", key, limits)?) } # } ``` Use the existing explicit `CloudStorageConfig` and its `require_production` guard at startup. The checkpoint key is a separate 32-byte random secret encoded as canonical unpadded base64url; provision it through your secret store. All instances resuming the same uploads need the same key and exact configuration. Rotation requires retaining the previous key/configuration for its existing uploads until completion or abort. Debug/error output omits keys and checkpoints. Limits fix a maximum total length, one part size from 5 to 64 MiB, at most 256 parts and an exact lifetime from one minute through seven days. Total capacity therefore cannot exceed 16 GiB. Each `begin(total_bytes)` fixes the actual length; all non-final parts have the configured size and the last has the exact remainder. An application must separately bound concurrent requests, active uploads and tenant quotas. Ordinary `get` retains its existing configured download limit; use an authorized signed GET for objects larger than that limit. ## Persist and resume 1. Call `begin` for a fresh, server-allocated private/quarantine object name. 2. Persist the checkpoint through `expose_encoded()` in an authenticated, durable application record. Restore with `MultipartCheckpoint::from_encoded`. 3. Call `upload_part` with the checkpoint, one-based number, exact bytes and expected SHA-256. Persist the returned replacement checkpoint atomically. 4. After restart, recreate the same uploader and call `progress`. Every expected part is reported; `matches_checkpoint` is true only when remote length and ETag match a receipt authenticated in the checkpoint. Re-upload absent or unmatched parts explicitly, including a part whose response was lost. 5. Call `complete` once all receipts are present. Retain the checkpoint until completion is acknowledged or explicitly reconciled. Serialize or compare-and-swap checkpoint updates in the application. Concurrent workers starting from the same old checkpoint can otherwise lose each other's receipt updates; `progress` detects this, but does not merge unknown receipts. The backend retains uploaded bytes; checkpoints retain authenticated metadata, not file content. Provider lifecycle rules must leave enough time to resume. Checkpoints use versioned AES-256-GCM with a fresh nonce, authenticated before JSON decoding. Encoded checkpoints are capped at 256 KiB. The associated data binds endpoint, bucket, region, mock/live profile, exact object and policy, plus the ephemeral store instance in mock mode. Changing a tenant, object, endpoint, policy, key or ciphertext denies the operation. The host maintains a disciplined clock; expiry does not substitute for current membership/object authorization or a durable application revocation record. ## Integrity and uncertain outcomes Part bytes must match the supplied SHA-256 before any upload request. SigV4 signs the exact payload digest; ETags remain opaque provider receipts and are not treated as a cryptographic content digest. This is transport/content consistency, not a malware scan or proof that the client supplied safe content. Complete-file scanning and release from quarantine remain explicit application steps. Completion sends all consecutive ETags in order and parses the bounded response. An HTTP 200 containing an XML error never counts as completion; S3 can return errors after sending its success status. See the [AWS completion contract](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html). `CompletionUncertain` means the sender cannot establish the outcome. Do not automatically initiate another upload or publish the object. Call `reconcile_completion`: `Confirmed` requires both the expected length and the random upload marker attached during initiation. Object existence alone is insufficient. This observation does not prove a complete-file digest, scanning, continued object immutability or authorization. A same-name upload can replace an existing object as with ordinary `put`; allocate fresh names and control reads until the application has accepted the content. ## Abort and abandoned uploads `abort` accepts authentic expired checkpoints so cleanup can continue after the upload window closes. It sends abort and checks whether the provider still reports the upload. `RetryRequired` or an error means retain the cleanup record, stop part writers and try again. `Gone` refers to the multipart session; abort never deletes an already-completed object. Concurrent writes can require repeated aborts, as described by the [AWS abort contract](https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortMultipartUpload.html). Schedule cleanup from durable application records and provision provider lifecycle expiration for abandoned multipart uploads. A lost initiation response may leave an upload whose identifier never reached the application; the checkpoint API cannot clean that up. Bucket provisioning and a global orphan sweeper are not automatic. R2 supports a subset of S3 features; consult the [provider compatibility table](https://developers.cloudflare.com/r2/api/s3/api/) for account/deployment requirements. ## Acceptance and operational boundaries Requests use the existing explicit HTTPS endpoint rules, no ambient proxy, no redirects and credential-expiry checks. Responses are capped at 256 KiB and 4,096 XML nodes; DTDs, excessive depth, foreign resource identities and ambiguous or truncated part lists fail closed. At most one 64 MiB part is supplied per call, with a bounded request copy; application concurrency multiplies that memory cost. Empty or `mock_*` provider credentials select the bounded offline store. It has at most 256 active upload/object entries and a shared 64 MiB content budget. It does not survive process restart and cannot pass `require_production`. Local Core regression and owned HTTP failure tests passed. The independent, digest-pinned S3 service passed both endpoint profiles, tenant isolation, authenticated part upload/list/complete/abort, new client instances, service and process restart, and completion reconciliation. Restart uses a fixed loopback port so the approved endpoint remains identical. No owner account was used. The archive-only facade consumer also passed the ordinary failure contracts and the native process/service restart journey. Seventeen archives were audited and all 135 Core source files matched the extracted bytes. Strict all-target Clippy passed; the standalone production feature graph contains no SQL backend or ORM. Combined hosted workspace/platform and extracted-package source admission subsequently passed in [PR #239](https://rullst.github.io/Rullst/book/v13-delivery-plan.html#seven-increment-source-admission-and-package-preparation-on-september-22). Final release admission remains separate. --- # Shared mail suppression Source: https://rullst.github.io/Rullst/book/shared-mail-suppression.html # Shared PostgreSQL mail suppression (v13 candidate) Mail's optional `postgres` feature exposes `PostgresSuppressionStore`, `PostgresSuppressionConfig` and `SuppressionKey`; the facade feature is `mail-postgres`. It implements the existing `SuppressionStore` and `MutableSuppressionStore` contracts across hosts sharing one authoritative writable PostgreSQL database. It does not enable SQLite or change default mail delivery. It passed combined hosted and extracted-package source admission in PR #239; final release acceptance remains separate. ## Configure and compose the store Choose a server-owned namespace and immutable independent quotas of 1–1,000,000 recipient records and replay identities. Use a separate random 32-byte HMAC key from the deployment secret store. `SuppressionKey::new` rejects trivially weak inputs; this check cannot make a human-chosen key cryptographically random. Namespace/key selection must come from trusted application configuration, never from arbitrary request data. Use opaque namespaces rather than email addresses or other personal identifiers. Run `PostgresSuppressionStore::initialize(database_url, key, config)` explicitly as a deployment task. Applications use `connect` with the same configuration; missing namespaces, changed keys and quota drift fail closed. Runtime does not create tables or reconstruct deleted state. Protect database credentials and disable SQL statement/parameter logging outside the adapter as well. Wrap the configured mail transport in `SuppressionGuard::new(driver, store)`. For multiple tenants, register one correctly scoped guard per authenticated tenant in `TenantMailResolver`; avoid a fallback that silently selects another tenant's store. Install the composed driver through `Mail::set_driver` in every web process and queue worker. Apply the guard to every physical provider attempt when composing failover/retry transports, so a retry does not reuse an earlier suppression decision. The guard checks authoritative SQL state immediately before `send`, `send_for_tenant` and `send_with_delivery_id`. The normal Mail pipeline still validates messages. `Mail::enqueue_for_tenant` preserves the trusted tenant context; `register_mail_handler` resolves the current driver when the worker dispatches. Feedback received after enqueue can therefore block delivery. Plain drivers are not silently wrapped; applications must install this policy. ## Verified events and minimized storage Authenticate provider webhook bodies/signatures and freshness before producing a `SuppressionEvent`. The existing Resend verifier can turn verified permanent bounces/complaints into such events. Other providers require their corresponding verified adapter; this store does not authenticate arbitrary JSON or establish consent. Manual suppression requires authorized application/operator policy. Mount ingestion behind the appropriate signature verifier, bounded requests, secure headers, WAF and ingress limits. `record` atomically binds provider/event identity to its recipient, reason and observation time. Identical replay is idempotent; conflicting reuse is rejected without changing state. Reason precedence only increases: manual suppression, hard bounce, then spam complaint. Earlier events can strengthen a reason but cannot undo a complaint. Recipient normalization uses the delivery pipeline's recipient parser: `Name <address>` and `<address>` reduce to the bare address, and lists, groups, comments, padding or other unparsable forms are rejected so the guard fails closed. It then preserves the local part, which must be ASCII, lowercases the domain and keys an internationalized domain by its IDNA A-label, matching the existing stores; it does not infer aliases. The database stores HMAC-derived recipient/event identifiers and fingerprints, the authoritative bounded provider/reason and first/last observation times. It does not persist raw addresses, provider event IDs, message bodies or delivery history. Callers still handle addresses in memory, and keyed identifiers remain pseudonymous data, not anonymous data. Keep keys, backups and database access protected. Debug/errors omit addresses, event identities and keys. `lookup` returns the current record for the supplied normalized address. `snapshot` exposes counts and quotas only. `prune_events_before` deletes old replay evidence and retains every recipient suppression. Choose a positive cutoff no later than now and older than all provider redelivery/reconciliation windows: pruned identities may be accepted again. This API does not automatically reenable delivery, remove complaints or implement a privacy-deletion workflow. When capacity is exhausted, ingesting a new event fails explicitly. Investigate and retain/reconcile verified feedback instead of acknowledging a failed write as successfully persisted. ## Database and failure boundaries The private pool has at most four connections, verified TLS for effective remote hosts, five-second SQL/lock deadlines and ten-second operation deadlines. Local loopback/socket fixtures can use local transport. Every operation checks that the three fixed `public.rullst_mail_pg_suppression_*` tables are permanent, the database is writable and `fsync`, `full_page_writes` and synchronous commits are enabled. Runtime needs: - USAGE on the trusted `public` schema and SELECT/UPDATE on the control table. - SELECT/INSERT/UPDATE on recipient state. - SELECT/INSERT/DELETE on replay events. Keep CREATE/ALTER and recipient deletion with the deployment/operator role; revoke schema CREATE from untrusted roles. Namespace row locks serialize lookups, ingestion and retention. Quotas, clock observations and both event/recipient writes commit together. Cancellation before commit rolls back; an uncertain commit requires reconciliation. Storage/configuration failures make the guard return `SuppressionUnavailable` without invoking its transport. That error is classified `Transient`, so an outbox retries it later, but it is never failover-eligible: `FailoverDriver` does not hand the unchecked recipient to another provider. Every operation advances a per-namespace clock high-water mark monotonically in whole seconds. A host whose clock is at most 300 seconds behind that mark, such as synchronized hosts crossing a second boundary at slightly different instants, adopts the recorded time instead of failing. A larger backwards step fails closed with `InvalidConfiguration("server clock")`, which the guard reports as `SuppressionUnavailable`. The host owns clock synchronization, database availability, encryption at rest, replication fencing, a bounded number of namespaces and capacity planning. A stale database restore can lose newer complaints or replay evidence; quiesce delivery and reconcile authoritative suppression before resuming. Configuration changes need a deliberate migration. The store does not implement backup anti-rollback or automatic failover. A suppression committed after the guard's database check cannot recall mail already handed to a provider. Distributed provider delivery is not atomic with this SQL transaction, and retries can still deliver duplicates. These controls do not prove inbox acceptance, provider account readiness or legal compliance. ## Automated evidence The candidate's owned PostgreSQL suite exercises independent initializers/pools, replay and conflict races, reason precedence, recipient/event quotas, SQL failure rollback, cancellation, lock deadlines, clock/configuration drift, restricted runtime privileges, non-durable storage rejection and close/reopen. Fresh processes and an actual database restart verify persistent suppression and replay. The real Mail worker/tenant resolver/guard composition uses a bounded queue fixture and isolated memory transports to prove feedback after enqueue blocks the selected tenant without cross-delivery. No external inbox is contacted. Run `python3 .github/check-mail-postgres.py` for the disposable loopback Docker database. The package campaign repeats this contract through extracted facade/Mail/Core archives; full workspace, coverage, security and release gates remain separate. --- # Durable recurring publications Source: https://rullst.github.io/Rullst/book/recurring-publications.html # Durable recurring publications The v13 prerelease (13.0.0-alpha.1) adds Messaging `schedules-postgres` and facade `messaging-schedules-postgres`. Independent application instances coordinate through one authoritative PostgreSQL database, then relay frozen occurrences into the existing `MessageBroker` contract. Core's process-local `Scheduler` keeps its existing behavior. This feature alone selects neither SQLite nor ORM. The host owns authenticated tenant membership, schedule authoring, inspection, cancellation, operator retry and selection of the destination broker. Schedule names and namespace strings received from a request are not authorization. Apply the normal authenticated, ownership-checked, CSRF/WAF/header-protected management endpoints. Payloads must contain only necessary application data. ## Configure and run Load a 32-byte key from your secret manager and retain previous decryption keys while any retained record uses them. `initialize` creates the fixed schema and namespace once during deployment. `connect` requires existing state and never repairs missing tables or changes namespace quotas. Both take an explicit clock; use `SystemClock` in production. No database failure falls back to memory. ```rust,no_run # #[cfg(feature = "messaging-schedules-postgres")] # async fn schedule<B: rullst::messaging::MessageBroker>(database_url: String, # key_bytes: [u8; 32], first_after_ms: i64, payload: Vec<u8>, broker: B) # -> Result<(), Box<dyn std::error::Error>> { use rullst::messaging::{MessagingKeyring, MessagingStorageKey, SystemClock}; use rullst::messaging::schedules::{ MissedRunPolicy, PostgresRecurringStore, RecurringConfig, RecurringDefinition, ScheduledMessage, }; let keys = MessagingKeyring::new(MessagingStorageKey::try_new("2026-09", key_bytes)?); let store = PostgresRecurringStore::connect( database_url, RecurringConfig::new("school-events", 100, 10_000)?, keys, SystemClock, ).await?; store.create(RecurringDefinition::new( "daily-reminder-v1", "0 9 * * *", first_after_ms, MissedRunPolicy::Coalesce, ScheduledMessage::new("reminders", "reminder.requested", payload)?, )?).await?; store.tick(20).await?; for lease in store.claim(20).await? { store.relay(&lease, &broker).await?; } # Ok(()) # } ``` The application supplies the worker loop and its shutdown/backoff policy. Calls have a 10-second storage deadline; pools have at most four connections, with 5-second acquisition/SQL/lock deadlines. Broker publication is bounded by the remaining lease and at most 10 seconds. Database and broker work do not share a transaction. Calling `close` closes that pool and its clones, not other instances. ## Calendar and delivery contract Definitions are immutable. Identical creation replays the original metadata; conflicting reuse fails. Cancellation is permanent. Edit by cancelling and creating a new name. Retained definitions reserve names and count against the quota, even after cancellation; retiring a namespace is an explicit deployment operation. A random generation and scheduled UTC instant identify each occurrence. The five fields use the Rust `cron` crate's calendar semantics: minute, hour, day-of-month, month, weekday; seconds are zero, years span 1970 through 2100. Weekdays are **1=Sunday through 7=Saturday**, or names such as `Mon`. Day-of-month and weekday restrictions intersect. This is not POSIX crontab syntax: weekday zero and seconds/year fields are rejected. There is no implicit local timezone or daylight-saving conversion. `first_after_ms` is exclusive. Exhausted calendars stop producing occurrences. `CatchUp` preserves every missed due time; a tick emits the oldest due times first, up to its 1–100 occurrence budget. `Coalesce` emits the oldest outstanding due time once, then advances past observed current time. Materialization and advancement commit atomically. Capacity failure rolls back the **whole tick**; reduce the batch or purge terminal occurrences, then retry. Definitions are limited to 10,000, occurrences to 100,000 and raw message payloads to 16 KiB. Every materialized occurrence has a fixed delivery window starting at creation, including caught-up work. The default is one day, with a seven-day maximum. Leases default to 60 seconds, configurable from 1–300 seconds; the delivery window must accommodate two configured leases. A claim fences earlier workers with a fresh random capability and revision. A lease near the end of its delivery window is shortened to that window; a lease that has already expired when the claim commits is not returned (its occurrence expires or is reclaimed normally) and no longer fails the rest of the batch. Automatic publication failures back off exponentially and stop after ten attempts or delivery-window expiry. `retry_failed` starts a new explicit operator attempt budget within the **original** window and reports success once that reset is committed. It cannot revive published, cancelled, expired or purged occurrences. A relay checks the live lease immediately before publication. Repeated attempts use exactly the same message and purpose-separated idempotency key. Broker acceptance followed by an uncertain acknowledgement returns `RecurringRelayError::Acknowledgement`, retaining the broker receipt. A broker error or timeout may also follow remote acceptance; recovery repeats the same key. An invalidated lease cannot acknowledge/retry the new worker's occurrence. A cancellation after the final check cannot recall an in-flight publication. Broker deduplication must remain available for at least the delivery/retry window. Early broker purging can remove that protection. Consumers must check current authorization and deduplicate external effects at the destination. A successful relay proves broker acceptance, not handler completion or exactly once external delivery. Select the same broker namespace on every retry. ## Storage, roles and retention Definitions and occurrence content use AES-256-GCM with explicit rotation keys, bound to namespace, purpose and generation/occurrence identity. Configuration has an authenticated encrypted binding; key/configuration drift fails closed. Retain old keys as long as records need them; automatic re-encryption of definitions and occurrences is not included. The namespace configuration binding is re-sealed under the current primary key by the first operation after a rotation, so the key present at initialization is not needed for it afterwards. Definitions and retained occurrence content sealed under a key still need it whenever they are read (ticks, relays and re-creating the same schedule name), and a keyring holds at most eight keys. Debug output omits content, headers and lease credentials. Metadata names are server-owned configuration and should not contain personal data. PostgreSQL must use permanent tables, fsync, full-page writes, synchronous commits and a writable primary. Remote connections require verified TLS. All operations serialize through the namespace control row and recheck persisted server time after lock waits and around commit. Instances share that recorded time but not a clock: an instance whose clock trails it by at most five seconds adopts the recorded time, so shared time never moves backwards and no lease or deadline is judged earlier, while a larger regression fails with `RecurringError::Clock`. The host remains responsible for trusted clock discipline (a clock running far ahead still blocks the others until real time catches up), database credentials, backup encryption, anti-rollback policy, failover and capacity monitoring. These checks do not certify a deployment's backup/failover design. The runtime role needs `USAGE` on `public` and these table privileges: | Table | Runtime privileges | | --- | --- | | `rullst_recurring_control` | SELECT, UPDATE | | `rullst_recurring_definitions` | SELECT, INSERT, UPDATE | | `rullst_recurring_occurrences` | SELECT, INSERT, UPDATE, DELETE | `initialize` uses a separate deployment role with schema privileges. Ordinary workers use `connect` without DDL rights. Do not let other application roles mutate this subsystem's tables directly. `schedules` and `occurrences` return metadata pages of 1–100 records with exclusive cursors, without payloads or credentials. `occurrences`, `claim` and terminal purge classify expired work and erase expired retained content. Publication and cancellation erase occurrence ciphertext immediately; retryable dead letters retain it until expiry or explicit purge. `purge_terminal` removes only terminal occurrences older than the caller's cutoff, up to 100 per call. Run maintenance regularly: data is not removed while every worker is stopped. ## Acceptance status Local native contracts cover concurrent initializers/instances, immutable replay, catch-up/coalescing, real SQL rollback and lock deadlines, cancelled futures, lease expiry, cross-namespace denial, encryption binding, restricted roles, non-durable tables, quotas, retry limits, cancellation, fresh processes and actual PostgreSQL restart. An encrypted SQLite broker journey verifies acceptance before lost ACK, reopen/replay deduplication and real consumer delivery. No provider account is used. The same native journey passed through an extracted facade consumer, with Messaging source bytes matched against its archive. Combined hosted and extracted-package source admission then passed in PR #239; final release acceptance remains separate. --- # Durable outgoing webhooks Source: https://rullst.github.io/Rullst/book/outgoing-webhooks.html # Durable outgoing webhooks The v13 prerelease (13.0.0-alpha.1) adds Messaging `webhooks` and facade `messaging-webhooks`. It provides a private encrypted SQLite outbox for one immutable destination per server-owned namespace, signed HTTP deliveries, bounded retry, cancellation and minimized failure inspection. It reuses the existing broker's idempotent publication and fenced delivery state. This profile supports independent processes sharing a **local persistent file**. It does not coordinate databases on different hosts, support network filesystems or turn ephemeral container disks into durable storage. The application owns file/directory permissions, persistent volumes, backup custody and restart policy. The feature does not enable itself by default. ## Publish and supervise delivery Authorize the tenant/account and operation before selecting a namespace, creating a destination, publishing, cancelling or inspecting events. Never accept an arbitrary request URL as an approved destination. An authenticated management endpoint must retain ownership checks and the normal CSRF/WAF/secure-header baseline. The library does not add HTTP management routes or grant authority. ```rust,no_run # #[cfg(feature = "messaging-webhooks")] # async fn deliver(database_url: String, encoded_signing_key: String, # storage_key_bytes: [u8; 32], json_bytes: Vec<u8>) # -> Result<(), Box<dyn std::error::Error>> { use rullst::messaging::{MessagingKeyring, MessagingStorageKey, SystemClock}; use rullst::messaging::webhooks::{ WebhookConfig, WebhookDestination, WebhookOutbox, WebhookSigningKey, }; let config = WebhookConfig::new( "school-webhooks", WebhookDestination::approved_https("https://receiver.example/events")?, 10_000, )?.require_production()?; let signing = WebhookSigningKey::new("receiver-key-2026-09", encoded_signing_key)?; let storage = MessagingKeyring::new( MessagingStorageKey::try_new("storage-key-2026-09", storage_key_bytes)?, ); let outbox = WebhookOutbox::open(database_url, config, signing, storage, SystemClock).await?; outbox.enqueue("completion/42/v1", "course.completed", json_bytes).await?; let outcome = outbox.dispatch_next("worker-1").await?; # Ok(()) # } ``` `encoded_signing_key` is canonical unpadded base64url for exactly 32 CSPRNG bytes. Use a separate secret for encrypted storage. Empty or `mock_*` signing credentials select deterministic offline delivery with an explicit `offline: true` result; production configuration rejects them. Offline signing keys never authenticate incoming HTTP signatures. Test destinations accept only literal loopback HTTP(S) and optionally an owned test CA; production configuration rejects that mode. `open` creates or reopens the encrypted outbox, reserves one internal control record and subscribes its private delivery group. The encrypted control record binds destination, key identity/fingerprint, mode, retention quota and delivery window. Configuration/key drift fails instead of sending old payloads to a new receiver: reopening with a changed destination, signing key, window, mode or retention, or with a storage keyring that cannot open the namespace, returns `WebhookError::Configuration`. Changing destination or signing key requires an explicitly new namespace and an application-owned drain/cutover policy; receiver key overlap is managed by the host. Storage keyrings can retain old decryption keys while new writes use a new primary. Do not remove keys while retained records depend on them. Opening with a new primary re-seals the private control record under it, so once events sealed under an old key are acknowledged or dead-lettered and purged, that key can be removed. Enqueue preserves exact JSON bytes up to 64 KiB and a bounded event kind. Reusing a retained application event key with different bytes or kind fails; identical retries return the original message ID. Persisted application event keys are HMAC-derived; the raw event key is not retained. The application database transaction and this outbox are separate commits. Bridge from an application transactional outbox when atomic domain-change publication is required. Run `dispatch_next` from a supervised loop with bounded concurrency, backoff and shutdown. It claims one event at a time with a 30-second lease, resolves the endpoint, rechecks the authoritative SQL lease, sends and then acknowledges. Storage calls have a 10-second deadline. DNS/connect operations are bounded by 3 seconds, and HTTP by the remaining lease/window and at most 10 seconds. `close` closes that outbox pool and its clones, not independently opened workers. ## Destination and receiver security Production uses HTTPS with certificate validation, disables proxies and redirects, and resolves the approved hostname freshly per attempt. All returned addresses must pass the public-address policy, with a 16-address ceiling. The approved addresses are pinned to that attempt's connection; a subsequent DNS answer cannot choose another destination. Private, loopback, link-local, multicast, reserved, documentation and translation/transition ranges are denied. IPv6 admission is conservative ordinary global unicast. Deployments still own resolver trust, network routing and egress policy; this does not replace a firewall. These controls were checked against the [IANA IPv4](https://www.iana.org/assignments/iana-ipv4-special-registry/) and [IPv6](https://www.iana.org/assignments/iana-ipv6-special-registry/) registries and [OWASP SSRF guidance](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html). Review address policy when registries change. Deliberate conservative denial can exclude specialized globally reachable protocol addresses. Every POST sends `Content-Type: application/json` and five signature headers: | Header | Value | | --- | --- | | `Rullst-Webhook-Id` | Stable broker message ID, preserved on retry | | `Rullst-Webhook-Type` | Frozen event kind | | `Rullst-Webhook-Timestamp` | Canonical integer Unix seconds for this attempt | | `Rullst-Webhook-Key-Id` | Server-selected signing-key identifier | | `Rullst-Webhook-Signature` | `v1=` plus canonical unpadded base64url HMAC-SHA256 | The MAC input concatenates six byte strings, **each preceded by its 8-byte unsigned big-endian byte length**, in this order: `rullst.webhook.signature.v1`, key ID, delivery ID, event kind, decimal timestamp and the exact body bytes. Do not parse/re-serialize JSON before verification. The signature header is marked sensitive in the HTTP client's header map. Error/Debug surfaces omit keys, payloads, destination URLs and response bodies. Receivers should call `WebhookSignature::from_http_headers` to reject missing, malformed or duplicate signature headers, then `WebhookSigningKey::verify` on a bounded raw body and trusted clock. The maximum accepted skew is explicitly 1–300 whole seconds. Axum/http 1 header maps are compatible. The lower-level `from_headers` constructor assumes the caller already rejected duplicates. Verification uses constant-time HMAC validation and binds every signed field. It **does not store replay state**. Authenticate before creating the receiver's transactional deduplication record, deduplicate by delivery ID, authorize the event's application context and record the side effect atomically when possible. Signature verification alone neither grants tenant authority nor prevents replay. ## Retries, cancellation and retention Any 2xx status means receiver acceptance, not completion of its external effects. 408, 425, 429 and 5xx responses, DNS failures and transport errors use bounded exponential backoff. A bounded integer `Retry-After` up to one hour can lengthen the delay. Redirects, other permanent responses and forbidden destinations become terminal. Arbitrary receiver bodies are never buffered or persisted. Automatic delivery stops after ten attempts or its immutable delivery window. The window defaults to one day and is configurable from one minute through seven days. A timeout can happen after the receiver accepted a request; retries preserve the exact body and ID. If a successful HTTP response precedes a failed/uncertain local ACK, `WebhookError::Acknowledgement` retains the stable ID and status. Receiver-side effect deduplication remains necessary. `cancel` permanently invalidates pending/leased/failed work. It cannot recall a request already in flight or undo a receiver effect. Stale workers cannot acknowledge or retry the cancelled claim. `retry_failed` starts a new explicit operator attempt budget with the same ID and original deadline; it cannot revive cancelled, accepted, purged or expired work. Host applications authorize these management actions and own disciplined trusted time. The shared broker does not provide persistent clock anti-rollback or certify restore/failover behavior. `failed` returns up to 100 minimized terminal records: ID, kind, attempts, bounded failure code and time. No payload or destination is returned. At most 99,999 application events can be retained, in addition to the control record. Accepted and failed events retain encrypted content until explicit maintenance. `purge_terminal` deletes at most 100 terminal events older than the creation cutoff; the cutoff must precede now by the full delivery window. Active deliveries and control state are preserved. Purging also removes sender idempotency evidence; choose receiver retention and business idempotency accordingly. ## Local acceptance status Owned HTTP/TLS tests pass exact-body signatures, receiver deduplication after a lost response, certificate trust, redirect/private-address denial, concurrent workers, cancellation during delivery, bounded retries, manual retry, expiry, quota/configuration/namespace isolation, actual SQL outage/cancelled futures and terminal retention. A fresh child process delivers a persisted event, and a second process observes its durable acknowledgement. Broker lock-contention tests also reproduce and correct the earlier pre-lock clock sampling defect. No provider account or external recipient is used. Seventeen extracted archives were audited; the facade consumer passed the same HTTP/TLS and fresh-process journeys with 53 Messaging source files byte-matched to the candidate. All-feature Messaging regression (63 ordinary tests), strict all-target Clippy and the native recurring PostgreSQL regression passed. Combined hosted and extracted-package source admission then passed in PR #239; final release acceptance remains separate. --- # Redis Streams Messaging Source: https://rullst.github.io/Rullst/book/redis-messaging.html # Redis Streams messaging The v13 candidate adds `RedisBroker` to the existing `rullst-messaging` crate. Enable `redis-streams` directly, or `messaging-redis` on the `rullst` facade. The default messaging build remains network-free; the facade feature does not enable ORM, mail, cache or Core queues. Hosted workspace/platform and extracted-package source admission passed in [PR #236](https://rullst.github.io/Rullst/book/v13-delivery-plan.html#six-increment-source-admission-on-september-21). Final release admission remains separate. Acceptance uses an owned, digest-pinned Redis 7.4 service, not an owner's account or production data. ## Configuration and startup ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["messaging-redis"] } ``` This version is the 13.0.0-alpha.4 prerelease, which `cargo add rullst` does not select; name it explicitly. Final release admission remains separate. ```rust,no_run # #[cfg(feature = "messaging-redis")] # async fn startup() -> Result<(), Box<dyn std::error::Error>> { use rullst::messaging::{BrokerConfig, RedisBroker, RedisBrokerConfig}; let configuration = RedisBrokerConfig::try_new( BrokerConfig::try_new("academy-notifications")?, "deployment-2026-09", // Stable deployment generation, shared by all workers. std::env::var("MESSAGING_REDIS_ENDPOINT")?, // rediss://host:port/0 std::env::var("MESSAGING_REDIS_USERNAME")?, std::env::var("MESSAGING_REDIS_PASSWORD")?, )?.require_production()?; // An operator previously called RedisBroker::provision with this configuration. let broker = RedisBroker::connect(configuration).await?; # let _ = broker; # Ok(()) # } ``` Call `provision` once for an intentionally empty namespace. On ordinary process startup call `connect`, which checks stored generation and limits and rejects missing state. Never respond to a startup error by automatically provisioning a replacement. Keep the deployment generation stable across process restarts; restoring a backup requires explicit reconciliation, not a new random generation on every startup. Changing generation or limits for retained state fails. The stored state format is part of that check: a namespace provisioned by an earlier v13 development build (format v1) fails with `ConfigurationConflict`. Drain it with that build, then provision a new namespace or generation. Credentials are explicit and separate from the URL. Only database zero is supported. URL credentials, fragments, query options and insecure TLS overrides are rejected. TLS validates the certificate and hostname. `with_ca_certificate` accepts an explicit PEM trust bundle up to 64 KiB; it preserves hostname checks. The normal trust roots come from the maintained Redis client's WebPKI profile. For TLS connections, the adapter preserves an installed Rustls process provider; if none exists, it installs the ring provider before Redis builds its TLS client. Applications requiring another provider must install it before opening the broker. This avoids implicit-provider panics when dependencies enable multiple providers; certificate, hostname and trust-root validation remain required. The host owns credential rotation and secret storage. Diagnostics redact endpoint, password, deployment generation, payload, header values and ACK capabilities. An empty or `mock_*` password selects the explicit process-local `InMemoryBroker` fixture without a network connection. Its clones share state; separate `connect` calls do not create a distributed fixture. `require_production` rejects mock credentials and the `allow_loopback_for_tests` transport override. That override permits plaintext only for a literal loopback IP and cannot be enabled after sealing a configuration for production. Live connection or protocol errors never activate the fixture. The mock implements the common broker contract; Redis-specific byte budgets and remote failures require the actual service tests. ## Supported contract Redis Streams retains canonical `rullst.messaging.v1` envelopes. Rullst owns the group, ready-time, attempt, terminal and lease indexes in the same namespace. These groups implement `MessageBroker`; they are **not native `XREADGROUP` consumer groups**. Other clients must not mutate these keys or mix native ACK, trimming or group operations into this layout. | Operation | Supported behavior | | --- | --- | | `publish` | Exact topic/key replay returns the original ID and publication time; changed payload, headers or content metadata conflict. A server response acknowledges the append and index mutations. | | `subscribe` | Idempotent group registration with earliest-retained or future-only start. Separate groups receive independent copies. | | `receive` | Non-blocking bounded claims shared between consumers; authoritative Redis time determines lease expiry. A fresh random capability replaces an expired lease. | | `ack` | The current unexpired capability is consumed once. An expired or replaced capability cannot settle a newer delivery. | | `retry` | Delays up to seven days; the next receive claims due messages. Reaching the configured attempt ceiling sends the delivery to the group's DLQ. | | `dead_letter` / `dead_letters` | Explicit current-lease terminal failure and bounded inspection without ACK secrets. Lease-expiry attempt exhaustion is processed on subsequent operations. | | `purge_terminal` | Removes at most 100 messages per call, even if a larger limit is requested, and only after every current group is terminal or started after the message. With no groups it removes nothing. It also removes publication deduplication records. | Server-side scripts serialize mutations, so independently connected consumers cannot claim the same live lease. Delivery remains **at least once**: an external effect can succeed before its ACK fails. Deduplicate effects using the stable message ID or a domain key. Retain deduplication state long enough for the application's retry/recovery horizon; republishing after terminal purge creates a new message. Messages that become available at the same time, for example every retained message of a new earliest group, are claimed in publication order. Delayed retries and competing consumers do not promise global or strict FIFO processing order. ## Limits and failure handling The profile accepts at most **10,000 retained messages**, **128 subscriptions**, **100 attempts** and **1 MiB per payload**, with the lower `BrokerConfig` limits also enforced. Canonical retained envelope bytes have a separate **64 MiB** ceiling. Header/routing overhead counts toward that ceiling. Delivery and DLQ responses stop at **4 MiB** of encoded envelopes, so a batch can contain fewer than the requested maximum. These bounds are not a total Redis or process memory quota: indexes, protocol buffers, concurrent calls and separate namespaces also consume memory. The server is a trusted authenticated dependency, not an untrusted RESP parser sandbox. At most **eight operations per cloned broker family** are admitted concurrently; excess calls fail immediately. The default total operation deadline is **ten seconds**; `with_timeout` permits 100 milliseconds–30 seconds. There is no automatic write retry. A timeout or lost response does not establish whether a write committed. Replay a publication with the same key/content; let uncertain delivery claims expire and preserve destination idempotency. A later operation may establish a fresh connection to the same endpoint and must recheck persisted namespace state. Redis scripts provide isolation, not rollback after an execution error. Every mutation writes a persistent in-progress marker first and clears it only after all commands succeed. A partial failure leaves the namespace quarantined; subsequent operations and startup reject it. There is deliberately no public “clear dirty” repair helper. Recover to a reviewed consistent snapshot or rebuild in a separately provisioned namespace from authoritative source events, checking already-completed effects. Keep the original evidence for diagnosis. The marker cannot detect arbitrary administrator edits or a coherent old backup rollback. Publication fans out over registered groups. Creating an earliest subscription indexes retained messages; purge examines current groups. Those operations have bounded work but can briefly block the dedicated Redis server. Subscription creation belongs in deployment/configuration work, not per-request handlers. ## Deployment responsibilities Use a dedicated **standalone Redis 7.4+ database**, protected network and narrowly scoped ACL identity. The key prefix is `rullst:messaging:v1:<SHA-256(namespace)>:`; namespace hashing avoids delimiter collisions but is not authorization or encryption. The SDK needs `EVAL`, `TIME` and the hash, set, sorted-set and stream commands used by its fixed scripts. It does not need configuration, ACL-management, `FLUSHDB` or arbitrary key administration permissions. The application must authorize namespace/topic/group access before invoking broker operations; never hand the raw Redis credentials to an untrusted client. Lua source is included in the crate archive for audit. Configure **AOF persistence**, an appropriate fsync policy and **no eviction**. The acceptance fixture uses `appendfsync always` and abrupt process restart. The SDK does not change or certify server configuration. Persistence guarantees still depend on storage and server operation; acknowledgements are not universal zero-data-loss guarantees. Monitor disk/memory, server time and failed operations. Clock regression relative to persisted operation time rejects mutations until the server's time catches up or a reviewed recovery occurs. Redis contents, headers, deduplication metadata and leases are plaintext at rest; protect AOF, volumes and backups separately. The encrypted SQLite profile is not automatically applied to this remote adapter. Replication/failover, Sentinel, Cluster, managed-provider certification, native consumer-group interoperability, automatic trimming/repair, unbounded replay, multi-region ordering and exactly-once effects are outside this profile. ## Outbox and executable evidence Add `messaging-orm-outbox`, with the application's database feature (`strict-postgres`, `strict-mysql`, `strict-sqlite` or `drivers-all`), to relay committed relational events through the existing `OrmOutboxRelay<RedisBroker>`. Commit the domain write and event in the same ORM transaction. A worker publishes the stream-scoped durable event key, then ACKs its exact outbox claim. Crashing between these operations produces an exact broker replay when reclaimed. This still does not create a distributed transaction. The [disposable service runner](https://github.com/Rullst/Rullst/blob/main/.github/check-messaging-redis.py) executes the common broker contract, competing clients, exact/conflicting replay, expired/replaced leases, retry/DLQ, retention/reply limits, partial-write quarantine, missing state, timeout reconciliation, trusted/untrusted TLS, hostname checks, an actual ORM outbox and an AOF server restart. Test fixtures use only temporary data, local certificates and public test credentials. The [facade consumer](https://github.com/Rullst/Rullst/blob/main/.github/fixtures/messaging-redis-facade.rs) is also compiled from extracted package sources. Hosted full-workspace and package checks passed in PR #236; final release checks remain a separate admission requirement. --- # Recoverable Live UI Source: https://rullst.github.io/Rullst/book/live-recovery.html # Recoverable server-driven Live UI The v13 prerelease (13.0.0-alpha.1) adds `rullst::live::recovery` in the existing Core and facade, with no extra feature or browser dependency. It is an explicitly mounted native-server API; the browser uses the supplied small ES module. Protocol and Chromium acceptance have passed. Hosted workspace/platform and extracted-package source admission passed in [PR #236](https://rullst.github.io/Rullst/book/v13-delivery-plan.html#six-increment-source-admission-on-september-21). Final release admission remains separate. The older `LiveComponent`, `Live::mount` and `make:live` example keep their per-connection state and HTMX WebSocket protocol. Their `live_ws_handler` now limits incoming frames and messages to 64 KiB (previously the 64 MiB transport default) and ignores Ping/Pong keepalive frames instead of ending the session, but still has no origin check, connection cap or idle timeout. Adopting the recovery API is an application change; upgrading a dependency does not migrate those components. ## Application contract Implement `RecoverableLiveView` with three asynchronous methods: | Method | Required application behavior | | --- | --- | | `authorize` | Recheck the captured authenticated session, expiry, current tenant membership and permission for this exact component/resource. Return `Unauthorized` on revocation; storage failure is an error. | | `snapshot` | Read authoritative state and render a complete `LiveSnapshot` with its persistent monotonic `u64` revision. Escape untrusted values before including them in HTML. | | `apply` | Validate the requested action, fields and current write permission. Atomically compare `command.expected_revision()` with the domain revision in the same transaction as the mutation, then advance it. On mismatch return `Conflict` without writing. | `LiveScope` binds an authenticated tenant, account and component key, not a browser-supplied role or identity. Construct it from verified server state and `TenantMembership::select`. Scope construction alone does not authorize access. The implementation uses static dispatch and mounts no routes automatically. For SQL, a bound update such as `UPDATE ... SET revision = revision + 1 WHERE tenant = ? AND id = ? AND revision = ?` belongs inside the domain transaction, alongside any required current authorization check. Check the affected row count before reporting success. A separate read followed by an unconditional write cannot protect two simultaneous sockets. Never interpolate form fields into SQL identifiers or treat a field named `tenant` as the trusted scope. ## Route and browser composition This adapter receives a scope and view already resolved by the host's authenticated route. It is not an authentication extractor: ```rust,no_run use rullst::live::recovery::{ LiveRecovery, LiveRecoveryConfig, LiveScope, RecoverableLiveView, }; use rullst::web::axum::{ extract::WebSocketUpgrade, http::HeaderMap, response::Response, }; fn configured_live() -> Result<LiveRecovery, rullst::live::recovery::LiveRecoveryError> { Ok(LiveRecovery::new( LiveRecoveryConfig::try_new("https://academy.example")?, )) } async fn authorized_upgrade<C: RecoverableLiveView>( shared: &LiveRecovery, websocket: WebSocketUpgrade, headers: &HeaderMap, trusted_scope: LiveScope, authenticated_view: C, ) -> Response { shared.upgrade(websocket, headers, trusted_scope, authenticated_view).await } ``` Create the handler once in application state; its clones share admission limits. Use `Server` or `apply_security_baseline` for the HTTP routes and enforce ordinary application authentication/ownership. Serve `LIVE_RECOVERY_MODULE` at an explicit same-origin JavaScript route with a JavaScript content type. Load a separate application module under the site's CSP; no inline-script exemption is needed. The browser application module can contain: ```javascript import { connectLive } from '/assets/rullst-live.js'; const live = connectLive( document.getElementById('lesson-view'), '/live/lesson', document.getElementById('connection-status'), ); // A false return means the action was not sent. There is no offline queue. // live.send('save-answer', { answer: '42' }); ``` The root and optional status element must already exist. Render `<button type="button" data-live-action="increment">` for an action without fields, or a form with `data-live-action="save-answer"` and named string inputs. Duplicate field names and file inputs are rejected. Submit buttons delegate to the form; the named fields, rather than the submit button value, form the command. Only one action can be pending. Controls with `data-live-action` are disabled while unavailable or awaiting a response; programmatic `send` enforces the same admission even when a form's submit button has no such attribute. Every accepted snapshot replaces the root's HTML. This is not DOM diffing: unsent edits, focus, selection and root-child event listeners may be replaced. Keep unsaved drafts outside this root if the application needs to preserve them, and use delegated listeners. Server-rendered HTML is trusted content; the transport is not a sanitizer. Never include secrets, executable user HTML or unescaped form values in a snapshot. ## Recovery and outcomes On connection or reconnection, the server sends current full state. Commands use `rullst.live.v1`, a random 32-hex correlation ID and a decimal-string revision to avoid JavaScript integer rounding. A stale command yields a fresh snapshot with `conflict`. A successfully committed action yields `applied` and a newer revision. The browser emits `rullst:live-state` with `{ state, pending }` and `rullst:live-result` with `{ id, outcome }`. Outcomes are `recovered`, `applied`, `conflict` or `unknown`. If the connection fails while an action is pending, `unknown` means it may already have committed. The client never automatically replays it. Recover authoritative state before allowing a person to retry an external effect; use separate durable domain idempotency for payments or email. The correlation ID alone does not supply deduplication. Reconnect attempts use capped exponential backoff with jitter (250 milliseconds to a base cap of ten seconds, at most twelve consecutive failed attempts). A received valid snapshot resets the retry budget. Handshake/action waits have a thirty-second browser deadline. `reconnect()` explicitly retries with a fresh budget; `dispose()` removes listeners and closes the socket. Recovery converges to state, not replay of every missed transient event or automatic broadcast of changes made by other clients. A still-connected client learns other changes on its next action/conflict or explicit reconnect. Origin must match the configured HTTPS origin exactly; omitted, multiple, `null` and foreign origins fail. The only offered subprotocol must be `rullst.live.v1`. For local fixtures, `loopback_for_tests` permits HTTP at a literal loopback IP. The production route must not infer this configuration from an untrusted Host or forwarded header. Use secure application cookies and trusted TLS/proxy setup. Authorization runs before upgrade, before domain actions, after domain I/O and before sending each snapshot, and periodically while connected. An observed revocation closes with 4401; the client clears its root and reports `denied`. Changing or clearing a browser cookie alone cannot revoke an existing socket: revoke its captured server session. Already-committed actions are not rolled back by later revocation. A race after the final authorization check cannot recall bytes already sent; the host owns transaction-level permission checks. HTTP upgrade failures are intentionally opaque to browser WebSocket clients; they use bounded reconnect and eventually `unavailable`. Ordinary disconnects can retain the previously displayed HTML. Do not interpret that cached display as a current authorization decision. An explicit forbidden/invalid close clears it; applications can also clear their view on logout or any disconnected state. ## Bounds and deployment | Resource | Default and hard boundary | | --- | --- | | Shared connections | 128; configurable 1–1,024 per cloned handler family | | Actions per connection | 128; configurable 1–1,024, then reconnect required | | Callback or send | Five seconds; configurable 100 milliseconds–ten seconds | | Session revalidation | Every five seconds; configurable 100 milliseconds–thirty seconds | | Connection lifetime | One hour; configurable one second–one hour | | Command/frame | 16 KiB; 32 unique fields, each name/action at most 64 ASCII bytes, each value at most 2 KiB | | Snapshot | 64 KiB UTF-8 HTML; encoded response at most 512 KiB | | Socket buffers | 16 KiB read/write buffer targets; 1 MiB maximum write buffer | The server pings periodically and closes a peer that stays silent for more than three revalidation intervals of waiting. Time the server spends in its own callbacks and sends, when it does not read the socket, is not counted as the peer's silence. Domain callbacks must perform asynchronous, cancellation-safe work; a Tokio timeout cannot preempt blocking CPU work or prove that a canceled database operation rolled back. Limits bound this handler, not all process memory or aggregate users across replicas. Use ingress/per-account rate limits for repeated reconnects and infrastructure-level abuse controls. Oversized frames terminate the connection before domain actions. Rejecting the payload at the transport limit can reset TCP before a close frame reaches the peer; clients must not depend on receiving an application close code in that case. Application persistence and shared revision/permission authority are required for restart or cross-replica recovery. Neither this handler nor the browser persists domain state. Keep revision epochs consistent when restoring backups. Configure the proxy for WebSocket upgrades and suitable idle timeouts; TLS, proxy behavior, replication and failover need deployment-specific validation. This candidate does not add WebSocket ticket issuance, frame encryption, DOM patching, an offline mutation queue or automatic starter migration. ## Executable acceptance The [protocol suite](https://github.com/Rullst/Rullst/blob/main/rullst-core/tests/live_recovery/protocol.rs) uses actual WebSockets, a parameterized SQLite domain transaction and the production HTTP baseline. It tests concurrent revision conflicts, uncertain committed actions, origin/session/tenant denial, revocation before disclosure, binary/oversized input, admission, idle peers, action limits, lifetimes and callback deadlines. The [Chromium journey](https://github.com/Rullst/Rullst/blob/main/.github/live-recovery-browser.mjs) exercises two pages, offline/online transitions, exactly one send of an uncertain action, read-only learner permissions, CSRF-protected logout and recovery after killing and restarting the actual application process against the same database. It is explicitly executed in the existing Linux workspace and coverage jobs. These disposable fixtures use no real account credentials. They establish local behavior, not release admission or a guarantee for every proxy/browser/deployment. --- # Distributed Operation Tracing Source: https://rullst.github.io/Rullst/book/distributed-tracing.html # Tracing operations across processes The v13 prerelease (13.0.0-alpha.1) extends Core/facade's optional `telemetry` feature with `telemetry::distributed`. It propagates an explicitly trusted parent and exports a minimized operation trace through the maintained OpenTelemetry SDK. The existing `SpanCollector` stays process-local; Studio's authenticated trace ingestion remains its separate protocol and is not an OTLP collector. Acceptance includes independent Messaging producer/consumer processes and the standard OpenTelemetry Collector 0.161.0 over verified TLS. Hosted workspace/platform and extracted-package source admission passed in [PR #236](https://rullst.github.io/Rullst/book/v13-delivery-plan.html#six-increment-source-admission-on-september-21). Final release admission remains separate. ## Explicit configuration and lifecycle ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["telemetry"] } tracing = "0.1.44" tracing-subscriber = "0.3" ``` This version is the 13.0.0-alpha.4 prerelease, which `cargo add rullst` does not select; name it explicitly. Final release admission remains separate. ```rust,no_run # #[cfg(feature = "telemetry")] fn configure_traces() -> Result<rullst::telemetry::distributed::DistributedTelemetry, Box<dyn std::error::Error>> { use rullst::telemetry::distributed::{DistributedTelemetry, TelemetryConfig}; use tracing_subscriber::prelude::*; let config = TelemetryConfig::try_new( "academy-web", std::env::var("ACADEMY_OTLP_TRACES_URL")?, // https://collector.example/v1/traces ["academy.request", "academy.publish", "academy.handle", "rullst.orm.query"], )?.with_sampling(0.1)?.require_production()?; let runtime = DistributedTelemetry::try_new(config)?; tracing::subscriber::set_global_default( tracing_subscriber::registry().with(runtime.layer()), )?; Ok(runtime) } ``` Retain the returned runtime for the application lifetime. Install its layer once, before `Server` initializes its legacy subscriber. It composes with an ordinary `tracing_subscriber` registry and does not replace a global subscriber or provider automatically. Other formatting/export layers have independent data policies. Use stable, non-personal service and operation labels: 1–64 ASCII letters, digits, dots, underscores or hyphens, with 1–64 distinct approved operations. Operation names must match the actual span names. Do not put an account, email, tenant, URL, SQL statement or request identifier into those configuration labels. They are approved by the application, not automatically classified as harmless. The endpoint is explicit, with the exact `/v1/traces` path and no URL credentials, query or fragment. `with_bearer_token` accepts an explicit token up to 8 KiB; `with_ca_certificate` accepts up to 64 KiB of PEM CA material and preserves certificate/hostname checks. `require_production` rejects offline or HTTP profiles. `loopback_for_tests` explicitly permits HTTP only at a literal loopback IP, including a separately protected local collector. Empty or `mock_*` endpoints select an explicit offline fixture: spans are counted and discarded without network traffic. `is_mock()` exposes that choice. A failed live collector never activates it. Offline counters do not prove interoperability or delivery, and the fixture retains no span payloads. ## Parent trust and async execution An incoming trace ID is correlation metadata, not proof of identity or tenant membership. Apply authentication and domain authorization separately. Default untrusted public ingress to `ParentPolicy::StartNew`; select `TrustedPeer` only after the host has established the upstream identity and correlation boundary. This also prevents an untrusted `sampled` flag from overriding root sampling. ```rust,no_run # #[cfg(feature = "telemetry")] async fn observed_request(headers: &rullst::http::HeaderMap) -> Result<(), rullst::telemetry::distributed::TelemetryError> { use rullst::telemetry::distributed::{ParentPolicy, TraceParent}; use tracing::Instrument; let span = tracing::info_span!(parent: None, "academy.request", otel.kind = "server"); TraceParent::apply_incoming(&span, headers, ParentPolicy::StartNew)?; async { // The application's separately authorized asynchronous work goes here. }.instrument(span).await; Ok(()) } ``` Use `Instrument` to enter spans while their futures are polled; do not retain an entered-span or thread-local context guard across `.await`. Existing ORM spans participate when their operation names are approved and the domain future runs inside the parent span. `TraceParent` accepts an exact 55-byte, lowercase, nonzero W3C version-00 `traceparent`. Duplicate HTTP values and unsupported/malformed trusted context return `InvalidContext`; the host can explicitly choose to start a new root for invalid tracing metadata without changing its authorization decision. The minimized profile deliberately omits vendor `tracestate` and arbitrary `baggage`; it is a documented subset, not full W3C forwarding conformance. `capture` obtains a current span's outgoing parent; `inject` replaces traceparent and removes those two metadata headers, leaving unrelated application headers alone. Authorize the destination before sending any headers. For Messaging, place `TraceContext::try_new(parent.as_str())` into `PublishRequest::with_trace_context`. The consumer reads the retained envelope's `trace_context`, validates it with `TraceParent::try_new`, and calls `attach_to` on its new consumer span only after trusting that producer/topic. A worker's new span ID remains distinct from the producer's. The [independent-process fixture](https://github.com/Rullst/Rullst/blob/main/rullst/tests/distributed_tracing.rs) executes this journey through the shared-local SQLite broker and checks a nested ACK operation. Redis and other validated carriers can carry the same values, but this particular acceptance does not claim every transport or deployment. ## Metadata and bounded export The profile exports approved operation/service names, trace/span/parent IDs, kind, timing and status code. Before queueing, it removes application attributes, events, links, vendor state and error descriptions. It also avoids ambient resource detectors, replacing instrumentation-scope metadata with a fixed label. The SDK span limits disable attribute/event/link retention for this provider. This boundary does not sanitize other log layers, user-supplied configuration labels, instrumentation allocations before SDK processing, or application data. | Export resource | Default and supported limit | | --- | --- | | SDK queued spans | 256; configurable 1–4,096 | | Batch size | 32; configurable 1–128 and no larger than the queue | | Scheduled export interval | One second; configurable 100 milliseconds–thirty seconds | | Total network operation | Three seconds; configurable 100 milliseconds–ten seconds | | Encoded request | At most 1 MiB | | Collector response | At most 16 KiB, including responses without Content-Length | | Explicit lifecycle operation | SDK flush/shutdown deadlines; shutdown uses five seconds | `with_limits` sets queue, batch, interval and network deadline. Export uses the SDK's dedicated thread and a compatible blocking HTTP client created on that thread. Request-side span completion never waits for collector I/O; a saturated queue drops telemetry. These are export-queue bounds, not a quota for every active application span or all process memory. The transport disables redirects and ambient proxies, verifies TLS and accepts only bounded OTLP/HTTP protobuf responses. Malformed data, partial acceptance, HTTP errors and timeouts count as failed batches. It never logs arbitrary collector error text. There is no application-side durable spool or automatic retry of an uncertain batch. Explicit configuration controls endpoint, bearer token, sampling, resources and queue bounds; ambient SDK header values are not forwarded by this profile. Incompatible SDK compression settings fail closed. `status()` exposes metadata-only counters. `forwarded_spans` means offered to the SDK, so it includes spans later dropped during saturation. `accepted_batches` means a valid fully accepted OTLP response, not proof of durable backend storage. `failed_batches` records transport/protocol failures; `offline_spans` records the explicit fixture; `rejected_spans` records disallowed names/invalid span metadata. These counters do not form an exact per-span loss accounting system. After ending the application's active spans, call `flush().await` and eventually `shutdown().await` from Tokio. These move blocking SDK lifecycle work off the request executor. Only one explicit flush is admitted at a time. A successful flush does not recover previously dropped or failed batches; a shutdown timeout does not prove all traces were delivered. A process kill can lose queued spans. Operate collector authentication, network policy, sampling budgets, retention, backups, availability and data access separately. Traces are not an audit ledger. ## Legacy initialization and executable evidence `init_telemetry` remains the legacy general tracing subscriber. The v13 candidate repairs its unsupported async-client/thread-processor combination with the same compatible bounded HTTP transport. Its default is now OTLP/HTTP at `http://127.0.0.1:4318/v1/traces`, rather than the gRPC port. An explicit `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is used exactly; the generic `OTEL_EXPORTER_OTLP_ENDPOINT` is a base with `/v1/traces` appended. Review existing environment configuration during migration. Legacy formatting, general span fields, ambient resource detection and explicit SDK headers keep their separate policy; they do not acquire the minimized profile automatically. Install the owned pipeline above when its privacy and lifecycle controls are required. Because legacy spans keep their attributes and events, the legacy exporter sends at most 64 spans per request unless `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` sets another size, so batches stay under the shared 1 MiB request limit. Unlike the minimized profile it honours `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`. Its counters are not exposed, so a dropped batch (oversized, rejected or failed) is reported on stderr at most once a minute with the number of further failures, never with the endpoint or span data. The [protocol tests](https://github.com/Rullst/Rullst/blob/main/rullst-core/tests/distributed_tracing/protocol.rs) decode real protobuf exports and exercise stripped fields, parent relationships, queue saturation, malformed/oversized/chunked/partial replies, redirects and slow/unavailable collectors. The [standard collector runner](https://github.com/Rullst/Rullst/blob/main/.github/check-distributed-tracing.py) uses an owned digest-pinned container, temporary CA/server identities, verified and rejected TLS, and actual separate producer/consumer processes. It validates the ancestry again in the collector's output, rather than accepting client counters alone. This runs explicitly in the existing Linux workspace and coverage jobs. The profile is grounded in the [W3C trace-context specification](https://www.w3.org/TR/trace-context/) and the [OTLP protocol](https://opentelemetry.io/docs/specs/otlp/). Its bounded subset and local evidence do not certify external tracing accounts, every OpenTelemetry configuration, production retention or legal compliance. --- # Deploy to a Linux VM Source: https://rullst.github.io/Rullst/book/vm-deployment.html # Deploy to a Linux VM This page is the shared part of the four provider guides ([Hetzner Cloud](https://rullst.github.io/Rullst/book/deploy-hetzner.html), [DigitalOcean](https://rullst.github.io/Rullst/book/deploy-digitalocean.html), [Oracle Cloud](https://rullst.github.io/Rullst/book/deploy-oracle-cloud.html) and [Google Cloud](https://rullst.github.io/Rullst/book/deploy-google-cloud.html)). It explains what happens on a virtual machine (VM) once you have one, in the order you will do it. Each provider guide covers only what is different: how to create the machine, open the firewall and point DNS. If you have never run a server before, read this page once from top to bottom first. Nothing here is specific to one company. ## What you will end up with ```text Internet --> 80/443 --> Caddy (HTTPS, certificates) --> your Rullst app (port 3000) | database volume ``` - One VM running Linux (Debian or Ubuntu are the easiest to follow). - A container engine: Docker or Podman. - Two containers started by one file: your application and [Caddy](https://caddyserver.com/docs/), a web server that asks for and renews HTTPS certificates by itself. - Only ports 22 (SSH), 80 and 443 reachable from the internet. The application port (3000) is never opened to the internet; only Caddy talks to it. ## 1. Generate the deployment files In your project, on your own computer: ```bash cargo rullst deploy ``` For a plain Linux VM this writes three files and does not contact any provider: | File | Purpose | | :--- | :--- | | `Dockerfile` | Builds the release binary in a build stage (with a separate dependency-cache layer, so later builds are faster) and copies it into a small runtime image that runs as a non-root user. | | `compose.yaml` | Describes the two containers (your app and Caddy), the private network between them and the volumes. The app reads its settings from `.env.production` and keeps its data in the `app_data` volume. It works with `docker compose` and with `podman compose`. | | `Caddyfile` | Tells Caddy to serve the domain in the `DOMAIN` variable and to forward requests to the application. | It also adds the Caddy container's fixed address to `[security] trusted_proxies` in `Rullst.toml` (see section 5), and prints the next steps. Existing files are kept: it writes each file only when it is missing. Open all three and read them. They are a starting point, and you are the one who deploys them. Do not commit files that contain secrets (see section 5). The existing generated `Dockerfile` has a note you should keep in mind: run `db:migrate` once as its own job before you start or replace the application (see [migrations](https://rullst.github.io/Rullst/book/tutorials/26-one-click-paas-deploy.html#startup-migrations)). ## 2. Create the VM and the DNS record Follow your provider guide. In every case you need: - **Size.** A small application (one process, SQLite or a small PostgreSQL) runs on 1 to 2 virtual CPUs and 1 to 2 GB of memory. The first image build can need more memory than running the app, so either add a swap file on the VM (see the note below), build the image on your own computer, or build it in CI and only pull it on the VM. Check each provider's current pricing and free-tier limits; they change. - **Firewall.** Allow inbound TCP 22, 80 and 443 only (and UDP 443 if you want HTTP/3). Everything else stays closed. - **SSH key.** Create the VM with your public SSH key and log in with `ssh <user>@<ip-address>`. Do not enable password logins. - **DNS.** At your domain registrar or DNS host create an `A` record (and an `AAAA` record if the VM has an IPv6 address) for your domain pointing to the VM's public IP address. Caddy can only get a certificate after this record works, so check it with `dig +short your-domain.example` first. > **Swap file (small VMs only).** If a build is killed with "out of memory", > create a swap file once: `sudo fallocate -l 2G /swapfile && sudo chmod 600 > /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile`. Add it to > `/etc/fstab` to keep it after a reboot. ## 3. Install Docker or Podman on the VM Use the official instructions for your distribution, so you get supported packages: - Docker Engine and the Compose plugin: <https://docs.docker.com/engine/install/> - Podman: <https://podman.io/docs/installation> (on Debian and Ubuntu, `sudo apt install podman podman-compose` is enough; check that your release ships a recent version). Check that it works: ```bash docker compose version # or: podman compose version ``` The commands below run `docker` without `sudo`. For that, add your user to the `docker` group once (`sudo usermod -aG docker $USER`, then log in again); members of that group can control the host as root, so add only yourself. Podman runs rootless as your normal user and needs no group. ## 4. Get the application onto the VM There are two simple ways. Pick one. **A. Copy the source and build on the VM.** Easiest, but needs enough memory (see the swap note above): ```bash # on your computer, from the project directory rsync -av --exclude target --exclude .git --exclude '.env*' ./ <user>@<ip>:~/app/ ``` **B. Build the image elsewhere and move it.** Faster to start on a tiny VM: ```bash # on your computer docker build -t my-app:2026-10-29 . docker save my-app:2026-10-29 | gzip | ssh <user>@<ip> 'gunzip | docker load' scp compose.yaml Caddyfile <user>@<ip>:~/app/ ``` The generated `compose.yaml` builds the `app` service from the source (`build: .`), which is not on the VM in this case. Add an `image:` line with your tag under `app:` so Compose uses the image you loaded: ```yaml services: app: image: my-app:2026-10-29 build: . ``` Then start it without `--build` in step 6 (`docker compose up -d`). If your computer and the VM have different CPU architectures (for example an Apple Silicon laptop and an x86-64 VM, or an x86-64 laptop and an ARM VM), add `--platform linux/amd64` or `--platform linux/arm64` to `docker build` so the image matches the VM. A private registry (Docker Hub, GitHub Container Registry, or your provider's) also works and replaces `docker save`/`load`. ## 5. Production settings that matter Put these in a file on the VM named `.env.production`, next to `compose.yaml` (never commit it, and keep it readable only by you: `chmod 600 .env.production`). The generated `compose.yaml` passes it to the `app` service, and Compose refuses to start while the file is missing: | Setting | Why | | :--- | :--- | | `RULLST_ENV=production` | Turns on the production behavior: Secure cookies, strict headers, and a refusal to start with unsafe configuration. The generated `Dockerfile` and `compose.yaml` already set it; keep it in this file too, so `deploy:doctor` (below) checks the environment you deploy. | | `APP_KEY=<long random value>` | The application secret that protects sessions. Create one with `openssl rand -hex 32`. Never reuse a development value. | | `DATABASE_URL=...` | For SQLite (the starters' default) the `Dockerfile` that `cargo rullst deploy` writes already points it at `/app/data/rullst.db`, inside the `app_data` volume; leave it out. A `Dockerfile` from an earlier `deploy` or `dockerize` may lack that line (`deploy` warns): add `ENV DATABASE_URL=sqlite:///app/data/rullst.db?mode=rwc` to its last stage, or set it here. For PostgreSQL or MySQL, the connection URL of your database. | | Other secrets | Mail, payment and AI keys also go here, as environment variables, never in the source or the image. | Where the CLI is installed (usually your computer, in the project directory), `cargo rullst deploy:doctor --env-file .env.production` checks a copy of this file and `Rullst.toml` without sending them anywhere; it exits with an error when, for example, `RULLST_ENV` or `APP_KEY` is missing or a placeholder (see [Local Deployment Configuration Diagnostic](https://rullst.github.io/Rullst/book/deployment-diagnostic.html)). **Trusted proxy.** Every request reaches your application through Caddy, so the application sees Caddy's address as the visitor's address. Rate limits and logs would treat all visitors as one. `cargo rullst deploy` already told Rullst to trust Caddy in `Rullst.toml` (if the list existed, it left it unchanged and printed a warning; then add the address yourself): ```toml [security] trusted_proxies = ["172.31.250.10"] ``` Use the fixed address that `compose.yaml` gives the Caddy container (read it in the file, and keep the two in step). List only your own proxy: any machine in that list can choose the client address. Background: [Client addresses behind the proxy](https://rullst.github.io/Rullst/book/tutorials/26-one-click-paas-deploy.html#client-addresses-behind-the-proxy). **Health checks.** Starters created by `cargo rullst new` answer `GET /health` (the process is alive) and `GET /ready` (the application is ready for traffic). Use them for monitoring: ```bash curl -fsS https://your-domain.example/health ``` **Optional monitor token.** If you enable the opt-in monitoring endpoint, set `RULLST_MONITOR_TOKENS` in the same file and read [Production monitoring](https://rullst.github.io/Rullst/book/production-monitoring.html) first. It is off by default; do not publish its token. ## 6. Start it Caddy serves the domain in the `DOMAIN` variable. Put it in a file named `.env` next to `compose.yaml` (Compose reads it for the variables in the file; it holds no secrets), or write `DOMAIN=your-domain.example` before each command: ```bash echo 'DOMAIN=your-domain.example' > .env ``` **Rootless Podman** cannot listen on ports below 1024 by default. Allow it once: `echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-rullst-ports.conf && sudo sysctl --system`. Docker and rootful Podman need nothing. Rootless containers also belong to your login: run `sudo loginctl enable-linger $USER` once so they keep running after you log out. Docker starts the containers again after a reboot (`restart: unless-stopped`); with rootless Podman, enable a user service that does it (for example `systemctl --user enable podman-restart.service`, or the [Podman Quadlet](https://rullst.github.io/Rullst/book/advanced-deployment.html#podman-quadlet-systemd-unit-for-the-container) units) and check it with one test reboot. To restart everything with podman-compose, use `podman compose down` and then `podman compose up -d` (the volumes stay): with Podman 5.7 and podman-compose 1.5 on Ubuntu 26.04, `podman compose restart` can leave Caddy stopped while it reports success, and `down` can print `rootless netns: kill network process: permission denied` (Ubuntu's AppArmor profile for `pasta` refuses Podman's stop signal) although it removes the containers. On the VM, in the directory that holds the generated files: ```bash docker compose up -d --build # or: podman compose up -d --build docker compose ps docker compose logs -f ``` If you use the Rullst database, run the migrations once, as their own step, before sending traffic (see the note on migrations above): ```bash docker compose run --rm app /app/<binary-name> db:migrate ``` (`<binary-name>` is your package name from `Cargo.toml`. The runtime image has no shell, so call the binary directly.) When the DNS record is correct and ports 80 and 443 are open, Caddy requests a certificate as soon as it starts, and retries until it gets one. Open `https://your-domain.example` in a browser. If it does not work, read `docker compose logs caddy`: the usual causes are a wrong DNS record, a closed port, or a typo in the domain. ## 7. Logs The application writes its logs to standard output, and the container engine stores them: ```bash docker compose logs --tail 200 app docker compose logs -f caddy ``` The app's first line says where it listens. Because `Rullst.toml` trusts a proxy, it adds "if it runs on this host, set HOST=127.0.0.1": ignore that hint here. Caddy runs in its own container and reaches the app over the compose network, so the app must keep listening on `0.0.0.0` (the `HOST` that `compose.yaml` sets); its port is not published on the VM. On the host, `journalctl` shows the engine's own messages (`sudo journalctl -u docker`). The default log driver can fill the disk on a small VM, so limit it: in Docker, set `"log-driver": "local"` (or a `max-size` and `max-file` option) in `/etc/docker/daemon.json` and restart Docker. Podman has `--log-opt max-size=...` for the same purpose. ## 8. Back up the database volume A backup that has never been restored is only a hope. Do both steps once. **SQLite.** Use SQLite's own backup command so the copy is consistent while the app is running (the runtime image has no shell, so run it from a helper container that mounts the same volume, or from the host path of the volume): ```bash docker run --rm -v <volume-name>:/data -v "$PWD":/backup docker.io/library/alpine:3 \ sh -c 'apk add --no-cache sqlite >/dev/null && sqlite3 /data/rullst.db ".backup /backup/rullst-$(date +%F).db"' ``` The volume is named after the directory that holds `compose.yaml`: for `~/app` it is `app_app_data` (`docker volume ls` lists them; with Podman use `podman run` and `podman volume ls`). `rullst.db` is the file the generated `Dockerfile` names in `DATABASE_URL`; use yours if you changed it. **PostgreSQL or MySQL.** Use `pg_dump` or `mysqldump` (or your managed database's snapshots) on a schedule. Then copy the backup off the VM (to object storage or another machine) with a scheduled job such as `cron`, and every few months restore one into a temporary copy of the app to prove it works. Provider "snapshots" of the whole VM are a useful extra, but they are not a substitute for a database-level backup. Check each provider's current pricing for snapshots and backups. ## 9. Update and roll back with image tags Never deploy the image called `latest`: you could not say afterwards what is running. Give every build a tag you can read, such as the date or the Git commit: ```bash docker build -t my-app:$(git rev-parse --short HEAD) . ``` Make the `app` service in `compose.yaml` use that tag (add an `image:` line under `app:`, as in step 4B, or change it), then: ```bash docker compose run --rm app /app/<binary-name> db:migrate # only if there are new migrations docker compose up -d app curl -fsS https://your-domain.example/ready ``` To **roll back**, change the `image:` line back to the previous tag and run `docker compose up -d app` again. Keep the last few images on the VM (`docker image ls`) and do not run `docker image prune -a` before you are sure the new version is fine. A rollback does not undo a database migration: write migrations so the previous version still works with the new schema, and take a backup (section 8) before any change that removes or rewrites data. Caddy keeps serving while the app container is replaced: the generated `Caddyfile` holds each request for up to 15 seconds while the app is down and checks `/health` every 5 seconds, so a quick restart shows as slower responses rather than errors. A single VM still cannot replace the app without any gap; if you need zero downtime, you need more than one machine, which is outside this guide ([Kubernetes](https://rullst.github.io/Rullst/book/advanced-deployment.html#kubernetes) is one option). ## 10. Keep the VM healthy - Apply operating-system security updates regularly (`sudo apt update && sudo apt upgrade`); consider unattended upgrades. - Keep SSH key-only and, if you wish, install `fail2ban`. - Watch disk space (`df -h`) and memory (`free -h`). - Keep the VM's firewall at 22, 80 and 443. Do not publish database ports. ## What CI checks Rullst's CI runs this guide's main path (`.github/check-vm-deploy.py`) on a GitHub-hosted Ubuntu runner, for the Blank and SaaS starters, with Docker and with rootless Podman (`podman compose` with podman-compose): - `cargo rullst new`, then `cargo rullst deploy`, then the copy of step 4A and a `.env.production` with `RULLST_ENV` and `APP_KEY` (SaaS adds its billing settings), checked with `cargo rullst deploy:doctor`; - `compose up -d --build` with `DOMAIN=localhost`, so Caddy uses its own local certificate authority instead of Let's Encrypt; - `/health` over HTTPS through Caddy, the HTTP-to-HTTPS redirect, that only Caddy publishes ports, Caddy's fixed address, and in both containers a read-only root filesystem, no capabilities except Caddy's `NET_BIND_SERVICE`, and `no-new-privileges`; - `compose run --rm app /app/<binary-name> db:migrate`, a page that reads the database (SaaS: registering an account, with a Secure session cookie), and the same after a restart (`docker compose restart`; Podman recreates the containers with `down` and `up -d`), with the SQLite file still in its volume; - the SQLite backup command of section 8, and no panic in the logs. It does not cover a real domain, DNS or Let's Encrypt, a provider account or firewall, an Arm machine, a reboot, path 4B, or PostgreSQL and MySQL. ## Where next - Providers: [Hetzner Cloud](https://rullst.github.io/Rullst/book/deploy-hetzner.html), [DigitalOcean](https://rullst.github.io/Rullst/book/deploy-digitalocean.html), [Oracle Cloud](https://rullst.github.io/Rullst/book/deploy-oracle-cloud.html), [Google Cloud](https://rullst.github.io/Rullst/book/deploy-google-cloud.html). - Other ways to run the same image: [Advanced deployment](https://rullst.github.io/Rullst/book/advanced-deployment.html). - Before real users: the [production monitoring](https://rullst.github.io/Rullst/book/production-monitoring.html) and [security architecture](https://rullst.github.io/Rullst/book/security-architecture.html) pages. --- # Hetzner Cloud Source: https://rullst.github.io/Rullst/book/deploy-hetzner.html # Deploy on Hetzner Cloud Hetzner Cloud sells simple virtual machines ("Cloud Servers") in European and some other regions. This guide covers only the Hetzner-specific steps. Everything after you can log in to the VM is the same on every provider and lives in [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html): read it alongside this page. Official documentation: <https://docs.hetzner.com/cloud/>. Check the provider's current pricing, locations and limits before you choose; they change. ## 1. Create the server In the [Cloud Console](https://console.hetzner.cloud/): 1. Create a project, then **Add Server**. 2. **Location:** pick the one closest to your users. 3. **Image:** Debian or Ubuntu LTS. 4. **Type:** a small shared-CPU type with 2 GB of memory or more is comfortable for one small app. Both x86-64 and Arm (Ampere) types exist; an Arm type needs a `linux/arm64` image (see [step 4 of the shared guide](https://rullst.github.io/Rullst/book/vm-deployment.html#4-get-the-application-onto-the-vm)). 5. **SSH keys:** add your public key (`~/.ssh/id_ed25519.pub`). Do not choose a password login. 6. **Firewalls:** attach the firewall you create in the next section. 7. Backups are an optional paid add-on for the whole server; they do not replace [database backups](https://rullst.github.io/Rullst/book/vm-deployment.html#8-back-up-the-database-volume). Note the server's public IPv4 address (and IPv6, if you want it). ## 2. Firewall: 22, 80 and 443 only Under **Firewalls**, create one with these inbound rules and apply it to the server: | Protocol | Port | Source | | :--- | :--- | :--- | | TCP | 22 | your own IP address if it is stable, otherwise any | | TCP | 80 | any | | TCP | 443 | any | Hetzner firewalls are stateful, so replies are allowed automatically. Port 80 must stay open: Caddy uses it to obtain and renew certificates. Add UDP 443 as well if you want HTTP/3; without it browsers use HTTPS over TCP. ## 3. DNS Create an `A` record for your domain with the server's IPv4 address (and an `AAAA` record for IPv6). You can use Hetzner DNS or any other DNS provider. Check with `dig +short your-domain.example`. ## 4. Log in and continue ```bash ssh root@<ip-address> ``` Create a normal user with `sudo` rights and use it from now on (root login is convenient but risky): ```bash adduser deploy && usermod -aG sudo deploy mkdir -p /home/deploy/.ssh && cp ~/.ssh/authorized_keys /home/deploy/.ssh/ \ && chown -R deploy:deploy /home/deploy/.ssh ``` Then follow [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html) from [step 3](https://rullst.github.io/Rullst/book/vm-deployment.html#3-install-docker-or-podman-on-the-vm): install Docker or Podman, copy or build the app, set the [production settings](https://rullst.github.io/Rullst/book/vm-deployment.html#5-production-settings-that-matter), start it with `compose up`, and set up [backups](https://rullst.github.io/Rullst/book/vm-deployment.html#8-back-up-the-database-volume), [updates and rollback](https://rullst.github.io/Rullst/book/vm-deployment.html#9-update-and-roll-back-with-image-tags). ## Hetzner notes - **Logs** stay on the server (see [Logs](https://rullst.github.io/Rullst/book/vm-deployment.html#7-logs)); the Cloud Console does not collect them. - **Snapshots** of the server are available in the Console and are useful before a risky change. They are not a database backup. - The Cloud Console's **Rescue** system and console access help if you lock yourself out of SSH. --- # DigitalOcean Source: https://rullst.github.io/Rullst/book/deploy-digitalocean.html # Deploy on DigitalOcean DigitalOcean calls its virtual machines "Droplets". This guide covers only the DigitalOcean-specific steps. After you can log in, everything is the same on every provider and lives in [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html): read it alongside this page. Official documentation: <https://docs.digitalocean.com/products/droplets/>. Check the provider's current pricing, regions and limits; they change. ## 1. Create the Droplet In the [control panel](https://cloud.digitalocean.com/), choose **Create**, then **Droplets**: 1. **Region:** closest to your users. 2. **Image:** Debian or Ubuntu LTS. 3. **Size:** a Basic (shared CPU) Droplet with 2 GB of memory or more is comfortable for one small app. The smallest sizes can run the app but may run out of memory while building the image; see the swap note in the [shared guide](https://rullst.github.io/Rullst/book/vm-deployment.html#2-create-the-vm-and-the-dns-record), or build the image elsewhere. 4. **Authentication:** choose **SSH Key** and add your public key. 5. Optional **Backups** are a paid whole-Droplet feature; they do not replace [database backups](https://rullst.github.io/Rullst/book/vm-deployment.html#8-back-up-the-database-volume). Note the Droplet's public IPv4 address. ## 2. Firewall: 22, 80 and 443 only Under **Networking**, **Firewalls**, create a Cloud Firewall with these inbound rules, leave outbound rules at their defaults, and apply it to the Droplet: | Type | Protocol | Port | Sources | | :--- | :--- | :--- | :--- | | SSH | TCP | 22 | your IP address if stable, otherwise all | | HTTP | TCP | 80 | all IPv4 (and IPv6 if you use it) | | HTTPS | TCP | 443 | all IPv4 (and IPv6 if you use it) | Port 80 must stay open: Caddy uses it to obtain and renew certificates. Add a UDP 443 rule as well if you want HTTP/3; without it browsers use HTTPS over TCP. If you also run a host firewall such as `ufw`, allow the same three ports there. Note that Docker publishes container ports by changing the firewall rules itself, which can bypass `ufw`; the cloud firewall above is the reliable one. ## 3. DNS Add the domain under **Networking**, **Domains** (or use any other DNS host), and create an `A` record pointing to the Droplet's IP address. Check with `dig +short your-domain.example`. ## 4. Log in and continue ```bash ssh root@<ip-address> ``` Create a normal user with `sudo` rights (`adduser deploy && usermod -aG sudo deploy`, then copy `~/.ssh/authorized_keys` to its `.ssh` directory) and use it from now on. Then follow [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html) from [step 3](https://rullst.github.io/Rullst/book/vm-deployment.html#3-install-docker-or-podman-on-the-vm). ## DigitalOcean notes - **Logs** stay on the Droplet (see [Logs](https://rullst.github.io/Rullst/book/vm-deployment.html#7-logs)). - **Snapshots** are available before risky changes and are not database backups. - A **Recovery Console** in the control panel helps if SSH stops working. - Managed databases are an alternative to a database on the Droplet: use the connection URL as `DATABASE_URL` and allow only your Droplet to connect. --- # Oracle Cloud Source: https://rullst.github.io/Rullst/book/deploy-oracle-cloud.html # Deploy on Oracle Cloud Oracle Cloud Infrastructure (OCI) offers an Always Free tier that includes small virtual machines. The Always Free Ampere machines use **Arm** processors (`aarch64`), so a Rullst image for them must be built for `linux/arm64`. This guide covers the OCI-specific steps. After you can log in, everything is the same as on every provider and lives in [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html): read it alongside this page. Official documentation: <https://docs.oracle.com/en-us/iaas/Content/home.htm>. Free-tier shapes, quantities and availability change and differ by region and account: check Oracle's current Always Free documentation before you rely on them. Free Arm capacity in a popular region is sometimes unavailable; try again later or pick another availability domain. ## Arm or x86? - **Ampere A1 (Arm).** Build for `linux/arm64`. Rust and the generated `Dockerfile` work natively on Arm, so the simplest way is to **build on the VM itself** with `docker compose up -d --build` (step 4A of the shared guide). Ampere VMs are usually fast enough for this. - **Building elsewhere.** On an x86-64 computer, build for Arm with `docker buildx build --platform linux/arm64 -t my-app:<tag> --load .`. This needs QEMU emulation (Docker Desktop includes it; on Linux install `qemu-user-static`) and an optimized Rust build under emulation is slow. For anything beyond a trial, build on the VM or on an Arm CI runner. - **x86 micro shape.** Oracle's x86 free VM is very small (see its current memory); it cannot comfortably compile Rust. Build the image elsewhere with `--platform linux/amd64` and move it (step 4B of the shared guide). ## 1. Create the instance In the OCI Console, **Compute**, **Instances**, **Create instance**: 1. **Image:** Ubuntu LTS (or Oracle Linux; this guide assumes Debian-family commands). 2. **Shape:** choose **Ampere** (`VM.Standard.A1.Flex`) and set the OCPUs and memory within the Always Free limits shown in the console. 3. **Networking:** use the default VCN and a **public subnet** with a public IPv4 address. 4. **SSH keys:** paste your public key. The default login user is `ubuntu` (or `opc` on Oracle Linux). ## 2. Firewall: two places OCI blocks traffic in the cloud network **and** the image may block it on the VM. You must open both. **In the cloud network:** open the VCN's **Security Lists** (or the instance's Network Security Group) and add ingress rules (source `0.0.0.0/0`, TCP) for destination ports 22, 80 and 443 (and UDP 443 if you want HTTP/3; without it browsers use HTTPS over TCP). **On the VM:** Oracle's Ubuntu images ship with `iptables` rules that reject other ports. Allow 80 and 443 before the final reject rule and save them: ```bash sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 80 -j ACCEPT sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 443 -j ACCEPT sudo apt install -y iptables-persistent && sudo netfilter-persistent save ``` Check the rule numbers with `sudo iptables -L INPUT --line-numbers` first: the new rules must come before the `REJECT` rule. On Oracle Linux use `firewalld` (`sudo firewall-cmd --permanent --add-service={http,https}` and `--reload`). Docker adds its own rules for published ports, so the cloud rules from the first part are the ones that decide what the internet can reach. ## 3. DNS Create an `A` record pointing to the instance's public IP address. For a stable address, reserve a public IP instead of the ephemeral one, so it does not change if you recreate the instance. Check with `dig +short your-domain.example`. ## 4. Log in and continue ```bash ssh ubuntu@<ip-address> ``` Then follow [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html) from [step 3](https://rullst.github.io/Rullst/book/vm-deployment.html#3-install-docker-or-podman-on-the-vm). If you move an image from another computer, make sure it was built for `linux/arm64` (`docker image inspect my-app:<tag> --format '{{.Architecture}}'` should print `arm64`). ## Oracle Cloud notes - **Logs** stay on the instance (see [Logs](https://rullst.github.io/Rullst/book/vm-deployment.html#7-logs)). - Free accounts and idle instances can be reclaimed under Oracle's current policy; read the Always Free terms and keep your own off-VM [backups](https://rullst.github.io/Rullst/book/vm-deployment.html#8-back-up-the-database-volume). - Boot-volume backups are available in the console; they are not database backups. --- # Google Cloud Source: https://rullst.github.io/Rullst/book/deploy-google-cloud.html # Deploy on Google Cloud (Compute Engine) This guide runs your app on a Compute Engine virtual machine. Google Cloud's free tier has included a small `e2-micro` instance in selected US regions; the terms, regions and limits change, so check Google's current "Free Tier" page before you rely on it. An `e2-micro` has very little memory, so **do not compile Rust on it**: build the image on your own computer or in CI, and only run it on the VM (step 4B of the shared guide, or a registry). After you can log in, everything is the same as on every provider and lives in [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html): read it alongside this page. Official documentation: <https://cloud.google.com/compute/docs>. ## 1. Create the VM In the [Google Cloud console](https://console.cloud.google.com/) (or with the `gcloud` CLI), enable the Compute Engine API for a project, then **Compute Engine**, **VM instances**, **Create instance**: 1. **Region and zone:** for the free tier, one of the eligible regions listed in Google's current documentation; otherwise the closest to your users. 2. **Machine type:** `e2-micro` for a trial. For a real app, a larger type (for example `e2-small` or `e2-medium`) is much more comfortable: check the current pricing. 3. **Boot disk:** Debian or Ubuntu LTS. Keep the disk type and size within the free-tier limits if you want to stay free. 4. **Firewall:** tick **Allow HTTP traffic** and **Allow HTTPS traffic** (see the next section; SSH is allowed by the default rules). 5. **Networking:** under network interfaces, use a **static external IP address** (reserve one) so the DNS record does not break after a restart. Check the current price of reserved addresses. ## 2. Firewall: 22, 80 and 443 only Compute Engine firewall rules apply to the whole VPC network. The default network already allows SSH (22). The HTTP and HTTPS checkboxes create rules for instances with the network tags `http-server` and `https-server`. To control this yourself: ```bash gcloud compute firewall-rules create allow-web \ --network default --direction INGRESS --allow tcp:80,tcp:443 \ --target-tags web --source-ranges 0.0.0.0/0 gcloud compute instances add-tags <instance-name> --tags web --zone <zone> ``` Also review the default network's other rules (for example the one that allows all internal traffic or RDP) and remove what you do not use. Port 80 must stay open: Caddy uses it to obtain and renew certificates. For HTTP/3 also allow `udp:443`; without it browsers use HTTPS over TCP. ## 3. DNS Create an `A` record pointing to the reserved external IP address (in Cloud DNS or any DNS host). Check with `dig +short your-domain.example`. ## 4. Log in and continue ```bash gcloud compute ssh <instance-name> --zone <zone> ``` or use the **SSH** button in the console. Then follow [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html) from [step 3](https://rullst.github.io/Rullst/book/vm-deployment.html#3-install-docker-or-podman-on-the-vm). On a small machine, build elsewhere and transfer the image. An alternative to `docker save`/`load` is Google's Artifact Registry (<https://cloud.google.com/artifact-registry/docs>): push the tagged image there, and pull it on the VM. ## Google Cloud notes - **Logs** from your containers stay on the VM (see [Logs](https://rullst.github.io/Rullst/book/vm-deployment.html#7-logs)). The Ops Agent can ship host logs to Cloud Logging if you want that; it is optional and may cost money. - **Snapshots** of the persistent disk are available, including schedules. They are not database backups: keep a [database backup](https://rullst.github.io/Rullst/book/vm-deployment.html#8-back-up-the-database-volume) too. - Compute Engine can use a managed database (Cloud SQL) instead of a database on the VM; use its connection URL as `DATABASE_URL`. - Cloud Run and GKE are different services with their own guides; for a Kubernetes outline see [Advanced deployment](https://rullst.github.io/Rullst/book/advanced-deployment.html). --- # Advanced deployment (Nix, Buildah, Quadlet, Kubernetes) Source: https://rullst.github.io/Rullst/book/advanced-deployment.html # Advanced deployment This page is for people who already run servers and want to deploy a Rullst application with a tool the CLI does not know about: Nix, Buildah, Podman Quadlet or Kubernetes. > **Read this first.** Everything below is illustrative. `cargo rullst` does > **not** generate, run or test any of these files, and the Rullst project does > not validate these snippets against every tool version. They show where > Rullst's own requirements (the binary name, the port, the environment, the > health routes, the trusted proxy) fit. Check each tool's official > documentation, adapt the names, and test in a staging environment. > If you are new to servers, start with > [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html) instead. ## What a Rullst application needs from any platform | Need | Detail | | :--- | :--- | | A binary | `cargo build --release --locked` produces one executable named after your package. It needs a glibc Linux (or a musl build you have tested) and CA certificates. | | Environment | `RULLST_ENV=production`, `APP_KEY` (a long random secret), `DATABASE_URL`, and any provider keys, as environment variables. | | Address and port | In production the server listens on `0.0.0.0`; set `PORT` (the generated image uses 3000) and, behind a proxy on the same host, `HOST=127.0.0.1`. | | Probes | `GET /health` (process alive) and `GET /ready` (ready for traffic). Starters mount both. A redirect to login is not a health signal. | | Trusted proxy | Behind any reverse proxy or ingress, set `[security] trusted_proxies` in `Rullst.toml` (or `Server::trusted_proxies`) to the proxy's address range, and only that. See [Client addresses behind the proxy](https://rullst.github.io/Rullst/book/tutorials/26-one-click-paas-deploy.html#client-addresses-behind-the-proxy). | | Migrations | Run `<binary> db:migrate` once per release, before new replicas serve traffic, or opt in to [startup migrations](https://rullst.github.io/Rullst/book/tutorials/26-one-click-paas-deploy.html#startup-migrations). | | Writable state | A SQLite file, uploads and similar state need a persistent writable directory. The generated image runs as UID/GID 10001. | ## Nix A [Nix flake](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html) can build the release binary reproducibly. Use `rustPlatform.buildRustPackage` (see the [nixpkgs Rust documentation](https://nixos.org/manual/nixpkgs/stable/#rust)): ```nix { inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-<release>"; # pin the release you test outputs = { self, nixpkgs }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; in { packages.${system}.default = pkgs.rustPlatform.buildRustPackage { pname = "my-app"; # your [package].name version = "0.1.0"; src = ./.; cargoLock.lockFile = ./Cargo.lock; nativeBuildInputs = [ pkgs.pkg-config ]; buildInputs = [ pkgs.openssl ]; # only if a dependency needs it }; }; } ``` ```bash nix build # result/bin/my-app ``` Commit `flake.lock` so the inputs stay pinned. On NixOS you can then run the binary from a `systemd.services.my-app` unit with `Environment=` or `EnvironmentFile=` for the settings above; keep secrets out of the Nix store (use a file outside it, such as one managed by sops-nix or agenix). The static `static/` directory and `Rullst.toml` must be installed next to, or referenced by, the service. ## Buildah (rootless image build) [Buildah](https://buildah.io/) builds OCI images without a daemon and without root. It can build the `Dockerfile` that `cargo rullst deploy` (or `cargo rullst dockerize`) writes: ```bash buildah bud --layers -t my-app:$(git rev-parse --short HEAD) . buildah push my-app:<tag> docker://registry.example.com/me/my-app:<tag> ``` `--layers` keeps the dependency-cache stage between builds. Rootless builds need `/etc/subuid` and `/etc/subgid` entries for your user (see the [Buildah tutorials](https://github.com/containers/buildah/tree/main/docs/tutorials)). For a different CPU architecture add `--platform linux/arm64` (this needs QEMU emulation or a native Arm builder). Use the resulting image with Podman, Docker or Kubernetes. Image and Kubernetes names must be lowercase and use `-`, not `_`. ## Podman Quadlet (systemd unit for the container) [Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) lets systemd run a Podman container from a small `.container` file. For a rootless service put it in `~/.config/containers/systemd/my-app.container` (system-wide: `/etc/containers/systemd/`): ```ini [Unit] Description=My Rullst application After=network-online.target [Container] Image=registry.example.com/me/my-app:2026-10-29 PublishPort=127.0.0.1:3000:3000 EnvironmentFile=%h/my-app/.env.production Volume=%h/my-app/data:/app/data:Z,U HealthCmd=none [Service] Restart=always [Install] WantedBy=default.target ``` ```bash systemctl --user daemon-reload systemctl --user start my-app.service loginctl enable-linger "$USER" # keep it running when you log out ``` The generated runtime image has no shell, so container-level `HealthCmd` checks do not work; use `HealthCmd=none` and probe `/health` and `/ready` from outside (your reverse proxy or monitoring). Put a reverse proxy such as Caddy in front for HTTPS, publish the application port to `127.0.0.1` only, and set `trusted_proxies` to the address the proxy's connections arrive from inside the container (with rootless port forwarding this can be a gateway address rather than `127.0.0.1`), and nothing broader. `:U` makes Podman chown the volume to the container user (10001). Pin the image to a tag or digest and change it to update; to roll back, set the previous tag and restart the unit. ## Kubernetes You can write the manifests yourself, or start from `cargo rullst make:k8s` (see [Tutorial 25](https://rullst.github.io/Rullst/book/tutorials/25-kubernetes-deployment.html)); the generated files also need review. The outline: ```yaml apiVersion: apps/v1 kind: Deployment metadata: { name: my-app } spec: replicas: 2 selector: { matchLabels: { app: my-app } } template: metadata: { labels: { app: my-app } } spec: containers: - name: my-app image: registry.example.com/me/my-app@sha256:<digest> ports: [{ containerPort: 3000 }] env: - { name: RULLST_ENV, value: production } - name: APP_KEY valueFrom: { secretKeyRef: { name: my-app, key: app-key } } - name: DATABASE_URL valueFrom: { secretKeyRef: { name: my-app, key: database-url } } startupProbe: httpGet: { path: /health, port: 3000 } failureThreshold: 30 periodSeconds: 2 livenessProbe: httpGet: { path: /health, port: 3000 } readinessProbe: httpGet: { path: /ready, port: 3000 } --- apiVersion: v1 kind: Service metadata: { name: my-app } spec: selector: { app: my-app } ports: [{ port: 80, targetPort: 3000 }] --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: { name: my-app } spec: ingressClassName: nginx tls: [{ hosts: [app.example.com], secretName: my-app-tls }] rules: - host: app.example.com http: paths: - { path: /, pathType: Prefix, backend: { service: { name: my-app, port: { number: 80 } } } } ``` Points to check against the [Kubernetes documentation](https://kubernetes.io/docs/home/): - **Probes.** Liveness on `/health` (process only, so a database outage does not cause restart loops) and readiness on `/ready`. The default routes return 200 while the process serves; the lifecycle-aware router in [Tutorial 25](https://rullst.github.io/Rullst/book/tutorials/25-kubernetes-deployment.html#step-2-mount-lifecycle-aware-health-routes) reports startup, dependencies and drain on `/ready`. - **Trusted proxy.** The application sees the ingress controller's pod address, not the visitor's. Set `trusted_proxies` in `Rullst.toml` to the pod network range of your ingress controller (look it up in your cluster) and nothing broader. - **Migrations.** Run `db:migrate` as a Kubernetes `Job` (or an init step with a lock) before rolling out a new Deployment, not in every replica unless you enabled startup migrations. - **Secrets, storage and security.** Use Secrets (or an external secrets manager) for credentials, a database that is not a file in a replicated Pod, a non-root `securityContext`, resource requests/limits, a PodDisruptionBudget and a NetworkPolicy. See the checklist at the end of Tutorial 25. - **Rollback.** `kubectl rollout undo deployment/my-app` returns to the previous ReplicaSet; it does not undo database migrations. ## Where next - [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html) and the provider guides. - [Two-Replica Deployment Acceptance](https://rullst.github.io/Rullst/book/deployment-acceptance.html) for what the project itself tests for more than one instance. - [Local Deployment Configuration Diagnostic](https://rullst.github.io/Rullst/book/deployment-diagnostic.html). --- # Two-Replica Deployment Acceptance Source: https://rullst.github.io/Rullst/book/deployment-acceptance.html # Two-replica deployment acceptance This v13 candidate exercises two independent Rullst application processes behind a digest-pinned Caddy proxy and an owned Redis server. The local contract passed, alongside that source's 242 Core and 163 Security library tests, strict lints and five detected body-lifetime mutations. Hosted workspace and installed-archive checks subsequently passed in [PR #224](https://github.com/Rullst/Rullst/pull/224); the final release campaign remains separate. It is a deployment test and reference boundary, not a new load-balancer crate or an automatic cloud/VPS deployment feature. The files `cargo rullst deploy` generates (one app container behind Caddy) have their own CI check, `.github/check-vm-deploy.py`, described in [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html#what-ci-checks). Run on Linux with Docker and the repository Rust toolchain: ```sh python3 .github/check-deployment-proxy.py ``` The runner builds an explicitly ignored test fixture, creates temporary application directories and fresh fixture keys, and starts uniquely named containers. Every listener is loopback-only. No real account, application data, cloud credentials, SSH changes or Docker socket mounted inside a container are required. The proxy uses host networking solely to reach the owned loopback listeners; this is not container network isolation. Its root filesystem and configuration are read-only, it runs as an unprivileged UID, and its only retained capability is `NET_BIND_SERVICE`, required by the official image binary's file capability. The runner removes its own processes/containers on exit. Redis state is disposable. ## Executable boundary | Case | Observed contract | | --- | --- | | Startup | Both dependency gates start unavailable. Core denies application admission; the proxy does not return a successful application response from an unready replica. | | Availability | Once ready, both independent processes serve. Marking one dependency unavailable removes that replica after health-check propagation; making it ready restores participation. | | Shared request budget | Both processes explicitly require the distributed Redis limiter and consume the same six-request fixture budget. A seventh request fails, including direct requests with forged forwarding headers. Redis loss returns `503`, with a bounded application timeout and no local fallback. | | Forwarded metadata | Core derives its default identity from the transport socket. The proxy has no trusted inbound forwarding hops, sanitizes `X-Forwarded-For`, and removes `Forwarded`. Forged headers cannot replace the identity. | | HTTP controls | The real `Server` production baseline supplies secure headers and CSRF enforcement. A route accepts a 1,024-byte body with a valid double-submit token and rejects 1,025 bytes. Missing CSRF is rejected. | | In-flight response | A real streaming response returns its first chunk, remains counted during drain and finishes only when explicitly released. The draining replica stops receiving new application work while another replica continues serving. | | WebSocket | The Security origin policy rejects missing/foreign origins. A valid connection exchanges frames through Caddy. Application-owned drain sends a restart close frame; ordinary HTTP body accounting is not used as WebSocket lifetime accounting. | | Process termination | After its stream completes, the drained process shuts down within the fixture's deadline. Killing the remaining process causes the proxy to report no healthy upstream. | The fixture's routing and control endpoints are test code. Its management channel is the parent-owned stdin pipe, not an HTTP administration route. There are no user accounts or domain authorization claims. A socket-level quota behind a proxy conservatively combines clients from that peer; real per-user quotas must use an authenticated application identity, or an explicitly reviewed trusted-hop policy such as Core's `Server::trusted_proxies`, which this fixture does not enable. Raw forwarding headers never supply a tenant, role or user identity. ## HTTP drain correction `ApplicationLifecycle` now retains an admitted request through its handler and the ordinary response body's data/trailers, releasing it exactly once on completion, error or drop. Previously, the guard was released when the handler returned response headers, which could report zero in-flight requests while a stream remained active. The regression was observed failing before the fix. The body wrapper preserves frames, size hints and end-of-stream state without buffering. Local tests cover a pending body, multiple data/trailer frames, errors, cancellation and an empty body. This is the lifetime observed at the middleware boundary; buffering by another layer can consume that body before bytes reach the client. It is not proof of TCP acknowledgement or successful client receipt. Applications still terminate detached work and upgraded connections separately. `wait_for_drain` has an explicit bounded wait and typed timeout. `Server`'s Axum graceful-shutdown future itself does not impose a universal kill deadline; the application/supervisor owns that deadline and the choice to abort unfinished work. ## Production differences The fixture uses cleartext HTTP on loopback, fixed test budgets and aggressive health intervals. It does not validate public TLS/ACME, a CDN, physical host or region failure, Redis failover, persistence rollback, autoscaling or production traffic capacity. Health changes take time to propagate; intermediate errors can occur. No zero-downtime or exactly-once claim follows from a passing run. The profile disables automatic proxy retries. Application retries of mutations still require domain idempotency and durable transactional state. Multi-host authentication must choose suitable shared storage; placing a process-local or shared-local SQLite store behind a proxy does not make it cross-host durable. The [shared passkey candidate](https://rullst.github.io/Rullst/book/shared-passkey-ceremonies.html) addresses one such state boundary and retains its own acceptance requirements. The offline [`deploy:doctor`](https://rullst.github.io/Rullst/book/deployment-diagnostic.html) inspects only a local configuration snapshot. A generated multi-replica profile and additional deployment topologies remain follow-up work. No firewall, SSH, IAM or host security configuration is changed by this increment. The reviewed proxy behavior follows Caddy's official [reverse-proxy documentation](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy), including health checks, forwarding headers and upgraded streams. The fixture configuration and runner pin the actual tested image independently of future upstream releases. --- # Local Deployment Configuration Diagnostic Source: https://rullst.github.io/Rullst/book/deployment-diagnostic.html # Local deployment configuration diagnostic The v13 `deploy:doctor` candidate inspects a bounded local configuration snapshot without starting the application, invoking other programs, contacting a provider or changing files. Its focused CLI tests include a real generated SaaS profile. Its source merged in [PR #225](https://github.com/Rullst/Rullst/pull/225) after hosted checks. The initially skipped archive gate subsequently passed in [exact-commit validation](https://github.com/Rullst/Rullst/actions/runs/35564762822). That retrospective repair and the remaining final release campaign are recorded in the [delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html). ```bash # Explicit file snapshot, independent of this terminal's environment: cargo rullst deploy:doctor --env-file .env.production --json # Inspect a selected TOML and the three allowlisted process variables: cargo rullst deploy:doctor --config Rullst.production.toml --process-env # Staging is an explicit target, not a production-mode alias: cargo rullst deploy:doctor --env-file .env.staging --target staging ``` Run the command from the intended application's directory. Without `--config`, it reads only `./Rullst.toml`, or uses Core defaults when that file is absent. An explicitly selected file must exist. Relative file paths resolve from the current directory; absolute paths are allowed without printing their names. This is useful for database-free starters that have no TOML file. Choose exactly one environment source: - `--env-file` reads the explicitly selected literal dotenv file. The shell's environment is not merged with it, and no parent/default `.env` is searched. - `--process-env` reads only `RULLST_ENV`, `APP_ENV` and `APP_KEY`. It never reads dotenv files or provider credentials from the process environment. - With neither flag, environment/key inspection is explicitly incomplete and exits unsuccessfully. Ambient settings cannot silently make the report pass. Resolution within that snapshot uses Core's `RULLST_ENV`, legacy `APP_ENV`, then TOML `[app].env` precedence. Conflicting legacy variables receive a review note. The requested target defaults to `production`; `staging` must be selected explicitly. Invalid or blank selected values do not fall through to a lower priority value. The running application may load or override configuration differently: the report does not observe that process. ## Checks and output Core's configuration validator checks browser-policy syntax, exact CORS origins/signed-webhook paths and `[security]` trusted-proxy networks. A zero TOML port is reported. Custom CSP, explicit browser-policy exceptions, CSRF webhook exemptions and configured trusted-proxy networks are flagged for application review; a syntactically valid CSP may still be weak. Unknown Core configuration fields receive a typo/application-specific-setting review note. The key check catches missing/short keys, known example/mock prefixes, control characters and single-symbol keys. It intentionally does not duplicate Auth's complete validation or infer cryptographic randomness. Supply an explicit `APP_KEY` in the chosen environment source; legacy TOML key fallback is outside this diagnostic's supported profile. Actual Auth validation, secure random generation, secret custody and rotation remain necessary. JSON uses `rullst.deployment-diagnostic.v1`, fixed check codes, `PASS`, `FAIL`, `REVIEW` or `NOT_INSPECTED`, and fixed remediation guidance. It always includes `deployment_verified: false` and lists controls that were not inspected. `inspection_complete` concerns only the selected local profile. Exit zero means the selected profile was inspected without local failures; review notes remain visible. It is not approval to deploy. A nonzero exit accompanies incomplete selection, invalid inputs, detected errors or output failures. The report excludes input paths, values, database URLs, secrets and hashes of secret-bearing files. Parser and filesystem error details are withheld. Text output has the same observations and boundaries as JSON. Redirect JSON yourself if a local report file is needed; the command does not create one automatically. ## Input boundary Files must be regular UTF-8 files of at most 64 KiB. Links, reparse points and special files are rejected, including linked supplied parent directories. The current directory's OS aliases are resolved once, which supports macOS temporary directories. Parent directories must be trusted against concurrent replacement; this inspection is not a filesystem sandbox or atomic snapshot of all application state. The parser rejects duplicate TOML fields. The literal dotenv profile permits blank/comment lines and unique ASCII `KEY=value` assignments, optionally prefixed by `export `, with simple matching single/double quotes and whitespace-separated trailing comments. A `#` embedded in an unquoted value remains part of the value. There are at most 512 assignments, 128 bytes per key and 8192 bytes per value. Interpolation (`$`), backslash escapes, multiline/concatenated quotes, control characters and duplicate assignments are rejected. Arbitrary dotenv syntax is not silently approximated. Other literal keys are parsed only to validate the file and are omitted from the report. This command does not verify provider credentials, mounted authorization or middleware, webhook signatures, TLS, trusted proxies, distributed budgets/replay state, readiness/drain, storage durability, host permissions or backup recovery. Use the [deployment acceptance fixture](https://rullst.github.io/Rullst/book/deployment-acceptance.html) and tests of the actual application for those boundaries. Neither a local report nor that fixture certifies the security of a cloud account or VPS. --- # Privacy & Proportional Age Assurance Source: https://rullst.github.io/Rullst/book/privacy-age-assurance-roadmap.html # Privacy and age assurance roadmap **Status: v13 P0, initial age-assurance foundation (first published in the 13.0.0-alpha.1 prerelease on 11 October 2026), reviewed 17 September 2026; persistence, consumer and packaging engineering updated 20 September 2026.** The [crate](https://github.com/Rullst/Rullst/blob/main/rullst-privacy/README.md) implements risk policies, bound challenges, signed threshold attestations, explicit outcomes and asynchronous replay-store contracts. The optional shared-local SQLite adapter adds atomic persisted consumption, quota and clock rollback checks. The optional PostgreSQL candidate adds shared claims across application hosts with explicit initialization and real-database concurrency, cancellation, expiry, configuration, restricted-role and server-restart tests; the optional `consent-postgres` adapter shares consent state the same way. Final release acceptance, broader privacy workflows and concrete live age providers remain open. Native declarations, independent optional consent and authenticated SaaS preference/profile-export journeys now have focused test evidence; v13 removed the generated LMS journeys with the complete Academy scaffold. These do not implement every roadmap responsibility. This engineering update does not revalidate the legal sources. Rullst should generate applications with privacy-preserving defaults and reusable, testable controls. It must not advertise automatic worldwide legal compliance. The applicable rules depend on the operator, audience, processing purpose, territory and service; organizational duties cannot be inferred from a Cargo feature. This is an engineering plan requiring jurisdiction-specific legal review before a profile is presented as supported. ## Existing foundations and the missing boundary The ORM already offers field encryption. Auth/Security provide identity, authorization and redaction primitives. The v12 LMS privacy templates record school-scoped policies, nominal `Adult`/`Minor` bands, guardian-consent records and bounded export/delete request processing. They do not independently verify a person's age or a guardian's authority, and their privacy worker requires an application adapter to perform actual export/deletion. v13 removed these templates with the complete Academy scaffold; the LMS example (the former v13 LMS starter) has no privacy lifecycle. Applications generated with those templates keep their code; reuse needs an explicit migration. Adding a consent row, encrypting a field or completing a mock job does not establish lawful processing or prove that downstream copies were erased. ## Package decision Use the optional **`rullst-privacy`** crate with its **`age-assurance`** feature and **`age_assurance`** module. Age assurance belongs with purpose, minimization, retention and child protection policy; Auth consumes its evidence when deciding access. A separate age-only crate is unnecessary for the first bounded contract. | Component | Proposed responsibility | | :--- | :--- | | `rullst-privacy` | Versioned purposes/policies, lawful-basis metadata, consent and withdrawal, rights-request orchestration, retention/hold contracts, guardian evidence and age-assurance policy/receipts. Static-dispatch adapters and typed errors. | | `rullst-auth` | Bind evidence to the authenticated session/subject and enforce the requested age-restricted action server-side. An age receipt never replaces authentication, tenant scope or ordinary authorization. | | `rullst-security` | Reuse encryption, abuse controls, signature primitives and minimized audit. Do not duplicate cryptography or bury the privacy domain inside WAF middleware. | | `rullst-orm` and Core | Optional persistence and bounded jobs. Application adapters enumerate actual stores; no reflection-based discovery or automatic traversal of arbitrary data. | | `rullst-connect` | Consume a reviewed issuer's identity/age claims only when their semantics are established; an OAuth login alone proves no age. | | `cargo-rullst`, Nexus and Studio | Generate explicit application configuration, accessible preferences/rights flows and authorized, minimized operational views. Never display selfies or identity documents as telemetry. | | `rullst-ai` | Optional explanations with approved, minimized context. An LLM's visual guess must not authorize age-restricted access or establish guardianship. | The base crate must not require a database, camera, remote provider, inference runtime or new default Core dependency. Provider/persistence integrations stay feature-gated. The package candidate is included in the v13 release-order manifest and has explicit optional umbrella features, with no Core dependency. The package was first published in the 13.0.0-alpha.1 prerelease on 11 October 2026. Live providers remain outside this independent foundation's advertised scope. A future local vision engine may need its own package/model lifecycle; that is separate from this first policy and adapter contract. ## Global policy coverage Profiles are versioned implementation targets, not legal certificates. Each needs authoritative references, applicability assumptions, effective dates, last-review date, reviewer, tested controls and explicit gaps. Do not select law solely from language, IP geolocation or a country dropdown. More than one regime may apply; unresolved requirements need an operator decision, not an automatic “strictest country wins” rule. | Wave | Jurisdiction and review scope | Initial source | | :--- | :--- | :--- | | First | Brazil: LGPD and child-protection/ECA Digital requirements | [LGPD](https://planalto.gov.br/ccivil_03/_ato2015-2018/2018/lei/l13709.htm), [ANPD age-assurance guidance programme](https://www.gov.br/anpd/pt-br/assuntos/noticias/anpd-tomada-de-subsidios-afericao-idade) | | First | EU/EEA: GDPR, country-specific child-consent ages, applicable ePrivacy and DSA duties | [GDPR](https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng), [EDPB age assurance](https://www.edpb.europa.eu/documents/statement/statement-12025-on-age-assurance_en) | | First | UK: UK GDPR/DPA, PECR, Children's Code and applicable Online Safety Act duties | [ICO Children's Code](https://ico.org.uk/for-organisations/uk-gdpr-guidance-and-resources/childrens-information/childrens-code-guidance-and-resources/), [Ofcom age assurance](https://www.ofcom.org.uk/online-safety/protecting-children/age-assurance) | | First | US: CCPA as amended by CPRA where applicable, COPPA, plus a maintained state/biometric-law applicability inventory | [California DOJ](https://www.oag.ca.gov/privacy/ccpa), [FTC COPPA amendments](https://www.ftc.gov/legal-library/browse/federal-register-notices/16-cfr-part-312-coppa-final-rule-amendments) | | Expansion | Canada: PIPEDA and provincial regimes, including Quebec; Australia: Privacy Act/APPs and applicable child-safety rules | [Canadian OPC](https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/), [Australian OAIC](https://www.oaic.gov.au/privacy/australian-privacy-principles) | | Expansion | Japan: APPI; India: DPDP Act/rules with phased commencement tracked explicitly | [Japanese PPC](https://www.ppc.go.jp/en/legal/), [India's rules and commencement documents](https://www.meity.gov.in/documents/act-and-policies/digital-personal-data-protection-rules-2025-gDOxUjMtQWa?pageTitle=Digital-Personal-Data-Protection-Rules-2025) | Additional named review targets are Switzerland's FADP, South Africa's POPIA, Singapore's PDPA, South Korea's PIPA and China's PIPL, followed by the countries actually served in Latin America and the Middle East. Their detailed mappings are not reviewed or supported by this document. A “global” label must never hide missing jurisdictions, sector rules, localization or cross-border duties. Rules also evolve within a release cycle. The ANPD opened consultation on age assurance guidance in May 2026; a draft must not be treated as a final technical mandate. The FTC's February 2026 age-verification enforcement policy has conditions and is not a general exemption from COPPA. Profiles need legal-update review separate from framework version updates. [ANPD consultation](https://www.gov.br/anpd/pt-br/assuntos/noticias/anpd-tomada-de-subsidios-afericao-idade), [FTC policy statement](https://www.ftc.gov/news-events/news/press-releases/2026/02/ftc-issues-coppa-policy-statement-incentivize-use-age-verification-technologies-protect-children). ## Controls that can be automated 1. **Purpose-bound processing:** explicit data-category and purpose inventory, legal-basis metadata, processor destinations and retention rules. Consent is one possible basis, not a substitute for every basis or a blanket terms checkbox. 2. **Conservative defaults:** unnecessary tracking, advertising, remote embeds and personal-data collection disabled. Essential session/CSRF controls stay distinct from optional analytics. Only activate integrations allowed by the reviewed policy and current preferences. 3. **Consent and choices:** purpose-specific, versioned receipts; equally usable refusal/withdrawal; server-side enforcement; age-appropriate notices. Treat GPC as the relevant sale/sharing opt-out signal under an applicable profile, not as a universal consent value. California describes this obligation for covered businesses in its [CCPA guidance](https://www.oag.ca.gov/privacy/ccpa). 4. **Rights workflows:** authenticated access/export/correction/delete requests, applicable objection/restriction/opt-out paths, representative authority, bounded deadlines, retries and delivery evidence. Verify identity proportionately and prevent cross-user/tenant exports. Minimize any retained verification data. 5. **Retention and erasure:** explicit adapters for SQL, files, caches, search and vector indexes, queues, mail, AI history and processors. Coordinate racing jobs so deleted data is not recreated. Legal holds have bounded purpose, restricted access and review; soft deletion is not erasure. Track backup expiry and reapply erasure records on restore. Pseudonymization is not a claim of irreversible anonymization. 6. **Evidence and operations:** minimized audit, data-flow/processor inventory, DPIA/RIPD inputs, incident workflow and policy-version reports. Export a control assessment with “unconfigured/unsupported” states, never a universal “legally compliant” badge. The operator still owns applicable-law decisions, lawful purpose, notices, contracts, transfer mechanisms, DPO/representative duties where required, responses to people/regulators and incident handling. The framework can record and enforce configured decisions; it cannot make those decisions correct. ## Age assurance contract Keep three policies separate: eligibility for a particular service/action, capacity to consent to particular processing, and verified guardian authority. There is no universal 18-year threshold for every application or consent flow. GDPR Article 8 concerns a particular consent-based information-society-service context and allows national thresholds within its bounds; it is not a universal account-registration age. [GDPR](https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng). An Academy may support minors with appropriate protections. Its risk assessment must determine when stronger evidence is needed; a camera check must not become mandatory merely to read a lesson. Guardian consent cannot override a legally prohibited activity. A recorded guardian relationship needs its own evidence, scope, expiry and withdrawal contract. | Method | Meaning and planned treatment | | :--- | :--- | | Self-declared age/band | Low-assurance declaration, clearly labeled. Never silently promote it to verified evidence or use it where the policy requires a stronger method. | | Facial age estimation | Probabilistic estimate from a specialized provider or evaluated local model; not exact age or identity. Optional adapter, subject to the gates below. | | Verified age attribute | A reviewed digital credential, provider/document check or other appropriate method returning the minimum threshold/band needed. Authenticity and subject binding must be verified. | Facial estimation is not inherently useless: Ofcom lists it among methods capable of being highly effective, subject to technical accuracy, robustness, reliability and fairness. A photo upload alone does not establish these properties. NIST's evaluation reports demographic and image-quality effects. [Ofcom guidance](https://www.ofcom.org.uk/online-safety/illegal-and-harmful-content/online-pornography), [NIST evaluation](https://pages.nist.gov/frvt/html/frvt_age_estimation.html). The planned contract must: - distinguish passed, below-threshold, inconclusive, unavailable, expired and offline-mock results; inconclusive/unavailable cannot grant restricted access; - offer an accessible alternative and appeal/review flow, without treating a provider outage or lack of camera as permission to bypass the required check; - use a validated challenge-age margin near the threshold and an alternative method for uncertain cases; a raw model confidence score is not a calibrated probability or independently verified accuracy; - test presentation/replay attacks, borrowed photos, synthetic media and capture injection; liveness is one control and needs evidence of its own; - bind issuer, audience, session/subject, tenant, policy version, threshold, method, expiry and a one-use challenge to authenticated provider evidence; verify signatures/callbacks, prevent cross-context replay and define key rotation/revocation. Browser-authored booleans or scores are never trusted; - retain only the policy result and necessary bounded provenance. Prefer a threshold proof over exact birth date, document number or a reusable face identifier; use pairwise references to limit cross-service correlation; - avoid retaining raw selfies/documents or their reusable hashes in application storage, logs, telemetry, queues or backups. Prefer direct provider capture under an explicit data-processing contract or suitable on-device processing; any transient application handling needs strict size/time/access limits; - require provider purpose limits, deletion evidence and no secondary training, advertising or identity profiling. Not storing a photo locally does not mean no personal data was processed; - retain deterministic empty/`mock_*` provider behavior for development, marked with mock provenance that production authorization must reject. Under GDPR, a photograph is not automatically Article 9 biometric processing; the processing purpose, including unique identification, matters. Other laws may classify facial data differently. Both the image and derived result need a reviewed processing basis and safeguards; “age estimation only” is not a blanket exemption. [GDPR, recital 51 and Article 9](https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng). ## Starters, examples and Academy All generated application shapes should share the reviewed privacy defaults. Generate an operator-completed data inventory and configuration, notices that identify missing information, and accessible preference/rights routes where the selected starter processes personal data. Do not insert camera flows, cookie banners or a heavyweight provider into a database-free starter that has no corresponding need. SaaS must distinguish billing records retained for a reviewed obligation from marketing data that can be removed. LMS/Academy must protect learner progress, guardian relationships, tutor history, submissions and any optional exam supervision separately. Age assurance does not authorize surveillance or prove exam misconduct. API-only projects need equivalent server-side enforcement. `Rullst/examples` and the private Academy remain separately released consumers. Use synthetic test subjects and publish only sanitized framework evidence. Record application/framework versions and provider environments. A generated mock flow is not deployment evidence for either production or staging. ## Prioritized delivery and acceptance | Phase | Delivery | Required evidence | | :--- | :--- | :--- | | **P0.0** | Threat model, jurisdiction/profile format, data flow, package ADR and LMS migration design | Reviewed applicability assumptions, unknown/conflict behavior, model/provider lifecycle and first Academy/SaaS journeys. | | **P0.1** | Bounded privacy policy, consent/withdrawal, rights/retention contracts and explicit age/guardian evidence types | Deterministic tests for withdrawal, purpose changes, expired evidence, replay, tenant isolation and production mock rejection. | | **P0.2 — approved 13.0.0 foundation** | First real privacy workflow and provider-neutral age gate, including an authenticated first-party declaration where the selected policy permits it | Durable state, session/tenant/action binding, effective rights-adapter execution, outage/cancellation handling and rejection of mocks or declarations used for stronger-policy actions. | | **Optional provider expansion** | One reviewed external age adapter with an appropriate alternative | Native protocol and authorized sandbox evidence, capture/session binding, evaluated method strength, data lifecycle and appeal flow. The independent foundation cannot claim this integration before these gates pass. | | **P0.3** | SaaS starter and LMS example adoption and versioned examples/Academy migration | Browser/API negatives, no optional tracking before permission, real export/delete execution, backup-restore behavior and no image/identity leakage. | | **After foundation** | More regional profiles, credential issuers and an optional local facial estimator | Per-profile review; model provenance/licensing, integrity-pinned weights, supported devices, threshold error rates, demographic fairness and attack evaluations. No bundled unvalidated “simple AI”. | The release owner approved the independent foundation and optional-provider approach on 20 September 2026 UTC. Facial estimation remains an optional, evaluated expansion in the v13 roadmap; a local facial engine is follow-up work outside the committed 13.0.0 scope. Any facial release claims still require false-accept/false-reject results near each chosen threshold, relevant audience and device coverage, data lifecycle evidence, accessibility and independent review appropriate to the risk. One provider's evaluation does not validate every adapter or jurisdiction. The first privacy/age journey takes precedence over additional learning-game, monitoring and speculative integration features. Existing v12.1 maintenance and verification-efficiency work remain separately deliverable. See the [SaaS maintenance triage](https://rullst.github.io/Rullst/book/saas-v12-1-v13-triage.html) for the payment work already required before enabling affected live operations. The planned [v13 Verus pilot](https://rullst.github.io/Rullst/book/verus-roadmap.html) starts with this crate's method, threshold and evidence-decision contracts. Its proofs must follow the actual production functions and state their clock, issuer and replay-store assumptions. They supplement the acceptance evidence above; facial accuracy, guardianship, live provider behavior and legal applicability require their own review. --- # Transparent Exam & Parental Supervision Source: https://rullst.github.io/Rullst/book/supervision.html # Transparent exam and parental supervision **Status: v13 candidate, first published in the 13.0.0-alpha.1 prerelease.** The implemented contract and observation extension passed combined source/archive acceptance in PR #239. The package is included in the release inventory, pending final release acceptance. It supplies no verified guardianship, device-wide control or legal-compliance certification. Its [maturity tier](https://rullst.github.io/Rullst/book/maturity.html) is **Experimental**. ## Responsible use `rullst-supervision` is designed for **consented, transparent** supervision: online exams and assessments, and parental controls over a learning application. Read this section before you integrate it. - **The person supervised knows and agrees.** Collection starts only after the learner acknowledges the exact policy and notice versions and the selected collection categories. The session state stays visible, and the learner can pause or end collection. Re-enabling a removed category needs a new session and a new acknowledgement. - **Only typed observations.** The store accepts the selected categories as typed events: page visibility, window focus, clipboard occurrences (never contents), fullscreen changes, capture status and optional camera-presence or audio-activity findings. Camera and audio findings come only from adapters your application supplies, for sessions whose acknowledged collection includes them. The crate captures no media itself and accepts no arbitrary payloads. - **No covert mode.** There is no hidden or silent capture mode, and the project will not add one. A request for covert collection is out of scope. - **Not for general surveillance.** The crate is not designed for security cameras, CCTV, workplace monitoring or watching people who have not been told. Observations are incomplete and forgeable; they never prove misconduct, identity or attendance, and they must not trigger automatic penalties. - **You are responsible for the law.** Operators must comply with the law where they and the people supervised are, including data-protection and children's-privacy rules. Take particular care with minors: confirm who may consent for them and verify guardian relationships independently. The crate provides no legal-compliance certification. ## Package and integration boundary The separate crate has optional `exam`, `parental`, `sqlite` and `analysis` features. The SQLite adapter selects both domain modules; neither the crate nor storage becomes a Core/default dependency. Its first backend uses a concrete adapter generic over a trusted server clock, with real SQLite and deterministic clocks in tests. Applications integrate it directly. v13 removed the generated full-LMS consumer (`make:supervision`) together with the complete Academy scaffold it targeted. The host authenticates the actor and checks current school membership, course access and administrative authority. Store context constructors validate opaque identifiers; they do not authenticate a browser or prove a family relationship. Host membership checks and supervision state are separate transaction boundaries. They must run on every request; do not reuse a browser's tenant/role claim. ## Scoped authority A trusted operator can install an expiring grant for one tenant, learner, resource, delegate and action after independently verifying the relationship. Provisioning includes a bounded opaque evidence reference; no identity documents or raw verification evidence are stored. `ExamReview` and `ParentalManage` are distinct. A parental manager cannot read exam observations through that grant. Generic teacher roles, age results, matching email names and checkboxes confer no such authority. No public self-provisioning route is planned. Every delegate operation checks the current grant in its SQLite transaction. Completed grant revocation blocks subsequent operations, including reads. It cannot recall an already committed response. Revoking a parental manager does not remove the learner's content restrictions; only the operator can remove management explicitly. A persistent global revision counter prevents stale forms from matching a recreated grant or policy after bounded cleanup. ## Visible exam sessions The learner explicitly starts collection for a server-selected resource and acknowledges exact policy/notice versions and the selected collection categories. Session IDs are random. The learner can pause, resume with fresh acknowledgement or end collection. End is terminal; every transition uses the displayed revision, and lifetime is at most eight hours. Starting also compares the last retained session revision atomically; a form from before a pause/end cannot silently start a new session. Application forms should expire well before the minimum session retention, as the removed generated forms did. Session access is tenant/subject bound; existing course/assessment access must still be checked by the application. Visibility remains the default. Explicit selection also supports browser focus, clipboard occurrence and fullscreen events, capture status and optional adapter findings. Each observation binds a session, its current revision and the exact next client sequence. Unknown, repeated, out-of-order, paused, ended, expired and over-budget submissions are rejected. Receipt time comes from the server; event rate and stored counts are bounded. Events do not extend lifetime. They are incomplete, forgeable reports and never establish cheating, attendance, attention, identity or time spent learning. No event changes grades, answers, enrollment or penalties. The crate captures no camera, microphone or screen media, and collects no key logging, clipboard content, browsing history, location, fingerprint or arbitrary payload; camera and audio findings come only from the host's adapters. The consumer must show active/paused/ended state and stop sending after pause/end. No-JavaScript controls must still work without claiming visibility observations. ## Observation extension candidate The September 20 extension keeps the implementation in `rullst-supervision`. There is no separate `rullst-proctoring` crate. It adds exact collection selection, typed browser/capture observations and bounded camera-presence/audio-activity adapter orchestration. The [integration guide](https://rullst.github.io/Rullst/book/supervision-observations.html) covers the APIs, source attribution, cancellation, schema transition and test boundaries. A browser consumer observes only selected events on its own page. It cannot enumerate every open window/application or recognize outside answers. The host owns media capture permission, a local or remote model, accessibility alternatives and human review; the framework includes no detector or automatic penalty. ## Parental application restrictions An operator enrolls a learner and a server-selected access-policy resource into parental management. Enrolled learners are denied while no valid policy exists. An explicitly authorized manager can set a bounded course allowlist and one absolute UTC access window, using the current revision. Missing/expired policy, clock failure and storage failure deny protected operations. Unmanaged learners retain the existing learning authorization. These restrictions cannot grant enrollment, unpublished lessons or assessment permissions. The host integration must enforce policy on the original lesson-play and progress paths. A new dashboard alone is insufficient. Existing independently authorized cross-subject administrator corrections remain administrative work, not learner access. They retain role, actor/subject membership and original course checks; they confer no reviewer or parental-manager authority. The learner can inspect the applicable window/policy. Already delivered content cannot be recalled. There is no operating-system control, recurring timezone schedule or daily screen-time accounting in this increment. ## SQLite, clocks and retention Use one private local database with explicit exclusive initialization, verified schema/configuration, WAL/full synchronization, bounded pool/lock waits and parameterized SQL. `open` never creates or repairs missing state. The deployment epoch must match the independently retained configuration. A trusted-directory assumption is required; the adapter is not a sandbox for hostile filesystem administrators or network filesystems. `BEGIN IMMEDIATE` serializes authority reads, state changes and clock/revision advancement. Check time after waits and before returning an authorized result. Clock rollback, malformed records, capacity exhaustion and uncertain commits fail closed. A failed or cancelled mutation can have an uncertain outcome; reload state before retrying, and never infer success from a timeout. Event retention is explicit, from one hour to seven days. Reads exclude expired events immediately; bounded operator cleanup removes eligible rows. Session metadata has bounded retention after its maximum lifetime. Grants and managed learners have separate hard capacities; active management is never evicted. Capacities are store-wide. An ended session keeps its slot until its retention (start, policy lifetime and event retention) ends, so one learner (tenant and subject) may retain at most 64 sessions, or the store-wide limit when lower; `Limits::subject_sessions` selects another bound that every opener must share. Events stay until their own retention ends, even after the session ends, so one learner may also hold at most 4096 unexpired events across all sessions, or the store-wide event limit when lower; a further observation returns `Capacity`. `Limits::subject_events` selects another event bound that every opener must share. When a store-wide session or event limit is reached, admission first removes a bounded batch of rows already past retention in any tenant, so logically deleted state never blocks new work. Hosts should still rate-limit starts. SQL deletion does not erase copies in WAL, free pages or backups. Operators own file permissions, encryption, keys, backups and retention beyond these records. Restoring a stale database with its old epoch can restore revoked authority: quiesce the feature and re-establish authority/policies with a new epoch before resuming. No automatic rollback or failover claim is made. ## Acceptance before package admission - Real database reopen and new-process operation, two independent pools, exact revision races, expiry during lock waits, cancellation, corruption, capacity, clock rollback and bounded retention/purge. - Cross-school/learner/delegate negatives; expired/revoked grants; no role-only authority, no parental-to-exam privilege escalation and no stale-form replay. - Real generated LMS authentication, CSRF, operator provisioning, session transitions and original learning-route enforcement; existing access denial remains effective for unmanaged and managed learners. - Actual browser exercise with visible controls, keyboard/no-JS behavior, minimal event payloads and no capture or background reports after stop. - Full workspace, strict Clippy, formatting, feature/panic boundaries and an installed-archive consumer before registry inclusion. Phase completion means the named behavior and tests exist; no mock-only package admission. The generated LMS consumer supplied the application and browser evidence above before v13 removed it. Applications that integrate the crate directly need their own equivalent acceptance. ## Application integration v13 no longer generates a supervision consumer: `make:supervision` targeted the complete Academy LMS, which v13 removed (the smaller LMS is now the `examples/lms` application, without supervision). Integrate the crate directly. The host application authenticates the actor, checks membership and authority, provisions operators out of band and wires the SSR controls and retention workers to the contracts above. Existing stores are never upgraded or enabled by default. The removed generator remains in the git history as a reference implementation. --- # Exam observation integration Source: https://rullst.github.io/Rullst/book/supervision-observations.html # Building an exam observation platform **v13 candidate, first published in the 13.0.0-alpha.1 prerelease.** `rullst-supervision` supplies scoped sessions, selected collection categories, bounded observation storage and optional adapter orchestration. It does not supply an entire assessment product or camera/audio model. A developer supplies the exam UI, authenticated membership and enrollment, media capture, model, reviewer workflow and accessibility alternatives. ## Choose and acknowledge exact categories `exam::Collection` selects from `Capability`: visibility, window focus, clipboard occurrence, fullscreen, camera/microphone/screen-share status, camera presence and audio activity. `Collection::visibility_only()` is the default. Use `ExamPolicy::with_collection` and `Acknowledgement::for_collection` with the same set and policy/notice versions. Show that set before starting; an older or broader acknowledgement cannot start/resume a differently configured session. The stored `Session` exposes both the initial and current collection. A learner can pause/end, or the host can call `restrict_collection` with the learner's current authenticated context and revision to remove categories. This changes the revision and fences pending results. Re-enabling a removed category requires a new session with a new acknowledgement. Existing retained observations remain available to the learner and separately authorized reviewers until expiry. Browser permission and the legal basis selected by the application are separate from this acknowledgement; selecting a capability proves neither. ## Browser observations v13 no longer generates a consumer (`make:supervision` targeted the removed complete Academy LMS). An application that integrates the crate selects the observation categories explicitly; visibility alone is the conservative default. The notice and start/resume acknowledgement must show and bind the selected categories. The crate includes no camera/microphone permission prompt, analysis endpoint or background media upload, and ordinary session controls should work without JavaScript. | Observation | Meaning and limit | | --- | --- | | Page visible/hidden | This page's visibility changed; no inventory of other tabs. | | Window focused/blurred | This window gained/lost focus; does not identify another window. | | Copy/cut/paste attempt | An occurrence on this page; never clipboard contents or selected text. | | Fullscreen entered/exited | Fullscreen changed; no forced fullscreen or integrity guarantee. | | Capture started/stopped/denied/unavailable | Host-supplied client status for camera, microphone or screen share; no independent verification of capture. | The browser collector is application code; the crate ships none. The removed generator's collector ignored synthetic DOM events, but a user can modify browser code or forge network reports: this is not authenticity or tamper protection. It serialized one request at a time, waiting at least 1.1 seconds between requests, with sixteen waiting occurrences at most. Queue overflow, network errors, non-204 responses, page exit, form/session expiry or a session-control submission stopped collection and cleared the queue. It did not automatically retry ambiguous writes. A pause/end submission stopped that page before the server confirmed the transition; other tabs are fenced by the committed server revision. Receipt time is server reception time, not necessarily occurrence time. Custom applications use `ObservationRequest::new` and `record_browser` or `record_capture`. The request binds authenticated context, tenant/learner/resource, session ID, current revision and the exact next sequence. Never accept `Context` or its tenant/actor directly from an untrusted payload. The host refreshes access before each operation. The store separately checks state, capability, revision, sequence, rate, capacity and expiry transactionally. A page must not be able to post an adapter finding through a browser-event route. ## Optional camera/audio adapters Enable `analysis` plus `sqlite` to call `SqliteSupervision::analyze`. The `analysis` feature alone exposes database-free, statically dispatched contracts: - `Analyzer`: a bounded implementation returning a typed `Finding` and a fixed `AnalyzerDescriptor` with opaque ID, version, kind and simulation attribution. - `AnalysisAuthorization`: application authorization refreshed before analysis and before recording, including current membership, entitlement and permission. - `MediaSample`: borrowed JPEG/PNG bytes up to 1 MiB, or up to five seconds of mono 16 kHz signed 16-bit little-endian PCM (160,000 bytes). Signature/length checks do not decode, establish media validity or bind it to a learner. - `AnalysisOptions`: a 1–15 second orchestration timeout. Simulated adapters are rejected unless `allow_simulated_for_testing()` is explicitly selected. A local model is allowed; no paid provider is required. Implementations must bound image dimensions/decoding, CPU, memory, network, retries and output, and honor cancellation. A valid small compressed image can still require excessive decoding resources. Run blocking or untrusted inference in an isolated worker with enforceable limits; a Tokio timeout does not stop synchronous CPU work or an external process. Do not log samples or credentials or include them in errors. The framework stores only typed findings and source metadata, with no raw media. The caller owns capture and transient sample lifetime and must stop/release its media tracks when permission is revoked, paused or ended. Camera results are person detected, no person detected or inconclusive. Audio results are speech detected, no speech detected or inconclusive. A detector failure returns `AnalysisUnavailable`; it must not become a negative observation. No result identifies a person, speaker, emotion, intent, supplied answer or misconduct. Missing reports never prove absence of an event. Source metadata attributes the host's selected adapter; it is not a cryptographic attestation. The SQLite orchestration checks authorization, then reserves one pending lease per session across all processes before invoking the adapter. It releases the SQLite transaction while analysis runs. Recording checks authorization again and rechecks current session/revision/capability/sequence and the exact unexpired lease. Pausing, ending, narrowing collection or expiring the lease fences the result. Browser and analysis submissions share one sequence; the application must coordinate producers, reload on conflicts and avoid replaying stale work. A host membership check and the supervision database are separate transaction boundaries, so the host owns atomic integration if its threat model requires it. Cancellation/failure can leave a lease until its deadline (at most fifteen seconds or session expiry). A timeout returns `UncertainCommit`: reload session and observations before retrying, because a write might have committed. There is no silent permission renewal, automatic scoring or eviction of active state. The timeout requires a Tokio runtime with its timer driver enabled. An executable offline example implements the traits with an explicitly simulated inconclusive result and a disposable local store: ```bash cargo run -p rullst-supervision --example observation_adapter --features sqlite,analysis ``` It uses no camera, microphone, network or third-party account. Replace its fixed fixture authorization with real current application authorization. A simulated pass does not establish model accuracy or provider interoperability. ## Storage transition and review This extension uses SQLite schema v2. Opening v1 refuses it without migrating its schema or data. Preserve old records under the existing retention policy; quiesce the old preview and prepare a separately named private v2 store, with a new independently retained deployment epoch and independently re-established authority/restrictions. A fresh store has no parental enrollment, so provision required restrictions before allowing protected learning. Do not delete old state or change only its epoch to bypass refusal. There is no live upgrade tool yet. `observations` returns at most 100 retained entries per page to the learner or a currently authorized exam reviewer. `events` preserves the legacy visibility-only view. Parental-management authority never grants observation review. The existing rate, capacity, grant, retention, private-file, clock and rollback boundaries in [the supervision contract](https://rullst.github.io/Rullst/book/supervision.html) apply. Deleting records does not erase WAL, free pages or backups. ## Acceptance evidence Tests exercise real SQLite persistence, exact acknowledgement, disabled categories, collection narrowing and stale revisions, separate source paths, all supported browser/capture kinds, legacy visibility, v1 rejection, simulated adapters, wrong-kind/failure handling, authorization revocation during analysis, shared leases, timeout and no retained sample contents. Before v13 removed the generated LMS consumer, deterministic JavaScript tests exercised its collector's queue, sequence, selected events and stop/failure behavior, and a generated LMS/Chromium journey validated HTTP/security/forms and browser behavior. Before that removal, local validation passed the five generator/process/composition contracts with Chromium enabled, the generated LMS's fourteen original library tests and production Clippy, 378 CLI unit tests, all-target strict Clippy for the CLI and crate, seven isolated feature configurations, the database-free analysis contracts and the executable offline example. The final focused sample caught all nine selected authorization/restriction mutations, including a regression for current-revision attempts against paused and ended sessions. This is a bounded sample, not a whole-crate mutation score. The extension passed hosted workspace checks in [PR #226](https://github.com/Rullst/Rullst/pull/226). Its initially skipped archive gate subsequently passed in [exact-commit validation](https://github.com/Rullst/Rullst/actions/runs/35564765646). The [delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html) records that retrospective repair. The final release campaign remains required; no live provider/model test is claimed. Browser boundaries follow [Page Visibility](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API), [focus events](https://developer.mozilla.org/en-US/docs/Web/API/Window/blur_event), [copy events](https://developer.mozilla.org/en-US/docs/Web/API/Element/copy_event), [camera/microphone permission](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) and [screen capture](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia). --- # Private Course Video Integration Candidate Source: https://rullst.github.io/Rullst/book/managed-video.html # Private course videos with Bunny Stream `rullst-media` is an optional v13 candidate, first published in the 13.0.0-alpha.1 prerelease. It implements a bounded private-video lifecycle: authorized creation and resumable upload, processing refresh, explicit publication, current-entitlement playback, withdrawal, metadata updates, deletion and durable reconciliation. Its standalone package is in the v13 release inventory, pending final release acceptance; it is not part of the framework facade. Existing starters are not changed. The [crate integration guide](https://github.com/Rullst/Rullst/blob/main/rullst-media/README.md) describes installation (`rullst-media = { version = "=13.0.0-alpha.4", features = ["bunny", "sqlite"] }`), separate provider keys, required embed/CDN/direct-file protection, HTTP security composition, browser uploads, captions/transcripts, typed failure handling, SQLite retention and backup responsibilities. Run `cargo run -p rullst-media --example private_course_video --all-features` to exercise the executable offline journey. Empty or `mock_*` credentials never become live playback authority; production store configuration rejects them. Applications supply authenticated tenant/course membership and current entitlement. Automated acceptance uses actual local HTTP, SQLite and Chromium with controlled media. It checks signatures, upload bytes and failure recovery, CSRF and tenant denial, media decoding, captions/keyboard interaction, withdrawal and deletion. Real Bunny account, transcoding, CDN and iframe interoperability remain unvalidated under the owner's no-live-provider-testing instruction. Signed URLs do not imply DRM, immediate revocation of existing capabilities or prevention of all copying. See the [managed-video acceptance roadmap](https://rullst.github.io/Rullst/book/managed-video-roadmap.html) and the [specification](https://rullst.github.io/Rullst/book/spec.html#v13-managed-video-implementation-boundary). Release acceptance remains separate from implementation and local test success. ## S3-compatible object storage (S3, R2, MinIO) **Status: Experimental; validated against the offline mock only; provider interoperability not yet validated.** The opt-in `s3` feature adds `rullst_media::s3::S3Storage`, a second `VideoProvider` beside Bunny Stream for AWS S3, Cloudflare R2 and MinIO (custom endpoint). It stores and serves the uploaded original. There is **no transcoding**, HLS or renditions: browsers play the file that was uploaded, so upload a format they can decode (for example H.264/AAC MP4). | Step | Mechanism | | --- | --- | | Create | The adapter derives the video id from the local creation marker and writes a small private `metadata.json` beside the object. | | Upload | `MediaService::upload_declared` checks the host's `Authorization`, then signs a presigned `PUT` for `{prefix}{video}/original`. The signature binds the declared `Content-Type` and exact `Content-Length`; types outside `with_content_types` and sizes above `with_max_object_bytes` are refused before signing. | | Confirm | `refresh` or `publish` sends `HEAD`; a non-empty object within `with_max_object_bytes` whose type is accepted becomes `Ready`, anything else `Failed` (a new grant may then replace it). | | Playback | `playback(..., PlaybackKind::Original)` returns a presigned `GET` after the usual entitlement and publication checks. `Range` requests go directly to the bucket. | | Delete | `delete` removes the original and its metadata object. | Object keys are always generated by the server: an optional validated prefix plus the adapter's UUID-shaped id; no request value becomes a path. Upload and playback grants expire after at most 900 seconds (`s3::MAX_GRANT_SECONDS`) and never after the host's permission. Signed URLs live only in the private `UploadGrant` signature and `PlaybackGrant::expose_url`; `Debug` output and `MediaError` never contain them, and signing runs with tracing disabled. Never log the grant JSON you send to the browser. ```rust,ignore use rullst_media::{LibraryId, UploadDeclaration, s3::{S3Config, S3Credentials, S3Storage}}; // AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (+ optional AWS_SESSION_TOKEN). // Unset keys select the offline mock, which StoreConfig::production rejects. let credentials = S3Credentials::from_env()?; // Cloudflare R2: https://<account>.r2.cloudflarestorage.com, region `auto`. let config = S3Config::r2(LibraryId::new(1)?, "<account-id>", "course-videos", credentials)? .with_key_prefix("lessons/")? .with_content_types(["video/mp4", "video/webm"])? .with_max_object_bytes(2 * 1024 * 1024 * 1024)?; // AWS S3: S3Config::aws(library, "eu-west-1", "course-videos", credentials) // MinIO: S3Config::custom(library, "https://minio.example.internal:9000", "us-east-1", "course-videos", credentials) // (path style; HTTPS required except for a literal loopback address) let provider = S3Storage::new(config)?; // In the CSRF-protected upload-grant route, after validating the browser's file: let declared = UploadDeclaration::new(file_type, file_size)?; let grant = media.upload_declared(&policy, &actor, &scope, &lesson, 600, &declared).await?; ``` The browser then sends exactly one request: ```js // grant.protocol === "PresignedPut" await fetch(`${grant.endpoint}?${grant.signature}`, { method: 'PUT', headers: { 'Content-Type': grant.content_type }, body: file, // its size must equal grant.content_length }); ``` Bucket CORS must allow the application origin to `PUT` with a `Content-Type` header and to `GET`/`HEAD` with `Range`. Keep the bucket private: no public access, no public `r2.dev` URL and no custom-domain public access. Credentials need `GetObject`, `PutObject` and `DeleteObject` on the prefix and `ListBucket` (so that `HEAD` on a missing object returns 404 instead of 403). An AWS S3 CORS configuration (R2 accepts the same rules as JSON in its dashboard or API): ```json [ { "AllowedOrigins": ["https://app.example.com"], "AllowedMethods": ["PUT", "GET", "HEAD"], "AllowedHeaders": ["content-type", "range"], "ExposeHeaders": ["ETag", "Content-Length", "Content-Range", "Accept-Ranges"], "MaxAgeSeconds": 600 } ] ``` Limits: one `PUT` uploads at most 5 GiB and is not resumable; there is no multipart upload, malware scanning, transcoding or thumbnailing. Whether a provider enforces the signed `Content-Length` and `Content-Type` is part of the unvalidated interoperability; the `HEAD` check after upload is the enforced bound. A presigned URL is a bearer capability until it expires, even after withdrawal or deletion. Provider event notifications are not consumed; confirmation is explicit through `refresh` or `publish`. --- # Framework Spec Source: https://rullst.github.io/Rullst/book/spec.html # Rullst Specification 📄 ### *"The Single Source of Truth (SST) for Framework Architecture & Conventions"* This document is the **Single Source of Truth (SST)** for the **Rullst Framework**. It specifies the exact conventions, API structures, naming rules, directory standards, and subsystem maturity lifecycles across all monorepo crates. > [!IMPORTANT] > **AI & Human Alignment Directive:** > Whenever updating, refactoring, or generating code/documentation for Rullst, **always** refer to this specification as the baseline. > Every capability in the framework is strictly tagged with its implementation lifecycle status: > - 🟢 **`[Implemented / Bounded]`**: A defined implementation exists with automated tests for the stated scope. This is not a deployment, provider-homologation, or certification claim. > - 🟠 **`[Partial]`**: Useful foundations exist, but a named interoperability, architecture, or conformance boundary is still incomplete. > - 🟡 **`[Offline Test Mock]`**: Deterministic offline sandbox fixtures for local development and offline CI without external API dependencies. > - 🔵 **`[Roadmap / Under Development]`**: Planned architecture, traits or domain models; consult each entry for what exists and what remains unimplemented. --- ## 📂 1. Directory Structure Conventions A standard Rullst application scaffold strictly adheres to this folder hierarchy: ```text my-app/ ├── src/ │ ├── controllers/ # Route controllers (async request handlers) │ │ └── mod.rs │ ├── models/ # Active Record & Repository Models (rullst-orm entities) │ │ └── mod.rs │ ├── pages/ # Shared HTML views, templates, and layouts │ │ └── mod.rs │ ├── middlewares/ # Custom application middleware layers │ │ └── mod.rs │ ├── migrations/ # Schema migrations (database-backed projects) │ │ └── mod.rs │ └── main.rs # Application entrypoint, server bootstrap & central routing ├── static/ # Same-origin static assets (HTML starters) ├── Cargo.toml # Project cargo dependencies └── Rullst.toml # Framework configuration (database, environment, secrets) ``` The SaaS starter creates every folder above. The Blank and API starters begin with `src/main.rs` and, for a database profile, `src/migrations/`; the `make:*` generators create the other folders and their `mod.rs` on first use. A `--no-database` Blank project has no `Rullst.toml`. Every starter also writes `.env.example`, `AGENTS.md`, `.llms.txt` and `.rullst/context-map.json` (section 11.1). --- ## 🛠️ 2. Naming Conventions To guarantee consistency, both humans and AI coders must adhere to the following naming normalization rules: * **File Names:** Standard Rust `snake_case` (e.g. `users_controller.rs`, `post_model.rs`, `billing_service.rs`). * **Struct / Model / Trait Names:** Standard `PascalCase` (e.g. `UsersController`, `PostModel`, `PaymentProvider`). * **URL Paths:** Lowercase kebab-case (e.g. `/users`, `/user-profiles`, `/billing/webhooks`). * **Database Identifiers:** Snake case (e.g. `user_id`, `created_at`, `billing_accounts`). ### Optional browser graphics example (v13 candidate) `examples/webgpu` is a non-publishable teaching application served by Core's production security baseline. It demonstrates two-source wave interference with browser-native WebGPU and a lower-resolution Canvas fallback. No GPU dependency or default feature is added to the framework, and no GPU work runs on the server. The example starts paused, bounds resolution and frame submission, handles GPU startup failure/device loss, and explicitly disposes resources on removal or navigation. It uses same-origin assets without a bundler, external service or capture permission. Client results are illustrative, never authorization or grading evidence. Automated Chromium/software-GPU checks are bounded example acceptance, not physical-device, browser-universal or performance evidence. --- ## ⚡ 3. Framework Crates & Capability Matrix The [v13 maintenance scope decision](https://rullst.github.io/Rullst/book/v13-maintenance-scope.html) governs future investment and proposed extension boundaries. It does not describe an already completed package split or change current APIs, defaults, support obligations or publication inventory. The implemented capability matrix below remains the source of truth until individual migrations are reviewed and validated. Each crate also has a v13 [maturity tier](https://rullst.github.io/Rullst/book/maturity.html): **Core** (`rullst`, `rullst-core`, `rullst-macros`, `rullst-orm`, `rullst-orm-macros`, `rullst-security`, `cargo-rullst`), **Extension** (`rullst-auth`, `rullst-connect`, `rullst-mail`, `rullst-capital`, `rullst-ai`, `rullst-nexus`, `rullst-studio`, `rullst-messaging`) or **Experimental** (`rullst-iot`, `rullst-privacy`, `rullst-supervision`, `rullst-media`, `rullst-labs`). The tier sets the 13.x stability and validation promise; the matrix below records what each crate implements. | Crate | Responsibilities | Status & Capabilities | | :--- | :--- | :--- | | **`rullst-core`** | Kernel HTTP runtime, `routes!`, Server bootstrap, HTML engine, async task queues, WebSockets, circular telemetry buffers, storage facade, and the default baseline CSRF/WAF/header/PII stack. | 🟢 **`[Implemented / Bounded]`**: Routing, server lifecycle, `html!` engine, graceful shutdown, backpressure guard, queues, and local storage with path-traversal protection. The process-local `RateLimiter` keys IPv6 peers per /64 and tracks at most 100,000 buckets, dropping refilled ones and evicting the least recently used beyond that cap. `ApplicationLifecycle` adds an opt-in process-local monotonic startup/ready/draining/stopped state, at most 32 immutable component readiness bits, secret-minimized `/ready`, fail-closed admission and a bounded drain wait. `Server` marks ready after binding, begins drain before Axum's graceful wait, and accepts an explicit supervisor shutdown future; deterministic tests cover startup failure, in-flight completion, rejection after drain and lock poisoning. It does not run dependency checks, coordinate replicas/load balancers or authorize domain requests. SQLite and Redis persist `dispatch_at` timestamps for at most 366 days and never claim them early; Redis promotion uses server time and a digest-pinned live CI/release contract. The Redis cache and queue drivers share one lazily opened multiplexed connection per driver and reconnect after a connection-level failure instead of opening a connection per operation. Execution starts on the first later worker poll and is at-least-once. The worker never cancels an in-flight claim; graceful shutdown requeues a job claimed after the request. Timeout and shutdown record a handler's own result when it finishes before cancellation takes effect, so only a cancelled handler is failed as timed out or requeued. Stalled-lease recovery runs at worker start and then every `min(stalled_after, 60 s)` over every processing lease of the shared SQLite table or Redis namespace, and in v13 workers claim with `QueueDriver::pop_with_lease` so SQLite and Redis store each claim's own `stalled_after` lease and recovery honours it; only claims without a lease need every sharing worker's `stalled_after` to exceed the longest `job_timeout` among them; SQLite and Redis count stalled leases per job and fail the job at its fifth stalled lease instead of requeuing it forever (`retry_failed_job` restarts the count; the v13 `try_with_max_stalled_leases` driver builders accept 1–1,000). Worker transitions are fenced by the claimed attempt number in SQLite and Redis, so a stale lease cannot complete, fail or requeue a recovered and re-claimed job; custom drivers inherit unfenced defaults, and SQLite and Redis `retry_failed_job` keep the counter so the fence holds across retries. A worker without a handler for a claimed job's name hands it back with a five-second delay in SQLite and Redis instead of failing it, until the claim's 720th attempt fails it (section 4.3); custom drivers without `requeue_attempt_after` still fail it. Redis retains at most 10,000 failed jobs and 10,000 dead letters by default (1–100,000 via `try_with_failure_retention`), evicting the oldest atomically, and implements bounded `list_all_jobs` (at most 1,000 rows), `retry_failed_job` and `purge_failed_jobs`. The v13 `QueueDriver::list_job_previews` returns those records with each payload and error cut to a caller-chosen byte budget on a UTF-8 boundary; SQLite cuts them in SQL and Redis in one atomic Lua script, while custom drivers inherit a projection of `list_all_jobs`. Worker and scheduler handles buffer at most 256 undrained errors; overflow is dropped, counted by `dropped_errors()` and logged as a `tracing` warning. `Server` drains the scheduler handle it owns, logging each task failure, and returns a scheduler error at shutdown only for a failed scheduler loop. Custom drivers fail closed for future scheduling until implemented. `TenantStorage`, `TenantCache`, `TenantRealtime` and `TenantPresence` bind those facades to a validated `TenantContext`, apply immutable tenant namespaces and prove same-name local non-interference; the realtime wrappers also bound channel/event/identity names and payload size. `BroadcastManager` retains a channel only while it has a subscriber or an outstanding handle (publishing never creates one) and `PresenceTracker` counts connections per user and drops empty rooms, so neither registry grows with abandoned names. Memory and Redis caches expose an opt-in, at-most-200-entry metadata snapshot containing logical key, UTF-8 value length and remaining TTL but never the value; custom drivers fail explicitly unless they implement that method. Remote bucket policy, distributed transport/liveness, cache operator authorization and application room authorization remain deployment/application work. In a supervised debug Development process (`RULLST_DEV_GENERATION` set by `cargo rullst dev`/`dash`), `Server` also serves `GET /_rullst/dev-telemetry` to loopback peers with a loopback `Host`: bounded request counters and the newest 64 requests (method, query-free path, status, duration), outermost `rullst.orm.query` operation counts with the newest 16 slow operations, and the pending count of a queue passed to the v13 `Server::with_dev_queue`, plus the newest 16 possible-N+1 findings (an ORM label fingerprint that one request, identified by its matched route, repeated at least three times, using the shared `rullst_core::query_patterns` rule that Studio's threshold also uses); bodies, headers, query strings, SQL, bindings and error text are never recorded, and release builds, Staging and Production never mount it. For deployed applications, the opt-in 13.0.0-alpha.3 `Server::with_monitor` serves the authenticated read-only `GET /_rullst/monitor/v1` document instead (section 10.1.1).<br/>🟢 **`[Implemented / Bounded]`**: The in-memory upload admission contract enforces a hard size/allowlist boundary, canonical tenant/name, recognized signature versus MIME/extension, active-text denial, randomized tenant quarantine keys, SHA-256 binding and fail-closed scanner release. It is not a deep parser, remote persistence or a production malware engine. Since 13.0.0-alpha.2 `uploads::Multipart` reads `multipart/form-data` under an explicit `MultipartPolicy`, streaming each file to memory or a private temporary file and recognizing content with the same signatures (section 4.1, "Multipart Form Uploads").<br/>🟢 **`[Implemented / Bounded]`**: Validated environment precedence is `RULLST_ENV`, legacy `APP_ENV` (from the process, then from the `.env` that `Server` reads, which `RullstConfig::environment` also honours once a `Server` has started), then `[app].env`; invalid values fail instead of silently enabling development.<br/>🟢 **`[Implemented / Bounded]`**: `apply_security_baseline` and `Server` compose configured CSP nonce headers, exact-origin CORS with explicit credential opt-in, bounded WAF, double-submit CSRF and optional PII masking in one tested order, with the per-application config installed outside every middleware. Browser/proxy/TLS deployment evidence and application-owned session/auth/tenant/authorization remain separate. A fail-closed typed Academy boundary-assessment contract records those application observations without certifying them, and the extended `rullst-security` stack is still composed explicitly. The opt-in v13 `Server::trusted_proxies` stage resolves clients behind listed reverse-proxy networks outside that baseline (section 4.1).<br/>🟢 **`[Implemented / Bounded]`**: `client_contract` exposes the portable `rullst.client` v1 typed JSON envelope, positive version negotiation, bounded correlation/idempotency/failure tokens, server-authored time and a fail-closed 2 MiB codec on native and Wasm. It deliberately contains no role, tenant or authorization assertion; durable replay and domain policy remain server/application work.<br/>🟢 **`[Implemented / Bounded, v13]`**: Optional `storage-s3` private S3/R2 operations passed hosted source/package admission in PR #236; final release admission and actual owner-account interoperability remain outstanding. See section 4.4. | | **`rullst-orm`** | Active Record & Repository patterns, parameterized SQLx connection pool (PostgreSQL, MySQL/MariaDB, SQLite), typed Turso/libSQL primary profile, schema migrations, AES-256-GCM privacy, Scout search, typed pgvector/Qdrant queries, Redis native structures, and optional capability-oriented persistence adapters. | 🟢 **`[Implemented / Bounded]`**: Relational CRUD, eager loading, type-safe queries, migration runner, versioned field encryption, and connection-pool resilience for supported SQLx drivers/features. PostgreSQL, MySQL, MariaDB and SQLite have distinct executable matrix contracts, while MariaDB intentionally shares SQLx's MySQL protocol/backend.<br/>🟢 **`[Implemented / Bounded]`**: `#[derive(Orm)] #[orm(backend = "turso")]` supplies typed CRUD, equality filters, ordering, pagination/counts and generated/app-assigned keys through a process-wide `TursoOrm`. Its migrations are ordered, checksummed, drift-detecting and reversible. The blank/API CLI profile generates, compiles, migrates, reports status and rolls back locally, while the same typed contract passes against the official remote libSQL server. Unsupported SQLx-specific model behaviors fail during macro expansion rather than being ignored. Other SQLx-specific blueprints, ORM relations/hooks, schema auto-diff, seed generation and transparent embedded-replica synchronization are not part of this bounded Turso profile.<br/>🟢 **`[Implemented / Bounded]`**: The optional persistence boundary supplies portable document CRUD for MongoDB and SurrealDB, parameterized OLAP queries through in-process DuckDB, explicit parameterized Turso/libSQL SQL/transactions, and bounded read-only ISO GQL through SurrealDB. These capability APIs do not claim shared semantics or cross-store transactions. External adapters select deterministic offline behavior for empty or `mock_*` credentials where documented; SurrealDB uses its HTTP protocol rather than embedding the BSL-licensed SDK.<br/>🟢 **`[Implemented / Feature-gated]`**: `scout-http` provides bounded Meilisearch, Elasticsearch and Algolia indexing/search adapters plus deterministic mocks. Meilisearch has a digest-pinned live lifecycle; Elasticsearch/Algolia have protocol fixtures, not hosted-provider certification. Generated projections are process-local post-commit effects unless the application explicitly composes the transactional outbox.<br/>🟢 **`[Implemented / Feature-gated]`**: `pgvector` with `strict-postgres` supplies typed SQL vector helpers. `qdrant` supplies a separate bounded dense-vector collection/upsert/delete/cosine-query contract, while `redis` supplies namespaced Hash, Set and Sorted Set operations. All three have digest-pinned live lifecycles; RAG orchestration, authorization, production ANN tuning and Redis cluster/failover remain application/deployment boundaries.<br/>🟢 **`[Implemented / Benchmark Evidence]`**: A lockfile-pinned Criterion target compares five equivalent typed-SQLite shapes through one Rullst, Diesel and SeaORM connection under the same schema, seed and SQLite policy. It is per-run evidence, not a superiority, negligible-overhead, networked-database or full-application claim. | | **`rullst-auth`** | Argon2id password hashing, encrypted cookie sessions (AES-256-GCM), opt-in application JWTs, Passkey ceremony foundations, RBAC context guards. | 🟢 **`[Implemented / Bounded]`**: Non-blocking `spawn_blocking` Argon2id hashing, versioned expiring AES-256-GCM sessions with opt-in per-user session-epoch revocation, fail-closed `RequireRoleLayer`, compile-validated `#[rullst::require_role]`, named `Policy<User, Resource>` decisions, and a feature-gated application JWT policy with required versioned claims, bounded TTL/scopes, strong HS256 keys, `kid` rotation and revocation contracts that reject process-local state in production mode.<br/>🟢 **`[Implemented / Feature-gated]`**: `sqlite` supplies bounded shared local auth state. `SqliteJwtRevocationStore` persists JTI expiry and monotonic subject session versions through serialized transactions, stored quota/configuration and async verification. `SqlitePasskeyStore` persists validated public credentials, bounded device inventory/rename/revocation and optimistic signature-counter CAS; executable restart, replay, quota, corruption/configuration and two-instance contention evidence covers both stores. Authentication, role persistence, resource/tenant/device ownership, trusted file permissions/encryption, backup and multi-host replication remain application/deployment boundaries.<br/>🟠 **`[Partial]`**: Passkey registration/assertion validates the documented ES256/`none`-attestation scope; the compatible `PasskeyAuth` challenge state remains process-local. The optional `passkey-postgres` v13 candidate adds a separate shared manager with bound single-use PostgreSQL ceremonies; independent-process/database-restart and Chromium virtual-authenticator contracts pass, and PR #223 passed hosted workspace/archive source acceptance. Final release admission remains separate. The v13 opaque-session inventory/logout increment extends the recovery store with process/database evidence and passed hosted source/package admission in PR #236. Final release admission remains separate. Normative WebAuthn conformance or adoption of an audited full server library, refresh tokens and complete recovery/session UX remain required before a general stable claim. | | **`rullst-security`** | Explicit extended defense-in-depth layers: bounded RASP, authenticated Vault, Login Jail, Secure Headers, rate limiting, DLP and security telemetry. | 🟢 **`[Implemented / Bounded]`**: AES-256-GCM envelopes with rotation/AAD, bounded URI/header/body RASP heuristics, local abuse controls, CSWSH origin guard, OS-random TOTP with SVG enrollment QR, strict JSON transport inspection plus an explicitly mounted reusable JSON Schema 2020-12/OpenAPI 3.1-component policy, explicit log redaction, file-backed SRI hashes, and a versioned/bounded `LiveSecurityEvent` v1 dashboard envelope. `DurableSiemSpool` preserves the compatible unsigned local format, while `AuthenticatedSiemSpool` offers an explicit HMAC-SHA256-chained format with named active/historical keys, zeroized key material, sequence/predecessor validation and byte/record quotas. Restart, forgery, wrong/missing keys, reordering, interior deletion, quota, symlink and external-length-change paths fail closed. Whole valid-tail rollback requires a separately trusted checkpoint, and the caller owns directory/key trust, permissions, retention and exclusive-writer operation. Schema construction caps bytes/nodes/depth, accepts only local references, disables network/filesystem resolution and uses linear-time regexes; auth/ownership/domain rules and query/header/form validation remain application contracts. A deterministic Sentinel classifies three caller-supplied aggregate patterns and can issue HMAC-authenticated, subject-bound, expiring, one-shot process-local proof-of-work challenges; it is not AI attribution, automatic blocking or distributed replay protection. The CLI emits bounded fail-closed evidence and a CycloneDX 1.5 Cargo SBOM; it does not certify the application.<br/>🟢 **`[Implemented / Feature-gated]`**: `redis-rate-limit` provides namespaced atomic Redis fixed-window counters over one lazily opened multiplexed connection per limiter (shared by its clones and reopened after a connection-level failure), hashes client keys and exposes an explicit process-local offline mode that production can reject with `require_distributed()`.<br/>🟠 **`[Partial]`**: Recovery-code consumption must be persisted transactionally by the application. The two-replica deployment acceptance (PR #224) shows two processes sharing one Redis rate-limit budget; Redis eviction and failover evidence is still required. CSP nonce composition is shared, but Core and Security are not yet one canonical Server stack; WebSocket CSRF tickets/frame crypto, trusted rollback checkpoints, spool compaction/remote acknowledgement and external SIEM delivery are not implemented. | | **`rullst-ai`** | Multi-provider LLM client (Gemini, OpenAI, Claude, DeepSeek, Ollama, explicit OpenAI-compatible endpoints), prompt injection defenses, PII masking, bounded tenant-aware RAG, guarded local tools, and conversational memory. | 🟢 **`[Implemented / Bounded]`**: Guarded `AiClient`, heuristic prompt filter, PII masking, machine-readable provider capabilities, configurable bounded live-request deadlines, a versioned deterministic injection/jailbreak/PII regression corpus, and a capability-declared OpenAI-compatible adapter. The adapter separates literal-loopback-IP HTTP(S) from HTTPS cloud configuration, supports optional Bearer authentication, disables ambient proxies/redirects, and bounds image/response bodies; unrelated protocols use `AiProvider`. A separate static-dispatch `StreamingAiClient<P>` enforces chunk/output ceilings and explicit cancellation; exact OpenAI-compatible configurations may declare strict incremental SSE with a required terminal marker and cancellation raced against request/body reads. `AdaptiveAiEvaluator<P>` runs caller-defined multi-turn strategies with independent turn/prompt/response/deadline limits, cancellation, typed pass/fail/inconclusive decisions and a versioned report that retains no raw prompt, response or provider error. Repository fixtures prove orchestration, not live-model behavior.<br/>🟢 **`[Implemented / Bounded]`**: Strict URL/resolved-IP/redirect/resource policy plus an opt-in deny-by-default HTTPS fetcher with exact-host allowlist, DNS pinning, proxy bypass, peer verification and streaming limits. Explicit vision helpers accept application-admitted bytes, canonical exact-root local files or URLs only through that fetcher; capability and prompt checks precede I/O, input is capped at 10 MiB, supported image signatures are sniffed and a supplied remote media type must match. Local tool dispatch separately requires allowlist, principal authorization, closed bounded JSON, call budget, audit sink, and payload-bound approval for destructive/financial calls. `RagPipeline::answer` composes guarded embedding, a static-dispatch application retriever, Unicode-safe context budgets, guarded generation, source metadata, and required secret-minimized terminal audit under a trusted `TenantContext`; a bounded tenant-partitioned process-local cosine retriever supplies the offline contract. `DurableRagAuditTrail` and `DurableToolAuditTrail` synchronously append minimized events to distinct versioned local files under byte/record quotas and fail closed on restart corruption, symlink targets, competing-writer growth or durability uncertainty. Their SHA-256 frames detect corruption but do not authenticate events. The separate opt-in `AuditDeliveryClient` exports a caller-minimized, at-most-16-KiB JSON envelope with an exact HMAC-SHA256 signature, key/timestamp metadata, stable event identity across bounded transient retries, explicit cancellation and a closed event-bound acknowledgement. Cloud delivery requires HTTPS and literal-loopback HTTP(S) is development-only; the receiver still owns freshness verification, deduplication, authorization, persistence, retention and key operations.<br/>🟢 **`[Implemented / Feature-gated]`**: `StatefulChat<M>` loads bounded tenant/conversation history, performs guarded generation and atomically appends one user/assistant exchange through a static `ChatMemory`. The bounded in-memory store is always available; `sql-memory` supplies fixed-schema SQLite/PostgreSQL/MySQL/MariaDB storage with an even monotonic revision and transactional compare-and-swap, so stale cross-process writers fail instead of silently reordering. Raw message encryption/retention, authenticated ownership within a tenant, provider audit, backups and conflict UX remain application contracts; the CLI scaffold remains the Turso/custom-model path.<br/>🟠 **`[Partial]`**: The egress fetcher is not automatically mounted around provider transports, RAG or arbitrary application clients; hosted-provider SSE conformance, non-compatible streaming protocols, image decoder safety, host path trust/authorization, provider-native tool calling, cancellation for ordinary non-streaming calls, automatic provider retries, durable audit outbox/receiver operations, approver authentication, first-party external vector-store retrievers, authoritative datastore/domain authorization, ingestion/deletion, maintained application-specific eval corpora and output policy remain application or roadmap work. Exact live-model evaluation execution/results and provider behavior remain external evidence.<br/>🟡 **`[Offline Mock]`**: Deterministic offline chat/vision/embedding fallbacks. | | **`rullst-capital`** | Provider-neutral SaaS billing contracts, SaaS MRR/ARR metrics, verified webhooks, quotas and entitlements, with a supported Stripe adapter and an experimental InfinitePay adapter. | 🟢 **`[Implemented / Bounded]`**: `BillingProvider`/`MeteredBillingProvider` contracts, the Stripe adapter, pooled fail-closed HTTP clients, explicit mock credentials, and signature/freshness/replay foundations for the methods documented by each adapter. Applications integrate other gateways by implementing these traits ([custom providers](https://rullst.github.io/Rullst/book/capital-custom-provider.html)).<br/>🟢 **`[Implemented / Feature-gated]`**: `webhook-sql` persists bounded provider-scoped payload digests or caller-supplied stable event identifiers across processes on SQLite, PostgreSQL, MySQL, and MariaDB. Immutable capacity/TTL, serialized claims, expiry, restart, contention, configuration drift, and fail-closed full/storage states have executable evidence. A caller-owned relational transaction can bind one semantic event claim to its domain mutation. Middleware admission and cross-system exactly-once are not implied; external effects still require an outbox, idempotent consumers, and reconciliation.<br/>🟢 **`[Implemented / Bounded]`**: Static-dispatch metered billing uses the current Stripe Meter Events request shape, binds accepted responses to the original event, caps response bodies, forwards a bounded provider identifier and exposes deterministic non-live mocks.<br/>🟡 **`[Experimental]`**: InfinitePay keeps deterministic offline fixtures; live plan-only checkout and live callbacks fail closed until validated against a live account.<br/>🟠 **`[Partial]`**: Uniform live method coverage, provider-account interoperability, cross-system exactly-once, and reconciliation are incomplete.<br/>⚪ **`[Removed in v13]`**: Paddle, Lemon Squeezy, Polar, Razorpay, Mercado Pago, Alipay, Coinbase Commerce and PicPay adapters, the Wise payout adapter and payout contracts, and the NFS-e fiscal preparation pipeline; see the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html). | | **`rullst-connect`** | Social login / OAuth2 / OIDC providers (Google, Apple, GitHub, Discord, Microsoft, Facebook, LinkedIn, X, Auth0, Cognito and generic OIDC) with PKCE and rotating JWKS. | 🟢 **`[Implemented / Bounded]`**: OAuth2/OIDC clients with constant-time PKCE comparison, validated discovery, bounded JWKS refresh/cache policy (the generic `OidcProvider` verifies a token without `kid` only against a single-key JWK Set whose key fits its asymmetric `alg`, per OIDC Core 10.1, and treats profile claims such as `name` as optional), deterministic mock credentials (empty or `mock_*` values, served only with the `mock` feature; without it the fallible constructors reject them as `InvalidConfiguration` for `credentials` and the deprecated ones return a disabled provider; the mock authorization URL is the configured callback with the fixed code `mock_authorization_code` and the state) and a credential-free `UniversalProfile` projection. `ConnectUser` serialization omits access/refresh tokens; deserializing that form restores a credential-free profile with an empty access token. Category-aware remote revocation rejects malformed or oversized tokens before transport: Google, Discord and Apple accept the documented access/refresh categories, GitHub accepts access tokens, and Auth0/Cognito accept refresh tokens; protocol fixtures bind method, endpoint, client authentication and form/JSON shape, while request/response `Debug` omits credentials, bodies and URL query data. Other providers fail explicitly as unsupported, and remote success does not clear application sessions or persistence. `AutoRefreshingSession<P>` validates and user-binds token generations, detects expiry with a bounded early-refresh window, serializes provider refresh through static dispatch, retains/rotates refresh credentials and swaps the access token only after a complete valid response; a rejected same-user response, or a `ConnectError::RefreshIncomplete` whose profile/ID-token step failed after the grant (carrying the issued tokens to direct callers), still keeps its valid rotated refresh token and advances the generation so the consumed credential is never retained. Callers waiting behind a successful refresh reuse that state. Its state/leases redact secrets. `EncryptedTokenSnapshot` supplies a bounded, versioned AES-256-GCM envelope that authenticates key ID, provider and trusted local-account binding, preserves the validated generation and rejects copied-owner/tampered records. The optional `sqlite` store persists only a pseudonymous binding digest, generation/key metadata and that ciphertext under an immutable row ceiling; `BEGIN IMMEDIATE`, exact-successor compare-and-swap and conditional deletion reject stale shared-local writers, with restart, contention, quota, configuration, corruption, key and symlink evidence. The application still owns secret-manager key custody/rotation, account authorization, a lease around the remote provider call, running an in-flight refresh to completion (dropping the `access_token()` future after the grant loses a rotation), losing-call reconciliation, retry/backoff, trusted directory/backup, reauthentication and multi-host replication. The optional Axum/tower-sessions lifecycle generates a ten-minute state + PKCE challenge, adds nonce for OIDC, keeps verifier/nonce server-side, removes and immediately saves the sole active challenge before validation and rejects sequential replay/expiry/mismatch. The host still owns durable session storage and cookie/TLS/account policy; the generic session-store API is not distributed compare-and-delete, so simultaneous already-loaded callbacks require idempotent effects or a stronger application store. The v13 `challenge` module (from 13.0.0-alpha.3) is that stronger store without a session layer: `begin_login` keeps the challenge in a `ChallengeStore` (bounded memory, or `SqlChallengeStore` over the application's SQLite, PostgreSQL or MySQL pool with the `sqlite`, `postgres` or `mysql` feature, which creates `rullst_connect_login_challenges`) under a SHA-256 digest of an opaque 256-bit `ChallengeId`; `ChallengeCookie` carries only that id (`HttpOnly`, `SameSite=Lax`, at most ten minutes, path-scoped, ambiguous duplicates refused); `finish_login` consumes it with one atomic removal (`DELETE … RETURNING`, or `SELECT … FOR UPDATE` then `DELETE` on MySQL) so exactly one of concurrent callbacks wins, refuses expired, altered and other-provider challenges, compares the state in constant time before trusting a provider error and passes the stored verifier and nonce to the exchange. `ChallengeCookie::for_cross_site_post` (v13) makes the cookie `SameSite=None; Secure` for a provider that POSTs the callback cross-site (`response_mode=form_post`). From 13.0.0-alpha.3 `GithubProvider` reports the `primary` address of `GET /user/emails` with GitHub's `verified` flag (falling back to the public profile email with `email_verified: None` when the call fails or lists no primary), `MicrosoftOidcProvider` signs in with the Microsoft identity platform for an explicit `MicrosoftTenant` (RS256 ID token against the tenant keys, issuer bound to its `tid`, tenant admission, subject `<tid>:<oid>`, `email_verified` only with `xms_edov: true`), and `AppleProvider` adds `verify_signing_key` (the ES256 client secret can be signed) and `name_from_user_field` (the bounded, unsigned first-login name). Entry limits are exact on SQLite and in memory and may be exceeded by concurrent saves on PostgreSQL and MySQL. SQLite contracts run in the workspace tests; the PostgreSQL and MySQL contracts run against owned servers in CI (`check-connect-sql.py`). `ReqwestClient` also exposes explicit HTTP(S) corporate-proxy constructors, which `OidcProvider::discover_with_client` also uses for discovery: endpoint shape is bounded, URL credentials are rejected, authenticated remote proxies require HTTPS, system-proxy lookup is disabled and a local protocol fixture proves routing/auth headers.<br/>🟢 **`[Implemented / Local Test Fixture]`**: The explicitly mounted Axum Mock IdP accepts only configured HTTP-loopback issuer/callback origins, binds one exact client, bounds process-local grants/tokens, consumes expiring authorization codes once, verifies S256 PKCE, signs nonce-bound EdDSA ID tokens and publishes discovery/JWKS. The deterministic key and credentials are public test fixtures; interactive login/consent, refresh/device/federation flows, durability, rotation, public exposure and OIDC conformance are not claimed.<br/>🔵 **`[Roadmap]`**: PAC/WPAD, SOCKS, proxy mTLS identity and enterprise deployment certification are not implied. Message brokers live in `rullst-messaging`, not this OAuth-focused crate. | | **`rullst-messaging`** | Broker-neutral event envelopes, idempotent publication, consumer groups, acknowledgement leases, retry, dead letters, durable local SQLite state, and future remote adapters. | 🟢 **`[Implemented / Bounded Foundation]`**: `rullst.messaging.v1` envelopes, bounded identifiers/headers/payloads/batches/retention, topic-scoped exact-replay idempotency, fan-out between groups, competing consumers, expiring single-use ACK leases, bounded retry/attempt ceilings, dead-letter views, explicit terminal purge, injectable time and a reusable static-dispatch contract suite. Debug output redacts keys/tokens/header values/payloads. A canonical bounded v1 envelope codec rejects unknown versions, non-canonical/truncated/oversized frames and namespace mismatch; a deterministic digest fixture freezes its bytes. Validated W3C version-00 `traceparent` and a conservative `tracestate` subset propagate through only those two allowlisted headers; arbitrary baggage, sampling and export remain host work. `InMemoryBroker` remains deterministic/process-local. The opt-in `SqliteBroker` uses a fixed schema and serialized `BEGIN IMMEDIATE` mutations for publications, subscriptions, claims, ACK/retry/DLQ and purge; exact limits are persisted per namespace. Its explicit AES-256-GCM profile encrypts header values plus payload with randomized nonces and AAD binding to immutable row metadata. A bounded primary/prior-key ring rejects missing keys until old records are purged, and plaintext/encrypted profiles cannot mix. The opt-in static `OrmOutboxRelay` binds one relational outbox stream to one topic, validates claimed JSON, publishes the durable event key scoped by its stream (SHA-256 of the stream, `/`, event key) as broker idempotency, so streams sharing a topic never deduplicate against each other, and only then ACKs the exact ORM lease; a publish-before-ACK crash/reclaim test produces an exact replay and one broker message. Shared-contract, raw-storage/restart, wrong-key/tamper/row-swap, symlink, rotation, expired-lease, two-instance contention, configuration-drift and malformed-row repair regressions are executable.<br/>🟠 **`[Partial]`**: Delivery is at least once. The default profile is plaintext. Even in the encrypted profile, topic/event/content metadata, IDs, timestamps, idempotency keys, fingerprints, rotation key IDs and delivery state remain visible; key custody, permissions, backup/rollback detection, retention, disk operations, topic/tenant authorization and destination-side idempotency belong to the host. Profile migration requires a new namespace/database and application-owned republishing. The outbox database and broker publication are not one atomic transaction; worker supervision, cleanup and destination idempotency remain application work. The local adapter does not provide replication or automatic failover. The envelope codec is not a remote transport and does not preserve caller publication keys or broker acknowledgements by itself. The optional standalone Redis Streams candidate passed source admission in PR #236, with Rullst-owned indexes, verified TLS, exact replay, server-time lease fencing, partial-write quarantine and disposable restart/fault/outbox tests; final release admission remains separate (see the Redis messaging guide below). Kafka, RabbitMQ, NATS/JetStream, SQS/SNS, Google Pub/Sub and Pulsar adapters, replication/failover and native Redis group interoperability remain roadmap work. | | **`rullst-iot`** | `no_std` sensor telemetry/protocol helpers and an Ed25519-signed firmware-manifest verification gate. | 🟢 **`[Implemented / Bounded]`**: Ed25519 manifest verification with target/hash/length/counter checks, an explicit durable monotonic-CAS store boundary, `no_std` telemetry/frame models, bounded MQTT 5 PUBLISH and RFC 7252 CoAP base-request encoders, a credential-free local HTML snapshot renderer, and a safe telemetry-module CLI scaffold. Protocol vectors and restart/retry/conflict tests prove these local contracts, not a broker, network or physical device.<br/>🟠 **`[Partial]`**: GPIO state, I2C/Modbus frames, BLE GATT records, RSSI topology, power recommendation and Digital Twin JSON are data/helpers only, not hardware, network or realtime drivers.<br/>🟡 **`[Simulador Dev]`**: Deterministic MQTT-value/HSM/PQC fixtures require `feature = "experimental-simulators"` and never represent broker or cryptographic capabilities.<br/>🔵 **`[Roadmap]`**: Concrete hardware-backed counter/boot integration, firmware download/flashing, MQTT/CoAP transports and state machines, hardware drivers/HSM and audited ML-KEM. | | **`rullst-mail`** | Transactional email engine with Resend, SendPulse, optional SMTP (implicit TLS on 465, mandatory STARTTLS on other ports), optional native AWS SES v2, and offline fixtures. | 🟢 **`[Implemented / Bounded]`**: Mandatory pre-flight pipeline, anti-CRLF validation, a 2 KiB subject and 2 MiB per-body bound enforced by rejection before linear-time disposable-domain/security/DLP heuristics, provider-specific transports (other providers are application-owned `MailDriver` transports; v13 removed SendGrid, Postmark, Mailjet, Mailtrap and ACS), seven safe scaffold variants (including bounded commercial payment receipts and explicit D+1/D+3/D+7 dunning), and expiring purpose-bound HMAC tracking tokens. `TenantMailResolver` selects an in-process driver directly from an explicit authenticated Core `TenantContext`; invalid IDs and unavailable registry state fail closed, and tests prove two contexts do not cross-deliver. `MailError` classifies permanent/transient/rate-limit outcomes; the in-process `FailoverDriver` forwards tenant context and delivery IDs to each driver it tries and sends another provider only transport/HTTP 5xx/429/transient-SMTP failures, stops at a fallback's permanent or fail-closed guard error and returns it typed, captures bounded delta `Retry-After`, fails closed on circuit-state errors and emits structured tracing without provider response bodies. The `Mail` facade fills a missing sender from a validated `MAIL_FROM`/`[mail] from` default (v13) and real transports never invent one. `Mail::enqueue` preserves tenant and bounded due-time metadata through SQLite/Redis without early claims; the worker consumes that timestamp only once it is due, allowing at most 300 s of worker clock lag behind the queue server. Direct Resend retains provider-native scheduling and rejects schedules beyond its documented 30-day window before network, while real SMTP/SendPulse/Log and SES paths reject future direct delivery; offline fixtures may retain it for assertions. The shared attachment contract accepts at most 32 items, 20 MiB each and 25 MiB raw aggregate; validates safe basenames, parameter-free MIME and unique HTML-referenced CIDs; redacts bytes from `Debug`; and feeds provider-native Resend, SendPulse (non-inline), native SES, the SES v2 bearer-proxy `Simple.Attachments` payload and nested SMTP MIME serialization. The opt-in static `AttachmentInspectionGuard` fails before transport on executable magic, executable/script-host extensions, spoofed known types, active PDF/SVG, recognized secrets and unsafe text links, choosing checks from the case-insensitive declared type, filename extension and content signature together, and classifying markup by root element and namespace as browsers parse it (strict mode also rejects HTML extensions/markup, script URIs, unknown extensions and declared types it does not inspect other than `application/octet-stream`); external scanners can implement the same contract. The provider-neutral `SuppressionGuard` checks process-local or opt-in shared-local SQLite state before transport, keyed by the bare address that one recipient parser extracts for the pipeline, the disposable-domain check, verified feedback and every store (display-name forms are normalized, internationalized domains are keyed by their IDNA A-label, unparsable forms and non-ASCII local parts fail closed as validation errors); verified event identities are replay-bound, suppression reasons escalate monotonically and immutable quotas are transactional. `ObservedMailDriver` emits only a bounded provider label, terminal outcome, latency, attachment count and scheduling/tenant booleans through a non-failing observer. With `aws-ses`, `AwsSesDriver` sends SES v2 Simple messages through the official AWS SDK and SigV4, including temporary credentials, caller-owned rotating providers/config, HTML/text, RFC 8058 headers and attachments/CID; it rejects provider field limits and an encoded estimate over 40 MiB before network, caps `Retry-After`, and a loopback contract asserts the signed regional `ses/aws4_request` request plus typed/redacted rejection. The legacy constructor remains only an offline-fixture or explicit trusted bearer-proxy boundary, never an unsigned AWS request; the proxy payload carries attachments/CID under the same SES field and 40 MiB bounds instead of dropping them. Payment receipts never claim tax authorization; dunning does not infer billing state or scheduling.<br/>🟠 **`[Partial]`**: Exact execution time, exactly-once delivery, live-account SES acceptance and inbox delivery are not implied. The local attachment inspector is not antivirus, sandboxing, recursive archive inspection or CDR; provider/account limits may be tighter. Provider webhook authentication/adapters, multi-host suppression replication, file encryption, distributed breaker/telemetry operations, durable encrypted tenant credentials, rotation and cross-process distribution remain application/deployment concerns; tracking payloads are authenticated but not confidential. SES identity/domain verification, sandbox exit, IAM least privilege, quotas, reputation and provider operations remain AWS/account/deployment work.<br/>🟡 **`[Offline Mock]`**: Memory/Log plus empty or `mock_*` provider credentials; SMTP only for an empty/`mock_*` host or a `mock_*` credential, so a real host without credentials is a real unauthenticated relay and partial SMTP credentials are rejected. | | **`rullst-studio`** | Local Developer Control Room (`http://127.0.0.1:5555`), clean route navigation, live system telemetry visualizers. | 🟢 **`[Implemented / Bounded]`**: Local control center, `RadarSnapshot` telemetry, database/migration surfaces when configured, and explicit `Unavailable` states for unconnected probes. The data browser reads/filters SQLx tables with 25-row pages, 256-character cells and 256-byte search terms and, only after the verified debug-loopback/same-origin middleware installs an unforgeable request marker, can update primitive non-key values or delete exactly one complete-primary-key-selected row. A table whose key includes a column outside the identifier boundary or column cap stays read-only, and each write commits in a transaction only when exactly one row changed. Values are bound, request/schema/value cardinality is bounded, backend-specific types remain read-only and SQLite/PostgreSQL/MySQL/MariaDB have executable mutation contracts. This is not application tenant/RBAC, audit, rollback or shared-production administration. The supplied queue snapshot exposes only backend records; SQLite can explicitly retain 1–100,000 successful jobs with atomic pruning and purge while deleting them by default. Retained payload access/policy belongs to the host. An explicitly supplied memory/Redis `Cache` exposes at most 100 metadata rows in the UI; logical keys become process-bound HMAC tokens, values never leave the driver, and only individual local invalidation is available. A separately mounted push-only trace router accepts 1–128 attribute-free v1 spans under 128 KiB after HMAC-SHA256, source/ID/clock/nonce validation and atomic replay rejection; the bounded in-process viewer derives slow-query and repeated-label heuristics without SQL or bindings. It is not OTLP, durable trace storage, a key manager or remote Studio authentication. Successful feature-flag toggles invalidate all warm `DbFeatureDriver` caches in the same process through a constant-size epoch. Cross-process/direct-writer invalidation remains TTL-bound unless the application distributes the signal. | | **`rullst-nexus`** | Auto-generated Admin CMS (`/nexus`), dynamic model CRUD, AI Admin Assistant (`/nexus/chat`), SOC Threat Radar. | 🟢 **`[Implemented / Bounded]`**: `#[derive(Nexus)]` emits registered named-field metadata with inferred primitive or explicit semantic widgets; the panel provides parameterized CRUD/search/sort/pagination plus bounded selected-record delete/deactivate. Construction is fail-closed, requires an authentication policy and admin role layer, validates bounded unambiguous model/field/enum/relation metadata, enforces server-side field policy, caps form pairs and field bytes, rejects unknown/protected/duplicate or semantically invalid form values, minimizes database errors returned to clients, and escapes record/model metadata on audited paths. Boolean widgets are inferred; enum options and multiline intent are explicit because an unrelated Rust field type does not expose those semantics to the struct derive. Deactivation requires a writable Boolean `is_active`/`active`. In v13 an opt-in application policy admits each request through the host's own sign-in (`NexusAuthorizer`), models may declare composite primary keys, and date/time fields bind typed values that native PostgreSQL/MySQL date and time columns accept.<br/>🟢 **`[Implemented / Opt-in Bounded]`**: a registered text `tenant` column scopes every built-in read, create, update, delete and batch operation to a trusted Core `TenantContext`; create injects the context value and missing context fails closed. `with_required_audit` transactionally couples successful mutations to a minimized fixed-schema row containing the built-in authenticated actor, optional tenant, table/action, optional known key, count, committed outcome, correlation ID, timestamp and format version; missing audit storage rolls back the mutation. The audit table is in the same relational database, mutable by its administrators, records no denied attempts, and may omit an automatically generated create key. Host identity/membership/domain policy, global-model and custom-route authorization, database privileges, schema/type compatibility, retention/backup/replication and immutable external audit delivery remain application/deployment contracts. | | **`rullst-macros`** | Procedural macros (`html!`, `#[server_function]`, `#[island]`, `#[live_component]`/`#[live_event]`, `#[memoize]`, `#[require_role]`, `#[derive(Billable)]`) and the deprecated `#[route]` compatibility marker. ORM derives live in `rullst-orm-macros`; `rullst::runtime::main` re-exports Tokio's entry-point macro. | 🟢 **`[Implemented / Bounded]`**: Compile-time `html!` escaping into composable `Markup` with explicit `RawHtml`, role/billing/live/memoize attributes, and `trybuild` diagnostics. A concrete async `#[server_function]` returning `RpcResult<T>` generates a matching explicit native router and Wasm caller over the bounded `rullst.client` v1 JSON envelope: owned Serde parameters/results, same-origin `/api/rpc/...` path, 256 KiB request/response policy, request correlation, media-type/version/schema checks, CSRF-cookie forwarding and message-free failure codes. The host must mount the route inside production security, authenticated identity, tenant, authorization and rate-limit layers; application idempotency and browser/network interoperability beyond CI are not inferred. `#[island]` hydration remains experimental. | | **`cargo-rullst`** | Developer CLI toolkit, scaffolding generators (`make:*`), three project starters (Blank, API, SaaS), AST IDOR static route scanner. | 🟢 **`[Implemented / Bounded]`**: Interactive wizard, generators, heuristic IDOR scanner, CycloneDX exporter, toolchain doctor and a fail-closed Academy evidence diagnostic that explicitly does not certify a deployment. Deterministic generation can explicitly select the starter (`--blueprint blank|api|saas`; since 13.0.0-alpha.4 the LMS, Blog, Portfolio and ERP blueprints are repository examples under `examples/`, and asking for one fails before anything is written with a report naming the example), primary database or database-free blank profile, AI, Redis and additive persistence capabilities. It deliberately fixes generated database-backed application code to Active Record and full-stack rendering to server-side `html!` plus HTMX; Repository/Data Mapper and the LiveView, Wasm Island, Pico.css and Tera foundations remain application APIs rather than equivalent v12 generator profiles. The optional storage multi-select remains public and accepts zero or more Turso/libSQL, MongoDB, DuckDB, SurrealDB and Qdrant add-ons with their distinct capability boundaries. SQLx manifests disable the umbrella and direct `rullst-orm` defaults and select one strict primary backend. Since 13.0.0-alpha.4 generated manifests also carry a `[profile.dev]` (line tables for the application, no debug information for dependencies; the release profile is unchanged), and the SaaS starter no longer declares the `rullst-connect` dependency its code does not use (`make:oauth` enables the facade's `oauth-*` feature). A structural gate retains 10 internal layouts: five directly linked public shapes and five legacy DLL regression shapes. A minimal four-case matrix still checks legacy templates and the SaaS release boundary without advertising DLL runtime support. Seven additional public-CLI profiles exercise the Blank, API and SaaS starters plus distinct database/AI/Redis/polyglot axes; the CLI-level polyglot case compiles while dedicated ORM matrices own adapter runtime evidence. `dash` uses bounded logs/input, probes application and Studio availability, observes its child process, reports configured rather than presumed-connected persistence, runs migrations asynchronously and restores terminal/process state on exit; neon motion is optional and has reduced-motion/color-free modes. In v13 `dash` also polls the application's loopback `/_rullst/dev-telemetry` feed once a second for requests/s, p50/p95 latency, 5xx errors, recent requests and, only when the application reports them, ORM operation and queue figures and a possible-N+1 panel (route, label fingerprint, count, hint); a missing feed shows a "telemetry not available" panel with enabling steps instead of numbers. `r` restarts the owned process from its current snapshot and `?` opens help, while `dev` stays plain apart from one hint line. Also in v13, `new` runs an interactive wizard only in a terminal (starter descriptions — Blank, API, SaaS — with file-tree previews from the real generators, database and optional-feature questions, a review with create/back/cancel); profile flags skip their questions, a run without a terminal requires `--default`, and `new --dry-run` prints the plan, file tree and commands without writing. `new` also initializes a Git repository like `cargo new` unless the destination is inside a work tree (`--vcs none` skips it), and `add <capability>` enables the `mail`, `auth`, `ai`, `nexus` or `studio` facade feature through a comment-preserving manifest edit, appends placeholder or `mock_*` variables to `.env.example`, prints the wiring snippet instead of editing application source, changes nothing on a second run and previews its diff with `--dry-run`. `tour` is an offline walkthrough, `doctor --json` emits `rullst.cli-doctor.v1` and exits 1 when a check fails, `completions <shell>` prints a completion script, `info [--json]` reports `rullst.cli-info.v1`, and `inspect routes --json` and `audit --json` emit `rullst.cli-routes.v1` and `rullst.cli-audit.v1`. Since 13.0.0-alpha.4 every `--json` command follows one contract (CLI reference, "Machine-readable output"): standard output is exactly one JSON document with a top-level `schema` field (`rullst.<name>.v1`; earlier documents keep their `schema_version` with the same value), human messages and failures go to standard error and exit statuses are unchanged; `inspect models --json`, `inspect schema --json` and `pkg list --json` emit `rullst.cli-models.v1`, `rullst.cli-schema.v1` and `rullst.cli-pkg.v1`. `audit --report` writes a Markdown, HTML or JSON (`rullst.cli-audit-report.v1`) static evidence report mapped to OWASP ASVS 5.0.0 Level 1, with a personal-data inventory, accessibility heuristics and a `NOT EVALUATED` list; it is not a certification and exits 1 when a check reports `FINDINGS` or `ERROR`. The Experimental `cargo rullst green` (formerly `footprint`) reports only measured figures, each with its method (release binary size, a named local image's size through `docker`/`podman image inspect`, the started app's idle Linux `VmRSS`, the `Cargo.lock` third-party package count and, with `--bench`, the app's `/proc` CPU time during a bounded closed-loop loopback load and per request); it measures no energy, gives a carbon figure only as an SCI (ISO/IEC 21031:2024) estimate labelled `estimate` from the measured CPU time, the user's `--cpu-watts` and `--grid-intensity` (and optional `--embodied`), reports unavailable values as `NOT MEASURED`, emits `rullst.green.v1` with `--json`, and is a measurement report, not a certification. `cargo rullst ai`, the terminal assistant of the earlier 13.0 prereleases, was removed in 13.0.0-alpha.4: any `cargo rullst ai …` prints its replacement and exits 1. Since 13.0.0-alpha.4 `deploy` has one path: it writes a Dockerfile, `compose.yaml` and `Caddyfile` for a Linux VM with Docker or Podman ([VM deployment](https://rullst.github.io/Rullst/book/vm-deployment.html)); the Foundry, `nixify`, Buildah and `make:grpc` generators and the Fly.io, Render and Railway targets were removed. A bare `cargo rullst` opens a context-aware home with a command palette (non-interactive runs print the commands and exit 0); failures render as one report with secrets masked, and unknown commands exit 2 with suggestions. The public development commands now use supervised process restart: coalesced source/asset/configuration changes trigger a real build, compile failures retain the current application, and successful candidates run from owned executable snapshots. A debug/development-only same-origin generation probe drives browser refresh and verifies startup identity; process state resets and shutdown is bounded. The CLI rejects legacy DLL profile generation after the Windows LMS/ORM state-split finding. The retained experimental loader is not a public v12 workflow or stable Rust ABI; see the release audit and supervised-reload tutorial. `make:chat-session` emits registered SQLx or Turso-primary models, reversible migrations and application-owned bounded chat memory; materialized contracts run persistent mock conversations on both backends and prove collision refusal. `make:billing --model` likewise emits SQLx/Turso-primary persistence plus Stripe pricing, authenticated checkout/portal and mandatory signed-webhook code; its materialized contract compiles, migrates, persists, denies cross-owner subscription mutation before customer binding and refuses existing outputs on both backends.<br/>🟢 **`[Implemented / Bounded]`**: The LMS example (`examples/lms`, generated by `--blueprint lms` until 13.0.0-alpha.4) is a small learning application: a catalog of courses, modules and lessons, an accessible lesson player, enrollment, owner-bound idempotent lesson progress, Argon2id sign-in/registration and a Nexus admin. v13 retired the complete Academy scaffold (school-scoped assessment/publication/completion, roles, leaderboard, automation/outbox/workers, in-app notifications and the privacy-request foundation) and its `--lms-modules` profiles. Its SSR catalog performs limited, ORM-parameterized title/category filtering; its pages use the Core CSP nonce without remote page dependencies and include keyboard landmarks, visible focus and reduced-motion handling. Lesson presentation distinguishes video/audio, accepts only HTTPS or root-relative media sources, requires a captions track URL for video and a bounded transcript/language for both. Its own tests (`cargo test -p rullst-lms-example`, part of the workspace suite) cover the production security baseline, catalog query bounds, progress idempotency keys and the learning service's owner boundary, not player rendering, real-browser playback or caption quality. It reloads through supervised process restart (`cargo rullst dev`); the public CLI rejects DLL hot-reload generation (the hidden legacy `--hot-reload` flag) for every starter.<br/>🟠 **`[Partial]`**: DLL hot-reload generation, complete generated frontend alternatives, full Turso-primary parity beyond Blank/API, media upload/hosting/transcoding, advanced/localized search, caption/transcript quality and localization, WCAG/browser evidence, distributed failover, PostgreSQL/MySQL isolation, visual authoring, exported telemetry and the separately operated Academy remain roadmap or release-engineering work. | The matrix omits `rullst-orm-macros`, which ships the ORM derives described in section 5, and the Experimental v13 crates `rullst-privacy`, `rullst-supervision`, `rullst-media` and `rullst-labs` (all four in the release inventory and first published in the 13.0.0-alpha.1 prerelease); the v13 sections below specify their boundaries. ### v13 roadmap package boundaries The [`rullst-labs`](https://rullst.github.io/Rullst/book/rullst-labs-roadmap.html) library is an implementation candidate in the 13.0 release inventory, first published in the `13.0.0-alpha.1` prerelease; final release acceptance remains outstanding. It is a **bring-your-own-runner** library: it owns trusted, versioned orchestration and grading contracts and never executes learner code. The separately deployed `rullst-labs-runner` candidate (a Linux Rust-to-Wasm/Wasmi executor that passed source admission in PR #228, with [recorded hosted evidence](https://rullst.github.io/Rullst/book/labs-first-profile.html#recorded-linux-acceptance)) was removed from the workspace for 13.0; its source remains in git history. Applications deploy their own runner against the documented [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html). Neither `rullst-labs` nor any runner may become a default framework dependency, execute learner code inside the HTTP process, or require the application to expose a container control socket. A complete offensive CTF arena is external, separately governed deployment infrastructure even when it uses Rullst identity, challenge, score and receipt contracts. Protocol version 1 defines one profile: a bounded Rust pure-function exercise (`solve(i64, i64) -> i64`) whose identifiers name a pinned Rust toolchain, import-free WebAssembly and a pinned Wasmi interpreter. This is not a native Rullst server, Cargo dependency, WASI or arbitrary shell profile. `rullst-labs` must not depend on an executor or spawn submitted code; the workspace boundary check rejects an execution-engine dependency in any member. Its default surface is validated contracts; opt-in shared-local SQLite stores dedicated, encrypted job content with current application authorization, idempotent submission, cancellation, leased execution, retention and result reconciliation. It must not reuse the application's authentication/database secrets as job keys. Keep both existing 90% line-coverage floors; `rullst-labs` remains in the framework-library aggregate. The application-owned runner's trusted controller accesses only that dedicated job plane, its receipt signing seed and runner-owned tools. It must never give submitted code the job database, signing keys, expected answers, application secrets, inherited environment, network or control sockets. Keep the content key and signing seed in owned private regular files under protected ancestors, and validate the opened no-follow descriptor and exact key length. Execution isolation, resource limits, observed enforcement and teardown are host responsibilities: `rullst-labs` binds pinned tool digests and verifies signed receipts but cannot check that isolation happened. Unsupported environments must fail closed without a less restrictive execution fallback. The removed candidate's [first-profile threat model](https://rullst.github.io/Rullst/book/labs-first-profile.html) records one reviewed Linux design (delegated cgroups v2, unprivileged namespaces, seccomp, separate Landlock domains and observed probes) as a reference baseline. Only an integrity-bound result matching the current job/lease, tenant, learner, exercise, grader, toolchain, source and execution profile may become a grade. Expected answers remain in the trusted grader; worker outputs are bounded data, not a passing-grade authority. Cancellation/expiry fence late results; worker loss never means success. Offline simulation is explicit and cannot establish execution evidence. Authorized course maintenance must expire queued submissions and remove their source without requiring a supported/available executor. It must not clear a running lease or infer teardown. Withdrawn exercise revisions may be removed only after every referencing job has been purged; immutable revision identifiers must not be reused after removal. Independent isolation review of the deployed runner and the roadmap's adversarial acceptance remain required before any production-ready untrusted-code claim. ### v13 managed-video implementation boundary The optional `rullst-media` candidate, first published in the 13.0.0-alpha.1 prerelease, owns managed private video, starting with Bunny Stream. This differs from Core's object-storage facade: remote video creation, upload capabilities, asynchronous processing, playback grants and lifecycle reconciliation require a separate explicit domain. It has no Core/Auth/ORM runtime dependency and no default network/database features. Core is a test-only dependency for the real HTTP/browser security composition. `bunny` selects the reviewed HTTP/signature adapter; `sqlite` selects durable shared-local asset/operation state and static-dispatch application orchestration. PRs #227 and #239 supply hosted source and extracted-package evidence. The standalone package now enters the v13 release inventory, without a facade dependency. It was first published in the 13.0.0-alpha.1 prerelease; final release acceptance remains outstanding. Actual Bunny account/CDN interoperability remains explicitly unvalidated. The host supplies authenticated actor/course membership and current management or playback entitlement through a checked authorization trait. Local asset IDs bind an immutable tenant/course/provider-library scope; provider IDs and signed webhooks never establish that ownership. Service operations recheck permission and local revision/state after external work and use bounded durable leases to reject concurrent/stale mutation results. Playback is a read: it takes no lease, so viewers never wait on one another's provider reads and an abandoned request leaves no intent; its short local transactions still serialize, and a lock or connection wait over 5 seconds is `Busy`; it fences withdrawal/deletion in the transaction that issues the grant. SQLite state binds its schema, provider mode, library, store-wide capacity and an optional per-tenant asset quota for shared stores, refuses clock rollback and requires trusted local files, backup policy and operator-owned keys. Multi-host replication is separate work. Only confirmed-deleted or explicitly discarded local tombstones may be purged, in batches up to 100 and after at least 24 hours; the host must retire purged creation IDs because their idempotency memory ends at that point. Tombstones keep no digest of the deleted title or description. Provider backups/cache erasure is separate. Creation is journaled before remote dispatch. Bunny's documented creation API does not supply an idempotency key: ambiguous creation must reconcile a persisted random opaque creation marker, never blindly retry or claim exactly-once remote creation. Updates/deletion are reconciled against authoritative reads; a metadata update writes and verifies the same documented `description` meta tag, never Bunny's separate top-level Description that Smart Generate may rewrite. A create or update that fails for a non-transient reason (definitive refusal, unconfirmed marker search, missing remote video, full tag list or failed verification) stops with a bounded non-secret reason instead of staying pending; it is never retried automatically and an authorized host explicitly retries or discards it. Retrying an unconfirmed creation repeats only the marker search; discarding a creation leaves a local tombstone and may orphan an undiscovered remote video. Webhook v1 authenticates exact body bytes with the read-only library key but has no signed timestamp; bounded durable duplicate suppression and serialized provider refresh prevent replay/reordering from granting access or publishing assets. Webhook status numbers and API video status numbers are distinct protocols. Publication is an explicit application mutation after current provider readiness; processing completion alone cannot publish. Playback requires current entitlement, fresh ready state and configured provider protection; withdrawal/deletion stop new grants but cannot immediately invalidate previously issued bearer tokens or provider caches. Upload grants are scoped and expiring, not cryptographic proofs of a one-shot upload, file type or byte limit. Library upload quotas and direct file protection remain required configuration. Embed SHA-256, TUS SHA-256 and CDN advanced HMAC-SHA256 directory tokens have independent documented formats. Redirects/ambient proxies/arbitrary remote endpoints are disabled; request, response, retry and total-operation budgets are finite. Secret-bearing errors, credentials, grants and debug output must not leak keys or bearer URLs. Empty or `mock_*` credentials select deterministic offline behavior; mixed modes fail configuration. Production rejects mock or loopback-fixture capabilities. Automated protocol/browser/disposable-state evidence is required. The owner's no-live-account-testing instruction remains in force: actual Bunny account, transcoding/CDN interoperability and paid DRM are not validated or implied. See the [supported delivery target](https://rullst.github.io/Rullst/book/managed-video-roadmap.html). The experimental `s3` feature adds S3-compatible object storage (AWS S3, Cloudflare R2, MinIO) behind the same provider, grant and SQLite contracts. It stores and serves the original object without transcoding. Keys are server-generated; uploads use host-authorized presigned `PUT` grants bound to a declared content type and exact length, playback uses presigned `GET` (`PlaybackKind::Original`), both expire within 900 seconds, and `HEAD` confirms size and type before an asset becomes ready. Signing reuses Core's `aws-sigv4` signer, not a second S3 SDK. It is validated against the offline mock and a loopback protocol fixture only; provider interoperability is not yet validated. See [S3-compatible object storage](https://rullst.github.io/Rullst/book/managed-video.html#s3-compatible-object-storage-s3-r2-minio). ### v13 privacy and age-assurance boundary Core and Security header layers share `apply_referrer_policy`: a response that explicitly supplies a canonical `no-referrer` value retains that restriction through composition, normalized to one header even if duplicate values exist. The Security layers replace other values with their configured policy; missing/invalid optional Security configuration retains its existing behavior. The Core baseline adds each security header, including `Referrer-Policy`, only when the response has none, so an explicit application value wins over it, and it leaves every security header alone on a response that an inner `SecureHeadersLayer` marked with `SecurityHeadersApplied` (omitted headers stay absent; `Cache-Control: no-store` and the shared `CspNonce` are unchanged). This narrow rule does not attempt to order every Referrer-Policy value. `rullst-privacy` is an opt-in v13 release candidate, first published in the 13.0.0-alpha.1 prerelease. Its initial `age-assurance` feature owns bounded risk policies, server-issued challenges, minimal signed age attestations, explicit decisions and replay-store contracts. Low-risk declarations, facial estimates and verified age attributes have different assurance semantics; configured policy determines their eligibility. No camera, image retention, external inference, database or Core dependency is enabled implicitly. Production must reject offline mock evidence and process-local replay protection. The host owns authentication, tenant/subject binding, risk/legal assessment and durable shared state. The v13 persistence contract uses static-dispatch asynchronous nonce claims. Verification samples a trusted server clock before validation and after the claim; expiry or clock rollback during storage cannot return permission. A claim that the high-water check rejected only because a concurrent request committed a later second is repeated, at most twice, with a fresh sample that has advanced past the rejected one; the check itself is never relaxed. The optional `sqlite` adapter is shared-local only: a private file-backed pool, WAL with full synchronization, serialized quota/expiry/claim transactions and persisted configuration/clock high-water state. It stores only domain-separated nonce digests and expiry. No unexpired claim may be evicted. Cancellation or an uncertain commit returns no assessment; the host must request fresh evidence when consumption is uncertain. File custody, clock synchronization and storage durability are deployment obligations. Restoring an older database requires quiescing verification and invalidating all outstanding challenges through a new policy/key epoch; SQLite cannot detect arbitrary backup rollback. Network filesystems and multi-host replication are outside this adapter's boundary. The v13 PostgreSQL adapter uses a separately enabled `postgres` feature and a private bounded pool against one authoritative writable database. Explicit deployment initialization creates the fixed `rullst_age_replay` schema; normal connection never creates or repairs missing state. Transactional startup locking and a metadata row lock serialize schema initialization and quota/expiry/nonce claims across application hosts. Metadata persists the schema version, capacity and accepted clock high-water mark. Tables must be permanent and WAL-logged; `fsync`, full-page writes and synchronous commit are required. Claims store only the same domain-separated nonce digest and expiry as SQLite. Remote connections require certificate/hostname-verified TLS; loopback and local sockets support disposable development databases. Errors must not expose connection credentials. Bounded pool acquisition, statement/lock waits and idle transactions prevent unbounded database waits; the caller still owns an overall request deadline. All verifiers must use that same authoritative database and synchronized trusted clocks. Replication configuration, failover fencing, durable hardware and backup restore remain operator obligations; asynchronous replica promotion or database rollback cannot be advertised as preserving consumed proofs automatically. The adapter's focused real-database tests cover concurrent initialization and claims, a restricted runtime role, quota, schema/durability drift, cancellation, clock changes during a row-lock wait and server restart. Combined hosted acceptance and deployment-specific failover/TLS evidence remain separate. This first contract does not implement a facial model, vendor transport, guardian verification or global privacy compliance. The [privacy and age-assurance roadmap](https://rullst.github.io/Rullst/book/privacy-age-assurance-roadmap.html) defines the remaining consent, rights, retention, provider and jurisdiction work. The owner-approved 13.0.0 delivery scope prioritizes the independent foundation and an authenticated first-party declaration journey for policies that permit that assurance. An external provider is optional and requires separate native protocol and sandbox acceptance before any live claim. A local facial engine is follow-up work. Neither an absent provider nor a failed check may downgrade the required assurance or make a declaration authorize a stronger-policy action. Publication of the privacy package still requires explicit acceptance of its advertised API, durable state, consumer behavior and release configuration. The release inventory (`.github/release-order.json`, 20 packages) lists this independent package before the umbrella. The facade exposes only explicit `privacy-*` features and `rullst::privacy`; default builds acquire no privacy, age, consent or database dependency from this addition. CLI age/privacy consumers select the matching registry version by default, with an explicit matching local source override for development. Existing dependency sources, versions and features must be checked before composing two consumers; no silent source switch is permitted. Both commands refresh the bounded project context after scaffolding, reporting a refresh failure without concealing the completed source edits. Archive-only acceptance must install the packaged CLI and compile generated SaaS consumers with both opt-ins and no workspace source paths. The native `DeclarationGate` contract processes an authenticated first-party `AgeDeclaration` without requiring an external issuer or a signing key for the declaration itself. It accepts only a retained server-issued `SelfDeclaration` challenge under the exact current policy and tenant/subject/session/action binding. It shares the signed verifier's durable one-use claim and post-storage expiry/clock checks. An affirmative answer can return only `Assurance::Declared`; negative or declined answers deny the operation. An estimated/verified method or stronger policy cannot be downgraded through this entry point. The host owns authentication, CSRF protection, explicit user choice and trusted challenge retention/transport. This contract is not a determination of the person's age. The optional `challenge-tokens` transport uses a bounded, versioned, HMAC-SHA256-authenticated server challenge with an explicit active key and at most seven previous verification keys. It authenticates the version, key ID and exact encoded payload before decoding challenge JSON. Opening revalidates the current policy, authenticated binding, clock and exact configured lifetime. There is no client-supplied algorithm or key discovery. Tokens contain only the existing opaque challenge references and are authenticated, not encrypted; applications must not put cookies, email addresses or document numbers into those references. A token restores a server-issued challenge, never an age decision or an authorization grant. Production still consumes its nonce in the shared durable replay store. The host owns independent secret provisioning, rotation/retirement, TLS, CSRF and endpoint limits; no external age provider is needed for this transport. The v13 `make:age-gate` consumer is an explicit opt-in for the recognized SaaS starter. It protects the existing dashboard action with an authenticated GET challenge and POST declaration; it never sets a reusable age-verified account flag. Both routes retain the starter's authentication, CSRF, headers and Server baseline. Policy version and threshold are chosen explicitly at generation time; the SaaS profile also requires its fixed application tenant. Low-assurance declarations are identified as such. Opaque binding references are domain-separated keyed digests of server-resolved user, tenant and authenticated session, never request-supplied identities. Application keys and replay storage are mandatory; initialization, timeout, stale context or unknown/negative answers cannot grant access. SQLite and PostgreSQL are explicit profiles with their existing deployment obligations. Generation refuses unknown or already-modified route shapes before writing. This command selects the matching registry dependency unless the caller explicitly supplies a matching local source (`--privacy-source`). Other blueprint adapters, provider flows, persistent age permissions and deployed browser acceptance remain separate work. v13 removed this generator's LMS target together with the complete Academy scaffold; the LMS example (the former LMS starter) has no school membership to bind. The v13 fuzz inventory adds two isolated privacy targets for authenticated challenge transport and signed attestations. Deterministic fuzz-only keys permit mutation beyond signature validation; assertions bind accepted results to the current context, assurance and one-use replay contract. These targets enable no database or network integration. They expand v13's required inventory to 42; the immutable v12 release line retains 40. Short diagnostics are not full release evidence, and the durable database lifecycle keeps its separate acceptance gates. The v13 `consent` contract is independent of age assurance and concerns only optional processing that the operator has assigned to an explicit purpose and notice version. Absence, refusal, withdrawal, version mismatch and expiry deny processing. An affirmative choice uses the exact observed revision; withdrawal atomically advances that revision even when its form is stale, preventing an earlier affirmative form from undoing it. The authenticated subject and tenant are server-owned inputs, never browser identity fields. The explicit submission also binds the displayed purpose/notice version to current server configuration, so a policy change cannot borrow an old form's unchanged revision. A bounded store must serialize reads/updates against a persisted clock high-water mark and preserve withdrawal tombstones; production rejects process-local state. The gate retries an operation that lost the lock to a later second the same bounded way. Permission must be checked immediately before each processing action, including deferred jobs. Already-started external effects are not cancelled retroactively by a later withdrawal, and consent does not authorize essential processing or establish age/guardian authority. The optional `consent-sqlite` adapter initializes a new local file explicitly; ordinary opening never creates or repairs missing state. It serializes reads/updates with `BEGIN IMMEDIATE`, WAL/full synchronization, a bounded private pool, immutable quota and persisted clock metadata. It retains scope digests, latest choices/versions/revisions and timestamps, without raw subject IDs. Those digests are pseudonymous data. Expired records and withdrawal tombstones are never evicted to create capacity. All processes must use that same trusted local file; multi-host replication is unsupported. Restoring stale state requires quiesced processing, reconciled withdrawals and fresh purpose versions. Deployment retention/restore review and hosted acceptance remain required before release admission. The owner-selected PostgreSQL consent increment passed combined source admission in PR #239; final release admission remains separate. Its separate `consent-postgres` feature exposes `PostgresConsentStore::initialize` for deployment bootstrap and `connect` for ordinary runtime access. Both accept a connection string and immutable capacity. One authoritative writable database owns the fixed `rullst_consent` schema. Explicit initialization serializes concurrent bootstrap without resetting existing state. A private bounded pool enforces verified TLS for remote TCP, fixed name resolution, synchronous commits and bounded acquisition/SQL waits. Every read/update validates permanent WAL-logged tables and locks the metadata row, serializing clock observations, quota and revision changes across hosts. Withdrawals remain durable tombstones; no expiry/capacity cleanup removes them. Stored subjects remain domain-separated digests. Replication fencing, clock synchronization, hardware durability and stale-backup reconciliation remain deployment obligations. The facade feature `privacy-consent-postgres` must not enable age assurance or add a database dependency to default builds. The existing CLI privacy consumer remains explicitly SQLite until separately extended; an ordinary typed application can use the PostgreSQL store directly. Disposable-database concurrency, restricted-role, cancellation, clock, restart, consumer and hosted acceptance are required before completion is claimed. The opt-in `make:privacy` consumer mounts authenticated preferences, an explicitly optional personalized greeting and an own-account JSON export inside the starter's existing CSRF/header/security boundary. It composes with the age consumer while preserving both consumers' dependency features. Choice forms bind the displayed notice/version and current revision, with a bounded expiring HMAC proof tied to the authenticated account, tenant and session; an old tab cannot apply a choice after switching accounts or sessions. Identity comes only from the authenticated user and the fixed application tenant. The export projects only account ID, name and email with explicit output bounds; it exposes no credential fields, produces no public artifact and does not mark broader queued rights requests as fulfilled. Its direct private response needs no retained export file and remains independent of optional-consent storage availability. Refusal/withdrawal changes the next greeting to generic content. Local materialized SaaS fixtures cover both age/privacy installation orders and both dependency sources, real authenticated profile queries, explicit choices, withdrawal/stale forms, session/account changes, CSRF, expiry, bounded input and missing or failed state. A normal SaaS using PostgreSQL as its primary database compiles; this is not live PostgreSQL consumer or browser/deployment acceptance. Bootstrap explicitly initializes a new private consent file; ordinary opening never recreates it. The generated consent profile remains shared-local SQLite only. The separate [SaaS triage](https://rullst.github.io/Rullst/book/saas-v12-1-v13-triage.html) assigns the examples' reported defects to compatible v12.1 maintenance and v13 contracts; it is not fix evidence. ### v13 source-train and adoption preparation The development train uses one version, currently `13.0.0-alpha.4`, for every publishable package, and exact (`=13.0.0-alpha.4`) internal requirements, so Cargo cannot pair packages from different prereleases. This prepares the CLI's actual major-version behavior; it is not a publication or stable-release claim. The privacy package joins the candidate inventory with explicit optional facade features; it was first published in the 13.0.0-alpha.1 prerelease on October 11, 2026. The `rullst-upgrade-rules-v4` migration catalog recognizes source majors 12 and 13 and carries the v12 → v13 source rules (see the assisted upgrade contract); v5/v6 applications, including their v11-era ecosystem crates, first upgrade to v12 with the v12 CLI. It keeps exact target-major CLI selection and rejects downgrades. Preparations from a previous catalog require fresh preparation and verification. The [12.1-to-13 source inventory](https://rullst.github.io/Rullst/book/migration-v13.html) adds opt-in privacy APIs, consumer generators and security header composition, and lists the changed APIs and behavior that the catalog reports as `must-change` or `review` findings. Automatic preparation remains limited to supported dependency manifests and the existing compiler/check/test workflow; it must not invent code rewrites, enable age or consent policies, bootstrap privacy state, replace authentication or deploy an application. Source fixes stay with the developer, guided by each finding's migration row (section 12). Migration evidence must distinguish small offline updater protocol fixtures from generated SaaS consumers and the workspace examples compiled against actual framework source. Release-time inventory and package checks must revisit this boundary as more v13 changes land. 13.0 intentionally removes supported commands, features and providers, among them `make:omni`, `cargo rullst omni` and the `offline-sync` feature (section 13); the Foundry, Nix, Buildah and gRPC generators and the Fly, Render and Railway deploy targets; the self-updater's binary replacement (section 12); `cargo rullst ai` (section 11.3); the Capital providers of section 6.4 and the Mail transports of the v13 Mail provider scope below; and the LMS, Blog, Portfolio and ERP starters, now applications in `examples/`. The [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html) lists every break; retain each in the migration inventory. ### v13 formal-verification pilot boundary The [Verus pilot](https://rullst.github.io/Rullst/book/verus-roadmap.html) has a locally verified candidate for production age-policy method decisions, with later evaluation of Auth authorization predicates and Capital integer money calculations. Specifications must remain linked to the executable implementation, with explicit trusted assumptions and external contracts. No dedicated public crate is proposed. A pinned, isolated verifier and manual workflow precede any required v13 check; compatibility, reproducible proofs, negative controls and measured CI cost are promotion criteria. This plan adds no v12.1 release gate or framework-wide correctness claim. The first implementation candidate verifies the existing `AgePolicy::permits` body with its actual enum variants and policy fields, extracted from Rust syntax. No public API or production dependency changes are required. Extraction must check the reviewed signatures/types and original structural equality derives, copy the executable body unchanged and record source/type/body fingerprints. Only verifier annotations, structural-equality support, a specification accessor and an erased reveal step may be added to the projection. The proof covers all nine risk/method combinations without narrowing valid runtime inputs; it does not prove age evidence, policy validation, timestamps, storage or authorization. The isolated verifier must reject weakened restricted/elevated policies and a denied low-risk policy as negative controls. This is a bounded pilot candidate, not a promotion into required release checks. ### v13 shared passkey ceremony increment The next Auth increment follows the [shared ceremony contract](https://rullst.github.io/Rullst/book/shared-passkey-ceremonies.html). Keep the existing synchronous process-local API compatible. Add a separate generic shared manager in `rullst-auth`, backed first by opt-in authoritative PostgreSQL, with tenant/account/session/RP binding, bounded expiry/quotas, atomic single use and fail-closed outage/cancellation. Reuse private cryptographic validation; do not expose a stateless acceptance bypass. Application credential ownership, revocation and signature-counter CAS remain mandatory before session issuance. Real independent-manager/database/process and HTTP/browser acceptance must precede an implemented claim. No new crate or broad blueprint expansion is required. ### v13 deployment acceptance increment Before expanding deployment generators, exercise two independent application processes behind one digest-pinned existing reverse proxy. Keep readiness, unhealthy-node exclusion, bounded shutdown, forwarded identity and WebSocket behavior explicit. Ordinary HTTP admission must remain counted until the response body completes, errors or is dropped, including streaming data and trailers; returning response headers alone is not completion. This counts application body lifetime, not client receipt or TCP acknowledgement. Upgraded connections and background tasks require separate application-owned termination. Prove the body boundary locally before using it in deployment acceptance. No new gateway crate, automatic host administration or general availability guarantee is introduced. ### v13 local deployment diagnostic `cargo rullst deploy:doctor` is an offline, read-only command of the main CLI. It inspects the selected bounded `Rullst.toml` snapshot (`--config`, or documented defaults) and optionally one explicit literal environment file (`--env-file`) or the three allowlisted process variables `RULLST_ENV`, `APP_ENV`, `APP_KEY` (`--process-env`), against a `--target` of `production` (the default) or `staging`. Never silently combine environment sources or search parent dotenv files. Reuse Core environment resolution and security validation; report obvious application-key mistakes separately from unobserved key generation/custody. Config validity cannot prove mounted middleware, webhook verification, proxy trust, shared state, TLS, backups or deployed behavior. Emit deterministic text/JSON with fixed check codes, source categories, actionable messages and explicit uninspected controls. Do not print values, input paths, parser details or secret-derived hashes. Reject malformed, duplicate, oversized, linked/special-file and unsupported interpolated inputs without executing them. No subprocess, network access, configuration/global-environment mutation or automatic repair belongs in this command. Test the installed command with real generated configuration and negative inputs; retain full hosted acceptance. ### v13 supervision observation extension The owner explicitly requested the reusable proctoring base on September 20. Extend `rullst-supervision`, not a second proctoring crate. Keep legacy visibility APIs working, but new sessions must persist an exact selected collection set and require matching acknowledgement at start/resume. Default selection remains page visibility. New browser event kinds are focus, copy/cut/paste occurrence and fullscreen transitions, never clipboard content or other-window inventories. Optional camera/microphone/screen capture-status reports are distinct from optional camera-presence/audio-activity adapter observations; preserve provenance and inconclusive/unavailable outcomes without a misconduct score. Add bounded, typed, statically dispatched analysis interfaces. Permission and active session/tenant/subject/revision must be checked before analysis and again before storing its result. Keep borrowed media samples bounded and out of durable event state/logs. No camera/audio model, identity inference, hidden recording, grading or automated accusation is introduced. Selected capabilities do not establish browser permission or prove media belongs to the learner; the host must bind capture to the authenticated session and implement adapter cancellation. The SQLite candidate moved to schema v2 before its first publication, with explicit rejection of old schemas. Never silently rewrite a v1 database: preserve it for review and require a fresh independently named store/epoch and authority provisioning for this prepublication transition. Session pause/end/expiry and policy revisions must reject pending results. Retain all existing capacity, sequence, clock, reviewer authorization and retention invariants. Expose new browser collection only by explicit application selection (the removed `make:supervision` generator selected it by CLI option) and a matching visible notice. The collector must bound its queue and stop on session control, expiry or failure; it must not prevent copying, request fullscreen/capture or start media analysis automatically. The crate's SQLite tests cover local adapter faults, disallowed capabilities, persistence, source attribution and legacy visibility compatibility; the extension passed hosted admission in PR #226. ### Conditional v13 supervision crate The owner requested transparent learner/exam supervision and parental controls as the first additional priority after the required v13 deliveries have been implemented and validated. `rullst-supervision` is now an implementation candidate, first published in the 13.0.0-alpha.1 prerelease, with bounded domain contracts and shared-local SQLite state. Its local generated-consumer and Chromium journeys passed before v13 removed that generated LMS consumer (`make:supervision`). The baseline also passed hosted checks and archive rehearsal in PR #222; the observation extension passed PR #226 and its retrospective archive check. The combined source passed PR #239. The package now enters the v13 release inventory with no default facade dependency. It was first published in the 13.0.0-alpha.1 prerelease on October 11, 2026; final release acceptance remains outstanding. Keep supervision policy/session/event and reviewer-access contracts separate from `rullst-privacy` age and consent primitives. Exam supervision and parental controls need distinct modules and authorization policies. The initial candidate had to deliver one real generated LMS journey (retired in v13 with the complete Academy scaffold; applications now integrate the crate directly), visible session status, explicit permissions, revocation, bounded collection/retention and actual application-side enforcement, with cross-school/subject and unauthorized-reviewer negatives. Parental time/content restrictions initially concern this application only. The host must establish guardian/reviewer authority independently; age results, account ownership, a checkbox or a claimed family relationship do not prove it. Browser observations are untrusted client reports, never proof of misconduct, identity or an automatic reason to change grades or impose a penalty. Collect only the documented minimal events with visible active/paused/ended state; do not add covert camera/microphone/location capture or unrelated browsing data. Native device-wide controls, camera inference and managed operating-system agents need separate platform integration and acceptance. Global legal compliance is not inferred from these controls. The exact API/storage boundary and its threat model must be specified before scaffolding the crate. Empty contracts or a mock alone do not meet its admission criteria, and it must not delay required release gates; retain it for a subsequent v13 release if capacity is insufficient. The initial implementation contract is now the [bounded supervision design](https://rullst.github.io/Rullst/book/supervision.html). Use a separate package with optional `exam`, `parental`, `sqlite` and `analysis` features, no Core/default dependency, and a concrete SQLite adapter generic over a trusted clock. Tests use real local SQLite and an injected deterministic clock; no memory mock is needed for this initial backend. One private initialized database owns scoped authority, sessions, parental enrollment/policy and allowlisted events. Serialize operations with `BEGIN IMMEDIATE`, persist configuration, a clock high-water mark and a global revision counter, and enforce bounded quotas and retention. One learner has bounded retained-session and unexpired-event quotas inside the store-wide limits, and rows already past retention never block admission. An opener must supply the independently retained deployment epoch. This detects epoch mismatch, not restoration of an old database with the same epoch. v13 removed `make:supervision` together with the complete Academy LMS it targeted. Applications depend on the crate directly with an exact version and default features disabled; the removed generator remains in the git history. Only trusted operator provisioning can establish expiring `ExamReview` or `ParentalManage` authority after independent relationship verification. The host binds current authenticated identity, school membership and resource access; the store rechecks its own scoped grants atomically on each operation. Those two authorization layers are not one transaction unless the host composes them that way. In-flight results cannot be recalled after external membership changes. Session start/resume requires explicit current policy/notice acknowledgement; pause/end and policy changes use exact revisions. Events are only visible/hidden page reports with an exact next client sequence and server receipt time. Their absence or contents never influence grading. Parental enrollment is operator managed: an enrolled subject with absent, expired or denied policy is denied; an unenrolled subject retains existing application authorization. Revoking a manager does not remove the subject's restrictions. The integrating application must enforce them on its original lesson/progress routes; the removed generated opt-in did so for the Academy scaffold. ### Versioned release-branch boundary Release policy schema 4 binds major 13 to development `main` and major 12 to maintenance `v12`. Schemas 2 and 3 retain their historical bindings; no existing tag or receipt is reinterpreted. The former transitional `v13` integration branch (schema 3) was promoted to `main` and retired. The source-admission gate requires a canonical matching tag, the exact checked-out/tagged commit at the current protected branch head, and every publishable inventory package at the tagged version before artifact builds. All declared automatic release workflows accept `main` and `v12`; their triggers still list the retired `v13` ref. Required manual/native/security evidence and the protected crates.io approval remain separate mandatory gates. Archive evidence requires the named archive job to succeed on the exact candidate; a green workflow with that job skipped is insufficient. Incompatible manual package selectors must fail explicitly. Observational CI reports may describe failed checks, but must stop on cancellation so an obsolete run cannot retain the concurrency slot needed by its replacement. Fuzz evidence must come from the candidate's release line and a source carrying that same policy; v12 results cannot be credited to v13 merely because they are recent. The existing immutable v12 tags retain their original workflow and policy. ### Stateless login cookie and session epoch (v13) `make_login_cookie` issues a stateless `rullst_session` cookie: a `v1.` AES-256-GCM token bound to `APP_KEY` that holds the user id and an expiry 30 days ahead. The server keeps no session list. `make_logout_cookie` only asks the browser to delete its copy, so a copied cookie stays valid until it expires or `APP_KEY` changes, which signs out every user, and a password change does not end other sessions. v13 keeps the format, lifetime and validity of these cookies. Revocation is opt-in and additive. `make_login_cookie_with_epoch(user_id, epoch, max_age)` and `encrypt_session_with_epoch` issue `v2.` tokens whose payload `user_id|epoch|expires_at` is authenticated with its own associated data, so neither version can be relabelled as the other. `max_age` counts whole seconds from 1 second to 400 days and sets both the token expiry and the cookie `Max-Age`. `decrypt_session_claims` returns `SessionClaims` for `v2.` tokens and, with epoch 0, for `v1.` tokens; tokens over 4096 bytes, unversioned, tampered, wrongly keyed, malformed or expired tokens fail, and a token remains valid through its `expires_at` second. `SessionClaims::verify_epoch` compares the token's epoch with the stored value in constant time and returns `AuthError::SessionRevoked` on a mismatch. `decrypt_session` rejects `v2.` tokens, so a path that still uses it fails closed instead of skipping the epoch check. `rullst-auth` has no session middleware: the application stores the epoch per user, checks it on every request and increments it atomically for sign-out everywhere, a password change or reset, and account deactivation. For an application that keeps its own users table, v13 adds single-use password-reset token primitives. `PasswordResetToken::generate` draws 32 bytes from the operating system's CSPRNG and encodes them as 64 lowercase hex characters (which the mail pipeline's secret redaction leaves intact in a `token=` link); `Debug` redacts it and its memory is zeroed on drop. Only `digest()`, a domain-separated SHA-256 (`rullst.auth.password-reset.v1`) in lowercase hex, is stored. `password_reset_token_digest` returns `None` for any submitted value that is not 64 lowercase hex characters, so malformed input never reaches a lookup, and `password_reset_digests_match` compares a stored digest in constant time and never matches a stored value that is not a 64-character digest. Expiry, single use and one live token per account are rules of the application's store. The `cargo rullst auth` and SaaS scaffolds (and the LMS example) store `users.session_epoch` (`BIGINT NOT NULL DEFAULT 0`, excluded from ORM writes with `#[orm(db_default)]` so a save cannot undo a bump), issue epoch cookies at sign-in and registration, refuse a cookie with another epoch in `auth_middleware`, and add `POST /logout/everywhere`, which increments the epoch in one `UPDATE` and clears the browser cookie. Plain logout still clears only the current browser. Revoking one device and listing sessions require server-side sessions (`SqlRecoveryStore`). The same scaffolds generate a password change and a password reset by email (`src/controllers/password_controller.rs`, `src/models/password_reset.rs` and a `password_reset_tokens` migration) and enable the `mail` feature. Every submission is behind CSRF and the per-client `credential_rate_limit`, and every new password follows the registration policy (12 characters to 72 bytes) and must match its confirmation. - `GET`/`POST /password/change`, behind `auth_middleware`, verifies the current password with Argon2id on the bounded blocking pool (an account without a password hash never passes) and answers a wrong one with the generic "The current password is incorrect". `User::change_password` stores the new hash and increments the epoch in one `UPDATE` that applies only while the account is still at the epoch of the asking cookie; that browser receives a cookie with the new epoch, so only it stays signed in, and the account's reset token is deleted, so a link requested before the change no longer works. A cookie revoked meanwhile changes nothing and is sent to `/login`. - `POST /password/forgot` answers with the same page, after at least 400 ms, whether or not the email has an account. For an account it issues a `PasswordResetToken` and stores only its digest with Unix-second expiry and creation times in `password_reset_tokens`, whose primary key is `user_id`: one live token per account, replaced by a new request, except that a request less than 60 seconds after the previous one for the same account issues and sends nothing. Tokens expire after `RESET_TOKEN_LIFETIME_MINUTES` of `src/models/password_reset.rs` (30 minutes as generated), which a compile-time assertion bounds to 10 minutes through 24 hours; the stored expiry, the reset email ("This link expires in 30 minutes.", or "2 hours" for whole hours) and the forgot-password answer all state that one value (from 13.0.0-alpha.3; alpha.2 hard-coded "30 minutes" in the answer and `AccountMail` refused more than 30). The link is `RULLST_PUBLIC_ORIGIN/password/reset?token=…` (process environment, then `.env`), validated by `ActionLink` and never derived from the request; when the origin is unset, development and test use `http://localhost:3000`, and staging and production log an error and send nothing. `AccountMail` delivers it through the `Mail` facade in a background task, so delivery never delays the answer. In a debug build in development without `MAIL_DRIVER`, where the default log driver records only metadata, the link is also printed to standard output. - `GET /password/reset?token=…` shows the form only for a live token, with `Cache-Control: no-store` and `Referrer-Policy: no-referrer`. `POST /password/reset` looks the digest up before any Argon2 work, then, in one transaction, deletes the token only while it is still live, stores the new hash and increments the epoch; a malformed, unknown, expired, used or replaced token changes nothing. A completed reset signs nobody in: it redirects to `/login?reset=done`, which shows a notice. - A completed change or reset emails a "password changed" notice. Deleting an account leaves its reset token row, which no longer redeems. - Account emails are written in the language of the request that caused them: the `rullst::i18n::Locale` that `LOCALES.layer()` negotiated when the router has one, else the `Accept-Language` header, each matched by language to the `MailLocale` set (English, Brazilian Portuguese, Spanish), else the editable `DEFAULT_MAIL_LOCALE` (English). The product name in their subject and heading is `APP_NAME` (process environment, then `.env`) or the editable `APPLICATION_NAME` constant, whose generated value is the package name. `cargo rullst make:oauth google|github|microsoft|apple` (v13, from 13.0.0-alpha.3) adds sign-in with that provider to the same account model without `tower-sessions`; each provider is one run, and the first creates the shared flow, the `oauth_identities` table and its only migration. `GET /auth/<provider>` and the callback sit behind CSRF and the password login's `credential_rate_limit`; the login challenge (state, PKCE verifier, OIDC nonce, provider, same-site return path) is stored with `rullst_connect::challenge` in `rullst_connect_login_challenges` of the application database, keyed by a digest of an opaque 256-bit id that is the only value in the `rullst_oauth_challenge` cookie (`Path=/auth/<provider>`, ten minutes, `HttpOnly`, `SameSite=Lax`, `Secure` in staging and production and on HTTPS). The callback consumes the challenge atomically before validating state, PKCE and, for OpenID Connect, the ID token and its nonce. A provider subject already in `oauth_identities` (provider and subject unique, user id cascading) signs in. Otherwise only an email the provider verified is used: an unknown one creates a password-less account linked to the subject, and one that already belongs to an account (including one that signed in with another provider) is refused unless the application switches `EXISTING_ACCOUNT_POLICY` to `LinkAndResetPassword`, which links it, removes the password and increments the session epoch in one transaction. Subjects and verified emails are: Google, the ID token `sub` and `email_verified`; GitHub (OAuth 2.0), the numeric account id and the `primary` address of `GET /user/emails` marked `verified`; Microsoft (`MicrosoftOidcProvider` with the required `MICROSOFT_TENANT` of `common`, `organizations`, `consumers` or a directory ID), `<tid>:<oid>` and the `email` claim only with `xms_edov: true`; Apple, the ID token `sub` and `email_verified`. Apple answers with a cross-site form POST, so its challenge cookie is `SameSite=None; Secure`, its callback also accepts POST and is listed in `csrf_signed_webhook_paths` (the challenge's state replaces the CSRF token), its client secret is a five-minute ES256 JWT signed with the `.p8` key from `APPLE_PRIVATE_KEY` (checked at startup, never written to a file), and the unsigned first-login name only names a new account. Sign-in uses `make_login_cookie_with_epoch` with the account's current epoch, refusing an account the password login refuses. Unset credentials turn a provider off; an incomplete or mock configuration without the `oauth-mock` application feature, mock credentials in staging or production, and a missing callback origin there stop startup. ### v13 active-session management (source admitted, prerelease) Extend the existing opt-in `SqlRecoveryStore` rather than introducing another authentication registry or crate. A currently valid opaque session may list only its account's at-most-20 active sessions, revoke a selected sibling or revoke all siblings. Management identifiers are purpose-separated keyed digests, never bearer credentials. Current-session logout keeps the existing revocation API. Every protected request still verifies authoritative SQL state; encrypted-only cookies and independent JWTs do not acquire revocation implicitly. Inventory may expose creation/expiry times, a current-session marker and an explicit bounded display label. Do not collect IP addresses, raw user agents, device fingerprints or activity history. Old sessions without the additive metadata keep working and report unknown creation/label values. Metadata follows expired-session retention, logout and password-reset deletion in the same transactions. An operator-owned task purges at most 100 expired sessions per transaction; time passing alone does not physically delete rows. Session operations have a ten-second database deadline; an error does not prove an uncertain write rolled back, and applications reconcile authoritative state before reporting success. Management requires current authentication inside its SQL transaction; selective revocation binds the target to that account and rejects the current session. Logout of siblings also advances the account session version while preserving the current session, fencing previously issued password-authentication proofs. The application supplies trusted server time and mounts management behind its authenticated tenant selection, CSRF, security headers, WAF and ingress limits. Account stores remain explicitly separated by application/tenant database and keys as documented in the recovery contract; request data cannot choose either. No device label proves device identity. Already-authorized in-flight operations, WebSockets and sessions managed by another identity system need separate policy. Required acceptance includes SQLite and PostgreSQL transactions, cross-account and cross-store denial, independent-process request verification, restart, expiry, password recovery, concurrent revocation and fail-closed storage errors. Source/package admission passed in PR #236; final release admission remains separate. ### v13 email-login implementation contract The owner-selected email-login increment passed combined hosted source/package admission in PR #239; final release admission remains separate. See [the operational contract](https://rullst.github.io/Rullst/book/email-login.html). It must stay explicitly optional in Auth, with SQLite shared-local and PostgreSQL shared state profiles composed with the existing authoritative recovery account/session registry. It must not turn a password-reset token into a login credential or silently enable email login for every existing account. The selected journey requires an explicit server-configured application/tenant namespace and account opt-in established through existing authenticated account proof plus the host's tenant/MFA policy. The application resolves membership and ordinary authorization; namespace strings from URLs are never authority. Email access alone does not satisfy a stronger authentication requirement. Issuance generates separate random 256-bit emailed and browser-held secrets, binds their digests to the account, namespace and current authentication epoch, and queues a minimized encrypted notice atomically. Known, unknown, disabled and throttled accounts receive the same public acknowledgement. Storage, account and request limits must remain bounded. Tokens expire after a fixed 15-minute lifetime and are invalidated by replacement, account-policy changes or a changed authentication epoch. Database time observations and post-storage expiry checks must prevent rollback or long lock waits from authenticating stale evidence. GET/HEAD requests may render the landing page but never consume a token or mint a session. Redemption requires deliberate CSRF-protected POST, the matching browser-held secret and the current configured context. Token consumption and creation of the existing revocable opaque session must commit in one database transaction. Any uncertain outcome returns no authentication credential. The session retains existing limits, inventory, logout and epoch revocation. Landing and destination URLs are explicit server configuration, with HTTPS in production, no request-derived host/redirect and no third-party page content. Responses require no-store and no-referrer protections; secret-bearing URLs, cookies and messages must not enter logs. A fenced encrypted delivery outbox supports bounded retries and stable delivery identifiers, with a dedicated Mail security template and deterministic delivery fixtures. An email scanner's GET must not invalidate the user's login. These token and prefetch choices draw on [OWASP's URL-token guidance](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens) and [Supabase's documented prefetch behavior](https://supabase.com/docs/guides/auth/auth-email-templates#email-prefetching). Required acceptance includes both relational backends, concurrent redemption, account/namespace/browser mismatch, account-policy and password-reset races, outbox failure/retry, session limits, server restart, a real HTTP/browser journey and extracted-package consumers. Production credentials and live inbox delivery remain outside the owner's authorized testing scope. The existing reset journey keeps its separate behavior and must retain its regression evidence. ### v13 application API-token implementation contract The third owner-selected increment is a separate optional Auth lifecycle for opaque application API tokens, with SQLite shared-local and PostgreSQL shared state. It must compose the authoritative recovery account epoch and private database connection safeguards without enabling email-login, JWT or OAuth by default. Provider credentials and browser-session tokens are separate purposes. Issuance requires a current password-authentication proof from the same account registry plus the host's tenant/MFA/permission approval. An immutable server namespace and literal scope allowlist bound every token. Requested scopes are nonempty, bounded and exact (no wildcards); the application intersects its current account permissions before issuance and checks current tenant/resource authorization on every domain request. A client-supplied namespace or subject never establishes authority. API tokens cannot mint other tokens through this management API. Return a random 256-bit secret once, with a separate opaque management ID and an unmistakable versioned prefix; persist only a purpose/namespace-bound HMAC. Store bounded labels, literal scopes, expiry and revision, without IP, user-agent or usage history. Enforce a per-namespace row quota, at most 20 active tokens per account, and a configured maximum lifetime no greater than 30 days. Verification must read authoritative state, validate the account epoch, exact required scope subset and trusted time, and fail on missing/unavailable/revoked state. Never cache a positive verification as a replacement for these checks. Owner-only inventory, idempotent revocation and atomic compare-and-swap rotation are required. Rotation keeps the management ID and scopes, advances the revision, replaces the secret and may renew only within the configured lifetime. A stale rotation cannot overwrite a newer credential or revive a revoked token. Deleting a token is authoritative revocation: caller-selected IDs cannot recreate it. Account epoch changes invalidate all older tokens. Concurrent checks have a documented database linearization point; already authorized external work cannot be recalled or made atomic with a later revocation. Reuse verified remote TLS, bounded private pools/SQL deadlines, durable storage checks, namespace configuration binding and persisted clock observations. An uncertain issuance/rotation returns no credential, and restart, expiry during lock waits, rollback, foreign-account management, scope escalation, token-purpose confusion and concurrent rotation/revocation need executable evidence. Document backup anti-rollback and host-owned authorization boundaries. Full workspace, native protocol/process, authenticated HTTP and extracted-package admission are required before a supported feature claim; no external provider accounts are needed or authorized. HTTP composition adds an explicit Core `MachineEndpoint::verified_bearer` constructor using the existing exact-route `MachineRequestVerifier` contract. Auth supplies a verifier that requires one Authorization bearer header, reads current token state and inserts a redacted principal. It never accepts cookies, origin-bearing browser requests or URL tokens as machine authentication. The existing static bearer constructor retains its behavior; only an authenticated exact method/path receives the machine CSRF exception, with WAF/headers retained. ### v13 shared PostgreSQL mail-suppression implementation contract The fourth owner-selected increment extends Mail's existing suppression traits with an optional PostgreSQL store and facade feature. It must preserve verified event replay binding, conflict rejection, monotonic reason precedence, immutable recipient/event quotas and final pre-dispatch checks through `SuppressionGuard`. It is not a new transport and must not change default Mail/SQLite features. Each instance selects an immutable server-owned namespace, quotas and a strong secret HMAC key. Persist purpose/namespace-bound keyed recipient/event identifiers and event fingerprints, not raw recipient addresses, provider event IDs, message bodies or delivery history. Keep only the authoritative reason/provider and first/last observation times needed by the existing record contract. Configuration and key drift fail closed. A namespace chosen in a request is never authority; the host authenticates tenant membership and verified provider events before selecting the corresponding store/driver. Memory remains the explicit offline test implementation; production database failures cannot silently use it. Provide explicit deployment initialization and ordinary startup without DDL or missing-state repair. Runtime operates without schema mutation privileges, on one authoritative writable PostgreSQL database with verified remote TLS, permanent tables, durable commit settings, bounded pools/waits and whole-operation deadlines. Serialize event application, lookup and retention through the namespace control row. Atomically record replay evidence and recipient state; cancellation, conflicting events or quota failures must not leave partial state. Recheck persisted server time and storage durability before reporting outcomes. The persisted time is a monotonic high-water mark: a host clock at most 300 seconds behind it is cross-host skew and adopts the recorded time; a larger regression fails closed. Retention may prune replay identifiers after the host-selected provider replay window, but never remove recipient suppression as a side effect. Lookup/storage faults block delivery. Check immediately before dispatch, while documenting that a suppression committed after that check cannot recall in-flight external mail. This does not promise exactly-once provider delivery, inbox acceptance, backup anti-rollback, automatic failover, legal compliance or automatic opt-in after a complaint. Required evidence includes independent pools/processes, concurrent replay/conflicts/quotas, rollback/cancellation, restricted roles, service restart, real guard/worker composition and extracted-package consumers. ### v13 durable recurring-publication implementation contract The fifth owner-selected increment belongs to the existing Messaging crate as optional `schedules-postgres`, with an explicit facade feature. It coordinates bounded UTC cron occurrences across instances through one authoritative PostgreSQL database and relays them to the existing static `MessageBroker` contract. Core's process-local `Scheduler` retains its API; it evaluates POSIX five-field expressions in UTC (weekday 0/7=Sun, either restricted day field matches), or in a named IANA zone through the opt-in `scheduler-tz` `task_in_tz` (section 4.3), unlike the projection below. A broker publication is not proof of handler completion or an exactly-once external effect. Require a server-owned namespace, immutable limits and explicit encrypted storage keys. Schedule names are bounded application configuration, not tenant authority. The host authenticates authoring/inspection/cancellation and selects the correct namespace and broker. Creation is idempotent for the same immutable definition; conflicting reuse fails. An edit requires cancellation and a new schedule name, with immutable generation identities protecting retained occurrence keys. Use the cron crate's five-field projection in UTC (weekday 1=Sun through 7=Sat, calendar-day and weekday restrictions intersect; names are accepted), with a one-minute minimum resolution, bounded definitions/payloads, explicit first-after time and explicit missed-run policy. Bounded catch-up emits the oldest due occurrences within the caller's tick budget; coalescing emits one oldest outstanding occurrence and advances after the observed current time. Advancing the schedule and persisting frozen occurrence content must be atomic. Storage quota exhaustion cannot silently discard due occurrences. Persist definitions and occurrence content using authenticated encryption bound to namespace, schedule generation and occurrence identity. Do not silently adopt plaintext storage. Lease ownership, attempts, retry deadlines and terminal state are authoritative SQL data. Leases are unpredictable and exactly fenced; stale workers cannot acknowledge/retry/revive a cancelled or superseded occurrence. Broker requests use deterministic purpose-separated occurrence idempotency keys, and retries preserve exact content. Recheck a live lease immediately before publication and acknowledge only after successful broker acceptance. If acceptance precedes an uncertain ACK, retain evidence of acceptance and reconcile/retry the same key. A cancellation after the final check cannot recall an in-flight broker request; consumers must deduplicate/authorize at their side-effect boundary. Expose bounded inventory/terminal inspection, explicit failed-occurrence retry, permanent cancellation and retention of terminal rows only. Automatic attempts and the delivery window are bounded. Deadlines, clock rollback beyond a five-second cross-instance skew tolerance (a trailing instance adopts the monotonic recorded namespace time), namespace/key drift and non-durable/unavailable state fail closed. Reuse verified TLS, bounded pools/SQL deadlines, explicit initialization and runtime without DDL privileges. Document UTC-only behavior, unchanged per-process cron, broker deduplication retention, backup/failover obligations and host-owned authorization. Required acceptance includes independent instances/processes, real PostgreSQL rollback, restart and restricted roles, cancellation/lease races, durable broker consumption, publication-before-ACK replay and extracted-package/facade consumers. ### v13 durable outgoing-webhook implementation contract The sixth owner-selected increment extends Messaging behind opt-in `webhooks` and facade `messaging-webhooks`. Compose the existing encrypted SQLite broker as a private durable outbox rather than adding a second queue. The first profile supports independent processes sharing one local database file; remote/multi-host webhook state remains separate. Do not change default dependencies. App-domain SQL transactions and this outbox are distinct; applications needing atomic domain publication must bridge from their existing transactional outbox. One server-approved immutable destination, signing identity, delivery window and bounded policy belong to each server-owned namespace. Persist an encrypted control publication with a fixed idempotency key: configuration/secret drift must conflict, not redirect queued data. Never expose this private broker or purge/subscribe to its control topic. Freeze bounded JSON bytes and event kind; retries keep the stable message ID and exact body. The host authorizes publishing, inspection, cancellation and manual retry before selecting the namespace. Production accepts only explicitly configured HTTPS destinations, with verified TLS, no proxies/redirects, bounded timeouts and fresh public-address resolution pinned to the actual connection. Deny private, loopback, link-local, multicast, reserved, documentation and transition address ranges, including mixed DNS answers. An explicit literal-loopback development mode and deterministic offline mode may support owned fixtures; production configuration rejects both. Empty or mock signing credentials select offline delivery, never a network fallback. Sign exact body bytes, stable ID, kind, key ID and attempt timestamp with a purpose-separated HMAC-SHA256 contract. Provide strict constant-time receiver verification with bounded time skew; receivers must also store processed IDs and authorize the event. Do not claim signatures prevent replay by themselves. Keep signing/storage keys separate and redact credentials, URL, payload and response bodies from Debug/errors/terminal inspection. Use bounded attempts/backoff and an immutable delivery window. Revalidate the actual SQL lease immediately before external dispatch, after DNS work. A late worker cannot acknowledge/retry/revive another worker's claim or cancelled work. Acceptance before an uncertain ACK remains an at-least-once condition; preserve the accepted HTTP status/ID and retry the same identity. Retry transient failures, terminally reject redirects and permanent failures, and expose bounded minimized terminal inspection, explicit retry within the original window, cancellation and terminal retention. Retention must not delete active work or control state. The host owns disciplined trusted time, persistent disk/backup policy, receiver deduplication, endpoint approval and worker supervision. Native acceptance must include actual HTTP/TLS protocol fixtures, SSRF/redirect/signature/replay negatives, independent instances/process restart, SQL cancellation/outage/lease contention, retry/dead-letter/retention and extracted-package/facade consumers. Real provider accounts are not required or implied. ### v13 SQLite messaging lock-time correction Before composing outgoing webhooks, real lock-contention tests demonstrated that SQLite broker operations sampled time before `BEGIN IMMEDIATE`: an ACK could be accepted after its lease expired while waiting, and a new claim could already be expired on acquisition. Publication, claim, ACK, retry and dead-letter operations must sample trusted time after acquiring their write transaction. Retry availability and new lease duration start from that admitted instant. Expired workers fail with `LeaseExpired`; their work remains recoverable. The process-local `InMemoryBroker` (also the Redis mock backend) likewise samples time after acquiring its state lock. This correction preserves public API/schema and does not add persistent clock anti-rollback or change at-least-once delivery semantics. ### v13 shared-local SQLite bootstrap connections The Labs, Media and Supervision stores and the Privacy consent and age-replay SQLite stores bound every wait for a pooled connection at 5 seconds, while statements and commits wait for the disk. Establishing a connection (opening the file and its WAL, and in `initialize` the WAL conversion with its full syncs) runs inside that bound. Loaded hosted Windows runners stall a process's file I/O for 5 to 12 seconds at times, so `initialize` failed there with `Storage` or `Busy` and left a new file that a second `initialize` refuses. Since 13.0.0-alpha.3, the steps of `initialize` and `open` (for the age-replay store, `open`, which also creates a missing file) are tried again when their connection times out, three attempts in total (about 15 seconds): no other task can use the new pool yet, so the timeout means a slow establishment, not contention. Every other operation keeps the single 5-second bound and its `Busy` or `Storage` result. Labs keeps the SQLite result code, I/O error kind or pool condition behind the last `Storage` error of a thread for its test fixtures, never a message, SQL text or path. ### v12 audit correction invariants **12.1 account mail:** `rullst-mail::ActionLink` validates an exact application-configured origin (HTTPS, or explicit loopback HTTP); action URLs and `Message` contents are redacted from Debug. The compatible `Message` pipeline preserves only bounded opaque `token` query values inside safe body URLs, including HTML-escaped query separators. This context-aware exception does not apply to subjects, arbitrary non-URL text, URL credentials, other secret parameters, fragments, provider errors or telemetry. Applications own the destination authorization; a generic body URL is not an identity proof. `AccountMail` builds versioned deterministic English, Portuguese and Spanish account notices. A link's remaining lifetime is stated as 1–1440 minutes (24 hours) for `PasswordReset` and 1–30 minutes for the verification and email-change links, in minutes or, when whole, hours ("1 hour", "24 hours"); anything else is a validation error. Reset issuance expires after 20 minutes; delivery uses an absolute UTC expiry so a stable idempotency key retains identical content. Templates generate neither action tokens nor marketing consent and add no tracking. Provider tracking configuration remains a separate obligation. `LogDriver` records only delivery metadata. Opt-in Auth `recovery-postgres`/`recovery-sqlite` owns an authoritative account registry, purpose-separated keyed token digests, revocable opaque sessions and an AES-GCM transactional outbox. Account creation enqueues welcome; password replacement, reset consumption, sibling invalidation, session revocation and a password-change notice commit atomically. The facade `account-mail-*` features compose this store with Mail without another publishable crate. Existing password tables and encrypted-only cookies require explicit migration; no implicit application rewrite or cross-database transaction is promised. Bounded claims, retry, idempotency propagation and Resend/Svix feedback verification are additive foundations; the 12.1 ACS Managed Identity transport was removed in 13.0. Redis recovery, other identity-action transactions, marketing consent and audited administrative resend are not implemented by this contract. See [account mail](https://rullst.github.io/Rullst/book/account-mail-v12-1.html) for exact limits, deployment obligations and external acceptance boundaries. **12.1 additional Mail transports:** native SendPulse static Bearer authentication preserves the mandatory pipeline and deterministic offline credentials. Real sends require an explicit verified sender, due message, bounded JSON and validated success body. SendPulse requires account-level tracking configuration and rejects inline CID/unsubscribe-header messages outside its reviewed REST contract. The local `MailTrap` harness is not the hosted Mailtrap service. Provider account acceptance and durable operation reconciliation remain external; no exactly-once promise is inferred from a delivery ID. HTTP failures omit provider bodies while retaining status and bounded retry metadata. The Mailjet and Mailtrap transports added in 12.1 were removed in 13.0. **v13 Mail provider scope:** `rullst-mail` keeps Resend, AWS SES (native SigV4 behind `aws-ses`, or the offline fixture and explicit bearer proxy), SendPulse and SMTP, the `log`, `memory` and offline `mock_*` drivers, and the shared pipeline, failover, tenant resolver, attachment inspection, suppression, observation, feedback and queue support. The SendGrid, Postmark, Mailjet, Mailtrap and Azure Communication Services transports, their `MAIL_DRIVER` values and the settings only they read were removed; the code remains in Git history and the 12.x line. A `MAIL_DRIVER` or `[mail] driver` naming a removed transport must fail facade resolution with `MailError::ConfigError` naming the driver and the migration row, and must never fall back to `log`, `memory` or an offline mock. Queued `rullst_mail_send` jobs carry no provider name, so a 13 worker delivers 12.x jobs through its configured driver or fails them visibly. Suppression rows and observations keep free-form provider labels and stay readable. Applications implement other providers with `MailDriver` ([Writing your own mail transport](https://rullst.github.io/Rullst/book/mail-custom-transport.html)); the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html) lists the removed names, and `cargo rullst upgrade` reports them as `V13-MAIL-REMOVED`. **12.1 one-time Stripe foundation:** a separate fixed-price `mode=payment` contract checks active provider catalog amount/currency before creation and binds session, owner, customer, attempt, price and mode. Receipt reads validate the expanded PaymentIntent/charge, with partial refunds and disputes preventing `Paid`. Signed event hints must be reconciled through application-owned durable inbox/entitlement transactions. Existing generated billing remains recurring; new sandbox evidence and merchant refund/dispute operations are still required. **12.1 authenticated machine endpoints:** an immutable `MachineEndpointPolicy` on `Server` exempts only registered write-method/path pairs from browser CSRF after proving a strong bearer secret or an explicit host-supplied signed-webhook/mTLS verifier. Cookie/Origin/Sec-Fetch-Site inputs are rejected on these routes; body size is bounded and WAF/secure headers remain composed. The development hot-reload server (`Server::new_hot`) authenticates the same routes in front of each reloaded router. A path segment after the first may be a `{name}` placeholder matching one non-empty segment other than `.` or `..` (v13); two routes with the same method that one request path could match are refused. Wildcards and weak secrets fail at construction; a static bearer token needs 32–200 printable ASCII bytes with at least 8 distinct values. Applications own ingress limits, replay storage, certificate trust and authorization; an unverified proxy header is not mTLS. The current [release audit](https://rullst.github.io/Rullst/book/v12-release-audit.html) reopens earlier readiness claims. A historical score, checked roadmap item or green mainline run is not evidence that the current revision satisfies these contracts. - Core's production COEP remains `require-corp` by default. Application configuration accepts only `require-corp`, `credentialless`, or `unsafe-none`; choosing a less isolated policy and the matching CSP/media allowlist is an explicit application threat-model decision. The `html!` parser accepts and strips source-only `` comments; comments do not create a raw-HTML trust boundary or client-visible output. - Generated safe ORM projections validate column identifiers. Empty membership predicates match nothing. Mandatory tenant, global and soft-delete scopes remain grouped outside application `OR` expressions; nested queries preserve validation errors. Bulk mutation must not bypass model authorization. - Queries inside a managed transaction use its executor. Callbacks that already borrow a mutation executor must fail explicitly on unsupported reentrant ORM access rather than deadlock or silently use an unrelated connection. - Local durable stores use SQLx-owned transactions so dropping a cancelled operation schedules rollback. A cancellation racing an already dispatched commit can still have an uncertain outcome and requires reconciliation. - Authentication validates expiry independently of clock-skew allowances; provider-specific identity claims, nonce and refresh semantics cannot be replaced by successful offline fixtures. - Payment adapters must not fabricate live checkout prices, portal URLs or mutation success. Unsupported provider operations fail explicitly. Signed webhook bytes still require provider-specific schema and lifecycle validation. - Development restart owns builds, application snapshots and migrations. It is not a stable Rust DLL ABI or production rolling-deployment mechanism. Windows descendant cleanup remains best effort without a Job Object implementation. These describe required behavior, not a declaration that every final release gate has passed. The audit records the current evidence and remaining work. ### Application-key maintenance invariant (12.1.1 and v13) Application-key validation rejects public scaffold/documentation placeholders after trimming and case normalization. Legacy `Rullst.toml` discovery accepts only exact `app_key` or `key` field names, not fields sharing a prefix. It keeps the historical value before the next `=` so valid existing sessions remain readable; it does not rewrite configuration or silently rotate secrets. Prefer `APP_KEY` for new values containing `=`. Moving an existing TOML value must preserve its effective bytes or explicitly rotate the secret and invalidate sessions. Session operations and optional JWT signing-key construction reject the documented placeholders before accepting session/token processing. The maintenance correction is carried from v12 PR #259 and was published in 12.1.1; v13 release admission is separate from its presence in development source. The corresponding facade fuzz harnesses exercise real configuration parsing, authenticated session decryption/tamper rejection, tenant membership resolution and tenant-scoped realtime publication, with deterministic positive/negative controls. The realtime target does not exercise TCP/WebSocket frame decoding. Historical runs of the replaced no-op/weak-fixture harnesses are not evidence for these contracts. v13 campaign admission remains outstanding; the port does not import the v12-specific source-equivalence policy. ### Studio browser composition invariant (12.1.0) Studio browser composition in the published 12.1.0 maintenance release preserves both root and `/studio`-nested same-origin asset routes. A raw browser without a supplied cache renders an explicit unavailable state and exposes no cache mutation endpoints. The full local builder installs the configured cache once and retains its verified-loopback/same-origin protection. Assets and navigation fixes do not constitute a shared-production authentication mode; existing application-level workaround routes must be removed before upgrading. ### Mobile presentation invariant (12.1.0, carried forward to v13) The published 12.1.0 mobile maintenance contract keeps Nexus navigation dismissible by close control, backdrop, Escape and links, with keyboard focus containment/return and visible no-JavaScript navigation. The Portfolio example (`examples/portfolio`, the former scaffold) must reflow at phone widths and wrap long content instead of hiding overflow. These are presentation fixes, not changes to server authorization; pre-existing generated source and showcase HTML-rewriting workarounds need explicit migration. ### Pre-release scaffold source invariant An unpublished pre-release `cargo-rullst` may reuse only local framework crates whose package names and versions exactly match that CLI. The invocation directory is preferred; otherwise the exact still-present checkout from which the CLI was compiled is used. Stable or version-mismatched packages fall back to crates.io. This permits evaluation outside the repository without silently mixing release trains, but generated absolute path dependencies remain non-portable until the matching immutable release is published. ### Distributed operation tracing candidate (v13, source admitted) The fifth approved September increment extends Core/facade's optional `telemetry` capability, using the maintained OpenTelemetry SDK and OTLP/HTTP protobuf transport. The existing process-local `SpanCollector` and authenticated Studio ingestion remain separate interfaces. A new explicit configuration must select service and bounded operation labels, a trusted collector, bounded queue, batch and export deadlines, and an owned flush/shutdown lifecycle. It must not infer user identity or authorization from trace identifiers. Context propagation accepts canonical nonzero W3C version-00 `traceparent` only after an explicit upstream trust decision. Untrusted ingress starts a new trace; arbitrary baggage and vendor tracestate are not propagated by this minimized profile. An actual independent producer/consumer journey must preserve parent relationships through the existing Messaging carrier and reach a disposable standard collector. Export only bounded approved operation/service names, trace/span relationships, timing, kind and status; omit application attributes, events, error descriptions, SQL, tokens, bodies and ambient resource metadata. This profile does not sanitize other tracing/logging layers. Export must run outside request execution with a compatible SDK processor/client, bounded response handling and no redirects or ambient proxies. Production uses verified HTTPS; literal-loopback HTTP is an explicit test/local-collector choice. Remote failure never falls back to a fake success. Malformed propagation, queue pressure, unavailable/slow collectors, TLS denial, exact package consumption and flush/shutdown require executable evidence. Sampling, retention, collector access, durability, availability and deployment operations remain host responsibilities; trace delivery is best effort, not an audit ledger or application transaction. The implementation and actual collector/TLS/process evidence are documented in [the tracing guide](https://rullst.github.io/Rullst/book/distributed-tracing.html). Hosted source/package admission passed in PR #236; final release admission remains separate. Legacy initialization uses compatible threaded export and the corrected OTLP/HTTP endpoint; it retains its separate general logging/metadata policy. ### Recoverable Live UI candidate (v13, source admitted) The fourth approved September increment extends `rullst-core::live` with a separate opt-in typed recovery API, preserving the legacy component interface. An application-bound tenant/account/component scope and mandatory static-dispatch authorization callback must be checked before upgrading, before actions and outputs, and periodically while connected. Exact browser origin and protocol validation supplement, rather than replace, application authentication. The v1 transport sends bounded complete server snapshots on initial connection, reconnection and conflicts. Commands carry a bounded correlation ID and expected revision; the host must check that revision atomically with its persistent domain write. The client must not queue or automatically replay mutations after uncertain disconnects. Recovery converges to authoritative application state, not replay of every missed transient event. Browser/server buffers, admitted connections, callbacks, sends, session lifetime and event counts must be bounded. Expired or revoked access closes the session without exposing a new snapshot. A small optional same-origin browser module and real browser/server tests must prove reconnect convergence, action conflict, session/tenant denial, revocation, bounded malformed/slow peers and recovery after an application restart. Domain persistence, transactional revision checks, HTML escaping, authorization policy, replication and proxy configuration remain explicit application responsibilities. No automatic durability or upgrade of legacy `LiveComponent` code is implied. The legacy `live_ws_handler` bounds incoming WebSocket frames and messages to 64 KiB before JSON parsing; its origin, admission and idle policy stay with the application route. `ws::WebSocket::recv` skips Ping/Pong control frames, so client or proxy keepalives do not end a legacy Live session. A `#[live_component]` dispatches only on the first present `rullst_event`, `action` or `event` string field and runs at most one `#[live_event]` handler per message; other payload keys and values, including form inputs, never select a handler. A path-qualified marker such as `#[rullst::live_event]` marks a handler exactly like the bare attribute. The implementation and its local protocol/Chromium acceptance are recorded in [the recovery guide](https://rullst.github.io/Rullst/book/live-recovery.html). Hosted workspace/platform/package admission passed in PR #236; final release admission remains separate. The API uses complete snapshots; it does not imply DOM diffing, broadcast of other clients' changes or automatic event replay. ### Remote messaging candidate (v13, source admitted) The first remote adapter is an opt-in Redis Streams profile in the existing `rullst-messaging` crate. Redis stores the canonical envelope log; Rullst owns bounded group, retry, dead-letter and single-use lease indexes. These are Rullst consumer groups, not interoperability with arbitrary native `XREADGROUP` clients. Every lease mutation must check authoritative server time and the exact current capability atomically. Publication replay must retain the original receipt until explicit terminal purge. Empty/mock credentials select an explicit process-local fixture; a failed live connection must never downgrade to that fixture. The initial profile targets a dedicated standalone Redis 7.4+ database with verified TLS outside an explicit literal-loopback test mode. Exact configuration and an application-supplied deployment generation bind persisted namespaces; provisioning is explicit and normal connection must not recreate lost state. The TLS adapter preserves an installed Rustls process provider and otherwise installs ring before Redis creates its client, avoiding implicit-provider panics when dependency features enable multiple providers. Applications requiring a different provider install it before broker startup; trust and hostname checks remain mandatory. Lua mutations are isolated but do not roll back on runtime errors: a persistent in-progress marker must quarantine partial changes rather than silently continue. Bounded batches, retained bytes, subscriptions, operation deadlines and admission limits are required. AOF/no-eviction, clock discipline, credentials/ACLs, backups, restore reconciliation, topic authorization and destination idempotency remain deployment responsibilities. Cluster, replication/failover, native-group mixing, automatic repair and exactly-once effects are excluded from this profile. Admission requires the shared broker contract plus an actual disposable Redis restart, conflicting publication, concurrent consumer, expired/late ACK, retry, DLQ, outbox replay, configuration-loss and partial-write failure evidence. The implementation candidate and its capability/operational limits are described in [the Redis messaging guide](https://rullst.github.io/Rullst/book/redis-messaging.html). Hosted source/package admission passed in PR #236; final release admission remains separate. ### OAuth authorization request invariant The built-in Connect authorization-code providers explicitly request `response_type=code`. With ordinary authorization endpoints, the plain URL, state, PKCE, and combined trait variants each contain exactly one response type and retain the configured client, redirect, scopes and encoded state/PKCE. Apple retains `response_mode=form_post`; offline mock redirects remain local fixtures rather than real authorization requests. The managed `AuthSession` extractor reads only the callback query string, so it does not complete Apple's cross-site POST callback; an Apple host keeps its state, PKCE verifier and nonce behind a `SameSite=None; Secure` challenge cookie and validates the posted form itself. The v13 `AuthSessionForm` extractor consumes the same managed challenge from a `POST` `application/x-www-form-urlencoded` body of at most 16 KiB, rejecting another method, content type or oversized body before touching the challenge; the `SameSite=None; Secure` cookie requirement remains. Application configuration, nonce/callback validation and provider-account interoperability remain separate. The generic `build_oauth_params` helper and exported redirect macro remain response-type-neutral for downstream custom providers. Callers own optional endpoint query data and must not supply conflicting reserved parameters. ### Client-supplied token invariant `Provider::get_user_from_token` is a profile lookup for a token the server obtained for its own client. It is not an authentication primitive for a token supplied by a native or mobile client: except for Apple, the adapters ask the provider's userinfo/profile endpoint about the bearer token, which does not prove that the token was issued to the configured `client_id`. Sign-in must use the application's own authorization-code exchange or an ID token verified for this client's issuer, audience, expiry, signature and nonce. Apple's adapter validates an ID token's audience there, but checks no nonce. In v13, `GoogleProvider::verify_id_token` and `OidcProvider::verify_id_token` are that audience-bound entry point: they require a non-empty expected nonce, reuse the code-exchange ID-token validation (JWKS signature, exact issuer, `aud`/`azp` equal to the client ID, except that Google's `verify_id_token` also accepts an `azp` from its explicit bounded native-presenter list while `aud` stays exact, `exp`/`iat`, nonce) and never call userinfo. ### Shared-local facade composition invariant The umbrella features `auth-sqlite`, `capital-quota-sql`, `oauth-sqlite`, `mail-sqlite`, `messaging-sqlite`, and `queue-sqlite` may deliberately share one file-backed SQLite database for a bounded single-host deployment. Each subsystem owns a fixed, distinct table namespace. The host prepares and checks every store sequentially and keeps `ApplicationLifecycle` unready until all required components have succeeded. Restart must reuse the same validated database URL, quotas, namespaces, and encryption keys; all handles must close before a host copies, restores, or replaces the file. The executable facade contract proves persistence and idempotent replay across restart, encrypted token/message plaintext absence from the database/WAL/SHM, queue recovery, aggregate readiness, and fail-closed token corruption without breaking an unrelated mail read. It does not create a cross-subsystem transaction, consistent online backup, key manager, distributed database, or multi-host coordination. Whole-file snapshot consistency, filesystem trust, permissions, key/backup operations, contention policy, and recovery drills are host responsibilities. Separate databases remain preferable where failure isolation or write throughput matters. ### Native relational enum invariant `rullst-orm` has one bounded native-enum contract. `#[derive(Enum)]` generates a closed label set shared by `Display`, parsing, Serde, `RullstValue` and SQLx codecs. A database enum has 1–64 unique labels; its type identifier and labels use the documented bounded ASCII allowlists. `Blueprint::native_enum` emits: - a named PostgreSQL enum with exact existing-label drift detection only under `strict-postgres`; - an inline `ENUM` for MySQL/MariaDB; and - a `TEXT CHECK` constraint for SQLite. The PostgreSQL type is created as a quoted, case-preserving identifier, and the generated SQLx codec names it quoted as well, so a mixed-case `type_name` resolves to exactly that type. A manual `sqlx::Type` implementation for a mixed-case `DatabaseEnum::TYPE_NAME` must quote the name the same way. PostgreSQL enum creation, its label drift check and `Schema::drop_native_enum` run through the task-scoped transaction when one is active, exactly like the table DDL: they roll back with it, and dropping the type after its tables in one transaction cannot block on that transaction's own locks from a second pooled connection. Generated builder comparisons (`=`, `!=`, `<`, `>`, `IN`, `BETWEEN`, with their `or_`/`not_` and `where_<column>` forms) on a persisted field whose type implements `DatabaseEnum` bind the value as `CAST(? AS "<type_name>")` on PostgreSQL, where a text parameter has no operator against a named enum; the other backends keep `?`. The live PostgreSQL matrix filters such columns. PostgreSQL through SQLx Any must fail before DDL because that driver cannot decode custom PostgreSQL types. Adding, removing or reordering variants, deployment order, dependent-object removal and rollback remain explicit, reviewed migration work. The schema helper does not auto-migrate an existing type or infer application compatibility. `Blueprint::timestamps()` adds nullable `created_at`/`updated_at` columns that default to the database's current time. With a strict driver feature they are `timestamp_tz()` columns (below), read as `chrono::DateTime<Utc>`. The SQLx `Any` pool cannot read date/time values, so there they stay `TEXT` columns for `String` fields: SQLite and PostgreSQL receive `DEFAULT CURRENT_TIMESTAMP`, and MySQL/MariaDB, which accept a default on `TEXT`, `BLOB`, `JSON` or `GEOMETRY` columns only as an expression, receive `DEFAULT (CURRENT_TIMESTAMP)`; the builder parenthesizes every other non-`NULL` default on those column types (MySQL 8.0.13+, MariaDB 10.2.1+). The strict-pool columns are a v13 change; alpha.1 created `TEXT` on every pool. ### Schema constraints, indexes and date/time columns The `Blueprint` declares constraints and indexes next to the columns, and every table, column, constraint and index name passes the identifier grammar (`validate_table_name`); constraint and index names are at most 63 bytes so PostgreSQL never truncates them. - `Column::unique()` emits a column-level `UNIQUE`. `unique(&[cols])` emits `CONSTRAINT <table>_<cols>_unique UNIQUE (...)` inside `CREATE TABLE`. - `index(&[cols])` and `unique_index(&[cols])` (`<table>_<cols>_index` / `_unique`) run as `CREATE [UNIQUE] INDEX IF NOT EXISTS` after `CREATE TABLE` on PostgreSQL and SQLite and are `INDEX` clauses of `CREATE TABLE` on MySQL/MariaDB, which has no `IF NOT EXISTS` for indexes. - `foreign(col).references(table, col)` emits `CONSTRAINT <table>_<col>_foreign FOREIGN KEY ... REFERENCES` with the quoted, PostgreSQL-folded referenced table and optional `ON DELETE`/`ON UPDATE` `CASCADE`, `RESTRICT`, `SET NULL` or `NO ACTION`; `foreign_id(col, table)` also adds the `INTEGER NOT NULL` column and references `table(id)`. `SET NULL` on a `NOT NULL` column fails the build. - `check(expr)` emits `CHECK (expr)`. The expression is trusted developer SQL: the build rejects an empty or longer than 512-byte expression and one with `;`, `--`, `/*`, `*/`, `\`, `$`, a backtick, a control character, unbalanced parentheses or an unterminated quote, so it cannot leave its clause; it is never built from request data. MySQL enforces CHECK from 8.0.16, MariaDB from 10.2.1. - On MySQL/MariaDB a key (column `UNIQUE`, unique constraint, index or foreign key) on a column whose type is `TEXT`, `BLOB` or `JSON` fails the build: those servers index such columns only by a prefix, which would make a unique key compare only the prefix. `Schema::create` validates every statement before running the first. Without a table name, `Blueprint::build()` leaves the default constraint names to the database and omits the separate PostgreSQL/SQLite index statements. `Schema::table` alters an existing table: `ALTER TABLE ... ADD COLUMN` per column, then `ADD CONSTRAINT ... UNIQUE`, `CREATE [UNIQUE] INDEX`, `ADD CONSTRAINT ... FOREIGN KEY` and `ADD CHECK`, all validated first and run in order in the task-scoped transaction when one is active (MySQL/MariaDB commit each DDL statement). SQLite cannot add a constraint to an existing table: a unique constraint or `unique()` column becomes the equivalent `CREATE UNIQUE INDEX`, and a foreign key on a column added in the same call becomes that column's `REFERENCES` clause (the column must then be nullable without a non-`NULL` default). A SQLite foreign key on an existing column, a second foreign key on the same new column (one `REFERENCES` clause fits), a CHECK, a primary-key column, a `NOT NULL` column without a default and a current-time default return `Error::UnsupportedSchemaChange { driver, change }` before any statement runs. `date()`, `time()`, `timestamp()` and `timestamp_tz()` are columns for `chrono::NaiveDate`, `NaiveTime`, `NaiveDateTime` and `DateTime<Utc>` fields (`rullst_orm::chrono`): `DATE`, `TIME`, `TIMESTAMP` and `TIMESTAMPTZ` on PostgreSQL; `DATE`, `TIME(6)` and `DATETIME(6)` (a UTC instant for `timestamp_tz`, because `TIMESTAMP` ends in 2038, with SQLx setting the session time zone to `+00:00`) on MySQL/MariaDB; and ISO-8601 `TEXT` on SQLite in the forms SQLx writes (`YYYY-MM-DD`, `HH:MM:SS[.f]`, `YYYY-MM-DD HH:MM:SS[.f]`, `YYYY-MM-DDTHH:MM:SS[.f]+00:00`), so values of one column sort as text. A `ColumnDefault::CurrentTimestamp` default is the current UTC time in the column's form (`CURRENT_TIMESTAMP` for `TIMESTAMPTZ`, `CURRENT_TIMESTAMP AT TIME ZONE 'UTC'` for the zone-less PostgreSQL types, `CURRENT_TIMESTAMP(6)`, `(CURRENT_DATE)` or `(CURRENT_TIME(6))` on MySQL/MariaDB, and on SQLite `strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now')` for `timestamp_tz`). These columns and `chrono` fields need a strict driver feature: on the SQLx `Any` pool `Schema::create`/`Schema::table` reject the native helpers on PostgreSQL and MySQL/MariaDB with `Error::Validation` before DDL, because a table holding such a column could not be read. `RullstValue` gains `DateTime`, `NaiveDateTime`, `Date` and `Time` variants, bound through SQLx's `chrono` codecs on strict pools and as that ISO-8601 text on the `Any` pool. `soft_deletes()` keeps a `TEXT` `deleted_at` column. The Turso adapter (`TursoMigration`, `TursoModel`) writes its own SQL, so none of these builders or date/time codecs apply to it; its timestamps stay text. `Blueprint::float` is an `f64` column on every driver: `DOUBLE PRECISION` on PostgreSQL (whose `REAL` is single precision and cannot decode as `f64`), `DOUBLE` on MySQL/MariaDB and `REAL` (8-byte) on SQLite. `Column::col_type` still reads `REAL` before the schema is built; an explicitly replaced `col_type` is emitted unchanged. This affects DDL built from now on; columns created by earlier versions keep their type until a reviewed migration alters them (for example `ALTER TABLE t ALTER COLUMN c TYPE DOUBLE PRECISION`). `Blueprint::boolean` is a `bool` column: `BOOLEAN` on PostgreSQL, which has no implicit integer/boolean casts for bound parameters or decoding, and an `INTEGER` 0/1 flag on MySQL/MariaDB and SQLite. On PostgreSQL an integer `ColumnDefault` of `0`/`1` renders as `FALSE`/`TRUE` and any other integer default fails the build. `Column::col_type` still reads `INTEGER` before the schema is built. Only newly built DDL changes: an existing PostgreSQL column stays `INTEGER` until migrated (`ALTER COLUMN c TYPE BOOLEAN USING c <> 0`), and a model that paired `boolean()` with an integer field on PostgreSQL must use `bool` for new tables. The Capital row also includes one implemented, feature-gated quota boundary: `BillingSubject` binds a shared team/workspace counter to trusted tenant state, `Billable::quota_request` derives the limit from the subscription owner, and `QuotaStore` atomically reserves idempotent units before resource creation. The deterministic local store is always available; `quota-sql` uses fixed-schema SQLx transactions on SQLite/PostgreSQL/MySQL/MariaDB, with exact replay/release and a caller-owned transaction path for atomic domain writes. Live container contracts exercise all four protocols. Membership establishment, plan state, migrations, reconciliation and non-relational adapters remain application boundaries. ### 3.1. Generated Academy activity boundary (removed in v13) v13 replaced the complete Academy scaffold with the LMS starter, which became the LMS example (`examples/lms`) in 13.0.0-alpha.4. The generated `ActivityEvaluator`, activity attempts, `ScoreEvent` recording, leaderboards and the spaced-review queue are no longer generated. Their design remains in the git history and on the frozen v12 branch; an application that adds them owns the same server-authoritative controls. ## ⚡ 4. Core API Specifications (`rullst-core`) `rullst-core` provides the runtime kernel. Database and queue drivers are modular and feature-gated. ### 4.1. Server & Routing (`rullst::routing`) * **Routing Macro:** Central declarative routing declared via the `routes!` macro wrapping Axum routing handlers: ```rust use rullst::{response::Html, routes}; async fn home() -> Html<&'static str> { Html("Home") } async fn posts_index() -> Html<&'static str> { Html("Posts") } let router = routes![ get("/" => home), get("/posts" => posts_index), ]; ``` * **Server Lifecycle & Graceful Shutdown:** ```rust,no_run use rullst::{Server, routes}; async fn serve() -> Result<(), rullst::server::ServerError> { let router = routes![get("/" => || async { "OK" })]; Server::new(router).run(3000).await } ``` `ApplicationLifecycle` is the opt-in orchestration contract behind a lifecycle-aware server. Its phase is monotonic, its immutable registry has at most 32 validated required-component labels, and request admission requires both `Ready` and every component bit. Exact `GET`/`HEAD` health probes bypass admission, and the `Server` rate limiter and Traffic Shield, so `/ready` can return a bounded `503` during startup, dependency failure or drain while `/health` stays process-only. The JSON reports counts, not labels or dependency errors. Its `version` is the framework version; the v13 `health_router_with_version` and `health_router_with_lifecycle_and_version` add the application's own version as `app_version`. `Server::run_with_shutdown` accepts a caller-owned future; when it resolves, the lifecycle changes to draining before Axum waits for accepted requests and then becomes stopped. Dependency checks/timeouts, component updates, replica consensus, load-balancer timing, authorization and the deployment termination deadline remain host contracts. * **Access log and default log filter:** `Server` prints one `[HTTP] METHOD path -> status (ms)` line per request on stdout. Since 13.0.0-alpha.3, in Staging and Production a `/health` or `/ready` request with a status below 400 is not printed (a failing probe still is); `RULLST_LOG_HEALTH_PROBES` (`true`/`false`, also `1`/`0`, `yes`/`no`, `on`/`off`, from the process environment and then `.env`) overrides that default in every environment, and any other value stops startup with `ServerError::Configuration`. The tracing subscriber that `Server::run` and `telemetry::init_telemetry` install reads `RUST_LOG`; when it is unset or does not parse, the filter is `info,sqlx=warn`, so SQLx's own connection and statement events below WARN are dropped while `rullst_orm` spans stay at INFO. * **Trusted-Proxy Client Resolution (v13):** The socket peer (`ConnectInfo<SocketAddr>`) is the default client identity. `security::TrustedProxyLayer` implements `ProductionMiddlewareStage::TrustedProxy` and `Server::trusted_proxies(TrustedProxyConfig)` mounts it outside the security baseline, lifecycle, Traffic Shield and rate limiter in both the static and hot-reload servers (the hot-reload server now supplies `ConnectInfo` too). It never rejects a request, so exact health probes keep their rate-limit and Traffic Shield exemption behind it. `TrustedProxyConfig::new` validates at most 64 IPv4/IPv6 addresses or CIDR networks, rejects `/0`, host bits and duplicates, and normalizes IPv4-mapped IPv6; an empty policy never reads forwarding headers. Only a trusted socket peer's `X-Forwarded-For` (default) or RFC 7239 `Forwarded` header is read, never both. The list is walked right to left, skipping trusted hops; the first untrusted address is the client, or the left-most when every hop is trusted. Text left of the client entry is never parsed, and at most 4,096 bytes and 32 entries are examined. Ports, brackets and IPv4-mapped forms are normalized; `unknown`, obfuscated identifiers and malformed entries are rejected. A missing, malformed, oversized or over-long header keeps the peer without failing the request and emits one `tracing` event without header contents. On success `ConnectInfo` becomes the client IP with port 0; every request with peer metadata receives a `ClientAddr` extension without a public constructor. `trust_forwarded_proto(true)` reports the trusted peer's scheme (one `X-Forwarded-Proto` value or the selected `Forwarded` element's `proto=`) as `ClientAddr::forwarded_proto`. `Rullst.toml` accepts the same policy as `[security]` `trusted_proxies`, `trusted_proxy_header` and `trust_forwarded_proto`, validated with the rest of the security configuration; an explicit builder policy replaces it. Only the real proxy networks may be listed, because any host inside them can choose the client address. Since 13.0.0-alpha.2 the static server's startup line (`Rullst framework serving on http://<address>`) marks an address that covers every interface (`0.0.0.0` or `::`, the staging and production default without `HOST`) and, when a trusted proxy policy is enabled, suggests `HOST=127.0.0.1` so a proxy on the same host cannot be bypassed; the default bind address is unchanged. * **Default Dynamic Cache Boundary:** `headers_middleware` supplies `Cache-Control: no-store` only when the handler has not already selected an explicit cache policy. Versioned public/static responses can therefore opt into reviewed caching without weakening the default for dynamic data. The standard `/static` mount does so (v13): `2xx`/`304` responses for a content-hashed file name get `public, max-age=31536000, immutable`, other static files `no-cache` with `ServeDir`'s `ETag`/`Last-Modified` validators; see [static assets](https://rullst.github.io/Rullst/book/tutorials/10-static-assets-and-compression.html#step-3-cache-headers). Since 13.0.0-alpha.2 `rullst::cache_control` offers explicit policies for dynamic routes: `cache_control::public(max_age)` and `private(max_age)` are layers that set `public, max-age=<seconds>` or `private, max-age=<seconds>` on the `2xx`/`304` responses of the routes they wrap when the response has no `Cache-Control` yet (errors keep `no-store`), and `CacheControl` can also be returned from a handler as a response part. The CSRF middleware never sets its cookie on a response that shared caches may store (a `Cache-Control` with `public` or `s-maxage` and without `private` or `no-store`). * **Strict Transport Security (v13):** `headers_middleware` adds `Strict-Transport-Security` when the response has none, built from `[security]` `hsts_max_age_seconds` (default 31,536,000), `hsts_include_subdomains` (default `true`) and `hsts_preload` (default `false`): `max-age=31536000; includeSubDomains` by default. `preload` is an opt-in consent to the browsers' preload list; `SecurityConfig::validate` (and therefore `Server` startup) rejects it without `includeSubDomains` or with a max-age below 31,536,000. 13.0.0-alpha.1 always sent `max-age=63072000; includeSubDomains; preload`. `rullst_security::SecureHeadersConfig::default()` uses the same default value. * **WAF Body Exemptions and Development Report (v13):** `[security]` `waf_body_exempt_paths` lists exact absolute paths (validated like `csrf_signed_webhook_paths`; invalid entries fail startup) whose request bodies `waf_middleware` neither reads nor inspects, a deliberate per-route risk acceptance; their path, query, cookies, `Referer` and `User-Agent` stay inspected. In development `apply_security_baseline` mounts a report-only WAF instead of none: it applies the same rules, passes every request (replaying bodies over the 1 MiB limit in full) and logs a `warn` event (target `rullst::security::waf`) with only the rule name and path for each request staging and production would refuse. The WAF never reads a `multipart/form-data` body; it marks such a request on a non-exempt path so that `uploads::Multipart` inspects each text field it reads (see Multipart Form Uploads). * **Double-Submit Form Contract:** `csrf_middleware` installs the exact request-scoped `CsrfToken` used by the CSRF cookie on eligible `GET` and `HEAD` requests (HEAD mirrors GET's headers, including a new cookie, so a `get` route extracting the token also answers HEAD) and preserves it after a valid state-changing request. Server-rendered forms must echo that value in `_token`; HTMX/JavaScript may instead send it through `X-CSRF-Token`. A `multipart/form-data` form (such as a file upload) must place its `_token` field before any file input: the middleware reads at most the first 64 KiB of that body to find the field, compares it in constant time and then passes the whole body on unchanged. Since 13.0.0-alpha.2 a multipart body without a readable `_token` field is refused with a `403` that names this rule (`Missing CSRF token: a multipart/form-data form must send its _token field before any file field …`) instead of the generic `Invalid or missing CSRF token`; a readable but wrong token keeps the generic text. A `GET` or `HEAD` for a static-looking path (`/robots.txt`, `/sitemap.xml`, `/favicon.ico`, `/static/*`, or a `.txt`, `.xml`, `.ico`, `.json`, `.css`, `.js`, `.png`, `.jpg`, `.jpeg`, `.svg`, `.webp` or `.wasm` suffix) never mints a token or sets the cookie; since 13.0.0-alpha.2 it still receives the `CsrfToken` of a valid existing cookie. Nested application and `Server` baseline composition is request-idempotent: exactly one CSRF layer owns token validation/cookie emission, so an explicitly protected router remains valid when the production server wraps it. The cookie intentionally remains script-readable and must not be confused with an authentication or session cookie. * **Secure Framework Cookies over HTTPS (v13):** the `rullst_csrf` cookie carries `Secure` in staging and production and, since 13.0.0-alpha.2, in any environment when the request arrived over HTTPS: an `https` request URI (HTTP/2 over TLS or an absolute-form target) or a `ClientAddr::forwarded_proto` of `https`, which only `TrustedProxyLayer` sets for a trusted peer with `trust_forwarded_proto`; a forwarding header from any other peer is ignored. `apply_security_baseline` (and so `Server`) mounts a layer outside CSRF in every environment that appends `; Secure` to each `Set-Cookie` of a `rullst_*` cookie (such as the `rullst_session` cookie of `rullst-auth`) without it on a response to such a request. Other cookies are untouched and `Secure` is never removed. * **Multipart Form Uploads (v13):** `uploads::Multipart` is an axum extractor for `multipart/form-data` (a missing or malformed boundary is `NotMultipart`, 415); `Multipart::read(&MultipartPolicy)` parses the body incrementally into an `UploadForm` of text fields and `UploadedFile`s. `MultipartPolicy::default()` allows 10 MiB per file, 16 MiB per body (a larger `Content-Length` is refused before reading), 4 files, 32 text fields of 64 KiB, 128-byte field names, 32 KiB of each file in memory (at most 1 MiB) and 30 s between body chunks; part header blocks are limited to 8 KiB. Exceeding a limit stops reading with a typed `MultipartError` (413 for sizes and counts, 400 for a long name, oversized headers, malformed structure or a non-UTF-8 field, 408 for the idle timeout). The parser accepts a body that starts with the first boundary, CRLF or `--` after each boundary, never reads past the closing boundary, rejects nested `multipart/*` parts, repeated `Content-Disposition`/`Content-Type` headers and repeated disposition parameters, and stays linear for tiny frames and many tiny parts. A part with `filename=""` and no content (an empty file input) is skipped and not counted. Larger files stream to `.rullst-upload-<uuid>.part` in `temp_dir` (default `std::env::temp_dir()`), created with `create_new` and mode `0600` on Unix and removed when the `UploadedFile` drops, also on every error. Before a process first writes a temporary file to a directory, and again once 24 hours have passed (13.0.0-alpha.3), it sweeps the leftovers of a crashed process there: regular files named exactly `.rullst-upload-<32 lowercase hex>.part` and modified at least 24 hours ago, never a symbolic link or a directory, examining at most 10,000 entries and removing at most 1,000; a failure is a `rullst::uploads` warning and the upload continues. `UploadedFile::persist_to` never overwrites (an existing path or symbolic link is `PersistTargetExists`), requires a file name in an existing directory without `..` (`InvalidPersistTarget`), hard-links on the same file system, copies otherwise, and syncs the new `0600` file. `allowed_kinds` and `expect_signature` (1–64 bytes) check the bytes while they stream, with the `UploadPolicy` signature and plain-text rules (`ContentDenied`, 415); the declared type and the sanitized `display_name` (last path segment, no control or invisible format characters, at most 255 bytes) are metadata only. The extractor ignores axum's `DefaultBodyLimit`. Where the WAF runs and the path is not in `waf_body_exempt_paths`, it marks multipart requests and `read` applies the WAF signatures to each text field name and value: `WafRefused` with the WAF's `403` in staging and production, a `warn` log (rule `multipart-field-signature`, path only) in development. A first `_token` field that ends after the first 64 KiB without an `X-CSRF-Token` header is `CsrfTokenTooLate` (`403`, the middleware's text) in every environment. Error responses are fixed plain text with `Cache-Control: no-store`. * **Database URL Resolution:** `Server` and Artisan (`artisan!`, `check_and_run_artisan`, `db:*`, `studio`) share one resolver: a `Server::with_db` value when `Server::run` intercepts the command, the process `DATABASE_URL`, `DATABASE_URL` in `./.env` (never overriding the process environment), then `[database].url` parsed as TOML. There is no implicit SQLite fallback: a `db:*` command without a configured database, and any configuration or `Orm::init` failure, exits with status 1. Parse errors report positions only, never file content. `Server::run` has no migration or seeder registry, so a `db:*` command it intercepts (because the application never called `artisan!`) exits with status 1 and names `rullst::artisan!` instead of reporting success for an empty registry; `studio` still runs. Like `Server`, Artisan never loads `.env` into the process environment (13.0.0-alpha.1 did after resolving the database), so a migration or seeder reads other `.env` values with `rullst::config::project_setting` and behaves the same under `cargo run` and `cargo run -- db:seed`; the legacy `studio` page checks AI provider keys the same way. Rullst Studio reuses an existing ORM pool; only when none exists does its first database view resolve the current directory with this same resolver and initialize the pool once. Without a configured database Studio reports its database tools as unavailable and creates nothing. * **Startup Migrations (v13):** `Server::auto_migrate(bool)` and `RULLST_AUTO_MIGRATE` (`true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off`; the process environment, then `.env`, overrides the builder; default off) apply the migrations registered with `Server::with_migrations` (feature `orm`) after the pool opens and before binding, through the ORM `migrate` command, in both the static and the hot-reload server. PostgreSQL and MySQL/MariaDB runs hold the runner's advisory/`GET_LOCK` lock; an SQLite file run also holds an exclusive OS lock on `<database>.rullst-migrate.lock` (in-memory databases take none). A failed migration, a missing registry or database, an unparsable value or an untakeable lock stops startup with `ServerError::Migration` (or `ServerError::Configuration` for the value); without the `orm` feature an enabled setting is `ServerError::Database`. Since 13.0.0-alpha.2 the SQLx starters that `cargo rullst new` generates (Blank, API and SaaS, including their internal hot-reload layouts; the LMS, Blog, Portfolio and ERP starters of that release are now examples) call `with_migrations` with their registry and leave `auto_migrate` unset, so only `RULLST_AUTO_MIGRATE` turns it on. Turso starters register none: their migrations run through `db:migrate`, and the variable set to true there fails startup with `ServerError::Migration`. * **Transactional Migrations (v13):** A migration whose `within_transaction()` returns `true` runs `up()` (or `down()`) and its `migrations` record inside one `Orm::transaction`, which `Schema`, generated model methods, `execute_query!`/`dispatch_executor!` and `Orm::raw(sql).execute()` join. Since 13.0.0-alpha.3, while that body runs on the runner's task, `Orm::pool()`, `Orm::read_pool()`, their `try_` forms and `Orm::begin_transaction()` return `Error::Validation` naming the migration instead of a connection outside the transaction, so the run fails and the migration rolls back unrecorded. A pool obtained before the body ran, another crate's pool and tasks spawned from the body are not detected. PostgreSQL and SQLite roll the DDL back with the record; MySQL/MariaDB commit DDL implicitly. * **Application Settings (v13):** `rullst::config::project_setting` reads one application setting from the process environment, then from `./.env`, which never overrides the process environment and is never loaded into it, neither by `Server` nor by Artisan commands. An unreadable or malformed `.env` is a `ConfigError` that never contains file content. Nexus `basic_from_env` reads its administrator credentials and generated billing code its `BILLING_*` settings through it; generated billing treats a production `RullstConfig::environment()` (or an invalid one) as production. ### 4.2. Server-Side Rendering (`rullst::html!`) * **Macro:** `html!` expands supported HTML trees into ordinary Rust `String` construction at compile time and returns it as `rullst::html::Markup` (since 13.0.0-alpha.2; earlier versions returned `String`). * **`Markup`:** a newtype over a `String` of already-escaped HTML. It implements `Display`, `AsRef<str>`, `Deref<Target = str>` (read-only), `PartialEq` with `str`, `&str` and `String` in both directions, `Default`, `Clone`, `Hash`, `From<Markup> for String` and for the axum body, and `IntoResponse` with `content-type: text/html; charset=utf-8`; `into_string()` and `as_str()` return the HTML. There is no mutable access to the text and no conversion from `String`: only the explicit `Markup::from_trusted(impl Into<String>)` and `From<RawHtml>` trust their input. `render_page`/`render_page_with_lang` take `impl Into<Markup>`; `Live::mount` and native `#[island]` functions return `Markup`, and a `#[live_component]` `render` method may return `Markup` or `String`. * **Caller bindings:** Generated output-buffer locals use hygienic identifiers; interpolated expressions retain caller name resolution, including `s` in fragments, attributes, nested elements and closures. Each expression keeps its existing evaluation order and escaping behavior. * **XSS Protection:** Dynamic display values in the supported `{expr}` syntax are HTML-escaped by the generated code. * **URL attributes:** Dynamic values of `href`, `src`, `action`, `formaction`, `poster`, `data`, `cite`, `background`, `codebase`, `icon`, `longdesc`, `manifest` and `usemap` (names matched case-insensitively) are also checked at runtime. A `javascript:` or `vbscript:` scheme, or `data:` outside `img`, `audio`, `video`, `source`, `track` and `image`, renders as `#`. Leading C0 controls or spaces and embedded tab/LF/CR are ignored while reading the scheme, as browsers do. Static attribute strings are author-owned and unchanged; entity references inside an explicit `RawHtml` value are not decoded before the check. * **Event-handler attributes:** A dynamic value for an `on*` attribute or htmx's `hx-on`/`hx-on-*` (also `data-` prefixed) is a compile-time error, because the browser runs it as JavaScript and escaping cannot make interpolated data safe there. Static handler strings remain allowed; attach data-driven listeners from a nonce'd script and pass values in `data-*`. * **Trusted-only `style`:** Dynamic `style` values are only HTML-escaped. They cannot leave the attribute and supported browsers do not run script from CSS, but untrusted CSS can restyle or overlay the page and load remote resources, so pass only trusted or validated values. * **Composition:** An interpolated `Markup` (or `&Markup`) is inserted as it is, so a fragment built by another `html!` call nests without a wrapper, while `String`, `&str`, numbers and other values are escaped. `Option<T>` renders nothing for `None` and `T` for `Some`; an iterator of `Markup` collects into one `Markup` (`FromIterator`, `Extend`). Collecting `Markup` into a `String` deliberately does not compile. In an attribute value (`name={value}`) a `Markup` is escaped like text (`HtmlEscape::escape_attribute`), because HTML there could end the attribute; `RawHtml` stays verbatim in both contexts. * **Raw Unescaped HTML:** `rullst::html::RawHtml(String)` (or `Markup::from_trusted`) is the explicit escape hatch for trusted HTML produced outside `html!`, such as a sanitizer's or renderer's output. It is never needed for `html!` output and must never wrap request data. * **Attributes:** `name="text"` is author-owned source and written as it is. `name={value}` is always written with the value escaped: a `bool` as `"true"`/`"false"` (what `aria-*` and `data-*` expect) and `None` as an empty value. Since 13.0.0-alpha.2 a bare `name` (`<input required disabled>`) is written without a value, and a conditional `name?={value}` is decided at runtime through `rullst::html::OptionalAttribute`: a `bool` writes the bare name or nothing, and an `Option<T>` writes `name="value"`, escaped exactly like `name={value}` (URL attributes keep the scheme check above), or nothing for `None`. Any other value type is a compile-time error at the expression. HTML boolean attributes mean presence, so `disabled="false"` still disables a control: use `disabled?={flag}`. The quoted forms keep compiling. Attribute names are Rust identifiers (keywords included) joined by `-`, so no quote, `=`, `>` or whitespace can reach the output through a name; `:` and `@` name forms are not supported. A conditional value on an event-handler attribute is rejected like a dynamic one, while a bare handler name is static. * **Control flow:** Since 13.0.0-alpha.2, `@if condition { … }` (also `@if let PATTERN = value`), `@else if …`, `@else { … }` and `@for PATTERN in iterable { … }` take `html!` nodes in their braces; the heads are parsed like Rust's own `if`/`for` heads and keep caller name resolution. Since 13.0.0-alpha.3, `@match value { PATTERN => { … } … }` does the same for `match`: each arm is a Rust pattern (`|` alternatives and `name @` bindings included) with an optional `if` guard, its body is in braces, and commas between arms are optional. The arms expand to a Rust `match`, so exhaustiveness and pattern errors are reported by the compiler at the caller's tokens, and bodies follow the same escaping and `Markup` rules as every other node. An arm body without braces is a compile-time error that shows the braced form. No other node can start with `@`, so the forms are unambiguous. A plain `else` after an `@if` body and any other `@` form (`@let`, `@while`, `@loop`) are compile-time errors: other control flow stays a Rust expression that returns `Markup` inside `{…}`. * **Element names and the doctype:** Element names (opening and closing) may be Rust keywords, such as SVG's `<use>`. A leading `<!DOCTYPE html>` (any letter case) is emitted as written; it must precede every other node (comments, which produce no output, may come first), and other declarations are compile-time errors. * **Memoize keys:** `#[memoize]` keys its process-local cache entries by the function's `module_path!()`, name and attribute location plus the serialized arguments, so same-named functions in different modules, crates or `impl` blocks never share results. It remains tenant- and invalidation-unaware. The process-wide store is bounded: at most 4,096 entries and 32 MiB of key plus value bytes, each entry at most 256 KiB and one hour old. Oversized entries run uncached; otherwise expired and then the oldest entries are evicted. Arguments that `serde_json` cannot represent (a failing `Serialize` impl, or a `u128`/`i128` outside the 64-bit range) also run uncached instead of panicking, as do arguments whose JSON would equal another value's (NaN or infinite floats, `Some(())`, `Some(None)`); such results are never cached; an `#[island]` with such props renders its server HTML with an empty `data-props`, so hydration is skipped. A memoized function may return `Markup`; its HTML is cached and read back as the same `Markup`. * **Example:** ```rust use rullst::html; let username = "<script>alert('xss')</script>"; let rendered = html! { <div class="user-badge"> <span>"User: "{username}</span> </div> }; // Automatically escapes to: <script>alert('xss')</script> // `rendered` is Markup: another html! inserts it without escaping it again. let card = html! { <section>{rendered}</section> }; assert!(card.starts_with("<section><div class=\"user-badge\">")); // Bare and conditional attributes, `@if`, `@for` and `@match`. let tasks = [("Write tests", true), ("<Ship>", false)]; let list = html! { <ul> @for (title, done) in tasks { <li><input type="checkbox" disabled checked?={done} />{title}</li> } </ul> @if tasks.is_empty() { <p>"Nothing to do"</p> } @match tasks.iter().filter(|(_, done)| !done).count() { 0 => { <p>"All done"</p> } 1 => { <p>"One task left"</p> } left => { <p>{left} " tasks left"</p> } } }; assert_eq!( list, "<ul><li><input type=\"checkbox\" disabled checked />Write tests</li>\ <li><input type=\"checkbox\" disabled /><Ship></li></ul>\ <p>One task left</p>" ); ``` ### 4.2.1. Translations and Locale Negotiation (`rullst::i18n`, since 13.0.0-alpha.2) * **Placement:** the runtime is the Core module `rullst::i18n` (no Cargo feature, not built for `wasm32`); the catalog macro is `rullst_macros::i18n_catalog`, re-exported as `rullst::i18n::catalog!`. Guide: [Translations and Locales](https://rullst.github.io/Rullst/book/i18n.html). * **Catalogs:** `catalog!(default = "tag", dir = "path")` (`dir` defaults to `locales`, relative to `CARGO_MANIFEST_DIR`; absolute paths are rejected) reads every `*.toml` file of the directory at compile time, at most 64 files of at most 1 MiB. The file stem is the locale tag, `language[-Script][-REGION]` in canonical case. Nested tables form dotted keys of ASCII letters, digits, `_` and `-`. A table whose entries are all strings named after CLDR categories and that has `other` is a plural message; since 13.0.0-alpha.3 any of `zero`, `one`, `two`, `few`, `many` and `other` the locale's rule selects is accepted. Templates use `{name}` placeholders (Rust identifiers that are not keywords) and `{{`/`}}`. * **Compile-time checks:** the build fails on a key missing from any locale, differing placeholder sets or plural/single shapes between locales, a malformed template, a plural message whose forms do not match the locale's rule, a non-canonical file tag, and a `default` without a file (at most 30 catalog errors are listed). Each file is tracked with `include_bytes!`, so edits rebuild the crate. Stable Rust gives a macro no way to watch a directory, so the documented setup adds a `build.rs` printing `cargo::rerun-if-changed=<dir>`: Cargo then rescans the directory and rebuilds the crate when a locale file is added or removed (`rullst-macros/tests/locale_rebuild.rs` builds a probe crate with and without it). * **Generated items:** in the invoking module, `pub static LOCALES: rullst::i18n::Locales` (tags sorted, default index) and `macro_rules! t` re-exported with `pub(crate) use t;`. `t!(locale, "key", name = expr, …)` takes a `Locale` or `&Locale` and a string-literal key; an unknown key (with a near-miss suggestion), a missing, unexpected or repeated argument, and a plural message without `count` are compile errors at the caller's tokens. Arguments are evaluated once, in call order. After a catalog error the macro still defines both items so that only the catalog error is reported. * **Output and escaping:** `t!` returns `Cow<'static, str>` plain text (borrowed when the message has no placeholders) that `html!` escapes in text and attributes; arguments use `Display`. A key ending in `_html` returns `Markup`: its catalog template is trusted and its arguments are HTML-escaped. * **Plural rules:** Since 13.0.0-alpha.3 forms are selected with CLDR's cardinal rules (checked against CLDR 48's sample numbers) for `ar`, `cs`, `de`, `en`, `es`, `fr`, `he`, `it`, `ja`, `ko`, `nl`, `pl`, `pt` (`pt`, `pt-BR`), `pt-PT`, `ro`, `ru`, `tr`, `uk` and `zh`, any region or script. A plural message must define exactly the categories its locale's rule selects: `other` only in `ja`, `ko` and `zh`; `one`/`other` in `de`, `en`, `nl` and `tr`; `one`/`few`/`many`/`other` in `cs`, `pl`, `ru` and `uk`; `one`/`few`/`other` in `ro`; `one`/`two`/`other` in `he`; all six in `ar`. `es`, `fr`, `it`, `pt` and `pt-PT` need `one`/`other` and may add `many`, which CLDR selects for exact millions (`other` is used when it is left out). A missing category is a compile error naming it; a category the rule never selects is one too. A plural message in another language, or a Portuguese region other than Brazil and Portugal, is a compile error. `count` accepts any integer and `rullst::i18n::Decimal` through `PluralCount` (the absolute value; integers beyond `u64` saturate). A `Decimal` keeps its visible fraction digits, which CLDR's operands read (`1.0` is English `other`, `1.5` is French `one`); compact exponents are not modelled. `Locale::plural_category(count)` returns the `PluralCategory` (`#[non_exhaustive]`), or `None` for a language without a rule. * **Message selection:** a call selects the catalog locale with the `Locale`'s tag, else one with the same language (the catalog default first), else the catalog default, so a `Locale` from another crate's list still renders. * **`Locale` and `Locales`:** `Locale` is `Copy` and exposes `tag()` and `Display`; it is obtained from `Locales::get` (ignoring ASCII case), `iter`, `default_locale`, `negotiate`, `cookie_locale`, `accept_language` or the extractor. `Locales::new(&'static [&'static str], default)` validates 1–64 canonical, distinct tags and a listed default, returning `LocaleError` (`Empty`, `TooMany`, `InvalidTag`, `DuplicateTag`, `UnknownDefault`; non-exhaustive). * **Negotiation:** `Locales::negotiate(&HeaderMap)` returns the first supported tag in a `rullst_locale` cookie (`LOCALE_COOKIE`, across all `Cookie` headers), else the best match of the first `Accept-Language` header, else the default. An `Accept-Language` value over 4,096 bytes is ignored; only its first 32 ranges are read, and ranges over 35 bytes, with `q=0` or with a weight outside RFC 9110's `0[.ddd]`/`1[.000]` are skipped. Ranges are ordered by weight with header order kept on ties; each matches a supported tag exactly (ignoring case), else by primary language (the default first, then list order), and `*` matches the default. * **Layer and extractor:** `Locales::layer(&'static self)` returns `LocaleLayer`, whose service inserts the negotiated `Locale` into the request extensions. `Locale` implements `FromRequestParts`; without the layer it rejects with `MissingLocale`, a `500` naming the layer. * **Switch route:** `Locales::switch_route(&'static self)` is a `POST` `MethodRouter` reading a URL-encoded body of at most 4,096 bytes (`413` above) with `locale` and optional `next`. A malformed form or an unsupported locale is `400` without a cookie. Otherwise it sets `rullst_locale=<tag>; Path=/; Max-Age=31536000; SameSite=Lax; HttpOnly`, with `; Secure` when the request's `Environment` extension (else the detected environment, else secure) requires secure defaults, and answers `303 See Other` to `next` if it is at most 2,048 bytes of printable ASCII starting with one `/` and containing no `\`, else to `/`. With `HX-Request: true` it answers `204` with `HX-Redirect` instead of `Location`. CSRF protection comes from the application's `csrf_middleware`/security baseline; the route does not exempt itself. * **Page language:** handlers pass `locale.tag()` to `render_page_with_lang` for `<html lang>`. * **Formatting:** Since 13.0.0-alpha.3 `Locale::format_number(impl Into<Decimal>)`, `format_percent(ratio)`, `format_currency(minor_units: i64, Currency)`, `format_date(&impl chrono::Datelike, DateStyle)`, `format_time(&impl chrono::Timelike, TimeStyle)` and `format_datetime` return `String`s with CLDR 48 conventions compiled in (generated from ICU 78 and CLDR's date patterns by `.github/generate-i18n-format-data.py`; no runtime data or network). Data exist for `ar`, `cs`, `de`, `en`, `en-GB`, `es`, `fr`, `he`, `it`, `ja`, `ko`, `nl`, `pl`, `pt`, `pt-PT`, `ro`, `ru`, `tr`, `uk` and `zh`; another tag uses its `language-REGION` entry, else its language's, else `en`'s (`Locale::format_data_tag`). Numbers use the locale's decimal and grouping separators, minimum grouping digits (`es`, `it`, `pl`, `pt-PT` write `1234`), minus sign and Latin digits, and show every fraction digit of the `Decimal` (`Decimal::round_half_even` and `round_half_up` round explicitly). Percentages multiply the ratio by 100. `Currency` (`BRL`, `USD`, `EUR`, `GBP`, `JPY` constants with localized symbols; `Currency::new(code, minor_digits)` for other ISO codes, written as the code; `CurrencyError::{InvalidCode, TooManyDigits}`) takes amounts in integer minor units, so no floating-point rounding occurs. `DateStyle` is `Short`, `Medium`, `Long`, `Full` (CLDR's date formats) or `Numeric` (CLDR's `yMd` skeleton); `TimeStyle` is `Short` or `Medium` (no zone names); both are `#[non_exhaustive]`. Date and time are joined with CLDR's date-time format for the date style (ICU's `atTime` variant for `Long` and `Full`; `Numeric` joins like `Short`). Values are formatted in their own offset; years before 1 are written as ISO numbers. Outputs keep CLDR's U+202F and U+00A0 spaces. Tests compare every locale with ICU output. * **Out of scope:** runtime-loaded catalogs, ordinal plural rules, script-aware matching, compact and spelled-out numbers, time zone names, non-Gregorian calendars and non-Latin digits. `rullst-mail`'s `MailLocale` remains separate (`MailLocale::from_preference(locale.tag())`). ### 4.3. Durable Queue Timing and Completion History * SQLite and Redis persist `dispatch_at` for at most 366 days and never claim a job before its stored millisecond due time. Execution remains poll-dependent and at-least-once. * The v13 opt-in `Queue::dispatch_deferred` places a job by a deadline and up to 24 daily fixed-offset windows: the start of the next allowed window (immediately inside an open one), never after the deadline, and the deadline when no window opens before it. `dispatch_deferred_with` places it at the lowest-value slot of an application-provided `CarbonIntensitySource` inside those windows, falling back to the window rule when the source fails or exceeds its timeout. Placement goes through `dispatch_at`, so stored rows and keys are unchanged; Core ships no network source and makes no emissions claim. `Scheduler::deferrable_task` applies the window rule to each tick within a maximum delay. * `Scheduler::task` matches POSIX five-field cron against UTC. The v13 opt-in `scheduler-tz` Core/facade feature adds `Scheduler::task_in_tz(expression, zone, handler)`, which matches the same fields, day fields included, against the wall clock of an IANA time zone. An unknown zone is rejected at registration with `SchedulerError::InvalidTimeZone { zone, message }`. A wall time that a daylight-saving gap skips runs at the first instant after the gap (several skipped matches run once there); a wall time that a fold repeats runs once, at its first occurrence, and no match in the repeated hour runs a second time. Zone rules come from the system zoneinfo database when it has the zone, otherwise from the database bundled with the `jiff` dependency. Fixed instants in tests cover São Paulo, New York's gap and fold, and UTC equivalence with `Scheduler::task`. The 13.0.0-alpha.3 `Scheduler::deferrable_task_in_tz(expression, zone, deferral, handler)` matches the ticks as `task_in_tz` does and reads the deferral's windows as wall times of the same zone, with the same rule for their boundaries (a skipped boundary moves to the end of the gap, a repeated one is its first occurrence; a window a gap skips entirely does not open that day); `max_delay` stays elapsed time, and a window that sets a UTC offset is rejected with `InvalidConfiguration`. Fixed instants around New York's spring and autumn changes cover it. * SQLite claims pending jobs in order of their due time, and an immediately dispatched job is due at its enqueue time. A scheduled job, or one handed back for lack of a handler, that became due earlier is therefore claimed before later immediate jobs instead of waiting for the backlog to drain. * Successful SQLite jobs are deleted by default. The explicit `Queue::sqlite_with_completed_history` constructor validates a 1–100,000 row limit, changes a processing row to `completed`, and prunes excess history in the same transaction. * `Queue::purge_completed_history` removes those opt-in retained successes. Rows contain the original payload, so Studio access, data minimization and retention policy remain host responsibilities. Redis/custom drivers expose inspection or history only when their capability implements it. * Redis stalled-lease recovery measures each lease's age with Redis server time, the clock that scored the claim, so a worker host's clock skew cannot recover a running job early or postpone recovery of a crashed one. * The Redis queue waits up to 10 seconds for each command's reply (the cache keeps the 500 ms redis-rs default). Redis still applies a script whose reply times out, so a claim lost that way is returned only by stalled-lease recovery; the longer deadline keeps ordinary latency spikes from causing it. * SQLite never recovers a lease early. A claim with a lease stalls once its millisecond expiry has passed; a claim made without one (`pop`) records only whole seconds, so it is recovered between `stalled_after` and about two seconds later. * A worker without a handler for a claimed job's name hands it back with a five-second delay until the claim's 720th attempt, then fails it with `HandlerNotFound`, so a job whose name no worker registers reaches the failed state instead of cycling forever. `Queue::dispatch` and `dispatch_at` reject empty job names and names longer than 256 bytes. ### Local object replacement `LocalDriver::put`, and `Storage::local`/`TenantStorage` over it, never truncates an existing object. It writes a uniquely named temporary file in the validated destination directory (`create_new`), flushes it with `sync_all` and renames it over the destination. A failure removes the temporary file and leaves the previous version. Readers and concurrent writers observe exactly one complete version; the last rename wins. The replacement is a new file with default permissions, so permissions or hard links of the previous file are not carried over. The directory is not fsynced, so a power loss can roll a completed put back to the previous version. On Windows a replacement fails while another process holds the object open without delete sharing. Like the cloud backends, local `exists`, `get`, `metadata` and `delete` treat a key that names a directory, or that continues below a regular file, as a missing object rather than an I/O failure. ### Public object URLs `Storage::url` and `LocalDriver::url` percent-encode every byte of each key segment except the unreserved `A-Z a-z 0-9 - . _ ~`, so `#`, `?`, `%`, spaces and non-ASCII characters stay part of the object key. Local storage returns the root-relative `/storage/<key>` path whatever its base directory, so the filesystem path is never disclosed; the application must serve that directory at `/storage` (Rullst does not mount it). An unconfigured S3 driver returns the provider's unsigned object URL, following the cloud client's endpoint rules (`amazonaws.com.cn` for `cn-*` regions, path style for dotted bucket names). An R2 driver returns `StorageError::Unsupported`: R2's S3 API endpoint never serves anonymous reads, and its public `r2.dev` or custom domain is application-owned Cloudflare configuration. A configured private backend rejects `url()` and requires a signed download. --- ### 4.4. Private S3-compatible storage (v13, source admitted) The opt-in `storage-s3` Core/facade feature extends the existing `Storage` and `TenantStorage` APIs with explicit AWS S3 or Cloudflare R2 credentials. It must not add cloud dependencies to bare Core or infer credentials from the ambient environment. The provider constructors alone remain unconfigured until a validated cloud configuration is attached. The supported journey is bounded object upload, download, metadata, deletion and short-lived signed GET URLs. Use the maintained AWS SigV4 signer, explicit provider endpoint/region rules, HTTPS, no redirects or ambient proxies, bounded request/body budgets and redacted errors. A separate development configuration may target a literal loopback endpoint for disposable protocol acceptance; it must not pass a production-configuration check. Request paths are the SigV4 canonical URI: the bucket and each key segment are `UriEncode`d once, leaving only unreserved `A-Z a-z 0-9 - . _ ~` and `/` separators literal. Empty or `mock_*` credentials select a deterministic bounded in-memory fallback, also rejected by that check. No mock URL may impersonate a signed provider URL. Tenant wrappers bind all five operations to the authenticated namespace. The application still authorizes the current account and object before every operation or grant; namespacing alone is not authorization. Private access URLs are bearer secrets, remain usable until expiry/provider invalidation, and must not appear in Debug/errors. They are not single-use or individually revocable. Direct presigned uploads, bucket/IAM provisioning, image processing and arbitrary S3 operations are outside this increment. Existing file quarantine/admission remains an explicit application step before cloud persistence or sharing. Acceptance requires feature-off/on and MSRV checks, an extracted-package consumer, a tenant-bound application journey, independent signature/protocol fixtures, bounded failure/expiry tests and a real disposable S3-compatible service. Those tests do not establish interoperability with owner accounts, provider configuration or regional legal compliance. Source/package admission passed in PR #236; final release admission remains separate. #### Resumable multipart candidate (source admitted, v13 prerelease) The separate opt-in `storage-multipart` extends this adapter with server-mediated initiation, exact-size SHA-256-checked parts, bounded remote reconciliation, completion and abort. A server-owned `MultipartKey` seals resumable checkpoints with AES-256-GCM under a versioned purpose and exact endpoint/bucket/region/mock profile/object binding (including the ephemeral instance in mock mode). Each operation reopens the checkpoint and checks policy; tenant wrappers derive that object only from the authenticated namespace. The application must still authorize the current subject/object and serialize or CAS checkpoint updates. A checkpoint is sensitive recovery state, never identity, membership, malware clearance or permission to publish an object. The initial scope fixes total length, part size (5–64 MiB), at most 256 parts and a lifetime of one minute through seven days. Part bytes match a caller-supplied SHA-256 before signing the exact payload with SigV4. ETags are opaque provider receipts, not content hashes. Checkpoints retain receipts; resume lists provider parts and reports missing/mismatched receipts for explicit re-upload. Completion requires every consecutive receipt and rejects an embedded XML error even under HTTP 200. A provider marker and exact length support explicit reconciliation of lost completion responses, without guessing from object existence alone. Only one part is buffered per call; applications bound concurrent calls. Responses are byte/node bounded, prohibit DTDs and validate exact root/resource identity. No public ACL, direct browser upload or ambient endpoint is introduced. Existing ordinary-object download limits still apply to large completed objects. New objects may replace a same-name object as with `put`; applications must allocate fresh private/quarantine names and withhold reads until scanning and authorization. Abort remains available for expired, authentic checkpoints and checks provider absence; concurrent in-flight writes can require another abort. Retain cleanup records durably and provision provider lifecycle expiration for abandoned uploads, including initiation whose response was lost before an upload ID was obtained. No application database, global sweeper, bucket provisioning or durable revocation of checkpoints is implied. The offline backend has explicit bounded in-memory state; native disposable S3, process/service restart, forged checkpoints, tenant isolation, response failures and extracted consumers are required evidence. ## 🗄️ 5. Active Record ORM & Schema Engine (`rullst-orm`) ### 5.1. Model Definition & CRUD ```rust,no_run use rullst_orm::{FromRow, Orm}; #[derive(Debug, Clone, FromRow, Orm)] #[orm(table = "users")] pub struct User { pub id: i32, pub name: String, pub email: String, #[orm(encrypted)] pub secret_token: Option<String>, } async fn use_users() -> Result<(), rullst_orm::Error> { // Queries (after `Orm::init(...)` and schema migration at startup). let all_users: Vec<User> = User::all().await?; let user: Option<User> = User::find(1).await?; // Mutations let mut new_user = User { id: 0, name: "Alice".into(), email: "alice@example.com".into(), secret_token: None }; new_user.save().await?; // Auto-executes parameterized INSERT or UPDATE new_user.delete().await?; let _ = (all_users, user); Ok(()) } ``` `save()` of a persisted model without a `RowSnapshot` field is a full-row `UPDATE` of every persisted column except the soft-delete marker, so the last writer wins: a column another writer changed after the handle was loaded is overwritten with the handle's value. An `INSERT` likewise binds every persisted field, so database column defaults do not apply to them, except to fields marked `#[orm(db_default)]` (v13): the generated `INSERT` omits them, so the column `DEFAULT` applies, and no `UPDATE` of `save()` writes them, so the stored value stays; without dirty tracking the handle keeps its own value until the row is read again, `update_partial()` has no setter for them and `#[derive(Nexus)]` shows them read-only. The option is rejected on `id`, the tenant column, relations, skipped and encrypted fields, and by Turso-primary models. The generated `save()` rustdoc and the ORM guide name the two supported alternatives: `update_partial()` (§5.4.1), which merges selected fields into the freshly read and, on PostgreSQL and MySQL/MariaDB, locked row, and one atomic raw statement such as `UPDATE ... SET xp = xp + ? WHERE id = ?` for counters, which bypasses the model lifecycle (tenant scope, policy, hooks, audit, cache). The driver contract runs that counter and the stale-handle overwrite on SQLite, PostgreSQL, MySQL and MariaDB. **Dirty tracking (v13).** A SQLx model opts in with exactly one field of type `RowSnapshot` (`rullst_orm::RowSnapshot`, matched by its last path segment) marked `#[sqlx(skip)]`; a `RowSnapshot` field without `#[sqlx(skip)]`, or a second one, fails compilation. The tracked columns are the columns of the `UPDATE`: every persisted field except `id`, `#[orm(db_default)]` fields and the soft-delete marker. Rows decoded by the generated reads (`get` and the methods built on it, relations, streams, query-cache hits and the row `update_partial()` locks) record, after decryption and before `after_fetch`, one keyed 64-bit SipHash per tracked column of its `serde_json` serialization (`SecretString` contributes its plaintext; `#[sqlx(json)]`, `Json<T>` and `serde_json::Value` columns their document with object keys sorted at every level; a value that cannot be serialized never matches). The snapshot is 16 bytes plus 8 per column and stores no value. `save()` of a persisted record runs the `before_save` hook and `saving` observers, then compares: with no difference it returns `Ok` and runs nothing else (no `updating`/`updated`/`saved` observer, `after_save` hook, audit entry or post-commit effect); otherwise it runs the `updating` observers, recomputes the difference and issues `UPDATE <table> SET <changed columns> WHERE id = ?` (plus the tenant predicate) with `RETURNING *` on PostgreSQL and SQLite, or followed by `SELECT * ... WHERE id = ?` in the same transaction on MySQL/MariaDB. No row is `RecordNotFound` (the tenant-scope `Validation` error on tenant models). The handle's unwritten tracked columns, `#[orm(db_default)]` columns and soft-delete marker take the stored values, so observers, the audit diff, events and the Scout document see the stored row, and the snapshot is recaptured. An insert records the snapshot of the inserted values. Without a `before_save` hook, policy or registered observer, an unchanged `save()`/`save_with_tx()` returns before opening a transaction or savepoint (it still applies and checks the tenant context). A failed save restores the previous snapshot, so its changes stay pending; a rollback of a caller-owned transaction after a successful save is not visible to the handle. A record built in code, one from a Redis hash, or after `mark_all_dirty()` is untracked and writes every tracked column. Tracked models also get `dirty_columns()` and `is_dirty()`. `RowSnapshot` is bookkeeping: any two compare equal, hash alike and order equal; `Debug` shows only whether it is tracked; it implements no Serde trait. Turso-primary models do not track. Models without the field keep the full-row `UPDATE`. Since 13.0.0-alpha.3 every SQLx model the CLI writes (`make:model`, `generate:models`, `auth`, `make:billing`, `make:chat-session` and the `cargo rullst new` starters) declares `#[sqlx(skip)] pub snapshot: RowSnapshot` and builds new records with `RowSnapshot::default()`; `generate:models` adds `#[serde(skip)]` and omits the field for a table with a `snapshot` column. `From<sqlx::Error> for rullst_orm::Error` maps `RowNotFound` to `RecordNotFound`, a unique or primary-key violation to `UniqueViolation { constraint }` and a foreign-key violation to `ForeignKeyViolation { constraint }` (both v13, `#[non_exhaustive]` variants); every other database error stays `DatabaseError(String)`. `constraint` is the name PostgreSQL reports and `None` on SQLite and MySQL/MariaDB. Their `Display` names only the constraint, never the database message, which can quote the rejected value. Generated model writes and builders reach the caller through this conversion, and `Orm::raw` uses it too. `Orm::transaction` returns `Result<R, E>` for a closure error type `E: From<rullst_orm::Error> + Display`. After the rollback, the closure's `Err` reaches the caller unchanged, so a violation keeps its variant and an application error its type. Failures of the transaction itself (begin, savepoint, release, commit, post-commit effects) are `rullst_orm::Error`s converted with `E::from`; a commit rejected by a deferred constraint is a typed violation too. Only a rollback that fails after a failed closure returns `E::from(DatabaseError(..))`, naming both errors. 13.0.0-alpha.2 returned `Result<R, rullst_orm::Error>` and reported a failed closure as `DatabaseError("Transaction failed: ...")`. The `Orm` derive grammar is fail-closed. Model and field attributes are parsed as structured nested metadata; unknown or duplicate options are compile errors. Every SQLx model requires a persisted named `id` field; a model whose only persisted field is `id` inserts the column defaults (`DEFAULT VALUES`, or `() VALUES ()` on MySQL/MariaDB). Explicit table/column/relation identifiers use the 1–64 byte portable ASCII identifier grammar; the derived `<struct>s` default table name must match it only when no explicit `table` replaces it (for example for a non-ASCII struct name), and declared hook, scope, policy, relation-model, tenant, soft-delete, and embedding references are validated before code generation. A relation field accepts exactly one relation declaration; options that do not apply to that relation fail compilation. `belongs_to_many` requires `pivot_table` and defaults omitted owner/related pivot keys from the two model names. Generated SQL emits table and column identifiers unquoted, so the grammar check does not make a name portable: a word the target database reserves (for example `order`, `group`, `desc` or `user` on PostgreSQL, or `groups` and `rows` on MySQL 8, including a derived default such as `groups` for a `Group` struct) passes compilation and fails, or on PostgreSQL may even resolve to a built-in such as `current_user`, at runtime. The derive does not check reserved words; rename such a column or choose a non-reserved `#[orm(table = "...")]`. Because PostgreSQL folds those unquoted names to lower case, `Schema::create` and `Schema::drop_if_exists` quote the table name lower-cased on PostgreSQL, so `Schema::create("UserProfiles", ...)` creates the `userprofiles` relation that a model with `#[orm(table = "UserProfiles")]` queries. MySQL/MariaDB and SQLite keep the name as written. Only `skip`, `default`, `json`, and `json(nullable)` from SQLx field metadata are compatible with generated ORM persistence in v12 and v13. Two field options keep a field out of generated writes, with different meanings: | Option | The column | Generated reads (`FromRow`) | `INSERT` / `save()` / `update_partial()` | WHERE / ORDER BY / GROUP BY / `select` | | --- | --- | --- | --- | --- | | `#[orm(skip)]` | is not a database column for the ORM | the application's SQLx `FromRow` still reads it unless `#[sqlx(skip)]` | never written | rejected at run time with `Validation` | | `#[orm(db_default)]` | exists and is managed by the database (a default, a trigger or explicit SQL) | read | never written (the `INSERT` takes the column `DEFAULT`) | allowed | `#[orm(skip)]` removes a field from generated SQL (writes, filters and projections) but not from the application's SQLx `FromRow`, which still reads the column from `SELECT *`. A field without a table column therefore needs `#[sqlx(skip)]` (alone or together with `#[orm(skip)]`, which are distinct options) or `#[sqlx(default)]`. A column the table has that generated writes must not touch is declared `#[orm(db_default)]`, which keeps it filterable; the builder's error for a skipped column names that option. Until 13.0.0-alpha.2 the SaaS, LMS and `cargo rullst auth` `User` models declared `is_admin` and `session_epoch` `#[orm(skip)]`; from alpha.3 they use `#[orm(db_default)]`, and `cargo rullst upgrade` reports the old declaration (`V13-ORM-SKIP-MANAGED-COLUMNS`). `FromRow` decodes a `json` or `json(nullable)` field through SQLx `Json`, and generated INSERT/UPDATE statements (including `update_partial()`) encode it the same way: the value is bound as `sqlx::types::Json(value)`, and a `json(nullable)` `None` as SQL `NULL`. The field type therefore needs Serde `Serialize`/`Deserialize`, not an SQLx `Encode`, and the column the driver's JSON type (`JSONB`/`JSON` on PostgreSQL, `JSON` on MySQL, text on SQLite). SQLx implements `Json` only for the concrete drivers, so such models require a strict driver feature; the dynamic `Any` pool cannot decode them. `rename`, `try_from`, `flatten`, and unknown SQLx options fail compilation instead of letting the decoded shape drift from generated SQL. The same applies to struct-level SQLx metadata, which `FromRow` also reads: only `#[sqlx(default)]` is accepted on the model, and `rename_all` or any other container option fails compilation. Soft-delete sentinel expressions are bounded compile-time SQL fragments, not parameterized runtime values: they are capped at 128 bytes and reject statement separators, NUL, and SQL comments, while portability and semantic review remain the model author's responsibility. ### 5.2. Parameterized Queries & Privacy * Values accepted by non-raw query APIs use SQLx parameterization. Structural identifiers use the bounded ASCII grammar. pgvector helpers bind canonical vector/distance strings after rejecting empty/non-finite vectors and invalid distances; they do not interpolate those runtime values. Methods explicitly suffixed/named `raw` remain caller-owned escape hatches rather than an injection-safety claim. * Schema DDL cannot bind values. `ColumnDefault::Text` and `Blueprint::enum_col` variants are emitted as single-quoted literals with doubled single quotes, and building the schema rejects such text when it contains a backslash (an escape character in MySQL/MariaDB's default SQL mode) or a control character. * Generated builders assemble bindings by emitted clause position (CTE, SELECT, JOIN, WHERE/HAVING, ORDER BY), not by the order in which fluent methods were called. Nested typed subqueries export that ordered binding sequence. Caller-owned raw CTE and select fragments supply their own values through `with_raw_bindings`, `with_recursive_raw_bindings` and `select_raw_bindings` (v13); a marker/binding count mismatch fails closed, and `$n` markers are rewritten like typed subqueries. Pluck and count replace the select list and therefore omit its values. `with_raw`, `with_recursive_raw` and `select_raw` fail with `Validation` when their SQL contains a bind marker, because `bind()` appends WHERE values and a marker before FROM would take the tenant or model-wide scope binding. * Generated builders start with the global row cap from `Orm::set_max_query_limit` (1,000 by default; `0` disables it). `limit()` clamps to it and `unsafe_unlimited()` removes it for one query. An `offset()` without a limit is emitted after `LIMIT -1` on SQLite and `LIMIT 18446744073709551615` on MySQL/MariaDB, which accept `OFFSET` only after `LIMIT`; PostgreSQL receives `OFFSET` alone. `paginate(page, per_page)` clamps `per_page` to the same cap and reports the effective value in `PaginationResult::per_page` and `last_page`. * `count()` and the `paginate()` total count the rows `get()` would return without its limit and offset. With `distinct()` or `group_by()` they count that row query as a derived table (`SELECT COUNT(*) FROM (...) AS __rullst_count`, keeping a DISTINCT select list); otherwise they count the filtered rows directly. * `where_exists`, `or_where_exists`, `with_cte` and `with_recursive` embed a subquery with portable `?` markers, even when its own `to_sql()` rendered PostgreSQL `$n` markers. The outermost statement (including `delete_all`) is numbered once, so `$n` follows textual order and the mandatory tenant or model-wide scope binding can never shift onto a nested or caller value. A custom subquery whose `$n` markers are mixed with `?`, reference a missing binding or leave a binding unused fails closed with a `Validation` error. * `delete_all()` renders only the WHERE and soft-delete predicates. A builder with an explicit `limit()` (the implicit global cap does not count), `offset()`, `order_by()`, joins, `group_by()`/HAVING or CTEs fails with `Validation` instead of silently deleting every matching row. On a soft-delete model, `with_trashed()` and `only_trashed()` fail the same way: the soft-delete `UPDATE` matches only live rows, like the instance `delete()`, so a trashed row keeps its deletion time. Purge trashed rows with `force_delete()`. * Only PostgreSQL statements are renumbered. `delete_all()`, including the soft-delete `UPDATE` that `cascade_soft_delete` issues for child rows, keeps `?` markers on MySQL/MariaDB and SQLite; the SQLite test and the live PostgreSQL, MySQL and MariaDB matrices execute filtered, tenant-scoped and cascading bulk deletes. Instance `restore()` and `force_delete()` statements, including their tenant predicate, are numbered the same way and run in those matrices too. * The PostgreSQL renumbering skips quoted text using PostgreSQL's default `standard_conforming_strings = on` rules: a backslash escapes the next character only inside `E'...'` strings, so a raw fragment such as `ESCAPE '\'` does not hide the markers after it. A server configured with `standard_conforming_strings = off` is outside this contract. * Generated magic filters bind supported primitive fields to their Rust type at compile time (`String`, `i32`, `f64`, and `bool`), and generated column enums make unknown columns unrepresentable on typed paths. String-column builders, custom `RullstValue` conversions and raw SQL are explicit runtime-checked or caller-owned alternatives, not compile-time schema verification. * `RullstValue`, `#[non_exhaustive]` since v13, carries `String`, `Int` (`i32`), `Float`, `Bool`, `I64` and `Null`. `i64` and `u32` convert to `I64` and `Option<T>` to `T`'s value or `Null`; an integer literal stays `Int`. Generated builders and `Orm::raw` bind every value through one hidden `BindRullstValue` helper: `I64` as `i64` and `Null` as a text-typed SQL `NULL`, which PostgreSQL accepts in a non-text column only through a cast. A comparison with `Null` never matches (and PostgreSQL rejects it against a non-text column), so `where_null` expresses that filter. Tenant IDs take the same values: an `I64` tenant gets its own audit (`i64:`) and cache keys but does not convert to an `i32` tenant field (generated queries fail with `Validation`, §5.4), and a `Null` tenant fails the audit lookup with `Validation`. * A per-column helper (`where_{column}`, `or_where_{column}`, `where_not_{column}`, `order_by_{column}`, `order_by_{column}_desc`) is not generated when its name equals a fixed builder method (for example `where_raw`, `where_exists`, `where_column`, `where_col`, `where_similar` or `order_by_desc` for columns `raw`, `exists`, `column`, `col`, `similar` or `desc`) or when two columns would produce the same name (`x` and `not_x` both yield `where_not_x`). Those columns remain available through the string-column methods. The `update_partial()` builder keeps its model reference under the reserved `__rullst_` prefix and emits no setter for a column named `save` or `save_with_tx`. * `String` and `Option<String>` fields annotated with `#[orm(encrypted)]` are encrypted before generated ORM writes and decrypted after generated model reads using AES-256-GCM. Randomized ciphertext cannot be filtered, ordered, grouped, or explicitly selected by generated query-builder methods; use a separately reviewed blind index when equality lookup is required. Raw SQL remains an explicit, non-transparent escape hatch. The builder's encrypted, `SecretString` and skipped-column guards compare the (unqualified) column name ignoring ASCII case, like unquoted SQL identifiers, and `pluck_string` decrypts with the declared column name. * Generated secondary projections never carry `#[orm(encrypted)]` or `#[orm(masked)]` plaintext. `to_json()` (used for audit rows and committed `ModelCommittedEvent`/Redis `orm:events:*` payloads) omits `#[orm(hidden)]` fields and replaces encrypted and masked values with the fixed `"***"` marker. An update that changes only such a field still writes an audit row that records the field as `"***"` on both sides, and that revision cannot be restored. Scout documents omit hidden, encrypted and masked fields entirely. `save_to_redis` stores encrypted fields as the same table/column-bound envelope as the SQL column and `get_from_redis` decrypts them; hashes written by earlier versions with plaintext in those fields fail closed on read and must be rewritten. Masked and hidden values keep their database representation in Redis hashes and the query cache, like the SQL column. Audit rows, search documents and events written by earlier versions are not rewritten; purge or reindex them if they may contain plaintext. * `SecretString` never serializes its plaintext. Its SQLx codec encrypts and decrypts the column. `Serialize` emits an authenticated `RULLST:v2` envelope under the configured key, bound to a serde-specific context (a SQL-column envelope is not accepted), and fails when no key is configured; `Deserialize` decrypts such an envelope through the current key or keyring and still accepts any other string as plaintext input. The envelope binds only the key, not a model, field or owner, so that `Deserialize` is for trusted stores: given client input, it would turn an envelope exposed for one record into another record's secret. Fields filled from requests use `#[serde(deserialize_with = "rullst_orm::privacy::deserialize_plaintext_secret")]` (or `deserialize_optional_plaintext_secret`; v13 API), which reject any `RULLST:` value without decrypting it. Generated `to_json()` and search projections serialize it (also when nested) as `"***"`, and `SecretString`/`Option<SecretString>` model fields are audited, excluded and change-tracked like `#[orm(masked)]` fields. A plain `#[derive(Serialize)]` on a model therefore emits the envelope; call `reveal_audited()` for deliberate exposure. Each write encrypts a `SecretString` column with a fresh nonce, so generated builder filters, ordering and grouping on it (including `where_<column>` helpers) fail with `Validation` instead of never matching, and `pluck_string`/`pluck_i32` reject it rather than return envelopes. An explicit `select` of the column remains available because the codec decrypts it while decoding the model. ### 5.3. Generated Relationship Contract * An omitted `foreign_key` defaults to a lowercased model name plus `_id`: `belongs_to` reads `<related model>_id` from the declaring model (`post_id` for `belongs_to = "Post"`), while `has_one`/`has_many` and the owner side of `belongs_to_many` match `<declaring model>_id` on the other table. Models whose names are not single words usually need an explicit `foreign_key`. `local_key` (this model's matched key, default `id`) applies to `has_one`, `has_many`, `morph_one`, `morph_many` and `belongs_to_many`; `related_key` (the related model's key) applies to `belongs_to`, `belongs_to_many` and `morph_to`. Either option on another relation fails compilation instead of being ignored. * A relation key may be nullable (`Option<T>`, such as a nullable foreign key) on either model; generated loaders compare its inner value. A `None` key matches no row, like SQL `NULL`: the lazy loader returns `None` or an empty list without a query, an eager load assigns `None` (or `Some(vec![])` for a to-many relation) to such a parent, and a related row whose key is `None` belongs to no parent. The `morph_to` id field stays non-optional. * SQLx models may declare `morph_many`, `morph_one`, and one or more explicit typed `morph_to` targets. A polymorphic relation requires `morph_name = "..."` (`name` remains a legacy alias). * `morph_to` fails macro expansion unless the source has a persisted bindable `<morph_name>_id` field and a persisted `String` discriminator named `<morph_name>_type`. `foreign_key` may override the ID field and `related_key` may override the target key. * The discriminator stores the Rust target model name. Lazy loading returns `None` for a different target; eager loading batches each declared target and never guesses an undeclared runtime type. Target models used in eager inverse loading must implement `Clone`. * Each eager load (`has_many`, `has_one`, `belongs_to`, `morph_many`, `morph_one`, `morph_to` and the related query of `belongs_to_many`) issues one query for the whole parent batch. When that query still carries the global row cap, it fetches one row beyond the cap and fails with a `Validation` error if the cap would truncate it, so no parent silently receives an empty or partial relation. A constrained eager load that sets an explicit smaller `limit(n)` (applied to the whole batch) or `unsafe_unlimited()` is honored as written. A to-many eager load (`has_many`, `morph_many`, `belongs_to_many`) assigns `Some(vec![])` to a parent without related rows, so `None` always means "not loaded". * An eager `belongs_to_many` load hands each parent its related rows in the order of the related query, so the `order_by` of `with_<relation>_constrained` applies as it does to the lazy `<relation>_constrained` loader; a related row linked by several pivot rows appears once per pivot row. Without an `order_by` the order is unspecified. * Every parent receives the related rows it shares with other parents (one `belongs_to` parent of many children, a non-unique `local_key`, or duplicated parent rows). The shared value is cloned for every such parent but the last; when the related model does not implement `Clone`, a shared row fails the load with a `Validation` error instead of leaving a parent empty. * The relation's ownership predicate (foreign key, morph id/type pair or pivot key) forms its own `AND` group before a lazy `<relation>_constrained` or eager `with_<relation>_constrained` modifier runs, like the tenant and model-wide scopes, so an `or_where` in the modifier cannot return another parent's rows. ### 5.4. Tenant Scope Contract * A SQLx model declaring `#[orm(tenant_column = "tenant_id")]` must have a persisted `String`, `i32`, `f64`, or `bool` tenant field. The derive rejects a missing or unsupported field type. * Generated queries fail closed when called outside `with_tenant(...)` and bind the active tenant inside the scope, converted to the tenant field's type; a context of another type fails with `Validation` for reads and bulk deletes exactly as for mutations, instead of reaching a predicate that MySQL would compare numerically across tenants. Generated full/partial updates and instance delete/restore paths reject a model from another tenant. * The generated tenant predicate, the soft-delete filter and the `chunk_by_id` keyset name their column with the model's table (`<table>.<column>`), so a join with a table that has the same columns, such as a tenant-scoped or soft-deletable `belongs_to_many` pivot, cannot make them ambiguous. A model-wide `global_scope` is application code and qualifies its own columns. * `Model::unscoped()` is the explicit global escape hatch. Deciding who may use it, deriving tenant identity from authenticated state, and database-level RLS remain host responsibilities. * SQLx builders keep offset-based `chunk(...)` for compatibility and expose fallible `chunk_by_id(...)`/`chunk_by_id_with_tx(...)` for stable ascending keyset traversal over the generated `i32` primary key. This prevents deletes of processed rows from shifting later rows behind an offset; it is not a database-server cursor or a universal cross-shard snapshot. Without an `order_by`, `chunk(...)`/`chunk_with_tx(...)` order their pages by `<table>.id`, because SQL gives consecutive offset queries no stable order (a PostgreSQL synchronized scan, for example, can start mid-table). An explicit `limit(n)` caps the rows every traversal hands to its handler in total (the implicit global row cap does not), and an explicit `offset(k)` is where `chunk(...)` starts and how many rows `chunk_by_id(...)` skips before its first page. * A model delete with marked `cascade_soft_delete` has-one/has-many relations runs parent and direct-child mutations in one transaction. An existing explicit or task-scoped transaction is reused; otherwise `delete()` opens, commits, or rolls back its own transaction. The children are selected with the related model's tenant scope but without its `global_scope`, so a child hidden by that model-wide filter is trashed with its parent too. Recursive descendant/cycle traversal remains a separate contract. The related model must itself use soft deletes (a `deleted_at` field or `#[orm(soft_delete)]`): a cascade into a model without them fails to compile (no method `__rullst_cascade_soft_delete_with_tx`, reported at the relation field) instead of permanently deleting the children of a restorable parent. A policy-protected child cannot be authorized in bulk, so its cascade fails closed with `Error::Validation` and rolls the parent delete back; delete such children individually first. * Generated `#[orm(auditable)]` instance `save()`/`delete()` operations (and `restore()`/`force_delete()`, recorded as `restored`/`force_deleted`) write their bounded audit entry through the same explicit, implicit, or task-scoped transaction as the model mutation. Audit write errors fail the mutation and roll its savepoint back; direct `log_audit` calls also honor a task-scoped transaction. Every recorded mutation requires an `AuditContext` naming a validated user, service, or system principal; the record also carries the optional correlation identifier and derives its typed tenant key from the active `with_tenant(...)` scope. The host remains responsible for deriving both contexts from authenticated authority rather than client assertions. An auditable `save()` reads the pre-image of its diff inside its savepoint with `FOR UPDATE` on PostgreSQL and MySQL/MariaDB, so a concurrent writer cannot change the row between that read and the `UPDATE`. The old side of a `deleted`, `force_deleted` or `restored` entry is likewise the persisted row read the same way just before the statement, not the caller's possibly edited or stale handle. These reads, the revision-restore lookup, the row `restore()` re-reads and the `belongs_to_many` pivot query run within `Orm::set_query_timeout`, like the other generated statements. * `create_audit_table` creates the v2 schema and adds its columns to a legacy table without presenting legacy rows as v2 evidence. On MySQL/MariaDB a new table (or a newly added `restore_patch` column) stores `old_values`, `new_values` and `restore_patch` as `LONGTEXT`, because `TEXT` (64 KiB) cannot hold the bounded 5 MiB payloads. An existing table is never altered implicitly; while those columns are still `TEXT`, the call logs a warning naming the reviewed `ALTER TABLE ... MODIFY ... LONGTEXT` migration. JSON payloads are bounded and recursively mask sensitive names for create, update, and delete. The reverse patch records only the presence of a sensitive key, including a nested JSON key that an update adds or removes, and such an operation is not restorable. A reverse patch that would exceed its depth (64), operation (4,096) or 5 MiB size bound is omitted: the update and its audit diff are still recorded, and that revision cannot be restored. Audit/debug output does not expose principal, tenant, correlation, reason, or payload values. * An auditable model exposes `restore_revision(audit_id, reason)` and its caller-owned transaction variant. Only a bounded v2 update patch for the exact model, ID, and active tenant is eligible. The current row must still match the revision's recorded post-state; PostgreSQL and MySQL/MariaDB also lock that row during restoration. A successful restore is a compensating audited update referencing the restored audit ID and reason. Legacy, create/delete, oversized, stale, malformed, cross-tenant, or redacted-field revisions fail closed. Bulk builders still do not synthesize per-row history, and durable external export remains an explicit outbox/application contract. ### 5.4.1. Transactional partial-update candidate (source admitted) * `update_partial().save()` and its new explicit `save_with_tx(...)` variant merge selected typed values into the current persisted row within a savepoint. PostgreSQL and MySQL lock that row with `FOR UPDATE`; SQLite retains its transaction/locking semantics and can reject contended read-to-write upgrades. Missing rows and cross-tenant model handles fail before mutation. * The merged model uses the normal generated save lifecycle, including policy, hooks, observers, encrypted fields, atomic audit entries and post-commit cache/Scout/observer effects. This is a logical partial change implemented through `save()`: a full-row `UPDATE`, or, for a model with a `RowSnapshot` field, an `UPDATE` of the patched columns whose value differs from the locked row (none: no `UPDATE`). Existing model hooks can transform that candidate under the normal save contract: the loaded row runs `after_fetch` before the patch is merged, as `find()` would return it, and `restore_revision` runs it on the restored row before saving. Both run inside the borrowed transaction, where reentrant ORM access fails closed. * The caller's object is replaced with the fresh merged model only after the operation succeeds. A direct save waits for its transaction commit; an explicit or task-scoped save reflects the transaction's tentative state, so the caller must discard/reload it if the enclosing transaction rolls back. A `PostCommit` failure does not undo a durable direct save. An ambiguous database commit error requires reconciliation rather than blind replay. * An empty builder remains a no-op. A failed policy, hook, SQL write or audit rolls back the operation's savepoint and discards its pending effects; if that rollback fails too, the returned `DatabaseError` names both errors. This allows a managed outer transaction to catch that failure and continue. Strict post-commit timing requires `Orm::transaction` or the owned direct path; a raw SQLx transaction retains the documented observation limitation. * Unselected persisted values and relation fields in the caller's object are refreshed from the database representation. Unsubmitted edits on that object do not become an implicit patch. This is not an expected-version comparison: callers needing conflict rejection must still supply their domain revision contract. Local SQLite/PostgreSQL/MySQL, cancellation, cache/Scout and extracted-facade journeys pass. Full hosted workspace/platform/package source admission passed in PR #236; final release admission remains separate. ### 5.5. Process-Local Post-Commit Contract * A nested `Orm::transaction` (called while another managed or task-scoped transaction is active on the same task) does not open a second pooled transaction. It opens a SQLx-tracked savepoint on the active transaction and passes the same shared handle to its closure. An `Err` rolls back only to that savepoint and is returned unchanged; a success releases it, so the nested work commits or rolls back with the outer transaction. Its `after_commit` callbacks are promoted to the outer commit boundary only on success. Dropping an unfinished nested future rolls its savepoint back. The returned future is `Send` for `Send` results and errors, so it can be nested in a transaction closure or spawned. Holding the shared handle's lock across a nested call deadlocks, as it does for generated model methods. * Savepoints on one connection close in LIFO order. Sibling nested transactions started concurrently on one task take turns: each holds its level's turn from `SAVEPOINT` until its release or rollback, while a transaction nested inside it uses its own level and never waits for it. Plain statements from a sibling future are not serialized and run inside whichever savepoint is open. A savepoint left open (a deeper level still open at release, a failed release/rollback, or a nested future cancelled while the connection was busy) never commits partial work: the enclosing savepoint rolls back instead of releasing, a managed transaction rolls back and returns an error instead of committing, and the ORM pool closes a connection returned while SQLx still reports an open transaction. * A generated `stream()` running on a managed or task-scoped transaction keeps that transaction locked between rows until it is consumed or dropped. While it is open, every other generated ORM call, `Orm::transaction` and `Outbox::enqueue` on that transaction fails with `Validation` instead of waiting for the lock (the query timeout does not cover that wait). * `Orm::transaction` and direct generated model `save()`/`delete()`/ `restore()`/`force_delete()` operations own a post-commit callback scope. `after_commit` callbacks registered within it run only after SQLx confirms commit and are discarded on rollback. When no managed transaction is active, `after_commit` executes immediately for an already committed/autocommit operation. `#[rullst_orm::test]` runs its body in such a scope too and never commits it, so the observers, cache, Redis and Scout effects of sandboxed writes are discarded with the rollback. * Generated observers retain synchronous lifecycle callbacks such as `creating`, `created`, and `saved` for mutation validation. The separate `committed(ModelCommittedEvent)` callback receives an owned snapshot after the managed commit; it omits hidden fields and carries `"***"` for encrypted and masked fields. Generated Redis invalidation/pub-sub and Scout projections use this same post-commit boundary. * Builders add `with_trashed()` (include trashed rows) and `only_trashed()` (only trashed rows). A model without soft deletes has no trashed rows, so its `only_trashed()` fails reads, counts, plucks and `delete_all()` with `Validation` instead of treating every live row as trashed; a directly set `only_trashed` field matches no row. `with_trashed()` changes nothing there. * Only `delete()`, `restore()` and `force_delete()` change the soft-delete marker of an existing row. Generated `save()` leaves that column out of its `UPDATE` (an `INSERT` still writes it), so a handle loaded before `delete()` cannot undelete the row and bypass `can_restore`, the `restored` audit and the restore observers. An auditable save copies the stored marker into the handle before recording its diff, and `update_partial()` rejects a soft-delete value with `Validation`. * `force_delete()` and `restore()` check the tenant and their policy (`can_force_delete`/`can_restore`) before the transaction, then run in a savepoint. `force_delete()` runs the `before_delete`/`after_delete` hooks, the `deleting`/`deleted` observers and the same post-commit cache invalidation, Redis `deleted` event, `committed(Deleted)` and Scout removal as `delete()`; it does not cascade to `cascade_soft_delete` relations. `restore()` re-reads the restored row, runs the `updated`/`saved` observers with it and registers the update effects of `save()` (cache invalidation, Redis `updated`/`saved` events, `committed(Updated)`, Scout re-index). It runs no save hooks or `saving`/`updating` observers because it writes only the soft-delete column. Its `UPDATE` matches only a trashed row, so restoring a missing or live row is a no-op; likewise the soft-delete `UPDATE` of `delete()` matches only a live row, so deleting a trashed row again keeps its deletion time and fails like a missing row. * A `save()` update, `delete()` or `force_delete()` whose by-ID statement matches no row (for example a row deleted since its handle was loaded) fails with `RecordNotFound`, or with the tenant-scope `Validation` error on a tenant model, instead of reporting success; it rolls its savepoint back, so no post-mutation observer, hook, audit entry or post-commit effect runs. `restore()` of a missing row remains a no-op, also on tenant models. When the COMMIT of a direct `save()` (or the savepoint release of `save_with_tx`) fails, the handle's `id` is restored like on any other failure, so a retry inserts again; an ambiguous commit error still needs reconciliation. * Savepoint-scoped generated saves/deletes and revision restores collect their callbacks in a nested scope. The callbacks are promoted to the enclosing commit boundary only after that savepoint succeeds, so catching a failed mutation cannot leak a later `committed` effect. * Every `save_with_tx` (and a `save()` joining a task-scoped transaction), not only an auditable one, runs in a savepoint of the caller's transaction. A failed policy, hook, observer or SQL write rolls that savepoint back and restores the model's `id`, so a caller that catches the error and commits does not persist the failed write. A direct `save()` owns its transaction and needs no savepoint. * Every queued callback is attempted. A failure is returned as `PostCommit`, whose contract explicitly means the database mutation is already durable. Applications must not retry the database mutation blindly from this error. * A caller-owned raw SQLx transaction passed to `save_with_tx` or `delete_with_tx` does not expose its later commit/rollback decision to the ORM. Use `Orm::transaction` for the strict process-local boundary. * These callbacks do not survive process failure and provide no retry, idempotency or cross-node delivery. Use the explicit durable outbox below for an irreversible or externally delivered effect; it is not enabled automatically by a generated observer. ### 5.6. Durable Transactional Outbox Contract * `Outbox::enqueue` accepts only a currently managed `Orm::transaction` and writes `rullst_outbox` through that same transaction (for a nested call, through the outer transaction's savepoint). A domain rollback also removes the event. `enqueue_with_tx` provides the equivalent explicit path for a caller-owned SQLx transaction. No implicit independent commit is permitted. * `(stream, event_key)` is the database uniqueness boundary. Replaying the same key, exact event kind and JSON-equal payload (object key order is not content) returns the existing `i64` identifier, including a key committed by a concurrent enqueue after the caller's read snapshot (MySQL/MariaDB read it back with a locking `FOR UPDATE` read, as InnoDB's default `REPEATABLE READ` would hide it from a plain `SELECT`); reusing the key with different content fails closed with an error that does not echo the stream or key. `stream`, event key, event kind and worker identifiers use a bounded ASCII grammar, and serialized payloads are limited to one MiB. Streams, event keys and claim tokens compare case-sensitively on every backend; MySQL/MariaDB declare those columns `CHARACTER SET ascii COLLATE ascii_bin`, and `Outbox::install` converts an existing MySQL/MariaDB table whose key columns use another collation. * PostgreSQL, MySQL/MariaDB and SQLite share the outbox state machine. A claim increments attempts and receives a random token plus a bounded lease. Only that token may acknowledge or fail the event; expiration permits another worker to reclaim it. Failure schedules a bounded retry or moves the event to `dead_letter` at the configured attempt limit, including a worker that dies while holding its final lease. `claim_next`, `acknowledge` and `fail` are independent lease operations that commit on their own connection; inside an active `Orm::transaction` they return `Validation` instead of committing before, and regardless of, the handler's work. Acknowledge after that work has committed. * Delivery is **at least once**, not exactly once. A worker may perform its external effect and crash before acknowledgement, so consumers must use the stable stream/event key as their own idempotency key. Ordering across retries or concurrent workers is not guaranteed. * `Outbox::install` is an explicit setup/test convenience and never runs at startup. `OutboxMigration` puts the same schema under the built-in reviewed migration lifecycle. The ORM does not infer tenant authorization from `stream`, automatically serialize model observers, dispatch HTTP webhooks, purge delivered rows or promise cross-database transactions. ### 5.7. Generated Redis Query Cache Contract * The optional `redis` feature enables `.remember(seconds)` for generated SQLx reads. `Orm::init_redis_with_namespace(url, application_namespace)` is the recommended initializer when a Redis database is shared; the compatibility `init_redis(url)` initializer uses the literal namespace `default`. * Versioned SHA-256 cache keys bind the validated application namespace, a partition, table, generated SQL, and typed bindings. A model with a `tenant_column` partitions its entries by an opaque digest of the active tenant scope (`global` outside `with_tenant`); any other model keeps one `global` copy whatever tenant scope its reads run in. Raw tenant identifiers are not emitted in keys. * Generated reads always bypass Redis inside explicit and task-scoped database transactions, so cached state cannot replace the transaction's own view. `remember(0)` is invalid. Outside transactions, explicitly requesting cache without initializing Redis fails closed as a configuration error; transport failures and corrupt cached values fail open to the authoritative database, whose result replaces the entry. Only a JSON array whose rows all decode and decrypt is a hit: `null`, an object or a row whose encrypted or `SecretString` field no longer decrypts (for example after key retirement) is a miss, not an empty result or an error. * Cache writes occur only after a successful database read and retain encrypted model fields as ciphertext; `SecretString` fields are cached as serde envelopes and decrypted on a cache hit, and a result that cannot be serialized (for example without an encryption key) is not cached. Each generated cache write also records its key in a per-namespace/tenant/table Redis sorted set, scored by the entry's expiry time on the Redis server clock, in the same `EVAL` script; that script first prunes members whose entries already expired and extends the set's TTL to the longest entry TTL, so the index holds only entries stored within the last TTL window. Generated model `save()`/`delete()`/`restore()`/`force_delete()` operations invalidate the table's `global` partition and, for a tenant-scoped model, the partition of the tenant active at the write, only after commit (the tenant is captured when the callback is registered, so a `with_tenant` scope that ended inside the transaction closure still has its keys removed). An `unscoped()` read of a tenant-scoped model inside another tenant's `with_tenant` scope is cached in that scope's partition and is not refreshed by other tenants' writes before its TTL. Invalidation prunes expired members without counting them, then removes each index's live members in batches of 500 and `UNLINK`s their keys (at most 10,000 per write); they never `SCAN` the Redis keyspace, so their cost does not grow with unrelated keys in a shared database. Beyond the cap the write reports `PostCommit`, the remaining keys stay indexed for the next write, and the generated `orm:events:*` publication still happens. Keys are versioned (`rullst:orm:cache:v4:`); entries written by earlier versions are never read again and expire through their TTL, and during a rolling upgrade a write from an instance on the other version does not invalidate this version's entries before their TTL. A transaction rollback preserves existing entries. The scripts address keys they were not passed and are therefore outside Redis Cluster, like the rest of this contract. Raw SQL, bulk builders, caller-owned raw transactions and writes from other processes cannot be inferred. Callers must retain a defensive TTL and treat Redis cluster/failover and durable invalidation delivery as separate application contracts. * Generated `save_to_redis`/`get_from_redis`/`increment_redis_field` model hashes use `rullst:orm:hash:v1:<namespace>:<scope>:table-<digest>:<id>` keys, binding the application namespace, an opaque digest of the model's tenant (or `global`) and of the table. For a tenant model all three require `with_tenant(...)` with the tenant field's type, `save_to_redis` rejects a handle from another tenant, `get_from_redis` rejects a decoded hash whose tenant differs and the tenant column cannot be incremented; another tenant simply misses. 12.0 and 12.1 stored every hash under the shared key `orm:<table>:<id>`. For a model without a tenant scope, `get_from_redis` reads that key when the namespaced hash is missing, and the next `save_to_redis`/`increment_redis_field` atomically moves it to the namespaced key (an existing namespaced hash wins and the stale legacy hash is removed), so data migrates lazily. A tenant model never reads, moves or deletes the legacy key, which every tenant shared; its hashes are migrated explicitly (see the Redis guide). The `orm:events:*` pub/sub channel names are unchanged and not namespaced, so subscribers keep working; use a dedicated Redis database when applications must not observe each other's events. ### 5.8. Polyglot Persistence Boundary * Optional persistence adapters are disabled by default and selected one at a time with `mongodb`, `duckdb`, `turso`, `surrealdb` or `qdrant`; the umbrella crate exposes matching `orm-*` features. 13.0 removed the `polyglot` and `orm-polyglot` aggregates, which enabled every adapter at once, including DuckDB's bundled C++ build. * Only the SQLx backends (SQLite, PostgreSQL, MySQL/MariaDB) integrate with the `#[derive(Orm)]` query builder, `Schema`, `artisan!`/`db:*`, `Server::with_db`, Nexus and Studio's database pages, and every `cargo rullst new` starter accepts them. Turso/libSQL is the primary database of the Blank starter only: `#[orm(backend = "turso")]` derives the separate `TursoModel` API, migrations are SQL `TursoMigration`s, the starter answers `db:migrate`/`db:rollback`/`db:status` itself and `TursoStore::transaction` runs atomic statement batches; it integrates with none of the SQLx parts above. MongoDB, DuckDB, SurrealDB, Qdrant and Redis data structures are side stores with their own APIs and no transactions. `cargo rullst new` states these limits next to the Turso and side-store choices in the wizard, in `--help` and on the review/dry-run screen. Live CI contracts are `matrix_postgres`, `matrix_mysql`, `matrix_mariadb`, `matrix_turso`, `matrix_mongodb`, `matrix_surrealdb`, `matrix_qdrant` and `matrix_redis_data`; SQLite and DuckDB run in-process. The [Database Support](https://rullst.github.io/Rullst/book/database-support.html) table is the user-facing statement of this boundary and must change with it. * `DocumentRepository<T>` provides create, find, replace, delete, and deterministic bounded listing. Collection names, document IDs, offsets and limits are validated before reaching a driver. * `DocumentInventory<T>` is the separate identifier-preserving extension used by recovery tooling. Its stable ascending pages avoid breaking existing third-party `DocumentRepository` implementations. * `export_document_snapshot` performs two matching bounded observations before sealing a versioned payload with AES-256-GCM. The authentication data binds the key-rotation ID, trusted application namespace and exact collection; decoding is capped at 64 MiB and 100,000 documents. Restoration accepts only an empty destination or an exact matching subset, never replaces or deletes, tolerates only an exact raced duplicate and verifies the complete final inventory. Applications must quiesce source/destination writers and durably store, rotate and protect keys/snapshots. Required destination schema must be provisioned first; driver/schema errors never become an implicit empty collection. The API is crash-resumable, not a cross-store transaction or managed backup service. * `MongoDbStore<T>` uses the official MongoDB Rust driver and stores the portable `DocumentId` as `_id`; portable models must not define `_id`. * `DuckDbStore` serializes access to its native connection and delegates every database operation to `spawn_blocking`. Dynamic values use prepared parameters, and callers must supply a `QueryLimit` before rows are materialized. Application-provided SQL text remains a trusted structural input. * `TursoStore` speaks the official Hrana HTTP v3 protocol directly for remote edge SQL. It uses positional typed parameters, conditional atomic batches, a 30-second request deadline, no redirects, a 16-MiB response bound, bounded row materialization, and ordered checksummed migrations. This avoids an unnecessary embedded/native SDK dependency while retaining conformance against the official libSQL server. Empty or `mock_*` endpoints use a one-connection SQLite fallback that exercises real SQL without pretending to be a remote replica. HTTPS/`libsql://` is required outside explicitly enabled loopback development. * `SurrealDbStore<T>` uses the documented `/key`, `/sql`, `/rpc` and `/gql` HTTP endpoints with namespace/database headers (`replace` is a non-creating `UPDATE` through `/rpc`, and document IDs are always string keys), no redirects, bounded streaming responses, HTTPS by default, and redacted authentication configuration. `GraphQuery::read_only` accepts one `MATCH` query, rejects mutation tokens and caller-supplied limits, then appends a bounded limit. * This boundary does not turn every backend into SQL Active Record, perform cross-database transactions, provide an online-consistent snapshot, synchronize records between engines, or prove a third-party deployment. See the [Polyglot Persistence guide](https://rullst.github.io/Rullst/book/polyglot-persistence.html). ### 5.9. Scout Search Projection Contract * Without a configured engine, `Model::search(query)` falls back to `CAST(column AS TEXT) LIKE ? ESCAPE '!'` over the persisted columns except `#[orm(hidden)]`, `#[orm(encrypted)]`, `#[orm(masked)]` and `SecretString` ones, so it cannot become a substring oracle for them. `%`, `_` and `!` in the query match literally, and the query uses the provider bounds (1,024 bytes, no control characters). * `#[orm(searchable)]` projects generated save/delete operations only after a managed relational commit. The table name is the Scout index, so a searchable model whose (explicit or default) table name does not start with a lowercase ASCII letter followed by lowercase letters, digits or underscores fails compilation instead of failing every projection. The indexed document omits `#[orm(hidden)]`, `#[orm(encrypted)]` and `#[orm(masked)]` fields. Search adapter failures remain visible; a failed query is not silently treated as an empty result, and `PostCommit` means a projection failed after the database mutation became durable. * `MockSearchEngine` is deterministic and always available. The optional `scout-http` feature adds Meilisearch, Elasticsearch and Algolia. Empty or `mock_*` credentials select the mock; keyless live constructors accept only loopback HTTP, while remote/custom origins require HTTPS without URL credentials, redirects, paths, queries or fragments. * A provider answers one shared per-table index and returns at most `scout::MAX_SEARCH_HITS` (1,000) IDs before tenant, model-wide and soft-delete scopes filter them. For a model with any of those scopes, an answer of that size is treated as truncated: `search()` answers from the SQL fallback instead, so one tenant's hits can never push another tenant's matches out of the result. Below the cap the scoped provider IDs are used; engine-side tenant filtering is not provided. * Provider matches keep the provider's ranking: `search()` orders them with an `ORDER BY CASE <table>.id WHEN ? THEN 0 ...` over the bound IDs, so `get()`, `first()`, `paginate()` and `pluck_*` follow relevance. An explicit `order_by` replaces that order, DISTINCT and GROUP BY statements omit it, and `delete_all()` does not treat it as a rejected `order_by()`. The SQL fallback has no relevance order. * Index names, positive IDs, object payloads, queries, response bytes and hit counts are bounded. Meilisearch/Algolia tasks use bounded polling; Elasticsearch requests use `refresh=wait_for`. Provider response bodies and credentials are not copied into transport errors. * The repository proves a real Meilisearch lifecycle in a digest-pinned container. Elasticsearch and Algolia protocol fixtures prove the documented HTTP shape and bounds, not hosted service operation, version-wide compatibility, ranking quality or cluster failover. * The generated hook remains process-local. Guaranteed crash recovery requires an application-versioned event in the transactional `Outbox` and an idempotent worker; the ORM cannot infer a safe event key or external retry policy from an arbitrary model save. ### 5.10. PostgreSQL pgvector Contract * The optional `pgvector` feature re-exports the SQLx-compatible `Vector` type. The supported execution profile combines it with `strict-postgres`; other SQLx backends do not pretend to implement PostgreSQL vector operators. * `where_similar`, L2, cosine and inner-product ordering validate column names, reject empty/non-finite vectors and invalid distances, and bind vector and distance values. ORDER BY bindings are assembled after WHERE bindings regardless of builder call order. * A digest-pinned PostgreSQL + pgvector container installs the extension, uses a typed vector model and proves L2 threshold/cosine ordering queries. The application still owns reviewed migrations, vector dimensions, embedding model compatibility, HNSW/IVFFlat index selection/tuning, tenant policy, context budgets, citations, ingestion/deletion and RAG evaluation. ### 5.11. Qdrant and Redis Specialized Store Contract * The optional `qdrant` feature exposes a separate `VectorRepository` rather than pretending Qdrant is SQL Active Record. Collection names, dimensions, vectors, cosine norm, point payloads, query limits and response bytes are bounded. The HTTP client rejects redirects and URL credentials, uses short connect/request deadlines, requires HTTPS outside loopback, redacts API keys, and never copies provider response bodies into errors. * `QdrantConfig::new` selects a deterministic in-process fallback for empty or `mock_*` endpoint/API-key values. `unauthenticated_local` is an explicit loopback-only path for self-hosted development. The supported live API is one unnamed dense cosine vector per numeric point with create, single-point upsert/delete and bounded nearest-neighbor query; named/sparse/multivectors, arbitrary filters, collection tuning and distributed topology are outside it. * The optional `redis` feature exposes `RedisDataStore` for explicitly namespaced Hash, Set and Sorted Set operations in addition to the generated query cache. Keys, fields, UTF-8 values/members, finite scores and scan/range materialization are bounded. Remote endpoints require `rediss://`; URL credentials are rejected, ACL credentials are redacted, operations have connection/response deadlines, and empty/`mock_*` credentials select a deterministic in-process fallback. * Digest-pinned Qdrant and Redis matrices prove their respective live lifecycles, including Redis namespace/structure separation. They do not prove hosted-provider availability, backups, cluster failover, tenant authorization, eviction policy, ANN quality, or cross-store transactions. ### ORM Driver Selection * `Orm::driver()` reports the SQL dialect from the connected DSN scheme, case-insensitively: `postgres`/`postgresql` select PostgreSQL and `mysql`/`mariadb` select MySQL/MariaDB, so dialect-gated SQL such as `FOR UPDATE` row locks, quoting and outbox/migration DDL follows the server. Any other scheme keeps the SQLite dialect (an unknown scheme fails to connect under SQLx `Any`). * A DSN that still contains a bracketed template placeholder such as `[your-database-id]` or `[YOUR-PASSWORD]` fails `Orm::init`, `init_with_options` and `init_with_replicas` (primary or any replica) with `Error::Internal` before connecting. A bracketed IPv6 literal host (`postgres://app@[2001:db8::10]:5432/app`, optionally with a `%25` zone identifier) is not a placeholder and is accepted by every entrypoint. * An in-memory SQLite DSN (`sqlite::memory:` or `mode=memory`) exists only while one of its connections is open, so its pool keeps one connection and never closes it for idleness or age (the Turso offline in-memory fallback does the same). * A SQLite file DSN without a `mode` parameter, or with `mode=rwc`, has its missing database file (and directory) created before connecting, at the percent-decoded path SQLx opens (`John%20Doe` is `John Doe`). An explicit `mode=ro` or `mode=rw` never creates one, so a wrong or unmounted path fails to open instead of becoming a new empty database. * Since 13.0.0-alpha.3 neither `rullst-orm` nor the `rullst` facade has a default feature (they were `drivers-all`, and `orm`, `drivers-all` and `queue-sqlite`), so a database-free application compiles no SQLx driver. A consumer selects `strict-postgres`, `strict-mysql` or `strict-sqlite`, each enabling only its own SQLx backend, or the `drivers-all` convenience feature for all three. On the `Any` pool, `Orm::init`, `init_with_options` and `init_with_replicas` turn SQLx's `no driver found for URL scheme` error for a `postgres`/`postgresql`, `mysql`/`mariadb` or `sqlite` URL into `Error::Validation` naming the `strict-*` feature (or `drivers-all`) that adds the driver; other connection errors are unchanged. The strict pool's existing precedence when multiple strict features are unified remains PostgreSQL, then MySQL, then SQLite. * Features are additive. Another dependency enabling ORM defaults, a SQLite queue, Turso's offline SQLite transport or another SQLx driver can broaden the final graph. Core, Studio, Nexus and the facade propagate an explicitly selected backend when defaults are disabled. Studio's SQLite queue is now explicitly optional; Studio's defaults retain `drivers-all` and `queue-sqlite`, and Nexus's `drivers-all`. Fifteen standalone normal/build graphs cover ORM/Core/Studio/Nexus/facade across all three SQLx backends. Capital/other optional capabilities may intentionally broaden those graphs. Turso enables SQLite for its offline contract. * A standalone consumer check must compile generated model/query/transaction code and inspect its normal/build graph for unrelated SQLx driver packages. Workspace all-feature or all-target checks cannot prove driver isolation. Applications previously disabling defaults without selecting a driver must choose a strict backend or explicitly restore `drivers-all`. * Native enum codecs follow the ORM's selected drivers, not identically named features in the consuming application. ORM 12.1 opts its paired macro crate into `runtime-driver-codecs` and exports hidden, driver-gated codec helpers. The macro crate's default expansion remains compatible with the all-driver 12.0 runtime; the opt-in helper expansion requires the matching 12.1 runtime. Standalone isolation checks include enum encoding/decoding as well as CRUD. * Optional generated APIs likewise follow the ORM's features, never features of the consuming application. With `rullst-orm/redis`, models get `.remember(...)`, commit-time query-cache invalidation, the `orm:events:*` publications and the Redis hash helpers; with `rullst-orm/ai` (which implies `pgvector`), `embedding_for` models get `save_with_embedding`. ORM 13 opts its macro crate into `runtime-feature-gates` and forwards both features, so the choice is made at expansion time. Without that opt-in (an older runtime resolving a newer macro crate) the legacy output keeps its application-evaluated `#[cfg(feature = ...)]` attributes. ### 5.12. ORM Telemetry Contract * Generated model/query entrypoints, transaction-aware variants, raw ORM queries and generated streams emit `rullst.orm.query` spans with only a static model, validated table and bounded operation name. SQL text, bindings, model values, DSNs and error strings are not fields of these Rullst-owned spans. The explicit debug query logger remains a separate opt-in surface. * Managed transactions emit begin and lifecycle spans. Their final outcome is one of the bounded commit/rollback states; transaction errors are returned to the caller rather than copied into telemetry. Generated stream spans are entered only while the stream is polled, so a tracing guard is never held across suspension. * Every pool constructed through `Orm::init*` emits SQLx pool-acquire timing at info level and promotes acquisitions slower than 500 ms to warnings. Primary and replica pools share this configuration. Direct pools constructed by the application are outside the contract. * These standard `tracing` spans/events are exported when the host enables the umbrella `telemetry` feature and initializes Core's OpenTelemetry subscriber. The host still owns OTLP endpoint security, filters, sampling, retention and collector availability. SQLx or application logging configured separately may have its own statement-data policy. --- ## 💳 6. Billing & Payments (`rullst-capital`) `rullst-capital` exposes provider-neutral billing contracts, a supported Stripe adapter and an experimental InfinitePay adapter. Applications integrate any other gateway by implementing the same traits in their own code; see [Writing your own payment provider](https://rullst.github.io/Rullst/book/capital-custom-provider.html). Every reviewed live adapter uses the same fail-closed outbound HTTP boundary: redirects and ambient proxy environment variables are disabled, connection and whole-request timeouts are finite, and a successful provider response is read only up to one MiB before JSON decoding. Failures expose a redacted typed `ProviderFailure` contract with permanent, transient, and rate-limited classes; only a bounded numeric `Retry-After` delta is retained. Raw response bodies, request URLs, credentials, and transport diagnostics are not included in the public error. Rullst deliberately does not retry billing mutations: callers may retry a transient or rate-limited result only when that exact operation has a persisted provider-forwarded idempotency key and a reconciliation policy. Returned checkout locations are accepted only as bounded, absolute, credential-free HTTPS URLs. Offline legacy checkout and portal fixtures use reserved `mock.<provider>.invalid` hosts and omit the customer email and return URL. Stripe's documented opaque hosted-URL fragment is preserved; other adapters reject fragments. Provider/account sandbox acceptance remains external evidence. ### 6.1. Provider Architecture Billing adapters implement `BillingProvider`; metered usage adapters implement `MeteredBillingProvider` with a provider-specific request type. Rullst ships two adapters: Stripe, the supported provider used by the generated billing flow, and InfinitePay, which is experimental until it is validated against a live account (its live plan-only checkout and live callbacks fail closed). The trait default for webhook verification mode fails closed, so a custom adapter must declare its verification mode before Rullst's webhook middleware accepts it. Individual billing operations may still return `Unsupported` when a provider adapter has no reviewed implementation: ```rust,no_run use rullst_capital::providers::stripe::StripeProvider; use rullst_capital::providers::BillingProvider; async fn create_checkout() -> Result<(), rullst_capital::CapitalError> { let provider = StripeProvider::new("mock_api_key", "mock_webhook_secret"); let session = provider .create_checkout_session( "customer@example.com", "price_monthly", "https://example.com/billing/complete", ) .await?; let _ = session; Ok(()) } ``` `#[derive(rullst::Billable)]` is the umbrella convenience for named structs with an `email: String` field. It preserves generics; optional `subscription_id: Option<String>` and `tier: Option<String>` fields expose the corresponding helpers. An all-or-none `grace_period_starts_at: Option<i64>`/`grace_period_ends_at: Option<i64>` pair exposes a validated half-open window of at most 366 days. A provider-bound `SubscriptionHandle<P>` delegates cancellation and pausing; the explicit `subscription_with` path keeps static dispatch. These values do not infer or persist ownership, team membership, entitlement, currency, payment methods, usage or provider scheduling. Shared quota accounting is a separate explicit boundary described below. The same derive inherits the bounded `charge_with`/`charge` helpers for an immediate off-session charge. A charge requires a positive integer amount in currency minor units (maximum eight digits), a three-letter currency, explicit provider customer and tokenized payment-method IDs, the model e-mail and an application-owned idempotency key of at most 255 bytes. `BillingProvider::charge` defaults to `UnsupportedOperation`; the reviewed live implementation is Stripe Payment Intents, which forwards the idempotency key, confirms off-session and fails closed on an amount/currency mismatch or a status other than `succeeded` or `processing`. Empty/`mock_*` Stripe credentials return a deterministic local receipt with the distinct non-success `Mock` status for exact retries. Rullst does not model raw payment credentials, prove that a stored method has a valid mandate, persist idempotency, grant an entitlement, reconcile webhooks or imply direct-charge parity across adapters. #### Customer-bound Stripe Subscription Checkout `StripeCustomerRequest` and `StripeProvider::create_customer` supply the preceding provider-customer operation. The immutable input binds an opaque local owner reference, a persisted retry key and optional bounded contact email. The response must be an undeleted customer with matching metadata, valid ID/time and the credential's test/live mode when that mode is known. Mock receipts are separate from provider creation. Requests carry a versioned digest, explicit idempotency header and the pinned checkout API version. The host must persist provisioning intent before HTTP, bind the result to the authorized account/owner before checkout, and reconcile unknown outcomes; provider idempotency retention is not durable application state. Email is not used to find or establish ownership of an existing provider customer. The additive `StripeCheckoutRequest` binds an existing provider customer, one server-owned recurring price, an opaque local owner reference, explicit HTTPS success/cancel URLs and an application-persisted idempotency key. The dedicated Stripe method sends that key and reference with the customer ID; it does not create or discover a customer by email. A bounded response must match the customer, owner reference, requested price/quantity, subscription mode and redirects before a checkout session is returned. Request/receipt debug output omits identifiers, URLs and keys. Empty/`mock_*` credentials produce an explicit deterministic mock, never payment evidence. The application must commit customer/tenant ownership and an immutable attempt with its request digest before dispatch, retain account/test-live namespaces, and reconcile unknown outcomes. Provider idempotency has a finite retention window; repeating an expired key is not a durable deduplication guarantee. Session creation and return navigation never grant paid access. The legacy email-based trait method remains source-compatible; new generated Stripe flows use the dedicated customer-bound contract and durable persistence below. `StripeSubscriptionLookup` and `StripeProvider::retrieve_subscription` provide an explicit read for reconciliation. The request binds subscription, persisted customer/local reference, expected price and test/live mode; the pinned-version response must match every binding and the bounded single-item state contract. `StripeSubscriptionSnapshot` preserves the exact provider status and separates retrieved state from deterministic non-entitled mocks. A retrieved snapshot is neither an event claim nor invoice-settlement evidence. Hosts must serialize reconciliation with their database update so a delayed earlier read cannot overwrite a newer state; fetching before an unrelated transaction does not establish event order. Account credential custody and durable intent remain application responsibilities. #### Provider-Specific Metered Usage `MeteredBillingProvider` deliberately uses an associated request type rather than pretending provider identities and retry semantics are interchangeable. `StripeMeterEvent` targets the current `/v1/billing/meter_events` API with the default `stripe_customer_id` and `value` payload mapping. It validates positive integer usage, the meter event name, customer, timestamp window and a forwarded identifier; the Stripe adapter sends that identifier as both event identity and HTTP idempotency key. The adapter limits response JSON to one MiB, binds identity/quantity fields before returning `UsageStatus::Accepted`, redacts request/receipt identities from `Debug`, and returns deterministic `Mock` receipts for empty or `mock_*` API keys. Stripe documents only a rolling provider deduplication window. A custom adapter whose provider has no equivalent event-key field reports `UsageDeduplication::ApplicationOutboxRequired`; the application must then claim `event_key` durably before submission. Provider-account acceptance, durable outbox storage, retries, reconciliation and entitlement/quota policy remain application/release evidence. The legacy uniform `BillingProvider::report_usage` is compatibility-only and fails closed for live Stripe rather than guessing the required provider-specific identity. #### Coupons and Relative Trial Extensions `CouponCode` accepts at most 256 ASCII identifier bytes and redacts its value from `Debug`. The Stripe adapter uses `discounts[0][coupon]`, requests expanded discount evidence and accepts only a response bound to both the subscription and requested coupon. InfinitePay and every adapter without a reviewed live contract return `UnsupportedOperation` rather than a false success. Empty or `mock_*` credentials retain the deterministic offline no-op required for local applications and tests. `TrialExtension` resolves 1 to 730 whole days against a trusted clock. `Billable` and `SubscriptionHandle` expose the historical ergonomic `extend_trial(15)` meaning plus `extend_trial_days_at` for a stable persisted command clock and `set_trial_end` for explicit reconciliation. Stripe sends `trial_end`, caps the provider response and binds the returned subscription and expiration. The host must authorize the subscription owner, persist a stable command time before retry, serialize conflicting changes, reconcile signed webhooks and evaluate provider-specific billing-cycle effects. Live-account acceptance is release evidence, not inferred from protocol fixtures. #### Server-side Plan Entitlements (v13 candidate) The candidate `rullst-capital::entitlements` module supplies a read-time gate, separate from usage quotas. `EntitlementScope` binds an authenticated tenant to an existing `BillingSubject`; `EntitlementPolicy` binds one server-selected feature to at most 64 exact plan IDs, an explicit live/sandbox mode and a 1–300 second maximum reconciliation age. Only active subscriptions qualify; trial, past-due, canceled, unknown and mock states deny access. Validity has an exclusive end, and both future observations and clock rollback during a read are rejected. No serialized snapshot or browser field is an authorization token. `EntitlementStore` is a static-dispatch trusted adapter contract. Every gate call reads it again and checks the returned tenant/subject binding, provenance, plan, status, expiration and trusted clock before and after the read. Store failures deny access. Hosts must supply authoritative current state and serialize reconciliation/revocation; this read gate does not make subsequent domain writes atomic or revoke an operation already authorized. Custom adapters and database backup/restore remain explicit application obligations. The generated SaaS candidate consumes this gate at an authenticated billing report route. Its single-tenant scope is the configured Stripe account/mode inside the application's database, never a request header or submitted owner. The existing durable billing intent and revision fence precede an actual ownership-bound provider refresh; the state is committed before authorization. It performs this refresh on every report, with a bounded deadline and no cached grant. The exact report-plan allowlist is separately configured on the server. Production requires live state; local sandbox state requires the explicit test profile, and offline credentials produce deterministic denial. Mutable CMS subscription projections, checkout redirects and unverified events cannot grant report access. Other providers need reviewed reconciliation adapters before this consumer can enable them. Existing profile/rights exports remain independent. The local implementation and generated HTTP/database contracts exercise current state, account/owner isolation, revocation races, expiry, provider/store failures, mock denial and cancellation at the request deadline. Hosted source admission passed in PR #221; final release admission remains separate, and protocol fixtures do not establish live provider-account acceptance. #### Shared Team and Workspace Quotas `BillingSubject` identifies one authoritative user, team, workspace or trusted tenant as the owner of both the subscription and its shared counters. `Billable::quota_request` derives the limit from that owner's tier policy instead of accepting it from an HTTP payload. A `QuotaStore` then performs an atomic, idempotent reservation before the application creates the resource. `InMemoryQuotaStore` is the deterministic offline/process-local contract. The opt-in `quota-sql` feature supplies `SqlQuotaStore` for SQLite, PostgreSQL, MySQL and MariaDB. Its conditional counter update and unique event claim prevent concurrent members from exceeding the same limit. Exact retries return a replay grant without consuming or executing again; a key reused with different units or limit fails closed. Subjects, features and event keys are case-sensitive on every backend: MySQL/MariaDB key columns use `ascii_bin`, and the store refuses a legacy table whose key columns fold case until its documented migration runs. A replay proves only that the key is claimed, not that the guarded work finished: the claim may belong to an in-flight call that later fails and releases it, or to an abandoned call. `QuotaGate::execute` blocks the callback before an over-limit creation and compensates an ordinary callback error. The convenience gate cannot make two unrelated storage systems atomic. A relational application that needs exact quota/resource atomicity must open a transaction from `SqlQuotaStore::pool`, call `reserve_with_transaction`, perform the domain insert through that transaction and commit once. Trusted middleware must establish membership and active tenant before constructing the subject. Plan/webhook reconciliation, migrations and custom/non-relational stores remain explicit application work. ### 6.2. Invoice Rendering `Invoice::generate_html` remains the source-compatible escaped HTML renderer. Trusted paths use `validate`/`try_generate_html`: the legacy public `f64` model accepts only bounded finite positive values with no more decimals than the currency's ISO 4217 minor unit (none for JPY, three for KWD, two by default), converts them to 1 to 99,999,999 integer minor units of that currency, and requires the exact item sum. HTML and PDF amounts use the same number of decimals, so the minor units match a provider receipt for zero- and three-decimal currencies too. The opt-in `invoice-pdf` feature adds bounded paginated A4 rendering. Its embedded Helvetica subset supports WinAnsi text; other scripts require a caller-supplied TTF/OTF of at most eight MiB containing every used glyph. PDF output is capped at sixteen MiB. `Invoice::bind_succeeded_charge` creates an immutable `PaidInvoice` only when a final, non-mock receipt exactly matches the recipient, minor-unit amount and currency. It derives a stable non-secret key from the invoice and provider evidence for use by an application outbox. The downstream `rullst-mail/capital-invoice` feature converts that value into a pipeline-validated HTML message with the PDF attached and exposes one-call facade, tenant-aware or static-driver delivery. It does not infer a webhook event, atomically claim the delivery key, guarantee provider acceptance or promise exactly-once delivery. Applications processing retries or multiple instances must persist/claim the key and reconcile payment state durably before sending. ### 6.3. Webhook Signature Verification * The experimental InfinitePay adapter has a body-only verifier but no reviewed authentication and authoritative payment-lookup contract for the documented checkout callback. Real-secret verification and normalization return `UnsupportedOperation`; explicit mock-secret fixtures remain offline-only. A local HMAC fixture is not evidence that the provider signs that protocol. Enabling live processing requires order/merchant/amount binding and provider reconciliation first. * The additive `StripeProvider::verify_subscription_event` returns an immutable `StripeSubscriptionEvent` after the existing signature/freshness check and bounded subscription normalization. It retains event ID/type/API version, creation time, matching event/subscription test-live mode, optional connected account and opaque local owner reference, plus the original provider status. A versioned mutation digest excludes contact email, delivery signature and unrelated JSON fields; a separate digest binds the exact raw payload. Neither digest is encryption or a persisted replay claim. Mock verification is explicit in the result and `require_real` rejects it. The host must bind the endpoint's provider account, mode, customer and owner, and atomically commit the event claim with domain writes. Event creation time is not a complete ordering/reconciliation protocol. The legacy `WebhookEvent` and `BillingProvider` contracts are unchanged. * Stripe subscription normalization requires an explicit supported lifecycle event and subscription object with bounded subscription/customer/price IDs. Its single-price v12 contract rejects multiple or truncated item lists and statuses outside Stripe's subscription vocabulary. `incomplete` and `incomplete_expired` map to the legacy non-entitled `Unpaid` status; they must not be interpreted as proof of an unpaid invoice. Billing-period end is read from the single item for Basil payloads, or from the subscription on older payloads; conflicting period values are rejected. Email remains optional contact data. Event identity, ordering and atomic inbox processing remain separate requirements; an active subscription is not settlement evidence. * The Axum and opt-in Actix middleware adapters call one canonical bounded verifier before dispatch. Built-in provider adapters that accept live deliveries use provider-appropriate cryptographic verification; legacy parsers without a reviewed live contract fail closed. Equality checks for derived signatures are constant-time where applicable. * Timestamped protocols enforce a bounded freshness window. The default replay store is bounded and process-local and fails closed instead of evicting an unexpired proof when full. It holds at most 10,000 proofs for 24 hours each, so the default middleware answers 503 after about 10,000 verified deliveries per rolling day in one process; `verify_webhook_with_state` accepts a sized or shared store. * A custom adapter for a provider that signs only the body checks no delivery timestamp (provider retries can span hours or days). An exact captured body therefore verifies again after its replay claim expires (24 hours by default, at most 30 days) or, with the process-local store, after a restart. Hosts must record the provider's event or subscription state durably and reject stale or repeated transitions instead of relying on the replay window alone. * The opt-in `webhook-sql` store shares bounded payload-digest or semantic-event claims across processes on SQLite, PostgreSQL, MySQL, and MariaDB. Its schema profile is immutable, claims serialize through one configuration lock, expiry uses the database transaction clock, and storage/configuration/capacity failures reject the request. MySQL/MariaDB claims use a plain `INSERT` whose duplicate-key error is a replay, so a caller-owned REPEATABLE READ transaction whose snapshot predates a concurrently committed claim cannot re-claim it. * SQL-backed middleware admission claims a payload before handler dispatch; it is replay protection, not an exactly-once delivery protocol. When billing correctness requires atomic domain mutation, the application must verify the provider payload, select the provider's stable event ID, and call `check_and_record_event_key_with_transaction` through the same relational transaction as the mutation. Cross-system effects still require an outbox, idempotent consumers, and reconciliation. * The additive `SqlStripeEventInbox` under `webhook-sql` owns the relational transaction for a verified subscription event and its caller-supplied SQL mutation. An immutable scope binds application namespace, configured Stripe account, endpoint kind and test/live mode. Mock or mismatched events fail before database access. A per-scope configuration lock serializes admission; the stable event ID and versioned mutation digest distinguish an exact retry from conflicting content. A committed retry returns the retained outcome without invoking the mutation again. Domain errors and cancellation before commit roll back both changes; uncertain commit requires replaying the same event to discover the retained outcome. Capacity is immutable and bounded; records are never automatically evicted. The host owns schema migration, retention/reconciliation, account credential custody, authorized customer binding, ordering and an outbox for external effects. No SQL transaction can undo HTTP or other external effects performed by the callback. This API does not migrate the generated handler or grant access from subscription status. * Generated SQLx and Turso billing persists an opaque authenticated-owner binding, immutable customer/checkout intents and session IDs before redirect. Account identity is checked through Stripe and test/live namespaces are separate. Customer email never discovers or authorizes ownership. Stripe is the only generated provider: `BILLING_PROVIDER` accepts only `stripe`, and any other value is a configuration error before I/O. Real Stripe credentials require an explicit account, HTTPS return URL, webhook secret and price allowlist; mixed credentials remain unavailable. Customer and session creation reuse persisted keys for at most 23 hours. Older unknown outcomes use bounded read-only recovery; absence never permits another mutation. Completed and expired Checkout notifications and subscription lifecycle events trigger ownership-bound provider reads. A random database revision is committed before reading and compared atomically when committing the resulting subscription projection and durable event receipt. Concurrent newer reads fence earlier work; failed/uncertain commits remain retryable. An open checkout is retrieved and reused, and rejected/repeated requests do not consume a newly created-session quota. Existing subscriptions use an ID-bound customer portal. Provider-portal price changes must remain in the application's allowed catalog. The application owns entitlement/settlement policy, inbox retention and capacity operations; a subscription state is not invoice settlement evidence. Existing application-owned code requires merging the generated modules and an additive migration; updating the package does not rewrite deployed controllers. * Hosted checkout forms require an explicit provider-specific CSP `form-action` origin on the document that submits the form, including its HTTP 303 handoff. The SaaS starter selects Stripe and adds only `https://checkout.stripe.com` to its generated policy; Core's default remains `form-action 'self'`. Replacing Stripe with an application-owned adapter requires reviewing that exact checkout origin, including any merchant/custom domain. `make:billing` advises this integration without overwriting an existing application's policy. This is a browser policy boundary, not provider URL authentication: real flows must independently validate HTTPS, exact host/port, absence of credentials and durable session, customer, tenant, product and test/live bindings before emitting a redirect. A real-browser POST/303 positive and disallowed-origin negative are required; HTTP-client status checks alone cannot establish this behavior. * Before the future generated live flow creates a checkout, resolve the authenticated owner's persisted attempt. Resume only a provider-retrieved open session with all the bindings above; completed, expired and uncertain attempts need distinct handling. A business new-session quota applies only to new attempts, with a useful `Retry-After` on rejection; a separate request abuse limit may still protect retrieval. Concurrent clicks must converge on one durable intent/idempotency key. A disabled submit button is only feedback, never the duplicate-payment or ownership boundary. ### 6.4. Removed in v13 v13 narrows Capital to its provider-neutral base, Stripe and the experimental InfinitePay adapter. The Paddle, Lemon Squeezy, Polar, Razorpay, Mercado Pago, Alipay, Coinbase Commerce and PicPay adapters, the Wise payout adapter and the payout contracts (`PayoutProvider`, `PayoutStatus`, `PayoutEvent` and the global payout registry) were removed, together with the Lemon Squeezy usage types and the generated Paddle billing candidate. The NFS-e Nacional preparation pipeline (the `fiscal` module, the `nfse` and facade `capital-nfse` features, `Invoice::to_dps` and `CapitalError::FiscalError`) was also removed: it was never validated with a real municipality, and it may return as a separate product outside Rullst. The code remains in Git history. Applications implement other gateways with the traits above ([Writing your own payment provider](https://rullst.github.io/Rullst/book/capital-custom-provider.html)); the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html) lists the affected APIs, and `cargo rullst upgrade` reports them as `V13-CAPITAL-REMOVED`. --- ## 🛡️ 7. Enterprise Security, RASP & Vault (`rullst-security`) ### 7.1. Rullst Vault (Authenticated Field Encryption) * **Algorithm:** AES-256-GCM with authenticated 96-bit random nonces and 128-bit authentication tags. * **Envelope Format:** `RULLST:v2:<key_id>:<base64_nonce>:<base64_ciphertext_and_tag>`. * **Key Rotation:** Built-in keyring support (`decrypt_with_keyring`) can read prior keys while new writes use the active key. Deployment coordination, re-encryption, key custody and retirement remain operator responsibilities. * **ORM Configuration:** `RULLST_ENCRYPTION_KEY`, `RULLST_ENCRYPTION_KEY_ID`, and `RULLST_ENCRYPTION_KEYRING` select the current and still-readable prior keys. Each is read from the process environment first and then from `./.env` (v13), the precedence of `APP_KEY` and `rullst::config::project_setting`; the file never overrides or enters the process environment, and a malformed or unreadable `.env` fails the lookup with an error that names at most the entry number. Versioned envelopes name their key ID; pre-v12 `SecretString` ciphertext names none, so it is tried with the current key and then every keyring key (`default` first), and AES-GCM authentication accepts only the key that wrote it. Rullst does not provide key custody or automatic retirement. ### 7.2. Runtime Application Self-Protection (RASP) * **Bounded Heuristic Inspector:** ASCII case-insensitive signature matching covers selected SQL injection, traversal, SSRF, shell/JNDI patterns across URI, non-secret headers, and supported bounded textual/JSON bodies. Header values are decoded lossily in RASP and Core's WAF, so an obs-text byte (0x80-0xFF) that hyper accepts cannot hide the rest of a value. Core's WAF and this inspector classify body media types case-insensitively, including `+json`/`+xml` suffixes and every `application/x-www-form-urlencoded`-prefixed type, so a body that axum's `Json` or `Form` extractor accepts is inspected. In `User-Agent` only, a complete PowerShell product token (`PowerShell/<version>` or `WindowsPowerShell/<version>`, delimited by whitespace) does not match the `powershell` keyword, so stock PowerShell HTTP clients are served; other occurrences and every other signature still match. The `| sh` signature matches only when `sh` ends at a word boundary and `; cat` only when a path-like argument follows, so "| shopping" and "dogs; cat food" pass. Percent decoding and body/JSON inspection allocate; this control does not replace typed parsing, SQL binds, validation, authorization, or SSRF allowlists. * **Login Guard Tarpit:** `record_login_failure` returns progressive delay decisions and `record_login_failure_and_wait` applies them asynchronously; both share bounded, temporary in-memory jails keyed by a hashed identity. At `max_identities`, a new identity evicts the least recently failed counter and a new offender evicts the soonest-expiring jail; expired state is pruned in time order on every call, and jail telemetry is emitted only for a jail that was actually created. ### 7.3. MFA and Security Evidence Boundaries * **TOTP enrollment:** Secrets contain 160 bits derived from the OS RNG, verification accepts exactly six ASCII digits with constant-time comparison, and enrollment can emit an `otpauth://` URI or bounded SVG QR. Secret custody, recovery workflow and durable rate limiting belong to the application. `verify_totp_code` is stateless and accepts steps -1/0/+1, so it does not reject a replayed code. `verify_totp_step_after` implements RFC 6238 section 5.2 by accepting only a step newer than the caller-supplied last accepted step; the application must persist the returned step per secret atomically before admitting the login. * **Security CLI:** CycloneDX generation, MSRV/tool diagnostics, unsafe/IDOR source heuristics, network observations and compliance evidence are bounded checks. They do not certify a deployment, prove absence of vulnerabilities or replace provider/CI evidence tied to an immutable SHA. ### 7.4. Bounded JSON Schema Enforcement * `JsonSchemaPolicy::from_schema` compiles an application-supplied JSON Schema 2020-12 document once; `from_openapi_component` selects one explicit `components.schemas` entry from OpenAPI 3.1. OpenAPI 3.0 is rejected because it is not the same schema dialect. * Construction caps serialized bytes, node count and depth, rejects non-local `$ref`/`$dynamicRef`, disables network/filesystem retrieval and selects the linear-time regex engine. The route-scoped Axum middleware first enforces the existing media-type, syntax, duplicate-key, payload-size and depth boundary, then returns `422` for schema mismatch without echoing values. A JSON media type is recognized in every spelling axum's `Json` extractor accepts (any ASCII case, parameters, a `json` subtype or `+json` suffix). * The policy validates JSON bodies only. Authentication, authorization, ownership, business invariants and query/header/form parameters remain separate application boundaries. ### 7.5. Deterministic Threat Sentinel and Proof of Work * `ThreatClassifier` assesses a bounded aggregate window supplied by the host against transparent thresholds for credential stuffing, API scraping and distributed automation. It does not collect traffic, infer identity, use a model or attribute a botnet. * `ProofOfWorkGate` issues OS-random, HMAC-authenticated challenges bound to one canonical application subject. Tokens have bounded difficulty, TTL and cardinality; successful verification atomically consumes local state so only one concurrent verifier succeeds in the process. * Classification is evidence, not authorization. The host chooses whether and where to challenge, provides an accessible fallback, rate-limits issuance and owns trusted proxy/device policy. Replay state is process-local; distributed enforcement, durable telemetry and cross-process one-shot consumption require an application adapter. ### 7.6. Authenticated Local SIEM Journal * `AuthenticatedSiemSpool` is the opt-in authenticated counterpart to the compatible unsigned `DurableSiemSpool`. It normalizes each local v1 event, writes it synchronously under 16 MiB/4,096-record ceilings and authenticates sequence, key identifier, predecessor tag, payload length and exact payload with a domain-separated HMAC-SHA256 chain. * `SiemKeyRing` accepts one active write key and at most seven historical verification keys. Key identifiers are bounded, key material is consumed into zeroizing storage, and secret-bearing `Debug` output is redacted. A rotation reopens the spool with the new active key plus every still-needed historical key; silently missing or wrong keys fail closed. * This journal detects forged records, interior deletion/reordering and external length changes. Removal of a complete valid tail is indistinguishable from an earlier valid file unless the operator retains a trusted external checkpoint. Path/key custody, permissions, single-writer enforcement, rotation retirement, compaction, retention, backup, transport, retry, acknowledgement and dead-letter handling remain operator/application work. --- ## 📡 8. IoT, Firmware Security & Protocol Frames (`rullst-iot`) ### 8.1. Ed25519 OTA Firmware Gate * **Firmware Verification:** Strict Ed25519 signature validation over a cryptographic manifest `[target, version, rollback_counter, firmware_len, firmware_sha256]`. * **Anti-Rollback Protection:** Verification rejects any counter lower than or equal to the state loaded into the manager. The recommended `RollbackCounterStore` path additionally performs an exact compare-and-set and may report success only after a strictly increasing value is durably committed across reset. A retry of the same verified manifest after an ambiguous store failure completes only when the store reports, and a fresh load confirms, exactly that manifest's counter; an equal stored counter without such an earlier attempt remains a conflict. Atomicity, integrity, wear-leveling and power-loss behavior are obligations of the caller's platform adapter and require hardware-specific evidence. * **Commit Invariant:** In-memory partition selection and store-backed counter commit are blocked until full cryptographic verification succeeds. `verified_target_partition` exposes the inactive bank (opposite `current_partition`) for platform flash/read-back before commit. The v13 `new_with_running_partition` constructor takes the bank the bootloader started, while the older constructors assume `PartitionA` and require platform code to set `current_partition`; a commit selects the other bank for the next boot but does not change `current_partition`, so an update verified again before reboot never targets the running bank. The compatibility `commit_verified_update` path is process-local and does not claim persistence, flash or bootloader control. ### 8.2. Embedded Sensor Frames (`#![no_std]`) * `rullst-iot` core models compile under bare-metal `#![no_std]` targets (STM32, ESP32-C3, Cortex-M). * `cargo rullst make:iot <DeviceName>` generates and registers a local telemetry module, enables the umbrella `iot` feature and refuses unsafe names or collisions. It does not install firmware, a HAL, MQTT or CoAP. * `IotDashboard` renders an escaped HTML snapshot. It does not infer online state or provide a live device connection. * `MqttPublish` encodes one bounded MQTT 5 PUBLISH packet with validated topic, minimal Remaining Length, QoS/packet-identifier invariants and an empty property section. `CoapRequest` encodes bounded RFC 7252 base requests with a token, ordered URI-Path (1-255 bytes per segment, never `.` or `..`)/Content-Format options and a non-empty payload marker. Both compile under `no_std`; neither opens a socket or owns protocol session state. * 🔵 **`[Roadmap]` MQTT/CoAP Transport:** Async connections, TLS/DTLS, broker negotiation, acknowledgements, retransmission/congestion control, block-wise transfer and interoperability matrices remain separate integration work. --- ## 🤖 9. AI Agent & LLM Orchestration (`rullst-ai`) ### 9.1. Guarded AI Client * `AiClient::auto()` and Nexus share `AutoAiConfig`. Resolution uses one snapshot in OpenAI/custom, Anthropic, Gemini, DeepSeek, Groq, Ollama order. Empty environment values are absent; explicit mocks remain offline. Groq requires `GROQ_API_KEY` plus `GROQ_MODEL`; a custom `OPENAI_BASE_URL` requires `OPENAI_API_KEY` plus `OPENAI_MODEL` and uses the HTTPS chat-only compatible adapter. Configuration indicators do not attest network/account health. * Provider-agnostic interface for **Google Gemini, OpenAI, Anthropic Claude, DeepSeek, Ollama, and explicit OpenAI-compatible endpoints**. The compatible adapter is chat-only by default; applications declare optional request shapes for one exact model. Loopback may be unauthenticated, cloud requires HTTPS/Bearer, and unrelated protocols implement the public `AiProvider` boundary rather than passing through arbitrary HTTP. * **Prompt Injection Firewall:** Real-time token heuristics intercepting prompt exfiltration, instruction overrides (`DAN mode`), and delimiter injection attacks. * **Automated PII Masking:** Scrubs check-digit-valid CPF/CNPJ numbers (canonical formatted or unformatted), card-like digit runs and email usernames prior to outbound LLM dispatch. Alphanumeric CNPJs and other identifiers are not recognized. * **Token usage (v13):** `chat_with_usage`/`send_with_usage` return a `ChatCompletion` with optional `TokenUsage` parsed only from documented provider fields (OpenAI/DeepSeek `usage`, Anthropic `usage`, Gemini `usageMetadata`, Ollama eval counts, compatible servers' `usage`). Streams report it through `AiStreamSink::usage` and `StreamSummary::usage()`; compatible configurations request it only with `with_stream_usage()`. Missing counts are `None`, never estimated. ### 9.2. Bounded Streaming and Cancellation * `StreamingAiClient<P>` preserves static dispatch, reapplies the mandatory input guardrails and enforces at most 4,096 non-empty chunks, 64 KiB per chunk and 2 MiB aggregate output independently of the provider. * An OpenAI-compatible configuration may explicitly declare SSE streaming. The transport requires `text/event-stream`, bounded raw bytes, supported chat deltas and `[DONE]`; malformed, truncated or oversized streams fail closed. * Anthropic (Messages SSE, requiring `message_stop`) and Gemini (`streamGenerateContent?alt=sse`, requiring a final `finishReason`) stream natively (v13) under the same bounds and guardrails; unknown Anthropic events are ignored and error events expose only their type. Truncated or withheld replies (Anthropic `max_tokens`/`model_context_window_exceeded`/`refusal`, Gemini `MAX_TOKENS` or safety-class reasons) fail in streaming and non-streaming paths alike. * `AiCancellation` races the initial request and every streamed body read. It drops local transport work but does not prove upstream cancellation or stop provider billing. Non-compatible protocols and ordinary non-streaming calls retain deadline/drop semantics. ### 9.3. Bounded Tenant-Aware RAG * `RagPipeline::answer` requires a trusted Core `TenantContext` and composes guarded embedding, a static-dispatch application `RagRetriever`, bounded context selection, guarded generation, source metadata, and one required terminal `RagAuditSink` event. * Retrieved documents carry the trusted tenant tag. The pipeline rejects mismatches, over-return, injection heuristics, empty context, non-finite embeddings, and unavailable mandatory audit evidence rather than silently generating an ungrounded response. A guardrail block of the assembled prompt is audited as `ContextRejected` before any provider call. * Context limits count Unicode scalar values per document and in total. The audit event omits raw question, context, embeddings, provider bodies, and answer; its SHA-256 query digest is correlation metadata, not encryption. * `InMemoryRagRetriever` and `InMemoryRagAuditTrail` are bounded process-local development/test implementations. `DurableRagAuditTrail` and `DurableToolAuditTrail` add bounded, synchronously persisted, versioned local evidence with restart validation and fail-closed corruption/quota handling. They are single-process writers; their SHA-256 frames detect corruption but do not authenticate events. Production hosts own authoritative tenant and ownership predicates, durable/external vector adapters, ingestion/deletion, model/vector compatibility, output policy, directory permissions, rotation/retention/backup, external audit delivery, tuning, evaluation and recovery. ### 9.4. Bounded Conversational Memory * `StatefulChat<M>` uses static dispatch over `ChatMemory`, requires a trusted `TenantContext` and validated `ConversationId`, loads only the configured even number of recent messages, applies the guarded `AiClient`, and persists the user/assistant exchange only after generation succeeds. A response that the guardrail would block on replay, or that is empty or larger than 64 KiB, is rejected as a generation failure and not persisted. * `InMemoryChatMemory` is deterministic, tenant-partitioned, cardinality-bound, and intended for tests/local use. The opt-in `sql-memory` adapter supports SQLite, PostgreSQL, MySQL, and MariaDB through a dedicated SQLx Any pool. * The SQL adapter advances an even conversation revision and inserts both messages in the same transaction. A compare-and-swap predicate rejects stale cross-process writers; Rullst deliberately does not retry the provider call. History reads bind the tenant/conversation and never include rows newer than the revision observed by that read. Tenant and conversation IDs compare case-sensitively on every backend: MySQL/MariaDB tables declare the key columns `CHARACTER SET ascii COLLATE ascii_bin`, and each tenant-scoped MySQL/MariaDB statement also compares the key byte-exactly. `prepare_schema` never alters an existing table; a legacy case-insensitive table fails closed for IDs that differ only by case (`InvalidConfiguration` on `ensure_conversation`) until the operator applies the documented migration. * Message text is not encrypted by this adapter. Authenticated conversation ownership within a tenant, retention/erasure, provider audit, backups, migration governance, and user-facing conflict retry remain host policy. The generated Turso/custom-model scaffold is a separate application-owned path. ### 9.5. Authenticated Audit Export * `AuditDeliveryClient` exports an application-minimized serializable event in a versioned JSON envelope of at most 16 KiB. Cloud endpoints require HTTPS; local HTTP(S) requires a literal loopback IP. Redirects and ambient proxies are disabled. * HMAC-SHA256 covers a domain separator, key ID, Unix-millisecond timestamp and the exact body. A caller-supplied event ID remains unchanged across one to five attempts. Only transport/deadline errors, HTTP 429 and HTTP 5xx are retryable, and a closed acknowledgement of at most 8 KiB must bind that ID. `AiCancellation` races request, body and retry waits. Empty or `mock_*` keys select deterministic offline behavior. * A timeout may follow remote acceptance. The receiver therefore owns signature and freshness verification, event-ID deduplication, authorization, persistence, retention, key distribution/rotation and operational availability. The client does not minimize arbitrary event values or provide a durable outbox/SIEM service. ### 9.6. Bounded Adaptive Evaluation * `AdaptiveAiEvaluator<P>` keeps static provider dispatch and reapplies the mandatory prompt guardrail on every strategy-generated prompt. A scenario is capped at 32 turns, 16 KiB per prompt and 2 MiB per response, with an independent per-turn deadline and `AiCancellation` raced against each call. * `AiEvaluationStrategy` temporarily receives the bounded response or a low-cardinality guardrail/provider/deadline outcome and explicitly chooses pass, fail, inconclusive or a next prompt. The synchronous strategy is application code and must not persist/log model text without policy. * The version-1 JSON report records caller-supplied suite and exact model/configuration subject labels, provider name, terminal code and only per-turn byte counts/outcomes. It retains no prompt, response or provider error. Subject labels are assertions, not automatic model discovery. * Deterministic repository fixtures prove bounds, feedback, redaction, cancellation and status semantics. Operators still version domain corpora, run them against every exact live model/configuration and review results; a pass is not universal safety, groundedness or jailbreak-resistance evidence. --- ## 📊 10. Control Center & Admin Interfaces (`rullst-studio` & `rullst-nexus`) ### 10.1. Rullst Studio (`http://127.0.0.1:5555`) * Local-first developer dashboard with a server-rendered dark interface; browser assets and final page policy remain deployment concerns. * Process observations sourced from `RadarSnapshot::collect()` and explicitly supplied local collectors; unsupported values remain unavailable. * Generated applications start the standalone Studio only in debug builds and bind it to loopback. Its local capability verifies the direct loopback peer, accepts only a local `Host` authority, requires same-origin `Origin` on unsafe methods, and rejects missing origins on mutations. Responses carry `Referrer-Policy: same-origin` so that browser form posts keep the real origin; `Origin: null` is accepted only with `Sec-Fetch-Site: same-origin`, never alone or from a same-site document. This is a local DNS-rebinding/CSRF boundary, not production authentication. * Queue, revenue, security and telemetry pages report only values supplied by their configured process-local source. The queue page labels its status counts as covering only its 50 most recent records; the pending count and purges cover the whole queue. Its previews are cut to 256 payload and 512 error characters. The listing uses `QueueDriver::list_job_previews` with a 2 KiB budget per payload and error, which SQLite and Redis apply in the store; a custom driver without its own implementation still loads complete records through the `list_all_jobs` projection. Unsupported driver operations and disconnected integrations remain errors or `Unavailable`. The standalone migration surface provides CLI guidance and returns `501` from legacy mutation handlers because no migration/seeder registry is installed. * Studio's database views use the application's ORM pool or, when none exists, the shared `Server`/Artisan database URL resolver (section 4.1). There is no implicit SQLite fallback; resolution errors never echo configuration content. * The database browser accepts a deliberately narrow ASCII SQL-identifier boundary. Reads are bounded: a page loads at most 25 rows, ordered by the complete primary key (otherwise by every selected column) so that pages neither repeat nor skip rows, the database cuts each cell's text to 256 characters (key columns keep up to 16 KiB so that rows remain addressable; a longer key makes the row read-only) and search terms are limited to 256 bytes. Search matches the displayed columns (at most 256), and the record count uses the same predicate. Writes require the crate-private proof inserted by the verified local middleware, database-inspected table/column/complete-PK metadata, a 64 KiB request limit, primitive typed binds and exactly one affected row. A row offers only the actions whose form (for Edit, with a maximum-size value) fits that limit, and an empty text key is addressable. A primary-key column outside the identifier boundary or the 256-column cap makes the table read-only instead of shortening the key, as does a floating-point key column, whose rendered text is rounded. Rows whose key is `NULL`, is not decodable as text or contains a line break or NUL (which browsers rewrite in form values) offer no actions. Each write runs in a transaction that commits only for exactly one affected row; zero or several rows are rolled back and reported as `404`/`409` (engines without transactions, such as MySQL MyISAM, cannot roll back). Primary keys/backend-specific values (including MySQL/MariaDB unsigned or zero-filled integers) are read-only, while delete requires `DELETE <table>`. SQLite, PostgreSQL, MySQL and MariaDB run separate mutation contracts; PostgreSQL runs under both the default `sqlx::Any` build, where Studio renumbers bind markers and casts information-schema identifiers to `VARCHAR`, and `strict-postgres`. On PostgreSQL Studio browses the `public` schema and names it in every data statement, so `search_path` cannot resolve a table to a same-named table in another schema. This is not application authorization, tenant scoping, audit, rollback or shared-production administration. The ER diagram inspects the same relational backends with bound lookup values and strict normalized Mermaid identifiers, lists each column once and pairs composite foreign-key columns by key position. The read-only API reference requires an application-supplied `OpenApi`. * Request SSE records method, URI, status, and latency without bodies or headers. Environment values are redacted by default; an allowlisted value is also redacted when it carries URL user information, a bearer token or a secret-named assignment. The typed config projection never renders connection URLs, filesystem paths, cookies, tokens, or credentials. A successful Studio database-flag mutation invalidates warm `DbFeatureDriver` caches in the same process; direct writers and other processes remain subject to TTL unless the host distributes invalidation. The driver also caches missing flags and failed or two-second-timed-out lookups for its TTL, serves the last value read after a failed refresh and bounds its cache to 4,096 flag names. SQLite removes completed jobs by default. An explicit 1–100,000-row history policy retains and atomically prunes real completion records for Studio, with a separate purge; retained payload access and lifecycle belong to the host. Redis lists at most 1,000 failed, dead-letter, processing, pending and scheduled jobs, retries failed jobs and purges failures; custom queue inspection remains capability-specific. Queue retry/purge and feature-flag toggles require the same crate-private verified-local marker as database writes, so their raw routers return `403` outside the local boundary. Safe `GET` views perform no schema or data writes; the feature-flag page reports a missing `rullst_feature_flags` table instead of creating it; other query failures (connection, permission, column types) are reported without migration guidance. * `Studio::with_cache` is an explicit metadata-only diagnostic capability. The memory and Redis cache drivers return at most 200 sorted entries containing logical key, UTF-8 value byte length and remaining TTL; custom drivers return `InspectionUnsupported` by default. Studio displays at most 100 keyed opaque identifiers and never cache values or exact logical keys. Individual invalidation requires its unforgeable verified-local marker and a fresh process-bound HMAC token. The page has no bulk flush operation. * Distributed trace producers use separately mounted, push-only routers; each endpoint binds one exact producer name to one key and multiple producers use separate endpoints/keys over the same shared store. A v1 batch is at most 128 KiB and 128 spans, carries W3C-compatible IDs, a bounded source/operation/kind/timing/status set, and accepts no attributes, SQL, bindings, headers, bodies or error strings. HMAC-SHA256 authenticates the exact body and source/timestamp/nonce headers; a 60-second window and bounded atomic nonce cache reject replay, while the shared in-process store is capacity-bound and idempotent by trace/span ID. The local viewer reports a fixed 100 ms slow-query signal and a three-equal-label possible-N+1 heuristic. This is not an OTLP collector, durable backend, secret manager, remote viewer/login, or proof that a repeated operation is defective. TLS, network policy, key distribution/rotation, clock synchronization, producer label redaction, retention and availability belong to the deployment. * Exposing Studio beyond the developer machine requires an explicit authenticated network boundary owned by the application; no environment variable silently converts the local server into a production admin surface. ### 10.1.1. Production monitoring endpoint (v13, 13.0.0-alpha.3) Phase 1 of the Studio production monitor: a read-only machine contract served by the application, which a later Studio "remote projects" mode reads. Guide: [production monitoring](https://rullst.github.io/Rullst/book/production-monitoring.html); threat model `TM-STUDIO-2`. * **Mounting.** `Server::with_monitor(rullst_core::monitor::Monitor)` mounts `GET /_rullst/monitor/v1` (Core's reserved `/_rullst/*` namespace, schema major version in the path) and a recording layer outside the framework layers, so their refusals are counted. At start-up the server adds the comma-separated tokens of `RULLST_MONITOR_TOKENS` (process environment, then `.env`) to `Monitor::with_token` values. With no token and no `MonitorAuthorizer`, nothing is mounted (`404`) and request handling is unchanged. Invalid tokens (fewer than 43 base64/base64url or 64 hexadecimal characters, other characters, more than 512 characters, fewer than ten distinct characters, placeholder words, more than eight tokens), invalid application labels (1–128 bytes, no control characters) and invalid queue names (1–64 of `[A-Za-z0-9._-]`, unique, at most 16) stop start-up with `ServerError::Configuration`, whose message names a position, never a token. The hot-reload development server does not mount it. Since 13.0.0-alpha.3 the `src/main.rs` of the Blank (web and JSON API) and SaaS starters (and of the LMS example) carries the `with_monitor` call commented out, and their `.env`/`.env.example` list `# RULLST_MONITOR_TOKENS=` commented out with a generation hint; nothing is enabled by default. * **Admission.** In order: a request with `Origin`, or `Sec-Fetch-Site` `cross-site`/`same-site`, gets `403`; in staging and production a request without TLS evidence gets `426` (evidence: `https` reported by a trusted proxy with `trust_forwarded_proto`, the application-inserted `MonitorVerifiedTls` marker, or, with `Monitor::allow_loopback_plaintext`, a direct loopback peer with a loopback `Host` and no forwarding header); a per-client budget of 30 requests refilled at one per second, then a lockout after 10 presented invalid `Bearer` values refilled at one per 90 seconds, both `429` with `Retry-After` and keyed per IPv4 address or IPv6 /64 after trusted-proxy resolution. A locked client's credentials are not evaluated. Then `Authorization: Bearer` is compared with every token's SHA-256 digest in constant time; otherwise the `MonitorAuthorizer` answers `Allow` (`200`), `SignIn` (`401`), `Deny` (`403`) or an error (`503`, reason logged). Missing and invalid credentials get one empty `401` with `WWW-Authenticate: Bearer realm="rullst-monitor"`. Every response of the route carries `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`; other methods get `405`. * **Document (`rullst.monitor.v1`).** `schema`, `generated_at`, `application` (`name`, `version` or `null`), `framework`, `uptime_seconds`, `process` (radar), `http` (`window_seconds`, `route_limit`, `routes_overflowed`, `requests_total`, `status_total`, `window`, `routes`), `database` (`observed` with `queries_total` and `n_plus_one`, or `unavailable` with a fixed reason) and `queues`. A test pins this exact field set. Route keys are the matched route pattern (Axum's nested tail shown as `{*rest}`, control characters replaced, at most 256 bytes) or `(unmatched)`, and a method from the standard set or `OTHER`; at most 256 pairs, then one `*`/`(other)` bucket. Windows are five one-minute slots with a 17-bucket latency histogram (0.1 ms to 10 s and an open bucket); a percentile is its bucket's upper bound capped at the window maximum. N+1 findings use the `rullst_core::query_patterns` rule on ORM label fingerprints attributed to a request scope, at most 64 entries. Queue `pending` comes from the store within 250 ms per queue (`timeout`/`driver_error` otherwise); in-flight, completed, failed, retried and stalled-recovered counts cover this process's workers. Raw paths, query strings, headers, cookies, addresses, user identifiers, bodies, SQL text or values, error messages, environment and configuration values are never recorded. * **Cost and scope.** State is process-local and resets with the process. The passive ORM query layer is part of the framework subscriber in every build and attributes operations only inside a recording scope. A release-build measurement adds about 0.6 µs per request (guide, "Overhead"). ### 10.1.2. Studio remote projects (v13, 13.0.0-alpha.3) Phase 2 of the production monitor; threat model `TM-STUDIO-3`; guide: [production monitoring](https://rullst.github.io/Rullst/book/production-monitoring.html#watch-your-projects-from-studio). * **Process.** `rullst_studio::remote::run_remote_studio` (the `rullst-studio` binary, also started by `cargo rullst studio --remote`) exists only with the non-default `remote` feature of `rullst-studio` (which adds `reqwest`, `ring` and SQLx's SQLite driver), so applications that mount Studio in development build none of it. It listens on `127.0.0.1` only (default port 5556) and needs no application process. Its router applies Studio's loopback boundary (loopback peer and `Host`, same-origin `Origin` on unsafe methods, crate-private marker) in release builds too, and mounts only `/studio` (projects), `/studio/radar`, `/studio/routes`, `/studio/queues`, `/studio/queries` and the assets; local surfaces (data browser, `.env`, AI, API, migrations, jobs, cache, features, requests) answer `404`. Add and remove forms also require a random per-process form token compared in constant time. * **Storage.** `projects.json` in `$XDG_CONFIG_HOME`/`~/.config`/`%APPDATA%` under `rullst/studio`, and `projects.key` (32 random bytes) under the data directory (`$XDG_DATA_HOME`/`~/.local/share`/`%LOCALAPPDATA%`). Files are written atomically, owner-only (`0600` in `0700` directories on Unix), read with a size bound, never through a symlink; unknown fields are rejected. At most 32 projects; names are 1–48 of `[A-Za-z0-9._-]` starting with a letter or digit. Tokens are sealed with AES-256-GCM (random 96-bit nonce) with the project name and normalized base URL as associated data; a token that does not decrypt is reported and never sent. * **Requests.** Base URLs are `https://`, or `http://` to a loopback host, without user information, query or fragment. Studio sends `GET <base>_rullst/monitor/v1` with `Authorization: Bearer`, follows no redirect, ignores proxy variables, validates certificates, and bounds each read to 1 MiB, 5 s total and 3 s to connect. It accepts only `200`, a JSON content type and `schema: "rullst.monitor.v1"` with bounded lists (512 routes, 128 findings, 32 queues) and labels (512 characters); unknown fields are ignored. Failures are fixed statuses; every remote value is HTML-escaped. * **History and alerts (phase 3).** Projects marked *watched* are read every 60 s (clamped to 15 s–1 h) while the process runs. Each read stores one row of aggregates (reachable, requests per second, window requests, 5xx rate, p95, total pending and failed queue jobs) in `history.sqlite` in the data directory (created `0600`, directory `0700`); every write deletes rows older than 7 days and beyond 10,080 per project, and keeps the newest 500 alerts. Rules per project (unreachable; 5xx rate, needing 20 window requests; p95; pending jobs; failures between two polls, ignoring counter resets) fire at most once per cool-down (15 minutes, 1–1,440) for each rule, with messages built from fixed wording, the project name and numbers. `/studio/history` charts the samples as inline SVG; `/studio/alerts` lists alerts, sets each project's watch flag, rules and webhook (same form token) and acknowledges alerts. A webhook URL must be `https://` without user information or fragment, is sealed like a token and is never displayed; each alert is posted once as JSON (`source`, `project`, `rule`, `message`, `at`) without redirects or proxies within 5 s, and the fixed outcome is stored on the alert. ### 10.2. Rullst Nexus (`/nexus`) * Auto-generated CMS with dynamic CRUD operations and AI Admin Assistant. * **Security Default:** Fail-closed by design; requires explicit authentication middleware and RBAC role validation (`admin`) on all mutating endpoints. * Generated applications may use `NexusAuthPolicy::local_development_or_basic_from_env()`: debug builds accept only a peer address verified as loopback through `ConnectInfo` (IPv4-mapped peers are canonicalized), a local `Host` and, for unsafe methods, a same-origin `Origin` or `Origin: null` with `Sec-Fetch-Site: same-origin` (local responses default to `Referrer-Policy: same-origin`), while release builds require validated Basic Auth credentials from the process environment, then `./.env` (`config::project_setting`; an unreadable or malformed `.env` is `NexusBuildError::InvalidDotenv`). Missing peer metadata is denied, and an environment mode flag cannot enable unauthenticated release access. * `NexusAuthPolicy::application(authorizer)` (v13) lets the application's own sign-in decide: Nexus calls the application's `NexusAuthorizer` with the request's method, URI, headers and extensions on every request, before any data is read or rendered, so a revoked session or permission takes effect on the next request. `NexusAccess::Allow { actor_id }` admits the request under that audit actor (1 to 255 bytes, unpadded, without control characters; any other value answers `503`); `SignIn` redirects a page request (`GET`/`HEAD` without `HX-Request`) to the login page with `303` and answers fragments and other methods `401` with `HX-Redirect`, which htmx and `nexus.js` follow; `Deny` answers `403`; an authorizer error answers `503`. The login page is `/login` or `NexusApplicationAuth::with_login_path`, which must be a local path of at most 512 visible ASCII characters starting with a single `/` and without `\` (`NexusBuildError::InvalidLoginPath` otherwise). The policy needs no TLS evidence; CSRF, the administrator role layer, tenant scope and the audit apply unchanged. `local_development_or_application` selects the debug loopback policy instead in debug builds. * The SaaS starter and the LMS example use `local_development_or_application` with a generated `NexusAdmins` authorizer (`src/middlewares/nexus_access.rs`) that admits the user of the epoch-checked session cookie whose `users.is_admin` column is true (default false, skipped by ORM writes, editable in Nexus; `cargo rullst auth` users have it too). The Blog, Portfolio and ERP examples (the former starters) keep `local_development_or_basic_from_env()`; when `NEXUS_ADMIN_*` are missing in a release build the Portfolio and ERP start without Nexus (the ERP also without its back office) and print one line naming the variables, and the Blog does not start; a weak, placeholder or unreadable credential stops startup in all three. * Basic Auth counts only presented Basic credentials that fail, per client bucket (IPv4 address or IPv6 /64 of the `ConnectInfo` peer); a credential-less `401` challenge is not a failure. A locked bucket receives `429` without credential evaluation, except for a browser holding the per-process `rullst_nexus_known_client` cookie issued on a previous success. Behind a shared reverse proxy, Core's `TrustedProxyLayer` (`Server::trusted_proxies`) configured with only the proxy's networks supplies the resolved client as `ConnectInfo`, isolating clients. * Basic Auth requires TLS evidence: the application-inserted `NexusVerifiedTls` marker, or a `ClientAddr` whose `forwarded_proto()` is HTTPS, which only the trusted-proxy layer can produce and only with `trust_forwarded_proto(true)`. A request URI or forwarding header alone is never TLS evidence. * A model may explicitly declare one text `tenant` column. Nexus then obtains the scope only from a trusted Core `TenantContext`, injects it on create, includes it in every built-in read/mutation/batch predicate, and denies a missing context. Models without that metadata remain global administrator models. Authentication and tenant-membership resolution remain host contracts. * Updates write only submitted fields, and the edit form (`data-nexus-mode="edit"`) submits only controls the administrator changed. NULL, unregistered enum values, offset or sub-millisecond date-times and undecodable values render without a widget default that could overwrite them. An emptied number/relation/date/date-time/enum/JSON value is NULL (omitted on create), never `''`; the semantic validator rejects `''` for those kinds. A `FieldKind::Integer { min, max }` value must be a whole number in range and is stored canonically; `Number` accepts any finite number. Date and time kinds are parsed per kind and bound as typed values: `Date` (`YYYY-MM-DD`, year 0001 to 9999) as a date, `Time` (`HH:MM[:SS[.fraction]]`, v13) as a time, a local `DateTime` (`T` or space separator) as a date-time without a zone, and `DateTimeUtc` (v13) as a UTC instant, where a value without an offset is a wall time in the panel's time zone and one with `Z`, `±HH:MM` or `±HH` is converted. The zone is UTC unless `Nexus::with_time_zone` selects a `NexusTimeZone` (v13, 13.0.0-alpha.3): a fixed offset, or with the `tz` feature (`nexus-tz` in the facade) an IANA zone whose rules come from `jiff` (system zoneinfo, bundled fallback). A wall time that a daylight-saving gap skips is a `422` naming the zone; one that a fold repeats is read as its earlier instant. A text column stores them as `YYYY-MM-DD`, `HH:MM:SS[.fraction]`, `YYYY-MM-DD HH:MM:SS[.fraction]` (the text form of `CURRENT_TIMESTAMP`, so text comparison and ordering agree with application-written rows) and RFC 3339 respectively; a `DateTime` value with an offset is bound as text, as entered, which only a text column accepts. A malformed value is a `422` validation error. `Time` fields use a `time` input and `DateTimeUtc` fields a `datetime-local` input labelled with the zone's name (`UTC`, `UTC±HH:MM` or the IANA name) that shows the stored instant as a wall time in that zone; the later occurrence of a repeated wall time is shown as its UTC value (`Z`) in a text input so saving the form unchanged keeps it, and a value either picker cannot hold exactly (finer than milliseconds) is shown in a text input. * Record keys follow the registered primary-key kind: a `number`, `integer` or relation key must be a canonical integer (`+1`, `01` and `1e3` name no record, so the audit key is the changed key); other kinds are compared as text. A model may declare a composite key (`NexusModel::nexus_pk_columns`, v13; at most 8 distinct registered columns, none of them the tenant column): a record is then addressed by its values in key order, each percent-encoded (`urlencoding::encode`) and joined by `,`; only that canonical spelling with the right number of values names a record, each value typed by its column, and every key column is bound in list, edit, update, delete, batch and create predicates, read-only on edit and a sort tie-breaker. The UI addresses records under `/table/{table}/record/{key}` so keys such as `new`, `search` or `batch` never meet an action route. Other form values are bound as text; on PostgreSQL, numbers and integers are written through `NUMERIC`, canonical-integer relations through `BIGINT`, Booleans as untyped `'0'`/`'1'` literals and dates and times (including NULL) through `DATE`, `TIME`, `TIMESTAMP` or `TIMESTAMPTZ` casts, so native columns accept them, while other kinds need text columns. MySQL/MariaDB receive a UTC instant as its UTC wall time. Search matches the typed text literally and case-insensitively (`ILIKE` on PostgreSQL) in visible text, textarea, e-mail and URL fields; a model without one shows no search box and a search query lists no rows. The tenant predicate compares binary strings on MySQL/MariaDB. * `Password` fields are never rendered: list cells show a fixed mask (the column is not selected or sortable; neither is a hidden field, so a `sort_by` naming either orders by the key) and edit forms an empty input; an empty submission keeps the stored value. Non-empty values are written verbatim; Nexus neither hashes them nor runs ORM hooks, so credential-hash columns must be read-only in Nexus and changed by an application flow that hashes. * `#[derive(Nexus)]` follows ORM field semantics: `#[orm(skip)]`/`#[sqlx(skip)]` fields are not columns; `#[orm(encrypted)]`, `SecretString` and `#[orm(hidden)]` fields become hidden, read-only `Password` fields that are never listed, searched, sorted, rendered or written (only `#[nexus(kind = "password")]` turns an `#[orm(hidden)]` field into a write-only field, and any other widget override on these fields fails compilation); `#[orm(masked)]` fields default to `Password` unless an explicit `#[nexus(kind)]` shows them. The record key is the struct-level `primary_key` (one column, or up to 8 distinct columns separated by commas), else every `#[nexus(primary_key)]` field in declaration order, else `id`; annotations must match a struct-level list. A single integer key is hidden and read-only as database-generated, while another key, and every column of a composite key, is entered on create and read-only on edit. `chrono::NaiveDate`, `NaiveTime`, `NaiveDateTime` and `DateTime<Utc>` fields become `Date`, `Time`, `DateTime` and `DateTimeUtc`. Rust integer fields (also under `Option`, and under an explicit `kind = "number"`) become `FieldKind::Integer` bounded by their type (`isize`/`usize` as 64-bit, `u64`/`usize` capped at `i64::MAX`); `f32`/`f64` stay `Number`. * The panel loads only same-origin assets under `/nexus/assets/` (stylesheet, behaviour script and a vendored, unmodified htmx 2.0.4 with eval, script tags, injected indicator styles and its `localStorage` history cache disabled). Nexus markup contains no inline script/style blocks, event-handler, `hx-on` or `style` attributes, so the default production nonce CSP applies unchanged; the application-wide `security.csp` must not be relaxed for Nexus. AI assistant replies are sanitized to an explicit text-formatting allowlist without any element that loads a resource by itself (no `img`), and links get `rel="noopener noreferrer"`. * `with_required_audit` requires the fixed `rullst_nexus_audits` schema and appends one minimized committed-mutation row in the same transaction. Audit unavailability rolls the data change back. This is not append-only, tamper-evident, denied-attempt, retention, backup, replication or external-SIEM evidence; those properties remain host responsibilities. --- ## 🛡️ 11. Architectural Guidelines for Backward Compatibility 1. **`#[non_exhaustive]` on Public Structs:** All configuration structs and enums must use `#[non_exhaustive]` to ensure minor versions can add fields without breaking downstream code. 2. **Deprecation Policy (`#[deprecated]`):** Public APIs will never be removed without at least one minor release cycle marked with `#[deprecated]`. 3. **Ergonomic String Constructors:** Public constructors accept `impl Into<String>` to support both `&str` literals and owned `String` parameters without boilerplate. 4. **Zero-Panic Invariant:** Production paths must never call `panic!()`, `unwrap()`, or `expect()`; domain errors must return typed `Result<T, AppError>`. --- ### 11.1. Generated Project Context (v13) `generate:ai-context` retains its existing helper signature but replaces raw source/configuration embedding with the `rullst.project-context.v1` inventory. It writes a readable `.llms.txt` and machine-readable `.rullst/context-map.json`, and creates a project `AGENTS.md` only when no user instructions already exist. New project generation installs the same instructions and inventory. Existing `AGENTS.md` files are never rewritten by regeneration or automatic scaffold hooks. The map contains bounded project/dependency/feature names, validated dependency version requirements, configuration key names, source paths and roles, file sizes and one deterministic input fingerprint. It contains no source bodies, configuration values, dependency URLs or absolute workstation paths. Only bounded `Cargo.toml`, optional `Rullst.toml` and `.env.example`, and regular `.rs` files beneath `src` are inspected. Secrets, databases, VCS metadata, build output, dependency directories, hidden descendants and symlinks are excluded or rejected explicitly. Directory depth, entries, individual files, aggregate reads and output size all have hard limits; errors must not echo configuration values. Generated instructions identify the map as an inventory, not evidence that a feature, deployment or test has passed. The dotenv inventory accepts single-line declarations and rejects multiline values before they can be mistaken for key names; it performs no interpolation. The selected root's `src` is explicit: external workspace members are not scanned. `--check` recomputes and compares the generated inventory without writing files; stale, missing or altered output fails. Normal regeneration uses preflighted atomic replacement/rollback, refuses links and unrecognized output, and retains the legacy generated `.llms.txt` migration boundary explicitly. The fingerprint detects changed Rust source and inventoried metadata, excluding configuration values and dependency URLs; it is not a signature or an authorization claim. The workspace directory is trusted against concurrent adversarial filesystem edits. Local unit and real CLI/new-project contracts passed for the bounded inventory. Hosted and installed-archive source acceptance passed in PR #221, as recorded in the [delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html); final release admission remains separate. ### 11.2. Schema-First API Profile (v13) `generate:api --schema <file> --output <directory>` is an opt-in generator with one explicit OpenAPI 3.1 JSON source. It must reject unsupported keywords and shapes, external/cyclic references, duplicate keys, ambiguous names/routes and unbounded input before writing anything. The existing route-scanning generators remain discovery aids; their placeholder schemas do not enter this typed profile. The initial profile admits closed named objects, bounded strings/arrays, booleans, JavaScript-safe integers, required nullable scalar/array fields and optional non-nullable fields. Optional nullable fields, arbitrary maps, polymorphism, floating-point/64-bit numeric wire values, formats and recursive schemas remain unsupported. References are local named object components. Parameters are explicit path strings and scalar query values; request and response bodies are named JSON objects with explicit status codes. Bearer authentication is described explicitly, while identity, ownership, CSRF and authorization remain host application responsibilities. Generated Rust DTOs use Serde, and explicit operation codecs reuse Security's bounded duplicate-key inspection and offline JSON Schema policies. They validate input before deserialization and output before serialization. Generated TypeScript uses strict types, runtime validation and typed status/body unions; its HTTP client bounds response bytes/time, encodes parameters, refuses redirects and never includes tokens or rejected payloads in errors. No route is silently mounted and no schema declaration constitutes authorization. Generation preserves unrelated files, preflights recognized outputs and exposes a read-only freshness check. Hard budgets apply to source/output bytes, schemas, operations, properties, parameters and reference depth. The trusted project directory is not an adversarial concurrent filesystem boundary. Acceptance requires compiled generated Rust and strict TypeScript, plus a real HTTP consumer journey covering Unicode, missing versus null, bounds, typed errors and denied cross-owner access. Local generation, compiled Rust/strict TypeScript and HTTP consumer contracts passed, including read-only APIs, exact safe-integer bounds, nullable nested arrays, duplicate queries/JSON keys and cancellation. The initial implementation passed hosted and installed-archive source acceptance in PR #221; final release admission remains separate. See the [supported profile](https://rullst.github.io/Rullst/book/typed-api.html) for exact limits and application wiring. ### 11.3. Terminal AI Assistant (removed in 13.0.0-alpha.4) `cargo rullst ai`, the terminal assistant of the earlier 13.0 prereleases (with `connect`, `status`, `disconnect`, `upgrade`, `fix` and `review`), is not part of 13.0. Its code remains in git history only and no package ships it. `cargo rullst ai …` is a hidden command that prints that it was removed, what replaces it and the migration guide link, and exits with status 1. Developers use their editor's or terminal's assistant with the project's `AGENTS.md` and `.llms.txt` (`cargo rullst generate:ai-context`) and the published framework index (`llms.txt`). The `rullst-ai` crate is unaffected. The development error page's "Ask your AI assistant" panel replaces `cargo rullst ai fix <error-id>`: a read-only prompt quoting only what the page shows (message, location, the first project stack frames, request method and path) and, when `Server` records the error, the path of its loopback-only JSON context (`/_rullst/errors/{id}`). The retired `POST /_rullst/autofix` points there. ## 🔄 12. Assisted Framework Upgrade Contract `cargo rullst upgrade` is the canonical application-upgrade boundary. It is an assistant, not a claim that compilation proves production compatibility. * 🟢 **`[Implemented / Bounded]` Planning:** `--dry-run` enumerates only Cargo workspace manifests, preserves TOML comments/order, understands normal, inline-table, workspace, target-specific and renamed Rullst dependencies, and reports path/git dependencies that have no version instead of guessing. `--dry-run --json` emits the versioned `rullst.upgrade-plan.v1` envelope. * 🟢 **`[Implemented / Bounded]` Versioned Rules:** source findings are selected from a versioned rule catalog using detected source majors and the exact target major. Every future major release must extend that catalog, migration documentation, negative tests and process-level fixtures for its supported upgrade paths. The v13 catalog `rullst-upgrade-rules-v4` parses Rust sources with `syn` (comments, doc comments and strings never match an API rule; macro bodies are read as tokens), Cargo manifests and generated project files read without following symlinks. Each finding has a stable `V13-*` code, a kind (`must-change` for a compile-breaking or API-shape change, `review` for changed behaviour of an API, feature or configuration in use), `file:line`, a one-line message and the first-column title of its row in `migration-v13.md`; the assisted-upgrade tutorial classifies every row. Each rule records the release that introduced its change (13.0.0-alpha.1 for the 12.x to 13 changes, a later prerelease for rules added or reworded since). The plan takes the project's Rullst version to be the oldest version a Rullst requirement admits or `Cargo.lock` holds and, for a project already on v13, applies only the rules introduced after it, reporting how many rules it skipped and how many findings they would have added; `--all-rules` applies every rule, and project preparations always do. `rullst.upgrade-plan.v1` is extended additively (`kind`, `introduced_in`, `migration_row`, `migration_url`, `finding_counts`, `unscanned_sources`, `migration_guide`, `rule_selection`). Findings never block `cargo rullst upgrade`; project preparations with must-change findings are refused at verification, review and application. * 🟢 **`[Implemented / Bounded]` Transaction:** the default target is the exact installed `cargo-rullst` version; `--to` accepts only the same major train as that CLI. Before writes, the command snapshots workspace manifests, the root lockfile and Rust sources under `target/rullst-upgrades`. It applies only dependency edits and compiler-provided `cargo fix` changes, then requires `cargo check --workspace --all-targets --locked` to pass. Managed version requirements are exact `=VERSION` pins, so an explicitly selected release cannot silently resolve a later patch/minor release. `cargo fix` resolves the candidate lockfile; the final check must use that same resolution. A failed gate restores the snapshot by default; `--keep-on-failure` is explicit, and `--restore` can recover a persisted, path-validated snapshot after an interruption. Process fixtures upgrade v12 origins, reject retired pre-v12 origins, prove restoration across multiple workspace members, preserve a failed edit only when explicitly requested, restore that persisted review state, and reject symlinked Rust sources before starting the transaction. **12.1.0 recovery hardening:** automatic and persisted restores validate the complete bounded index and every snapshot/target before staging all file replacements. Limits are 8 MiB/index, 100,000 entries, 64 MiB/file and 512 MiB/restore. Symlinks/reparse points, malformed or duplicate entries and non-regular files are rejected. Per-file replacement does not truncate a hardlinked destination. A later apply error may leave earlier files restored; it reports progress and retains the backup. Stop other writers first: this is neither an all-files atomic commit nor protection from hostile concurrent filesystem changes. Platform acceptance is scoped to the [published release evidence](https://rullst.github.io/Rullst/book/v12.html#1210-published-maintenance-release). * 🟠 **`[Manual Application Boundary]`** the command never installs a CLI, changes secrets, executes database migrations, invents authorization or tenant policy, exposes Nexus/Studio, validates providers, or declares an application production-ready. Database restore/migration/rollback, the full test suite, authorization negatives and deployment smoke tests remain mandatory human-owned gates. **Update notice (13.0.0-alpha.4):** the CLI never downloads, verifies or replaces its own binary; a newer one is installed with `cargo install cargo-rullst --version =X --locked`. At most once a day, in an interactive terminal (every standard stream a terminal, not `CI`, `RULLST_DISABLE_UPDATE_CHECK` or `CARGO_NET_OFFLINE`), the CLI prints that exact command when crates.io has a newer non-yanked release of its major: a stable release, or for a prerelease CLI also a newer prerelease; never build metadata or another major. The notice reads only the private catalog cache and is shown on the home screen or on standard error after a command. A background thread refreshes that cache at most once a day with one fixed HTTPS request (no redirects, 256 KiB cap, one four-second deadline); a failure is silent. Daily stamps in the user cache (`rullst-ui-v1/`) record the last notice and the last request. The cache keeps the 12.1.0 Unix and Windows private-directory checks (caller-owned `0700` directory or protected DACL, single-link private files, no-follow bounded reads, non-blocking staged replacement) and reuses a catalog for at most 24 hours. It grants no installation, project or deployment authority. **Native CLI artifact preparation:** the candidate pipeline builds both CLI entry points on the four targets in `.github/cli-artifact-targets.json` and runs each executable's version check on its native host. A bounded `rullst.cli-artifacts.v1` inventory binds file names, sizes and SHA-256 digests to version, target, build runner, repository and source commit. Ordinary CI inventories have no release tag and cannot be promoted to release artifacts. These checks establish file integrity, not publisher authenticity. Trusted tag-workflow provenance, client verification, staged installation, recovery and application acceptance remain separate required boundaries; no build or discovery command installs these files automatically. The tag-only release pipeline now calls the same native builder after exact protected-main admission. A separate job with signing authority checks the downloaded checksums and attests executables, manifests and inventory files without checking out source or executing downloaded binaries. GitHub release assets include those files only after crate publication and attestation pass. The development release workflow also retains the original Sigstore bundle and its signed in-toto envelope as release assets and verifies every artifact against it with exact repository/workflow/tag/commit identity before release creation. Releases from 12.1.2 on, including the 13.0.0 prereleases, carry `rullst-release.sigstore.json` and `rullst-release.intoto.jsonl`; earlier release assets remain unchanged. See the [OpenSSF evidence plan](https://rullst.github.io/Rullst/book/openssf-scorecard.html). Client-side verification must pin the publisher, tag workflow, source tag and commit and then compare the expected platform/version/file digest; metadata and matching checksums by themselves remain insufficient authority. **Self-updater removed (13.0.0-alpha.4):** `update check`, `update verify`, `update stage` and `update install review|apply|recover` (12.1.0 to 13.0.0-alpha.3) were removed with their download, provenance, installation receipt, rollback and pruning machinery. The native CLI executables and their `rullst.cli-artifacts.v1` manifests remain attested GitHub release assets that users verify by hand (see SECURITY.md, "Verifying a release"). **Guided update composition:** `update guided --to EXACT_VERSION` is an opt-in interactive composition of the project commands (prepare, verify, review, apply). It reuses the selected version, directories and review digest. Show each complete review before a default-no approval; distinguish trusted project execution/network access and original-file application. No blanket approval or noninteractive implicit yes is accepted. Declining stops before the next operation, preserving any already completed step and its recovery evidence. The current CLI's versioned migration rules remain authoritative, so `--to` must be in its major; since 13.0.0-alpha.4 the flow installs no CLI (`--scope`, `--root`, `--allow-major` and `--prerelease` were removed). Existing structured commands remain the automation interface. Report elapsed time per executed stage, excluding user input; do not promise instant upgrades. **Isolated project preparation (working source):** the opt-in `update project prepare` command snapshots tracked and non-ignored untracked files from the selected Git working directory into private caller-owned storage, preserving uncommitted source contents and deletions. Links, special files, unsupported paths and oversized inputs fail before migration. The copy is bounded to 100,000 entries, 64 MiB per file and 512 MiB total. Cargo metadata and dependency planning run only inside that copy, with Cargo network access and Rustup auto-installation disabled. The existing exact-version manifest editor and the versioned catalog records its review report; preparation executes no build scripts, procedural macros or application tests and grants no apply authority. Ignored files, including typical secret files and build outputs, are not copied, except the root `Cargo.lock` (legacy generators ignored that reproducibility input). Version requirements and existing locked Rullst packages must not imply a downgrade; unsupported or ambiguous requirements require manual review. This is a source snapshot, not a filesystem sandbox: later verification must explicitly authorize trusted project execution. Bounded reviewed application/recovery have separate explicit consent and acceptance gates. **Candidate verification (12.1.0):** `update project verify` reloads the private preparation, validates its baseline/current source/candidate against bounded records and recomputed migration plans, rejects stale inputs, and takes an exclusive operation lock. `--dry-run` shows the selected commands without running builds/tests. Execution requires `--allow-project-code` and uses another fresh private copy. Cargo resolves the lockfile; every locked managed Rullst package must match the exact target before checking all workspace targets and running workspace tests with `--locked` and the selected feature policy. Default features are the default policy, with explicit all/custom/no-default selection. Cargo is offline unless separately authorized; an offline environment cannot be overridden. Rustup auto-installation remains disabled. Build outputs live outside the verified source tree. Each tool has a bounded deadline (900 seconds by default, at most 3,600) and at most 8 MiB per output stream. Execution reuses the supervised child/process-group cleanup and responds to cancellation. This is best-effort process-tree cleanup, not containment of hostile descendants. Private logs include Cargo/rustc version probes, commands and their output digests. Configured compiler wrappers or alternate toolchains still require operator review; probes are not toolchain attestations. Original/prepared/baseline changes, unresolved must-change migration findings (review findings are only reported), failed commands and unexpected source writes reject acceptance. Only the candidate root lockfile may change during verification. `rullst.project-verification.v1` records successful commands, feature policy and final file digests, grants no application/deployment authority, and is not a reusable apply token. Native acceptance and complete application/recovery remain release gates. Tests execute trusted project code with the caller's environment: the copy is not a sandbox, and external effects cannot be reversed through source-file recovery. **Explicit candidate review (working source):** `update project review` revalidates the private preparation, verified file inventory and bounded successful command logs under both operation locks, then shows the complete manifest/lockfile diff and a SHA-256 digest binding that review. Git external diff/text-conversion helpers and paging are disabled; the diff is bounded to 8 MiB and no build/test or original-file edit occurs. The review digest grants no application authority. Native acceptance remains required. **Reviewed application/recovery (12.1.0):** `update project apply --verified PATH --approved-review SHA256` requires the exact review digest and fresh source validation under both preparation locks and a canonical-source lock in the configured private cache. Only the reviewed workspace manifests and root lockfile are eligible. Stage all replacements before changing originals, preserve original permissions and replace directory entries without truncating hardlinks. Unix mode/owner/group and Windows owner/group/DACL/integrity label enter the review digest, with a 1 MiB aggregate serialized access-policy limit. Unix extended ACLs/xattrs and special mode bits require manual updates, except the SELinux `security.selinux` label that the staged same-directory replacement receives anyway; Windows read-only/special attributes, alternate streams, resource/central-access policies and access policies that cannot be recreated exactly fail before source writes. Darwin extended ACLs are inspected through a narrowly scoped OS FFI module, since they are separate from xattr names. It owns and frees the returned ACL and never changes it; a native ACL regression and the exact unsafe-source allowlist govern this exception. The API follows Apple's [ACL entry contract](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/acl_get_entry.3.html). Staging installs the access policy before writing candidate contents. Keep a durable bounded intent record and before/after digests for interruption recovery. Recovery must refuse divergent user edits and may restore only files from that intent; absent original root lockfiles may be removed. Per-file replacement is not a single atomic workspace commit. `update project recover` with the same approval accepts only the recorded before/after states and supports repeated recovery. Locks coordinate this CLI under the same cache configuration, not editors, other caches or filesystem aliases. Stop other writers first; hostile concurrent renames, external test effects and databases remain outside file recovery. Forced process termination during staging can leave disposable sibling temp files; the private before/verified trees and intent must be retained. Timestamp, Windows audit-policy preservation and power-loss fault acceptance remain outside this implementation. Published acceptance covers only the declared platform and fault scenarios. Local process tests cover a killed per-file commit with a persisted intent and subsequent engine recovery, plus real CLI staging terminated by Linux's file-size limit without changing originals. They do not establish power-loss durability. --- ## 📱 13. Installable Applications: Progressive Web App Contract Since 13.0.0-alpha.3 an application is made installable as a Progressive Web App. The canonical product stays the Rullst web application; the browser, or a store package of the same site, only installs it. * 🟢 **`[Implemented / Bounded]` `cargo rullst make:pwa`:** for an HTML starter it writes `static/manifest.webmanifest` (`id` and `scope` `/`, `display` `standalone`, `name`, `short_name`, `start_url`, `theme_color`, `background_color`, `lang`, PNG icons 192 and 512 plus a maskable 512), four placeholder icons pre-rendered from the Rullst logo (no image library), `static/pwa.js`, `src/pwa.rs`, the worker `src/pwa/sw.js` and `PWA.md`. Values come from validated flags or the Cargo package name. It mounts `crate::pwa::router()` after the crate root's single `.merge_axum(rullst::health::health_router())` (merges inside a `return` expression are fallback routers and are ignored), adds the head tags to every recognized layout in `src/` (an `html!` document, a document in a string literal, Core's `render_page_with_lang`, rewritten to `render_page_with_head`) and declares `mod pwa` in `src/lib.rs`, else `src/main.rs`. The complete plan is validated before the first write: identical files and wired places are skipped, so a repeated run changes nothing, and a generated file with other content (edited, or rendered from other flags) refuses the run. Projects without an HTML layout (the JSON API starter), without the health merge or with a source file that does not parse are refused. * 🟢 **`[Implemented / Bounded]` Routes and headers:** `/sw.js` answers `text/javascript; charset=utf-8` with `Cache-Control: no-cache` from the site root, so the worker's scope is `/`; its cache is named after the package version. `/offline` is an `html!` page with `no-cache`. Both answer GET only and are mounted outside the CSRF layer. Static files keep Core's policy (`no-cache`, or a year-long `immutable` for content-hashed names) and the manifest Core's `application/manifest+json` type. * 🟢 **`[Implemented / Bounded]` CSP compatibility:** the registration script is a same-origin file allowed by `script-src 'self'`, it styles the update prompt only through the DOM, the worker is allowed by `worker-src 'self'`, and the manifest by the `default-src 'self'` fallback. No inline script or style is added. A policy with `'strict-dynamic'` needs the request nonce on the script. * 🟢 **`[Implemented / Bounded]` Worker storage rule:** it precaches `/offline` (required) and the PWA's own and the starter's static files (when present); navigations and unversioned static files are network-first with a stored-copy or offline fallback; content-hashed static files are cache-first; other requests pass through. It stores only same-origin `200` `basic` responses to GET requests whose `Cache-Control` has neither `no-store` nor `private` and whose `Vary` is not `*`; since Core answers `no-store` by default, authenticated pages are not kept. A waiting worker takes over only after the user accepts the page's reload prompt (`SKIP_WAITING`); the old version's cache is then deleted. `node cargo-rullst/src/generators/pwa/assets/pwa_tests.cjs` executes both scripts against in-memory browser APIs in CI, and a generated Blank application is compiled and checked for these headers. * 🟢 **`[Implemented / Bounded]` Google Play (Trusted Web Activity):** `make:pwa --android --host <host> --package-id <id>` also writes the Bubblewrap `android/twa-manifest.json` from the same validated values (`packageId`, `host`, `name`, `launcherName`, `startUrl`, `iconUrl`, `maskableIconUrl`, `webManifestUrl`, `fullScopeUrl`, theme, navigation and background colours, `display`, `appVersion`/`appVersionCode` 1, `enableNotifications: false` and a placeholder `signingKey` outside the repository) and `PWA-ANDROID.md`. The host is a lowercase DNS name with an alphabetic top label; the package ID has lowercase segments and may not be `com.example.*`. `rullst::pwa::AssetLinks` serves `/.well-known/assetlinks.json` (`application/json`, `public, max-age=3600`) with one `delegate_permission/common.handle_all_urls` statement per `AndroidApp`; package names are Android application IDs and each fingerprint is 32 colon-separated hexadecimal bytes (normalized to upper case), validated at construction with the typed `AssetLinksError`. `AssetLinks::from_project` reads `RULLST_ANDROID_ASSET_LINKS` from the process environment, then `.env`, then `[[android.asset_links]]` in `Rullst.toml`; parse errors report only a line and column. With no app the router has no route. The generated `src/pwa.rs` mounts it and reports an invalid configuration at startup instead of serving it. Bubblewrap, the signing key, Play App Signing and store review remain application work. * 🟢 **`[Implemented / Bounded]` `render_page_with_head`:** Core's `render_page_with_lang` with extra `Markup` at the end of `<head>`; an htmx request still receives only the content. * 🟠 **`[Application Boundary]`** icons, HTTPS, offline data and conflict handling belong to the application. Browser-side offline data follows the server-authoritative pattern of the PWA guide (an IndexedDB outbox replayed with idempotency keys); no sync engine is generated. * 🟠 **`[Documented / External Tools]` Other stores:** the PWA guide documents packaging the same site as MSIX for the Microsoft Store with PWABuilder and a Partner Center reservation, as Microsoft documents it, and installation on iPhone and iPad from Safari. No Windows or App Store package is generated or verified by the framework; App Store Review Guideline 4.2 usually rejects an app that only wraps a website. * 🟢 **`[Implemented / Bounded]` Shared Wire Contract:** `rullst::client_contract` provides the strict `rullst.client` v1 JSON marker, positive version negotiation, private typed request/success/failure envelopes, bounded log-safe correlation and idempotency tokens, server time, message-free dotted failure codes and a codec capped at 2 MiB. Unknown outer fields, unsupported versions and oversized bodies fail closed. The same code compiles natively and for `wasm32-unknown-unknown`. The envelope carries no role, tenant or authorization claim; the server must derive those from its authenticated context and persist idempotency atomically. * ⚪ **`[Removed in 13.0.0-alpha.3]` Omni:** `cargo rullst make:omni`, `cargo rullst omni` (including `omni android --release`), the Tauri shell templates, their three release workflows and the native-only `offline-sync` feature (`rullst::offline_sync`) were removed. The shells exposed no native API to the site, so an installed PWA gives the same result. `cargo rullst upgrade` reports an Omni shell or script (`V13-OMNI-REMOVED`) and a manifest that enables `offline-sync` (`V13-OFFLINE-SYNC-REMOVED`); see the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html). --- # CLI Reference Source: https://rullst.github.io/Rullst/book/cli_reference.html # 💡 Rullst CLI - Full Command Reference The Command Line Interface (`cargo-rullst`) scaffolds projects, invokes build tools, and provides bounded static-analysis and deployment helpers. The CLI's `--help` output is authoritative for the installed version. This page documents the principal commands of the version 13 prerelease CLI and their security boundaries. Cargo never installs a prerelease unless you name it, so `cargo install cargo-rullst` installs the end-of-life 12.x CLI; install this one with an exact version: ```bash cargo install cargo-rullst --version =13.0.0-alpha.4 --locked ``` `cargo-rullst` is a Core package. Two commands carry their own **Maturity** label below the command name, *Experimental*: `make:iot` and `green`. The [maturity tiers](https://rullst.github.io/Rullst/book/maturity.html#cli-generators) page explains what the label promises. ## Machine-readable output A command whose result is data accepts `--json`. The contract is the same for all of them: * Standard output carries exactly one JSON document and nothing else: no progress lines, banners, tables or ANSI escape codes, whether or not the terminal supports colour. * The document has a top-level `schema` field naming its shape and version, `rullst.<name>.v1`. A new field is added without a new version; a removed or renamed field, or a changed meaning, gets `v2`. Documents that predate this contract also keep their `schema_version` field, with the same value. * Human messages, progress and failures go to standard error. Exit statuses are the same with and without `--json`: a failed check or a refused request still exits non-zero, and the document is still printed when the command could compute one (`doctor`, `deploy:doctor` and `academy:doctor` print their report and then exit with status 1 when a check fails). * A request that has no JSON form (for example `pkg add --json`) is refused on standard error with a non-zero exit status and prints nothing on standard output. | Command | `schema` | | --- | --- | | `cargo rullst info --json` | `rullst.cli-info.v1` | | `cargo rullst doctor --json` | `rullst.cli-doctor.v1` | | `cargo rullst audit --json` | `rullst.cli-audit.v1` | | `cargo rullst inspect routes --json` | `rullst.cli-routes.v1` | | `cargo rullst inspect models --json` | `rullst.cli-models.v1` | | `cargo rullst inspect schema --json` | `rullst.cli-schema.v1` | | `cargo rullst pkg list --json` | `rullst.cli-pkg.v1` | | `cargo rullst upgrade --dry-run --json` | `rullst.upgrade-plan.v1` | | `cargo rullst deploy:doctor --json` | `rullst.deployment-diagnostic.v1` | | `cargo rullst academy:doctor --json` | `rullst.academy-diagnostic.v1` | | `cargo rullst update project prepare --json` | `rullst.project-preparation-result.v1` | | `cargo rullst update project verify --dry-run` or `--json` | `rullst.project-verification-plan.v1` (dry run), `rullst.project-verification.v1` | | `cargo rullst update project review --json` | `rullst.project-review-report.v1` | | `cargo rullst update project apply --json` and `recover --json` | `rullst.project-application.v1` | | `cargo rullst green --json` (experimental) | `rullst.green.v1` | `pkg list --json` prints `{"schema": "rullst.cli-pkg.v1", "table": "dependencies", "packages": ["rullst-auth"]}`; `table` is `workspace.dependencies` for a virtual workspace manifest. `audit --report json` is a different interface: it writes `SECURITY_REPORT.json` (`rullst.cli-audit-report.v1`) and cannot be combined with `--json`. The files `cargo rullst generate:ai-context` writes (`rullst.project-context.v1`) are described in [project context](https://rullst.github.io/Rullst/book/project-context.html). `db:status` is printed by your application, not by the CLI, and has no `--json` form yet. --- ## 🏗️ 1. Project Initialization & Maintenance ### `cargo rullst` (no arguments): home screen Running the CLI without a subcommand opens the v13 home: the RULLST wordmark in a blue → green → orange gradient, the slogan line with the installed version, and a summary of where you are. * **Inside a Rullst project** (the current directory or a parent has a `Cargo.toml` whose package depends on `rullst`): the package name, the enabled `rullst` features (`default` when default features are on), the database family and where it is configured (the process `DATABASE_URL`, then `.env`, then `[database].url` in `Rullst.toml`; `TURSO_DATABASE_URL` for a Turso-primary project), the number of `src/migrations` files and the latest one, and the Git branch. Connection URLs are never displayed. The menu leads with dev, dash, scaffolding, database, `doctor` and deploy, keeps every project operation of earlier releases, and offers **Green Report** ([`cargo rullst green`](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-green), Experimental). Started from a subdirectory, menu commands run at the project root (except `new`). The migration count is the files on disk; run `cargo rullst db:status` for the applied state. * **Outside a project**: the summary links the docs ([start here](https://rullst.github.io/Rullst/book/start-here.html) and this reference) and the menu leads with project creation, followed by the operations for an existing project. Both menus include **Search All Commands**, a fuzzy command palette over every command, including the ones attached at runtime (`update ...`, `make:privacy`, `deploy:doctor`, `completions`, ...). Type to filter by name, alias or description; every word must match, names rank above descriptions, and ties go to the shorter name, then alphabetical order. ↑/↓ (or Tab, Ctrl+P/N) move, Enter runs the selection (asking first for its required arguments, as a menu when the values are fixed), Esc returns to the home menu and Ctrl+C exits. `cargo rullst --help` is the non-interactive equivalent. The opening animates for about 0.7 s on every run; any key skips the animation. `RULLST_OPENING=daily` animates only the first run of each day (later runs draw the final frame instantly) and `RULLST_OPENING=static` never animates. For `daily`, the day of the last animation is kept in the user cache directory (`$XDG_CACHE_HOME/rullst-ui-v1` or `~/.cache/rullst-ui-v1`; `%LOCALAPPDATA%\rullst-ui-v1` on Windows), never in the project. If that file cannot be read or written, the opening is static. Colours use 24-bit RGB when `COLORTERM` is `truecolor` or `24bit` and the nearest xterm 256-colour entry otherwise. `RULLST_REDUCED_MOTION=1` (or `true`/`yes`) keeps the colours without animation. `NO_COLOR` prints the plain line `RULLST v<version> · SECURE, FAST AND AI-NATIVE RUST FRAMEWORK` while the menu stays interactive. When standard input, output or error is not a terminal, `CI` is set or `TERM=dumb`, the CLI prints the plain line, the summary and the equivalent commands, then exits successfully without prompting. In a terminal, at most once a day, the home (or any command, on standard error after it finishes) prints `A newer cargo-rullst is available: cargo install cargo-rullst --version =X --locked` when crates.io has a newer release of the same major; the catalog is fetched in the background at most once a day and cached privately, and nothing is installed. `RULLST_DISABLE_UPDATE_CHECK=1`, `CARGO_NET_OFFLINE=true` or `CI=true` turns it off. See [Update notice and installing a newer CLI](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-update-check-1210-working-source-unreleased). ### Errors and exit codes Every failure is printed on standard error by one shared report: ```text error: Not inside a Rullst project What happened `cargo rullst make:model` must run at the root of a Rullst project; ... How to fix The project root is `../..`: run `cd ../..` and try again. Docs https://rullst.github.io/Rullst/book/start-here.html ``` The common failures have their own title and fix: not inside a Rullst project (pointing to an enclosing project root when there is one), a missing tool (`cargo`, `rustc`, `rustup`, `git`, `docker`, `wasm-bindgen`, `kubectl` or `npm`), a missing Rust target (`rustup target add <target>`), an unreachable database, a port already in use, permission denied, invalid input, network failures, an interrupted prompt and a starter that moved to the repository's examples (`new --blueprint lms|blog|portfolio|erp`). Anything else gets a generic report naming the failed command. Connection-string passwords (up to the host, even when they contain an unencoded `@`), `*_KEY`/`*_SECRET`/`*_TOKEN`/`password` values (also under the quoted keys of JSON or `Debug` maps), bearer tokens and well-known credential formats (including Stripe `sk_live_`/`whsec_` secrets) are masked, and terminal control characters are replaced, in every line of the report. The whole message is redacted before a long one is cut to 4,096 characters. A command started from the home menu or the command palette prints its own report, and `cargo rullst` then exits with that command's status (for example `2` for a usage error) without a second report. `-v`/`--verbose` (accepted by every command) adds the underlying causes and the error's `Debug` form, still redacted. A panic prints a short "Internal error" report instead of the default panic message and backtrace unless `-v` or `RUST_BACKTRACE` is set. | Exit status | Meaning | | :--- | :--- | | `0` | Success (`doctor`: no failed check; warnings are allowed). | | `1` | The command failed, including `doctor` with a failed (✗) check. | | `2` | Usage error: an unknown command, invalid arguments or an argument that is not valid UTF-8. | An unknown command lists the closest commands at the level where it was typed (`cargo rullst make:modek` suggests `make:model`; `cargo rullst update projetc` suggests `update project`; `cargo rullst db` lists the `db:*` group). Ranking ignores case and separators (`make-model`), then uses edit distance with transpositions, an exact `group:name` segment (`migrate`) and prefixes. Colour follows the home screen rules on each stream: none for `NO_COLOR`, `CI`, `TERM=dumb` or a stream that is not a terminal. When standard output is not coloured, generator output is plain as well. Long steps (the first build of `new`, the Cargo gates of `upgrade`) animate a spinner only when standard output is a terminal without `NO_COLOR`, `CI`, `TERM=dumb` or `RULLST_REDUCED_MOTION`; otherwise, as in CI logs and pipes, they print one plain line without escape sequences or carriage returns. After a generator succeeds, the CLI prints up to three concrete **Next steps** (for example `cargo rullst db:migrate` after `make:model -m`). Generators that already print their own instructions (`new`, `auth`, `deploy`, `make:k8s`) add none, and `--json` invocations never print them. ### `cargo rullst completions <shell>` Prints a completion script for `bash`, `zsh`, `fish`, `powershell` or `elvish` generated from the complete command tree, runtime-only commands included. Scripts complete the `rullst` executable that `cargo install cargo-rullst` installs beside `cargo-rullst` (Cargo's own completion does not delegate the arguments of external subcommands); `--bin cargo-rullst` targets the other executable. ```bash cargo rullst completions bash > ~/.local/share/bash-completion/completions/rullst cargo rullst completions zsh > ~/.zfunc/_rullst # with fpath+=(~/.zfunc) cargo rullst completions fish > ~/.config/fish/completions/rullst.fish cargo rullst completions powershell >> $PROFILE ``` ### `cargo rullst info [--json]` (alias `version`) Prints the CLI version, platform, `rustc`/`cargo` versions and the detected project (name, root, `rullst` requirement, features, database family and source, migration files, Git branch). It reads local files and runs `rustc --version`/`cargo --version` only; connection URLs are never printed. `--json` prints: ```json { "schema": "rullst.cli-info.v1", "schema_version": "rullst.cli-info.v1", "cli": { "name": "cargo-rullst", "version": "13.0.0" }, "platform": { "os": "linux", "arch": "x86_64" }, "toolchain": { "rustc": "1.98.1", "cargo": "1.98.1" }, "project": { "name": "shop", "root": "/work/shop", "relative_root": null, "rullst": "13", "features": ["default", "orm"], "database": { "status": "configured", "kind": "SQLite", "source": ".env" }, "migrations": { "count": 2, "latest": "m20260101000000_create_users" }, "git_branch": "main" } } ``` `project` is `null` outside a project; `toolchain` values are `null` when the tool is missing. `database.status` is `configured`, `not_configured` or `unreadable` (with `reason`); `rullst` is the version requirement, or `path`, `git` or `workspace`. ### `cargo rullst new <name>` Creates a Rullst project from scratch. Since version 12, `new` intentionally generates one audited application architecture: Active Record for database-backed code and server-rendered `html!` views enhanced with HTMX for full-stack pages. In a terminal, `cargo rullst new` (with or without a name) runs the v13 create wizard; the home screen's **Create New Project** entry runs the same wizard. It asks, one screen at a time: 1. the project name, when none was given (letters, digits, `_` and `-`, starting with a letter; Rust keywords and existing paths are refused). A name given on the command line is checked by the same package-name rules before the first question; 2. the **starter** — **Blank** (a server-rendered web page with HTMX), **API** (the Blank starter as a headless JSON API) or **SaaS** (authentication, Stripe billing and a pricing page) — each with a one-line description and a compact preview of the file tree it generates. The preview is rendered by the real project writers into a private temporary directory that is removed immediately; nothing is built and no network is used; 3. the **primary database** (Turso and *No database* only for Blank and API); 4. **optional features** (zero or more): AI, Redis adapters, Dockerfile and the Turso, MongoDB, DuckDB, SurrealDB and Qdrant add-ons; 5. a **review** of the answers, the file tree and the commands `new` will run (the initial migration, unless skipped or database-free), with *Create the project*, *Back* and *Cancel*. ↑/↓ (or `j`/`k`, or a digit) moves, Space toggles a feature, Enter confirms and Esc, Backspace or ← returns to the previous question. On a terminal too short for every choice, the list scrolls with the highlighted entry and counts the hidden ones. Cancel and Ctrl+C create nothing. Colours follow the home screen: `NO_COLOR` removes them and 24-bit colour needs `COLORTERM=truecolor`. Every question has a flag; a question answered by a flag is skipped. When standard input, output or error is not a terminal, `CI` is set or `TERM=dumb`, `new` never prompts: without `--default` it exits with an error that names the flags instead. After creation, `new` prints numbered next steps: `cd <name>`, `cargo rullst dev` (which applies pending migrations) and the URL of the generated welcome page (`http://127.0.0.1:<port>`, where the port comes from `PORT`, the project's `.env`, `[app].port` in `Rullst.toml`, then 3000; JSON APIs show a `curl` command instead). The choices that materially change the generated application: * **Starter:** Blank, API and SaaS (`--blueprint` values `blank`, `api` and `saas`; `api` is the Blank blueprint with `--api`). The LMS, Blog, Portfolio and ERP blueprints left the CLI in 13.0.0-alpha.4 and are now complete applications in the repository's `examples/` directory; see [Starters that moved to the examples](https://rullst.github.io/Rullst/book/cli_reference.html#starters-that-moved-to-the-examples). * **Persistence:** a primary relational backend (SQLite, PostgreSQL, MySQL, MariaDB, or bounded Turso-primary for blank/API) plus optional Turso/libSQL, MongoDB, DuckDB, SurrealDB, and Qdrant capabilities. The optional selector accepts zero or more choices and omits capabilities already selected by the primary profile or flags. Specialized adapters remain separate from SQLx Active Record. * **Application profile:** the Blank and SaaS starters use the audited `html!` SSR/HTMX path; API (`--api`) uses the headless JSON path. Repository, LiveView, Wasm Island, Pico.css and Tera foundations remain application-owned APIs and are not presented as equivalent generated profiles. * **Arguments:** * `<name>`: The folder and package name (e.g., `my_startup`). * **Optional Flags:** * `--api`: Scaffolds a headless JSON API from the Blank starter (no HTML view rendering), the same as `--blueprint api`; `--blueprint saas` rejects it instead of ignoring it. Its example write route, `POST /api/messages`, is a bearer machine endpoint: clients send `Authorization: Bearer <API_TOKEN>` with the random `API_TOKEN` written to `.env`, and the server does not start without it (see [which security layer to use](https://rullst.github.io/Rullst/book/security-layers.html#what-generated-applications-already-have)). The interactive wizard then skips its starter question instead of letting its Blank default replace the flag. * `--docker`: Adds a multi-stage `Dockerfile` and `.dockerignore`. The `.dockerignore` mirrors the generated `.gitignore`: it excludes `.env` and `.env.*` (except `.env.example`), SQLite and DuckDB files (`*.db`, `*.sqlite`, `*.sqlite3`, `*.duckdb` and their journals, one list shared by both files) and the host-local `.cargo/config.toml` described below, so the builder's `COPY . .` never sends them to a (possibly remote) builder. An existing `.dockerignore` is kept unchanged. Since 13.0.0-alpha.3 the dependencies are built in their own cached layer without an extra tool: a `planner` stage copies only the manifests, `Cargo.lock` and `rust-toolchain(.toml)`, with `fn main() {}` standing in for every target (`src/main.rs`, `src/lib.rs`, `src/bin/*.rs`, explicit `path = "*.rs"` targets) and build script, and the builder runs `cargo build --release --locked` on that skeleton before `COPY . .`, touches the real `.rs` files and builds again with `--locked`. Docker reuses the dependency layer until a manifest, the lockfile or the set of targets changes; workspace members keep their paths. Since v13 the runtime stage is `gcr.io/distroless/cc-debian12:nonroot` (glibc, libgcc, libstdc++, CA certificates and tzdata; no shell or package manager). It runs as UID/GID 10001 with `HOME=/app`, sets the production bind address and copies local static/config assets when present. The builder stage prepares the `/app` tree, so an explicit SQLite selection still uses the writable `/app/data` directory. Without a shell the image has no `HEALTHCHECK`; let the platform probe `/health` and `/ready` over HTTP. The migration job runs the binary directly (`/app/<name> db:migrate`). The image size of this template has not been measured yet; measure it with `docker image ls` in CI or on a machine with Docker. Secrets are never embedded and no anonymous volume is declared. Run schema migrations as one deployment job before starting or rolling multiple replicas; the generated image deliberately does not race migrations from every application process. Compose services, persistent-volume ownership, backup/restore and platform deployment hardening remain explicit project work. Every starter mounts `rullst::health::health_router()`, so the `/health` and `/ready` probes used by `make:k8s` manifests and the `deploy` Caddyfile answer `200` without authentication. * `--turso`: Adds Turso/libSQL as a side store beside the primary database: the direct Hrana HTTP v3 adapter, checked migrations and its real-SQL offline development fallback. It does not imply transparent replication. * `--mongodb`: Adds MongoDB as a side store: typed document CRUD and its deterministic offline store. * `--duckdb`: Adds DuckDB as a side store for in-process analytics; the optional native dependency increases the first build time. * `--surrealdb`: Adds SurrealDB as a side store: HTTP document CRUD and bounded read-only graph queries. * `--qdrant`: Adds Qdrant as a side store for bounded dense-vector operations and generates empty/`mock_*`-compatible environment fields; it is not the SQL primary. * `--nix` and `--buildah` were removed in 13.0.0-alpha.4; see [Advanced deployment](https://rullst.github.io/Rullst/book/advanced-deployment.html). * `--default`: Skips every question and uses deterministic defaults for anything no flag sets (name `app`, Blank, SQLite, no features), intended for CI and reproducible scaffolding. * `--blueprint <blank|api|saas>`: Selects the starter; the wizard skips that question. `lms`, `blog`, `portfolio` and `erp` still parse but fail before anything is written, naming the example that replaced them (exit status 1). * `--database <sqlite|postgres|mysql|mariadb|turso>`: Selects the primary database; the wizard skips that question. Network databases must be configured before migration bootstrap. The SQLx backends (`sqlite`, `postgres`, `mysql`, `mariadb`) work with every starter. `turso` is accepted only with the Blank starter (web page or `--api`) and has no Nexus, Studio database pages, `Schema` migrations or SQLx ORM; the wizard's choice, `--help` and the review/dry-run screen say so. See [Database Support](https://rullst.github.io/Rullst/book/database-support.html). * `--no-database`: Generates the Blank or API starter without a primary relational database; it conflicts with `--database` and `--blueprint saas` rejects it. * `--ai`: Enables the umbrella AI facade in the generated manifest. * `--redis`: Enables the umbrella Redis queue and cache capabilities and the Redis-structures side store (the direct ORM Redis feature). * `--skip-initial-migration`: Generates the project without running the best-effort initial database migration. `cargo rullst dev` applies pending migrations when it starts; run `cargo rullst db:migrate` explicitly otherwise. * `--dry-run` (v13): Prints the plan (the answers, a compact file tree with the exact number of files and the commands it would run) and exits without creating anything. With `--default` it never prompts; in a terminal without `--default` the wizard's review ends with *Finish the dry run*. * `--vcs <git|none>` (v13, default `git`): Like `cargo new`, initializes a Git repository in the new project when `git` is installed and the destination is not already inside a Git work tree; `none` skips it. The generated `.gitignore` keeps `.env`, `.env.*` (except `.env.example`) and `/target` out. Without Git, `new` prints a warning and keeps the project; `cargo rullst audit` checks committed secrets through Git. Since v13, `--blueprint`, `--database`, `--no-database`, `--ai` and `--redis` no longer require `--default`: in a terminal they answer their questions and the wizard asks the rest. `--api` selects the API starter; `--no-database` and `--database turso` limit the starter question to Blank and API. The retired `--hot-reload` still requires `--default` and is rejected. When the generating Linux host has `mold` or `lld`, the project's `.cargo/config.toml` selects it to speed up local linking. That file describes the generating machine only: the generated `.gitignore` and `.dockerignore` exclude it, so CI runners, teammates and the container builder (which has neither linker) build with the toolchain default. Since v13 the generated `Cargo.toml` also has a `[profile.release]` with `lto = "thin"`, `codegen-units = 1` and `strip = "symbols"`; `panic` stays `unwind` so one panicking handler cannot stop the server. Release builds take longer and their backtraces lose symbol names; the [measured release defaults](https://rullst.github.io/Rullst/book/green-software-roadmap.html#measured-release-defaults) state the size and build-time effect on one starter. Since 13.0.0-alpha.4 it has a `[profile.dev]` as well: the application keeps line tables (file:line in panics and backtraces) and dependencies are built without debug information, which shortens debug builds and rebuilds; see [Build times](https://rullst.github.io/Rullst/book/build-times.html). Without `--skip-initial-migration`, project creation performs the first Cargo build before applying migrations. A clean first build can take several minutes, especially for the larger SaaS profile; the animated status remains visible while Cargo is working. Later migration and server runs reuse that project-local build cache. Preview a starter without writing anything: ```bash cargo rullst new shop --default --blueprint saas --dry-run ``` For example, the release gate can generate a SaaS starter without prompts or network-dependent bootstrap work: ```bash cargo rullst new packaged-saas --default --blueprint saas --skip-initial-migration ``` A deterministic profile can pin the starter, database and optional features: ```bash cargo rullst new operations-portal --default --blueprint saas \ --database mariadb \ --ai --redis --skip-initial-migration ``` Every generated manifest also declares `validator` 0.21 with its `derive` feature. `#[derive(rullst::Validate)]` expands to `::validator::…` paths that only a direct dependency resolves, so `ValidatedForm`/`ValidatedJson` DTOs compile without adding it by hand; Rullst Core already builds that version. Generated SQLx applications disable the default features of both the umbrella `rullst` dependency and the direct `rullst-orm` dependency, and select exactly one strict primary profile (`strict-sqlite`, `strict-postgres`, or `strict-mysql`; MariaDB uses the MySQL protocol). This prevents an implicit SQLite default from masking the chosen backend and keeps the other drivers, including bundled SQLite, out of the build. Turso-primary projects enable `rullst-orm`'s `turso` feature (which includes SQLx's SQLite driver), and database-free projects depend on neither the ORM nor a SQLx driver. #### Generated-project verification boundary The repository does not treat template rendering as sufficient evidence. A structural contract materializes 10 internal starter/profile shapes (five public directly linked layouts plus five legacy DLL layouts retained for regression) and checks paths, Rust syntax and manifests. A slower four-case set crosses every starter, hot and non-hot layouts, database/API boundaries and a SaaS release build, runs every generated test target, and constructs the public router of each hot-reload project using offline-safe defaults. A separate seven-case test invokes the public `cargo rullst new` binary and verifies exact feature selection for SQLite, PostgreSQL, MySQL, MariaDB, AI, Redis, Turso, MongoDB, DuckDB, SurrealDB and Qdrant across the Blank, API and SaaS starters plus a polyglot profile. The invocation starts outside the source checkout, proving that an unpublished pre-release CLI retains its exact matching checkout as a path source instead of requesting unavailable registry packages. The public polyglot profile uses `cargo check` in that CLI-level set because a second bundled-DuckDB test build adds no adapter behavior and can consume several GiB on small machines. DuckDB, MongoDB, SurrealDB, Turso and Qdrant runtime behavior is exercised by their dedicated ORM matrices instead. These gates prove reproducible local generation and bounded offline construction; they do not prove provider accounts, production deployment, browser behavior or application-specific authorization. #### Starters that moved to the examples Since 13.0.0-alpha.4 `cargo rullst new` offers three starters: Blank, API and SaaS. The other blueprints left the CLI and live on as complete applications in the Rullst repository, tested in its workspace: | Former `--blueprint` | Example | What it shows | | :--- | :--- | :--- | | `lms` | [`examples/lms`](https://github.com/Rullst/Rullst/tree/main/examples/lms) | Course catalog, modules and lessons, accessible player, enrollment, owner-bound progress, sign-in and a Nexus admin | | `portfolio` | [`examples/portfolio`](https://github.com/Rullst/Rullst/tree/main/examples/portfolio) | A developer portfolio edited in Nexus | | `erp` | [`examples/erp`](https://github.com/Rullst/Rullst/tree/main/examples/erp) | Products, stock movements and orders behind one administrator policy | | `blog` | [`examples/blog`](https://github.com/Rullst/Rullst/tree/main/examples/blog) | The blog showcase (a larger application than the old starter) | `--blueprint lms`, `blog`, `portfolio` or `erp` still parses but fails before anything is written: the report names the example directory and its GitHub URL and exits with status 1. The examples are applications to copy from, not supported starters: there is no generator or upgrade path behind them. Each README says how to run it and how to switch its workspace dependencies to the published crates. Applications generated earlier from these blueprints keep working: their code is theirs. ### `cargo rullst tour` (v13) A short guided walkthrough of the main commands in seven skippable steps: `new`, `dev`, `dash`, `make:*`, `db:*`, `doctor` and `generate:ai-context`. Each step explains what the command family does, lists commands to try and offers one read-only example that runs only when you pick it: `cargo rullst new tour-demo --default --dry-run` for `new` and `--help` for the others. Next, Previous (or Esc) and *Quit the tour* move between steps. The tour needs no network and never creates or changes a project. ```bash cargo rullst tour # guided, in a terminal cargo rullst tour --list # every step and its example, without prompting ``` Outside an interactive terminal (pipes, `CI`, `TERM=dumb`) `cargo rullst tour` prints the same list as `--list` and exits successfully. ### `cargo rullst upgrade` Plans or applies a transactional application upgrade. The target defaults to the exact installed `cargo-rullst` version; `--to <VERSION>` accepts an exact version in the same major release train as that CLI. The command fails before writing anything when the target is older than a managed requirement's lower bound or than a Rullst package locked in `Cargo.lock`, so an older CLI never downgrades the project; install a CLI that is not older than the project. ```bash # Human-readable plan; no writes or dependency resolution cargo rullst upgrade --dry-run # Versioned machine-readable plan cargo rullst upgrade --dry-run --json # Apply every source rule, not only those introduced after the project's version cargo rullst upgrade --dry-run --all-rules # Backed-up apply + cargo fix + cargo check cargo rullst upgrade # Deliberately inspect a failed partial migration instead of auto-rollback cargo rullst upgrade --keep-on-failure # Recover a persisted snapshot, including after interruption cargo rullst upgrade --restore target/rullst-upgrades/<run-id> ``` The CLI uses Cargo metadata to scope workspace manifests, preserves TOML comments/order, updates normal, inline, workspace, target-specific and renamed Rullst dependencies, and reports unversioned path/git entries. Before applying, it snapshots workspace manifests, the root `Cargo.lock`, and Rust sources under `target/rullst-upgrades/`; a failed Cargo gate restores them by default. The reports use the `rullst.upgrade-plan.v1` schema and include version-selected source findings. The v13 CLI's `rullst-upgrade-rules-v4` catalog (111 rules) parses Rust sources (with `syn`; comments, doc comments and strings never match an API rule), Cargo manifests and generated project files, and reports each finding as `MUST-CHANGE` (a compile-breaking or API-shape change) or `REVIEW` (changed behaviour of an API, feature or configuration in use) with a stable `V13-*` code, `file:line`, a one-line message and the title of its row in the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#changes-from-the-published-1210-source). Findings do not stop the transaction; a failed Cargo gate names the must-change findings and repeats the guidance of `V13-CAPITAL-REMOVED` and `V13-MAIL-REMOVED` (removed APIs reported for review). Each rule records the release that introduced its change. The plan reads the project's Rullst version (the oldest version a Rullst requirement admits or `Cargo.lock` holds); for a project already on a 13.0 prerelease it applies only the rules introduced after that version and prints how many earlier rules it skipped and how many findings they would have added. `--all-rules` applies every rule; a v12 project, or one whose version cannot be read, gets every rule anyway. `update project verify`/`review`/`apply` refuse a preparation with must-change findings; preparations apply every rule. In JSON, each `source_findings` element keeps `path`, `line`, `code`, `severity` (`BLOCKER` or `REVIEW`) and `message` and adds `kind`, `introduced_in`, `migration_row` and `migration_url`; the root adds `finding_counts`, `unscanned_sources`, `migration_guide` and `rule_selection` (`current_version`, `all_rules`, `skipped_rules`, `hidden_findings`). See the [tutorial](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#v12-to-v13-source-findings) and its [rule classification](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#v12--v13-rule-classification). Since 12.1, managed requirements use exact `=VERSION` pins. The final `cargo check --workspace --all-targets --locked` validates the lockfile produced by `cargo fix` without resolving a different version. Broader dependency ranges remain an application decision after reviewing the update. Process-level fixtures upgrade v12 origins, reject retired pre-v12 origins with v12-first guidance, verify restoration across multiple workspace members, retain a deliberately failed edit only with `--keep-on-failure`, and restore that retained snapshot on demand. Symlinked Rust sources are rejected before a transaction begins. This is recovery evidence for the bounded file and Cargo operation; it is not an automatic application, database or deployment migration. The command does not install the CLI globally, rewrite Axum/SQLx/Tokio imports, run database migrations, modify secrets or authorization, validate live providers, or replace the project's test suite. Follow the [assisted upgrade tutorial](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html) and the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html). <a id="cargo-rullst-update-check-1210-working-source-unreleased"></a> ### Update notice and installing a newer CLI The CLI never downloads, verifies or replaces its own binary. Install a newer one with Cargo, using its exact version: ```bash cargo install cargo-rullst --version =VERSION --locked ``` At most once a day, in an interactive terminal, `cargo rullst` tells you when a newer release of its major is on crates.io: a newer stable release, or for a prerelease CLI also a newer prerelease. The notice prints that exact command (`A newer cargo-rullst is available: cargo install cargo-rullst --version =X --locked`) on the home screen, or on standard error after a command finishes. It never delays or fails a command: - the crates.io catalog is fetched in a background thread at most once a day (one bounded HTTPS request, 4 seconds in total, no redirects) and kept in the private user cache (`$XDG_CACHE_HOME/rullst-update-v1`, `$HOME/.cache/rullst-update-v1` or `%LOCALAPPDATA%\rullst-update-v1`; unsafe owners, permissions or links make it unusable); - the notice itself reads only that cache, and the day of the last notice and of the last request are small stamps in `rullst-ui-v1/` in the same cache base; - `RULLST_DISABLE_UPDATE_CHECK=1`, `CARGO_NET_OFFLINE=true`, `CI`, and any standard stream that is not a terminal silence it completely: no request, no file and no output. **Removed in 13.0.0-alpha.4:** the self-updater (`update check`, `update verify`, `update stage` and `update install review|apply|recover`) and the CLI half of `update guided` (`--scope`, `--root`, `--allow-major`, `--prerelease`). The native CLI executables and their manifests stay attached to each GitHub release and can be checked by hand as described in [Verifying a release](https://github.com/Rullst/Rullst/blob/main/SECURITY.md#verifying-a-release). <a id="cargo-rullst-update-guided-1210-working-source-unreleased"></a> ### `cargo rullst update guided` (since 12.1.0) ```bash cargo rullst update guided --to VERSION --project ./my-app cargo rullst update guided --to VERSION --project ./my-app --offline ``` This interactive entry point composes the isolated project commands described below: prepare, verify, review and apply. `--to` must be in this CLI's major, because the running CLI carries only its own major's migration rules; to move to another major, install that CLI first and run its guided flow. Each complete review appears before its own default-no confirmation. The flow reuses the exact version, directories and review digest without shell commands or manual copying. It reports elapsed milliseconds per executed stage, excluding time spent answering prompts. Declining stops before the next operation; completed steps and their recovery records remain available. Project verification asks whether Cargo may use the network and requires explicit consent to execute trusted build scripts, macros and tests. `--offline` keeps it offline. Feature selection and command deadlines use the same `--all-features`, `--features`, `--no-default-features` and `--timeout-seconds` options as project verification. PATH, databases and deployments are not changed. Piped input/output rejects before I/O; use the explicit commands with JSON and review digests for automation. This flow does not bypass ownership or recovery checks. <a id="cargo-rullst-update-project-prepare-1210-working-source-unreleased"></a> ### `cargo rullst update project prepare` (since 12.1.0) Prepare dependency edits in a private source copy for review: ```bash cargo rullst update project prepare --project ./my-app --to VERSION --json ``` The project must be a Git working directory containing `Cargo.toml`. The default target is this CLI's exact version, within its major train. Tracked and non-ignored untracked files are copied with their current contents, including uncommitted edits and tracked deletions. The root `Cargo.lock` is retained even when ignored by an older generator. Other ignored files are omitted; tracked secrets are still tracked inputs. The original files and Git index are not edited. Limits are 100,000 entries, 64 MiB/file and 512 MiB/source snapshot; the two copies can consume about 1 GiB before any build. The command rejects linked/special inputs, unsupported paths, unknown migration origins, ambiguous/unversioned managed dependencies and version/lockfile downgrades. It accepts source majors 12 and 13 and records the `rullst-upgrade-rules-v4` source findings in the plan; verification, review and application refuse a preparation with must-change findings (fix them in the original project and prepare again), while review findings stay in the plan. Older applications first upgrade to v12 with the v12 CLI. The exact-version editor preserves TOML comments. Offline, locked, dependency-free Cargo metadata enumerates workspace members inside the copy. Rustup auto-installation is disabled using its [documented environment setting](https://rust-lang.github.io/rustup/environment-variables.html); Git and Cargo must already be installed on absolute trusted PATH entries. Every Cargo/rustc invocation in a project copy (preparation, verification, review and apply) runs with `RUSTUP_TOOLCHAIN` pinned to the caller's toolchain: an inherited `RUSTUP_TOOLCHAIN` (rustup sets it for `cargo rullst`), otherwise rustup's configured default. A `rust-toolchain`/`rust-toolchain.toml` in the project, including a `path` toolchain, is therefore not honored. Run the CLI from outside an untrusted project: invoking `cargo rullst` inside it lets rustup select that project's toolchain for the CLI process itself. The result points to `before/`, `candidate/` and `preparation.json` inside the private update cache. JSON uses `rullst.project-preparation-result.v1`, with a `rullst.project-preparation.v1` record of original file hashes and absences. Compare the two trees and inspect the plan. Failed preparation removes its own new staging directory. Successful preparations remain for review and can be deleted by the caller after use. Preparation executes no builds, procedural macros or tests, does not resolve a new candidate lockfile, and authorizes neither execution nor application. The copy is not a sandbox. Candidate verification, review, application and recovery are described below. The existing `upgrade` command remains separate. <a id="cargo-rullst-update-project-verify-1210-working-source-unreleased"></a> ### `cargo rullst update project verify` (since 12.1.0) Review the commands and then explicitly authorize trusted project execution: ```bash cargo rullst update project verify --prepared PATH --dry-run --all-features cargo rullst update project verify --prepared PATH --allow-project-code --all-features --json ``` `PATH` is the private directory returned by preparation. The command validates its records, before/candidate copies and current original files under an exclusive operation lock. Unknown/stale inputs and unresolved migration findings fail before project code runs. Fix findings in the original and prepare again. Builds and tests use another fresh private copy, preserving the reviewed copy. The sequence probes `rustc --version --verbose` and `cargo --version`, resolves `Cargo.lock` with `cargo update --workspace`, checks all workspace targets, and runs workspace tests with the resolved lockfile. That resolution keeps every existing lock entry the edited manifests still accept and resolves only what the dependency edits require; it does not upgrade unrelated pins (a missing lockfile is resolved in full). Every managed Rullst package must resolve to the exact target. Default features apply unless `--all-features`, `--features names` or `--no-default-features` selects another policy. This verifies that policy only; application-specific service/browser/deployment tests remain separate. Cargo is offline by default. `--allow-network` permits dependency retrieval but cannot override `CARGO_NET_OFFLINE=true`. Rustup does not install missing toolchains, and the project's toolchain file is ignored as described above; set `RUSTUP_TOOLCHAIN` explicitly to verify with the project's pinned channel. `--timeout-seconds` bounds each command (default 900, range 1–3,600); stdout/stderr are each capped at 8 MiB. Private logs retain failed Cargo output; failure, cancellation or timeout never records acceptance. Builds/tests inherit the caller's environment and can affect external files, databases or services: this copy and process cleanup are not a sandbox. Use trusted projects only. Success emits `rullst.project-verification.v1` and a private `verification.json` with commands, log hashes and final file digests. Its verified candidate has the resolved lockfile; original files are not edited. Source or baseline changes during verification and unexpected candidate writes are rejected. Review the complete diff and logs; compiler overrides/wrappers require separate review. The report is not a reusable apply token and grants no application/deployment authority. Final native application/recovery acceptance remains open. Retained source copies can consume up to about 2 GiB, plus build outputs/logs. ### `cargo rullst update project review` (since 12.1.0) ```bash cargo rullst update project review --verified PATH --json ``` Use `verified_directory` from the verification result. Review validates the stored command policy, successful statuses, bounded logs, original/prepared files and verified candidate again under operation locks. Changed evidence is rejected. Git produces the full dependency diff with external helpers, text conversion and paging disabled; output is bounded to 8 MiB. No build/test runs and original files remain untouched. JSON (`rullst.project-review-report.v1`) contains the `review` record (`rullst.project-review.v1`) with before/after file hashes, the full `diff` and `review_sha256` binding its evidence and contents. The digest is not authorization to apply changes; review does not invoke the legacy in-place upgrade command. The digest also binds the source access policies. ### `cargo rullst update project apply|recover` (since 12.1.0) ```bash cargo rullst update project apply --verified PATH --approved-review SHA256 --json cargo rullst update project recover --verified PATH --approved-review SHA256 --json ``` After reviewing the complete diff and logs, supply that exact `review_sha256`. Stop editors/builds and other writers first. The CLI revalidates all evidence, locks the canonical source within the configured private cache, stages every replacement and persists an intent before changing any original. Only reviewed workspace manifests and the root lockfile can change. Original hardlink aliases are not truncated, and a newly created lockfile cannot overwrite a competing file. Recovery checks every target before restoring any file. A changed file must match this operation's before or after state and access policy; later conflicting edits are refused. Unrelated files remain untouched. A root lockfile originally absent is removed only if it still matches the recorded created file. Repeating recovery is supported. Keep both private source copies and `application.json` until finished. Replacement is atomic per file, not for the whole workspace. Partial failures retain recovery evidence and report progress. Unix mode/owner/group and Windows owner/group/DACL/integrity label are bound to the review (1 MiB total policy budget). Unix extended ACLs/xattrs and special mode bits (except the SELinux `security.selinux` label, which the replacement staged in the same directory receives like any new file there), Windows read-only/special attributes, alternate streams, resource/central-access policies and policies that cannot be recreated exactly require manual handling. Other updater cache configurations and filesystem aliases do not share the lock. Forced termination during staging may leave sibling `.rullst-update-stage-*` files. Timestamp and Windows audit-policy preservation are not implemented; files that require them need manual handling. Native and process-interruption/power-loss acceptance remains pending. Recovery does not undo application-code effects, databases or deployments. ### `cargo rullst pkg <action> [name]` Adds or lists `rullst-*`/`rullst_*` crate dependencies in the local `Cargo.toml`. It executes no package code, queries no registry and changes no application routing; Cargo resolves the crate on the next build. * **Subcommands:** * `add <package_name>`: Adds the dependency (e.g., `cargo rullst pkg add rullst-auth`) to `Cargo.toml` with this CLI's version as its requirement; an existing entry is left unchanged. In a virtual workspace manifest it adds the entry to `[workspace.dependencies]`, for members to use with `{ workspace = true }`. * `list`: Scans and lists all active `rullst-*` community extensions installed in your project (the workspace dependencies of a virtual workspace manifest). An unknown action, or `add` without a package name, fails with a non-zero exit status. ### `cargo rullst add` `cargo rullst add <capability> [--dry-run]` (v13) enables a facade capability in an existing project. Capabilities: `mail`, `auth`, `ai`, `nexus` and `studio`. It runs only at the root of a Rullst project (a `Cargo.toml` whose `[dependencies]` declare `rullst`), asks nothing and: 1. enables the capability's `rullst` feature in `Cargo.toml`. The edit keeps comments, formatting and the other features; a feature that already implies it (for example `mailer` or `auth-sqlite`) counts as enabled; 2. appends the capability's variables to `.env.example` under a `# ── <Capability> (added by cargo rullst add …) ──` heading, creating the file when it is missing. Values are placeholders, empty or `mock_*` (the offline mocks of AGENTS.md 3.5), never real secrets; a variable that is already assigned or commented out is not repeated. `.env` is changed only when it lacks a variable the application needs to start in development, and then receives the same mock value and is reported; none of the current capabilities needs one, so `.env` stays as it is; 3. prints the wiring code to paste and where it goes. Generated projects have no marked insertion points, so `add` never edits application source; 4. prints one to three next steps. | Capability | `rullst` feature | `.env.example` | Next steps | | :--- | :--- | :--- | :--- | | `mail` | `mail` | `MAIL_FROM`, `# MAIL_DRIVER`, `# RESEND_API_KEY` | `make:mail Welcome`, `dev` | | `auth` | `auth` | `APP_KEY` placeholder | `auth`, `make:mfa` | | `ai` | `ai` | `OPENAI_API_KEY=mock_openai_key`, commented Anthropic, Gemini and Ollama entries | `make:chat-session`, `generate:ai-context` | | `nexus` | `nexus` | empty `NEXUS_ADMIN_USERNAME` and `NEXUS_ADMIN_PASSWORD` (required by release builds unless Nexus uses the application's sign-in, `NexusAuthPolicy::application`) | `dev`, then `/nexus` | | `studio` | `studio` | none | `dev`, then `http://127.0.0.1:5555` | Running it again changes nothing and reports that the capability is already enabled. `--dry-run` prints the planned diff of every file and writes nothing. Outside a project it fails with the shared error report (exit status 1); an unknown capability is a usage error (exit status 2). ```bash cargo rullst add mail --dry-run cargo rullst add mail ``` --- ## 🛠️ 2. Architecture Scaffolding (`make:*`) Rullst generators write the files described under each command. Some commands also register modules and refresh `.llms.txt`; this is command-specific, and a failed best-effort context refresh does not roll back generated source. `make:controller`, `make:model`, `make:middleware`, `make:worker`, `make:island`, `auth`, `make:billing`, `make:cors` and `make:jwt` also refresh the `generate:diagram` output `diagram.md`: a missing file is created, while a `diagram.md` the generator did not write is kept and reported instead of replaced. Review the diff and run `cargo check` after scaffolding. ### `cargo rullst make:resource <name>` Scaffolds the bounded starting files for a CRUD resource in one command: a Model (`src/models/<name>.rs`), Migration (`src/migrations/m<timestamp>_create_<plural>.rs`, for example `create_categories` for `Category`), Controller (`src/controllers/<name>_controller.rs`), and HTML view placeholders (`views/<name>/index.html` and `views/<name>/form.html`). It does not infer application fields, register routes, establish ownership/RBAC, or turn the placeholder handlers into a complete authorized CRUD implementation. Like `make:model --migration`, it keeps an existing model and does not add a second create-table migration for it. Mount the routes behind the canonical security baseline, render request-scoped CSRF tokens in state-changing forms, complete validation/persistence, and run the application's authorization-negative tests. * **Arguments:** `<name>` (e.g., `Product` or `product`). * **Optional Flags:** * `--api`: Scaffolds a headless JSON API resource controller instead of HTML views. ### `cargo rullst make:controller <name>` Generates `src/controllers/<name>_controller.rs` (`posts` and `PostsController` both produce `posts_controller.rs`) with placeholder CRUD handlers (`index`, `show`, `store`, `update`, `delete`). It registers the module in `src/controllers/mod.rs` and adds `pub mod controllers;` to `src/main.rs` when that file exists; it does not add application routes automatically. * **Arguments:** `<name>` (e.g., `UsersController` or `users`). A name whose module or type would not be a non-keyword Rust identifier (for example `Bad.Name`) is rejected before any file is edited. * **Optional Flags:** * `--api`: Instead of returning HTML Views via the `html!` macro, the generated methods will automatically extract/return `Json<T>`. The command prints the `routes!` lines (`/api/<name>` and `/api/<name>/{id}`) and the `MachineEndpoint::bearer` lines for `machine_endpoints()` in `src/main.rs`: in staging and production JSON clients reach the write routes only as machine endpoints, or the browser CSRF check refuses them with 403 (see [which security layer to use](https://rullst.github.io/Rullst/book/security-layers.html#what-generated-applications-already-have)). ### `cargo rullst make:model <name>` Creates a model struct in `src/models/` with the ORM annotations. SQLx projects receive `FromRow` plus `Orm` and import only those two derives (`use rullst::db::{FromRow, Orm};`), so the new file compiles without warnings; Turso-primary projects receive `#[derive(rullst_orm::Orm)] #[orm(backend = "turso")]` and an `i64` primary key. Backend detection reads the generated manifest and does not treat an additive `--turso` integration as the primary ORM. Like `make:resource`, it rejects a name whose module or type would not be a non-keyword Rust identifier (for example `Match`, which would declare `pub mod match;`) before writing. * **Arguments:** `<name>` (e.g., `BlogPost`). * **Optional Flags:** * `--migration` or `-m`: Simultaneously generates a reversible migration with the correctly pluralized table name. An existing model file is kept, and the migration is skipped when the model already existed or a `*_create_<table>.rs`/`*_create_<table>_table.rs` migration exists, since a second create migration would drop the live table on rollback. ### `cargo rullst make:chat-session` Adds application-owned conversational memory for the project's primary ORM. It generates and registers `ChatSession` and `ChatMessage`, a reversible migration, and `StatefulChat`. SQLx and the bounded Turso-primary profile receive backend-specific code; the command also enables the `orm` and `ai` umbrella features if necessary. ```bash cargo rullst make:chat-session cargo rullst db:migrate ``` Save the generated `ChatSession` before constructing `StatefulChat`. Each service instance serializes concurrent sends, restores at most the newest 100 messages in chronological order, persists the user message before provider dispatch and persists the assistant response only after success. Database and provider failures are returned as `StatefulChatError`; they are never silently discarded. Multi-process ordering, tenant authorization, retention and deletion remain application responsibilities. The command refuses to overwrite an existing chat scaffold. ### `cargo rullst make:middleware <name>` Generates a standard Axum/Rullst Middleware struct in `src/middlewares/`. Perfect for injecting headers, checking authentication, rate limiting, or logging. A name whose module and function would not be a non-keyword Rust identifier is rejected before any file is edited. ### `cargo rullst make:island <name>` Creates a frontend interactive "Islands Architecture" component (similar to Fresh or Astro) in `src/islands/`. It generates the Rust infrastructure that, during build, will be transparently compiled to WebAssembly to run in the browser. ### `cargo rullst make:worker <name>` Creates an asynchronous background worker in `src/workers/` against the queue backends currently implemented in Core (memory, SQLite, and optional Redis). RabbitMQ is not generated by this command. The generated handler logs only the job name and its number of payload fields, never the payload itself, because jobs often carry addresses, tokens or other personal data. ### `cargo rullst make:migration <name>` Generates a timestamped reversible Rust migration for the project's primary backend. SQLx projects use the schema DSL; Turso-primary projects use `TursoMigration` and parameterized `TursoStatement` values, and regenerate a fallible typed migration registry. The name is lowercased with `-` mapped to `_` and otherwise kept intact (`modify_users_email` produces `m<timestamp>_modify_users_email`); names with other characters, such as `add_index.v2`, are rejected. Regenerating `src/migrations/mod.rs` fails, naming the file, when an `m*.rs` file there is not a valid Rust module name. ### `cargo rullst make:migration:auto` Takes no arguments and needs a SQLite `DATABASE_URL` in `.env`; with another database, a Turso-primary project or no `.env`, it prints a notice and writes nothing. It compares the `#[derive(Orm)]` models under `src/` with the database. It ignores framework tables (`migrations` and `rullst_*`) and writes a migration only for additive changes (new tables or columns), with drops of model-less tables or columns included as commented-out code for review. When only such destructive differences remain, it lists them and writes no migration. Column types follow the field's Rust type: `i8`/`i16`/`i32` use `integer`, `i64` `big_integer`, `f32`/`f64` `float`, `bool` `boolean`, and `String`, `SecretString`, chrono date/time types and `Json` use `string`. A new table declares non-`Option` fields `NOT NULL`. A required field added to an existing table is declared `NOT NULL DEFAULT 0`, `0.0` or `''` (a commented backfill to review); a required date, encrypted or JSON field added to an existing table has no neutral value and is refused. Any other type (for example `Vec<u8>`, `Uuid` or an application enum) is refused, naming the field, and nothing is written; add such columns with `make:migration`. ### `cargo rullst make:billing` Scaffolds a SaaS billing starting point with subscription models, authenticated billing routes, and signed-webhook integration points. Provider credentials, tenant policy, and deployment behavior still require application configuration. * **Optional Flags:** * `--model <MODEL>` (default `User`): The billable model; the subscription and customer tables reference it through a `<model>_id` column (`Team` gives `team_id`). Empty or `mock_*` credentials select a local development fixture, which is refused in production. Other credentials select the real Stripe profile (`BILLING_PROVIDER=stripe`, the only accepted value), which creates provider customers, checkout sessions and portal sessions, persists the owner/customer/attempt bindings and processes signed webhooks atomically. It requires `BILLING_ACCOUNT_ID=acct_...`, an `sk_test_`/`rk_test_` or `sk_live_`/`rk_live_` `BILLING_API_KEY`, a strong `BILLING_WEBHOOK_SECRET` and an HTTPS `BILLING_REDIRECT_URL`; live keys additionally require `BILLING_LIVE_ACKNOWLEDGEMENT=I_UNDERSTAND_REAL_CHARGES`. To bill through another gateway, implement the Capital provider traits in application code; see [Writing your own payment provider](https://rullst.github.io/Rullst/book/capital-custom-provider.html). Mixed mock/real credentials, incomplete configuration and any other `BILLING_PROVIDER` value return HTTP 503. The generated `BILLING.md` lists the permissions, webhook events, recovery procedures and remaining limits. Hosted checkout also requires the submitting page's CSP to allow its exact reviewed destination in `form-action`. The SaaS starter selects Stripe and generates `form-action 'self' https://checkout.stripe.com` while retaining the rest of Core's strict policy. `make:billing` prints this requirement and leaves your existing policy for review. A custom provider requires its exact checkout origin; do not allow `https:` or wildcard domains. Keep a single `form-action` directive in `security.csp` and review proxy/CDN policies too: another restrictive CSP still applies. Independently validate each returned URL (HTTPS, exact host/port, no embedded credentials) and the session's owner, product and test/live mode before a 303. Test the form submission in a real browser: a successful HTTP redirect alone does not prove that CSP permits navigation. Before enabling live billing, resolve an owner's persisted open attempt before charging the new-session quota; resume only a retrieved, fully bound open session. Expired, completed and uncertain outcomes require separate handling and reconciliation. ### `cargo rullst make:mail <Name>` Scaffolds a registered transactional mailable. `--welcome`, `--reset`, `--otp` and `--invoice` select the bounded built-in variants (at most one); without a flag the command generates a custom message type. It enables the umbrella `mailer` feature, uses the `rullst::mail` facade, escapes dynamic HTML and refuses invalid identifiers, path traversal or an existing target. Generated mailables set no `from`, so the facade uses the `MAIL_FROM` (or `[mail] from`) default sender, which new projects list in `.env.example` next to a `MAIL_DRIVER` hint; staging and production must select a driver before sending. Delivery credentials, URL semantics, tenant policy and provider operation remain application responsibilities. ### `cargo rullst make:mail-invoice [Name]` Generates `PaymentReceiptEmail` by default and enables `mailer`. The result is a bounded commercial payment receipt (`international_receipt`, optional `with_document_url`) that states it is not a tax authorization; it runs the mail pre-flight before returning the message. A custom valid struct name may be supplied positionally. ### `cargo rullst make:mail-dunning [Name]` Generates `PaymentDunningEmail` by default with explicit gentle D+1, action-required D+3, and service-status D+7 stages. The application remains responsible for calculating the due state, scheduling delivery, enforcing its disclosed billing policy, and reconciling payment. The generated build path runs the mandatory pre-flight and rejects dangerous links. <a id="cargo-rullst-makeage-gate-unpublished-v13-preview"></a> ### `cargo rullst make:age-gate` (v13 prerelease preview) Adds an explicit first-party declaration before each visit to the recognized SaaS starter's `/dashboard`. Generation requires a server-owned policy version, threshold and single-tenant deployment reference; it never chooses a universal legal minimum age. The existing authentication and CSRF layers protect both GET and POST. A successful answer remains declared, and authorizes only that dashboard rendering after durable one-use consumption. ```bash cargo rullst make:age-gate \ --privacy-source /path/to/Rullst/rullst-privacy \ --minimum-age 18 \ --policy-version dashboard-v1 \ --tenant-ref application-tenant-ref \ --replay-store sqlite ``` The threshold above is an example for an application-assessed low-assurance policy. Select `postgres` explicitly for a shared database across hosts. Configure the required private key and replay database as described by the generated `AGE_GATE.md`; missing state or configuration denies access. The candidate defaults to the registry version matching this CLI. The source override shown above is required for development before that version is published, unless an explicit archive-only patch is configured. Generation refuses unknown privacy dependencies or unrecognized authentication/route shapes before writing. It composes with `make:privacy` when both use the same registry/local source; both consumers' explicit dependency features are preserved. Review the generated diff before deployment. It does not install facial models, verified guardianship, reusable age flags or global compliance. It targets the recognized SaaS starter (`--blueprint saas`, the default) and requires `--tenant-ref`. v13 removed the LMS target together with the complete Academy scaffold. The tenant is compiled into the generated configuration; no query parameter, header or form field is read to select it. <a id="cargo-rullst-makeprivacy-unpublished-v13-preview"></a> ### `cargo rullst make:privacy` (v13 prerelease preview) Adds authenticated preferences at `/privacy`, an optional personalized greeting at `/privacy/personalization`, and a private own-account JSON download at `/privacy/export`. The concrete export projects only the authenticated account's ID, name and email; it does not complete broader queued privacy requests or export subscription, learning, guardian, backup or processor records. ```bash cargo rullst make:privacy \ --privacy-source /path/to/Rullst/rullst-privacy \ --purpose-version greeting-v1 \ --validity-seconds 86400 \ --tenant-ref application-tenant-ref ``` The version and lifetime are explicit application choices; the engineering cap of 365 days is not a legal retention rule. It targets the recognized SaaS starter and requires `--tenant-ref`; v13 removed the LMS target. The tenant is compiled into the generated configuration. The privacy routes take no query string: a request carrying one, or an `x-school-id` header, is refused rather than allowed to select another tenant or account. The generated consumer composes with `make:age-gate` in either installation order. Omitting `--privacy-source` selects this CLI's matching registry version; before publication use the explicit local override shown above or a reviewed archive patch. Unknown authentication, dependencies, routes or existing output files require manual integration rather than overwriting application code. Follow generated `PRIVACY.md`: provision an independent random `RULLST_PRIVACY_FORM_KEY_HEX` and initialize a new private local consent file with `cargo run --bin privacy-init`, with `RULLST_PRIVACY_DATABASE` exported into that process environment. Ordinary opening never creates or repairs missing state. The generated 10,000-record SQLite store requires one trusted shared local file; it does not provide multi-host replication. Preferences and personalization deny unavailable state, while the authenticated export remains independent of that store and form key. Choices are initially unselected. Refusal and withdrawal produce a generic greeting; an earlier positive form cannot undo a completed withdrawal. Forms bind the displayed notice, revision, account, tenant and session and expire after five minutes. The server checks current permission before the optional name query. Add visible application navigation and review the documented backup, retention and broader rights obligations before deployment. This bounded consumer does not establish worldwide legal compliance or verify age. ### `cargo rullst make:jwt` Injects a pre-configured boilerplate Middleware into your project for strict JWT Authentication (verifying Bearer tokens in the `Authorization` header). ### `cargo rullst make:cors` Generates and configures full CORS (Cross-Origin Resource Sharing) options in your project with recommended security defaults (blocking unused methods, restricting origins). Projects generated by older CLI versions retain the middleware that was copied into their source tree and must be reviewed manually. Follow the [CORS scaffold security advisory](https://rullst.github.io/Rullst/book/cors-scaffold-security-advisory.html) to detect origin reflection/wildcards and migrate to the current fail-closed allowlist. ### `cargo rullst make:pwa` (13.0.0-alpha.3) Makes an HTML application an installable Progressive Web App and wires it into the starter's own router and page layouts: ```bash cargo rullst make:pwa cargo rullst make:pwa --name "Field Notes" --short-name Notes \ --theme-color '#0f172a' --background-color '#020617' --lang pt-BR --start-url / ``` | Flag | Default | Meaning | | :--- | :--- | :--- | | `--name` | `[package].name` in words (`field-notes` → `Field Notes`) | 1-64 letters, digits, spaces, `.`, `-`, `_` | | `--short-name` | the name when it has at most 12 characters, else its first word cut to 12 | label under the home-screen icon | | `--theme-color`, `--background-color` | `#0f172a`, `#020617` | `#RRGGBB` | | `--lang` | `en` | language tag such as `en` or `pt-BR` | | `--start-url` | `/` | a path on this site that the installed app opens | | `--android` | off | also write `android/twa-manifest.json` (Bubblewrap) and `PWA-ANDROID.md`; needs `--host` and `--package-id` | | `--host` | none | with `--android`: the site's HTTPS host, such as `notes.acme.dev` | | `--package-id` | none | with `--android`: the Android application ID you own (`com.example.*` is refused) | It writes `static/manifest.webmanifest` (`id` and `scope` `/`, `display` `standalone`, icons 192 and 512 plus a maskable 512), four placeholder PNG icons in `static/icons/` (the Rullst logo; replace them), `static/pwa.js`, `src/pwa.rs`, the worker in `src/pwa/sw.js` and `PWA.md`. `src/pwa.rs` serves `/sw.js` (`text/javascript`, `Cache-Control: no-cache`, scope `/`) and `/offline` (`no-cache`, so the worker may store it), and its `head()` returns the manifest link, `theme-color`, the apple-touch icon and the registration script. The existing source is edited in three recognized places: - the router: `.merge_axum(crate::pwa::router().into_axum())` follows the one `.merge_axum(rullst::health::health_router())` of the crate root (a merge in a `return` expression, such as the fallback router of an application generated from the former ERP starter, does not count); - every page layout in `src/`: `{crate::pwa::head()}` before the `</head>` of an `html!` document, the same tags before the `</head>` of a document written in a string, and `render_page_with_lang(...)` calls become `render_page_with_head(..., crate::pwa::head(), content)`; - the crate root (`src/lib.rs`, else `src/main.rs`) declares `mod pwa`. The registration script is a same-origin file, so the default Content-Security-Policy (`script-src 'self' 'nonce-…'`, `worker-src 'self'`, the manifest under `default-src 'self'`) allows it without a nonce. The worker precaches `/offline` and the PWA's static files, goes to the network first for navigations and unversioned static files, serves content-hashed static files from the cache first, and never stores a non-GET request, another origin or a response marked `no-store` or `private`; Rullst answers `no-store` by default, so authenticated pages stay off the device. A new worker waits until the user chooses **Reload** in the page's update prompt (`SKIP_WAITING`). The cache is named after `[package].version`. The generated router also serves `/.well-known/assetlinks.json` (Digital Asset Links for the Google Play package) once `RULLST_ANDROID_ASSET_LINKS` or `[[android.asset_links]]` in `Rullst.toml` names the app and its signing certificate fingerprints; see `rullst::pwa::AssetLinks`. The whole plan is checked before any write. Running the command again changes nothing; a generated file that now differs (edited, or another `--name`) stops the run without writing. `--android` can follow a plain run with the same flags and adds only the Android files. The JSON API starter, a project without an HTML layout, and a router without the health merge are refused; wire such a project by hand as the [PWA guide](https://rullst.github.io/Rullst/book/tutorials/43-progressive-web-apps.html) shows. ### `cargo rullst make:iot <DeviceName>` **Maturity:** Experimental · [tiers](https://rullst.github.io/Rullst/book/maturity.html#cli-generators) Scaffolds and registers a telemetry-only IoT module in `src/iot/` using the public `rullst::iot::SensorTelemetry` facade, and enables the `iot` feature in the application manifest. Unsafe identifiers/path traversal and existing target files are rejected. It does not install an MQTT/CoAP transport, HAL, firmware, or claim broker connectivity. ### `cargo rullst make:k8s` Scaffolds cloud-native Kubernetes manifest files in the `k8s/` directory (`deployment.yaml`, `service.yaml`, `configmap.yaml`, `hpa.yaml`, `ingress.yaml`, and `all-in-one.yaml`) pre-configured with liveness (`/health`) and readiness (`/ready`) HTTP probes. The command fails before writing anything when any of these manifests already exists, and it does not write through a symlinked `k8s/` directory or file; move customized manifests aside to regenerate the templates. Object names, the image reference and the ingress host use the `[package]` name as a lowercase RFC 1123 label: characters other than letters and digits become `-` (`my_app` becomes `my-app`), and the label is capped at 55 characters so suffixed names such as `<name>-service` stay valid. ### `cargo rullst make:scalar` Scaffolds a Scalar API Documentation controller at `src/controllers/docs_controller.rs`. The interactive view loads a pinned CDN asset; its local fallback is status-only and final CSP/network policy belongs to the application. ### `cargo rullst make:live <ComponentName>` Scaffolds a LiveView-style server component at `src/live/<name>.rs` using a WebSocket and HTMX out-of-band swaps. Application JavaScript may be unnecessary, but HTMX remains client-side JavaScript and the generated transport requires origin, reconnect, and backpressure review. It must run in a Rullst project root, and a name whose module or type would not be a non-keyword Rust identifier (for example `../notes`, `self` or `Bad.Name`) is rejected before anything is written. ### `cargo rullst deploy` Writes the files that run the application on any Linux VM with Docker or Podman, each only when it is absent (existing files are kept, and nothing is written through a link): * `Dockerfile` and `.dockerignore`: the same multi-stage build as `dockerize`, with its dependency-cache (planner) stage and a distroless, non-root runtime image. When the `[database] url` in `Rullst.toml` (or, without one, `DATABASE_URL` in `.env.example`) is SQLite, the image sets `DATABASE_URL=sqlite:///app/data/rullst.db?mode=rwc`, so the database lives in the `app_data` volume; an existing Dockerfile without it gets a warning; * `compose.yaml`: the app and a Caddy reverse proxy with automatic HTTPS for `DOMAIN`. Both containers have a read-only root filesystem, every capability dropped (Caddy keeps only `NET_BIND_SERVICE` for ports 80 and 443) and `no-new-privileges`; the app reads `.env.production` and keeps its data in the `app_data` volume; the app's `/health` endpoint is checked from the Caddy container, because the app image has no shell or HTTP client. It works with `docker compose` and `podman compose`; * `Caddyfile`: proxies to the app, checks its `/health` every 5 seconds and stops routing to it while that fails, and holds each request for up to 15 seconds while the app is down (a restart or an update) instead of answering `503` at once. Caddy is pinned to `172.31.250.10` on a `172.31.250.0/24` compose network, and the command adds that address to `[security] trusted_proxies` in `Rullst.toml` (before a missing `Dockerfile` is scaffolded, so the image copies it), so rate limits see each client instead of Caddy. An existing `trusted_proxies` list is left unchanged with a warning. The command prints the next steps: create `.env.production`, point DNS at the VM, run `DOMAIN=example.com docker compose up -d --build` (or `podman compose up -d --build`) and run `db:migrate` once per release. Nothing is uploaded, started or deployed. A `Cargo.toml` package name that is not a valid Cargo name is refused before anything is written, because the Dockerfile names the binary after it. Step by step: [Deploy to a Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html), and the guides for [Hetzner Cloud](https://rullst.github.io/Rullst/book/deploy-hetzner.html), [DigitalOcean](https://rullst.github.io/Rullst/book/deploy-digitalocean.html), [Oracle Cloud](https://rullst.github.io/Rullst/book/deploy-oracle-cloud.html) and [Google Cloud](https://rullst.github.io/Rullst/book/deploy-google-cloud.html). **Removed in 13.0.0-alpha.4:** the Fly.io, Railway and Render targets (`--platform fly|railway|render` now fails before writing anything; `--platform vps`, the former name of this path, is still accepted). Provider files you already have keep working with the provider's own CLI. Nix, Buildah, Podman Quadlet and Kubernetes are described in [Advanced deployment](https://rullst.github.io/Rullst/book/advanced-deployment.html). Credentials, migrations, availability, DNS and rollback remain operator responsibilities. ### `cargo rullst auth` Creates an authentication starting point in your codebase, including: - User model and migration with asynchronous Argon2 password hashing and a `session_epoch` column. - Auth Controllers (Login, Registration, Logout, and `logout_everywhere`, which increments the account's session epoch to end its session on every device). - Encrypted-session middleware that inserts the signed-in user's id as `Extension<i32>` and refuses a cookie whose session epoch is no longer the account's. - A password controller (`src/controllers/password_controller.rs`), a reset token model (`src/models/password_reset.rs`) and a `password_reset_tokens` migration: **change password** (signed in; checks the current password, keeps only the asking browser signed in) and **reset by email** (a 30-minute single-use token stored as a SHA-256 digest, one live token per account, the same answer for unknown emails; a reset signs out every device and redirects to `/login?reset=done`). Reset links use `RULLST_PUBLIC_ORIGIN`, never the request's `Host` header. Account emails are written in the request's language (the `rullst::i18n` locale, else `Accept-Language`) and name the product `APP_NAME` or the editable `APPLICATION_NAME` constant. - HTML Views for Login, Signup, Forgot password, Reset password and Change password. It targets the SQLx ORM: Turso-primary projects are rejected. The command enables the `orm`, `auth` and `mail` umbrella features, registers the generated `controllers`, `middlewares`, `models` and `pages` modules in `src/lib.rs` (or `src/main.rs`), and refreshes the migration registry. It fails before writing anything when `src/models/user.rs`, `src/models/password_reset.rs`, `src/controllers/auth_controller.rs`, `src/controllers/password_controller.rs`, `src/middlewares/auth_middleware.rs` or `src/pages/auth.rs` already exists, when a `*_create_password_reset_tokens_table.rs` migration exists, or when a `*_create_users.rs`/`*_create_users_table.rs` migration already creates the users table (the blank database starter and the SaaS starter ship one). It also refuses a project without a SQL migration runner (`pub mod migrations;` in `src/main.rs` or `src/lib.rs` plus a `rullst::artisan!(crate::migrations::get_migrations())` call, which database starters include), such as a `--no-database` starter, because nothing would compile or apply the users migration. Generated code names the ORM through the `rullst::orm` facade, so no direct `rullst-orm` dependency is needed. Mounting routes (including `POST /logout/everywhere` and `/password/change` behind `auth_middleware`, and `credential_rate_limit` on the password submissions), the security baseline and the mail settings (`MAIL_DRIVER`, `MAIL_FROM`, `RULLST_PUBLIC_ORIGIN`) remain application work, as does calling `User::revoke_sessions` when you deactivate an account. The SaaS starter and the LMS example (`examples/lms`) ship the same password flows already mounted. ### `cargo rullst make:mfa` Scaffolds a server-side RFC 6238 TOTP second factor: `src/controllers/mfa.rs` and a reversible `user_mfa_factors` migration (one factor per account). The secret is generated and stored on the server and bound to the signed-in account: `mfa_setup`, `mfa_confirm` and `mfa_verify` take the user id from the `Extension<i32>` that the `cargo rullst auth` middleware inserts, and a client never submits a secret. Verification uses `verify_totp_step_after` with an atomic conditional update of `last_accepted_step`, so each code is accepted at most once. Setup returns the secret and `otpauth://` URI once with `Cache-Control: no-store`; enrollment stays pending until `mfa_confirm` accepts a current code. The command targets the SQLx ORM (Turso-primary projects and projects without a migration runner are rejected, as for `cargo rullst auth`), enables the `orm` and `security` umbrella features, registers the module, refreshes the migration registry and refuses to overwrite an existing `src/controllers/mfa.rs` or `*_create_user_mfa_factors_table.rs` migration. Mount the handlers as POST routes behind the authentication middleware, CSRF protection and rate limiting, and require a recent password check before enrollment. To gate login, keep the session pending until `verify_second_factor` succeeds; the generated auth controller issues a full session after the password. Secrets are stored unencrypted in the database, so protect that table and its backups like credentials. Recovery codes and factor reset remain application work. <a id="cargo-rullst-makeoauth-v13-prerelease-preview"></a> ### `cargo rullst make:oauth <provider>` (v13 prerelease preview) Adds sign-in with Google, GitHub, Microsoft or Apple to the account model of `cargo rullst auth` or of the SaaS starter (and of applications generated from the former LMS starter, such as `examples/lms`). Run it once per provider; other providers are refused until they have a generator and tests. ```bash cargo rullst make:oauth google # or github, microsoft, apple cargo rullst db:migrate # once, after the first provider ``` The first provider writes `src/controllers/oauth/mod.rs` (shared flow, account linking and sign-in), `src/models/oauth_identity.rs` and an `oauth_identities` migration (provider and subject unique, the user id with `ON DELETE CASCADE`, created at). Every provider writes `src/controllers/oauth/<provider>.rs` (settings and the `/auth/<provider>` and `/auth/<provider>/callback` handlers), registers it in the shared module and documents its settings in `.env.example`; a second provider adds no migration and shares the table, the flow and the linking policy. The command enables the `oauth` umbrella feature and the challenge store of the project's database (`oauth-sqlite`, `oauth-postgres` or `oauth-mysql`) and adds an application feature `oauth-mock = ["rullst/oauth-mock"]`. In the SaaS starter and LMS-starter applications it also mounts the routes after the password login, with the same `credential_rate_limit`, calls `controllers::oauth::<provider>::initialize()?` at the start of `router()` and adds a "Sign in with …" link to the login page; for a `cargo rullst auth` project, whose router is the application's, it prints the routes to mount. The flow keeps the OAuth state, PKCE verifier and OIDC nonce in `rullst_connect_login_challenges` (created on first use) and only an opaque id in the `rullst_oauth_challenge` cookie (ten minutes, `HttpOnly`, `SameSite=Lax`, `Path=/auth/<provider>`, `Secure` in staging and production); see [Tutorial 42](https://rullst.github.io/Rullst/book/tutorials/42-server-bound-oauth-sessions.html#store-backed-login). The callback consumes the challenge once, checks the state and the PKCE verifier and, for OpenID Connect providers, the ID token and its nonce, then: - an identity already in `oauth_identities` (provider and subject) signs in to its account; - otherwise the provider must have verified the email; an unverified email is refused; - a verified email that belongs to no account creates one without a password, linked to the identity; - a verified email that already belongs to an account is refused by default (`EXISTING_ACCOUNT_POLICY = ExistingAccountPolicy::Refuse` in `src/controllers/oauth/mod.rs`), because registration does not prove that whoever registered the address owns it. `LinkAndResetPassword` links it instead, removes the account's password and signs out every device in the same transaction. The policy applies to every provider, including a second provider of an account that signed in with the first. What counts as the subject and as a verified email differs per provider: | Provider | Subject | Verified email | | :--- | :--- | :--- | | `google` (OpenID Connect) | ID token `sub` | `email` with `email_verified: true` | | `github` (OAuth 2.0) | numeric account id of `GET /user`, never the login name | the `primary` address of `GET /user/emails` marked `verified`; the default `user:email` scope grants it | | `microsoft` (OpenID Connect) | `<tid>:<oid>` (directory and object IDs) | `email` only with the optional claim `xms_edov: true`, which the app registration must request with `email` | | `apple` (OpenID Connect, form POST) | ID token `sub` | `email` with `email_verified`; possibly a private relay address | Sign-in issues the password login's cookie, `make_login_cookie_with_epoch` with the account's current session epoch; an account whose session epoch the password login refuses is refused the same way. The page to return to comes from `?next=` on `/auth/<provider>`, and only a same-site absolute path is kept. Settings (process environment, then `.env`); setting none of a provider's credentials turns it off and hides its link, a partial set stops startup: - Google: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`. - GitHub: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` of an OAuth App. - Microsoft: `MICROSOFT_TENANT` (required with the credentials: `common`, `organizations`, `consumers` or a directory ID GUID; there is no default, because it decides who can create accounts), `MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`. - Apple: `APPLE_CLIENT_ID` (the Services ID), `APPLE_TEAM_ID`, `APPLE_KEY_ID` and `APPLE_PRIVATE_KEY`, the PEM text of the `.p8` key (newlines may be written as `\n`). Rullst Connect signs a five-minute ES256 client secret with it for each code exchange; a key that cannot sign stops startup. Keep it in the environment or a secret store: the command never writes it to a file. - `<PROVIDER>_REDIRECT_URL` defaults to `RULLST_PUBLIC_ORIGIN` followed by `/auth/<provider>/callback`, and to `http://localhost:3000/...` in development; staging and production without either stop at startup. - `mock_*` values select the offline provider, which signs in `mock@example.invalid` without leaving the application; the command prints the exact settings, such as `GITHUB_CLIENT_ID=mock_github GITHUB_CLIENT_SECRET=mock_github cargo run --features oauth-mock`. Without that feature the application stops at startup with an error naming it, and staging and production refuse mock credentials. Apple POSTs its callback from `appleid.apple.com`, so `make:oauth apple` mounts `POST /auth/apple/callback` (plus a `GET` that only the offline mock uses), makes the challenge cookie `SameSite=None; Secure`, and lists `/auth/apple/callback` in `[security] csrf_signed_webhook_paths` of `Rullst.toml` (creating the file when missing): instead of the CSRF token, the handler requires the `state` of the login challenge that the browser's challenge cookie names. Its name arrives only on the first authorization, in the unsigned `user` form field, and only names a new account. Register the `MAIL_FROM` domain with Apple's Private Email Relay service so account emails reach relay addresses. The command requires a strict single-database profile (`strict-sqlite`, `strict-postgres` or `strict-mysql`, as `cargo rullst new --database ...` selects), the `User` model with `session_epoch`, `current_session_epoch` and `find_by_email`, the generated `auth_controller` helpers, `src/pages/auth.rs` and a migration runner. It refuses Turso-primary projects, and it fails before writing anything when any output exists or the provider is already installed. It does not edit `auth_controller.rs` or `user.rs`. --- ## 🗄️ 3. Database and Migrations (`db:*`) Every `db:*` command (including `cargo run -- db:migrate` and a deployed binary's `db:migrate` job) and `studio` select the database exactly like `Server`: the process `DATABASE_URL`, then `DATABASE_URL` in `./.env` (which never overrides a variable already set in the process), then `[database].url` in `Rullst.toml`, read with a TOML parser. A `Server::with_db` URL applies when `Server::run` intercepts the command. Without a configured database, a `db:*` command exits with status 1 instead of creating a SQLite file. Configuration errors (reported without file content) and database initialization failures also exit with status 1. ### `cargo rullst db:migrate` Runs `cargo run -- db:migrate`, which the application's `rullst::artisan!(crate::migrations::get_migrations())` call intercepts. It applies, in one new batch, every migration registered in `src/migrations/mod.rs` whose name is not yet recorded in the ORM's `migrations` table, calling each migration's `up`. ### `cargo rullst db:rollback` Reverts the last batch: it calls `down` for each migration recorded in the latest batch, newest first, and removes their records. ### `cargo rullst db:status` Lists every registered migration as `Applied` or `Pending` according to the `migrations` table. ### `cargo rullst db:seed` Runs the seeders (`rullst::db::Seeder` implementations) that `main` passes as the second argument of `rullst::artisan!(migrations, seeders)`, for example an initial administrator or development data. Turso-primary starters handle `db:migrate`, `db:rollback` and `db:status` in their generated `main.rs` through `TursoOrm` instead of `rullst::artisan!`. ### `cargo rullst studio` Runs `cargo run -- studio`, which Core's Artisan handler serves as a smaller local compatibility UI (database, AI, telemetry, security, capital and span pages) on `127.0.0.1:5555` until it stops; it is not the full `rullst-studio` router that debug builds of the generated starters start on the same port (see [Rullst Studio](https://rullst.github.io/Rullst/book/3-rullst-studio.html)). Treat it as a privileged development tool; do not expose it publicly without an independently reviewed authentication, authorization, and TLS boundary. `cargo rullst studio --remote [--port PORT]` needs no project: it starts the installed standalone `rullst-studio` binary (`cargo install rullst-studio --version =13.0.0-alpha.4 --features remote --locked`; Cargo installs a prerelease only when its version is named), which serves Studio's remote-projects mode on `127.0.0.1:5556` (or `PORT`) and reads the [production monitoring endpoint](https://rullst.github.io/Rullst/book/production-monitoring.html#watch-your-projects-from-studio) of the projects you add. Without the binary it explains how to install it. --- ## 🧠 4. Analyzers and Code Generators (`generate:*`) ### `cargo rullst generate:openapi` Reads recognizable route and Rustdoc patterns and writes an OpenAPI 3.0 draft to `openapi.json` in the project root. Dynamic routes, custom extractors, and semantic constraints may require manual edits; validate the result with an OpenAPI validator before publishing it. ### `cargo rullst generate:ts` Scans recognizable route declarations in `src/main.rs` and `src/lib.rs` and emits `rullst-client.ts` with unchecked request/response placeholders. Axum `{name}` and `{*name}` captures (and legacy `:name` segments) become method arguments interpolated with `encodeURIComponent` (per segment for a wildcard). State-changing requests (anything but `GET`, `HEAD`, `OPTIONS` and `TRACE`) follow the CSRF middleware's double-submit contract: the client echoes the `rullst_csrf` cookie in the `X-CSRF-Token` header and sends cookies with `credentials: 'same-origin'`. The cookie is set by a `GET`/`HEAD` response, so load a page or issue a `GET` before the first state-changing call. Outside a browser, or for a cross-origin `baseUrl`, pass `new RullstClient(baseUrl, { csrfToken: () => token, credentials: 'include' })`. Review the output before use; route scanning does not establish DTO shapes, serialization or authorization. ### `cargo rullst generate:api` (v13 candidate) Consumes one explicit bounded OpenAPI 3.1 profile and generates Rust DTOs/codecs, a typed TypeScript HTTP client and a canonical schema copy. Requires `--schema` and `--output`; `--check` verifies freshness without writes. Unsupported shapes fail before generation. See the [profile and executable acceptance](https://rullst.github.io/Rullst/book/typed-api.html). ### `cargo rullst generate:diagram` Analyzes primary and foreign keys defined in your Models and exports a `diagram.md` file containing Mermaid.js code, visually generating an Entity-Relationship (ER) diagram. The file starts with a generator marker comment. An existing `diagram.md` is replaced only when it carries that marker (or is the single unmarked Mermaid block earlier releases wrote); the command refuses a hand-written file or a symlink, so move it aside to regenerate the diagram. ### `cargo rullst generate:models` / `cargo rullst make:models-from-db` Connects to an existing database and generates reviewable starter structs from the tables and columns visible in SQLite or the current PostgreSQL/MySQL schema. Table lookups are parameterized and SQL identifiers are allowlisted. PostgreSQL and MySQL metadata columns are cast to text and aliased, so PostgreSQL 12+ `sql_identifier` columns and MySQL 8 upper-case labels are read portably; a missing or undecodable metadata column fails with an error rather than a panic. Table module names are normalized, while collisions and database columns that would require an unsupported ORM field remapping fail before the output directory is written. Existing model files are never replaced: if any `<table>.rs` target already exists, the command fails before writing anything. An existing `mod.rs` keeps its content and receives only missing `pub mod` declarations. The bounded type mapping falls back to `String`; review keys, relations, custom types, schema selection and generated files before compiling them. * **Required Flags:** * `--driver`: `postgres`, `mysql`, or `sqlite`. * `--url`: The complete connection string. * **Optional Flags:** * `--output`: Where to save the generated structs (Default: `src/models`). ### `cargo rullst generate:ai-context [--check]` The v13 candidate writes a bounded `.llms.txt` and `.rullst/context-map.json` with dependency metadata, configuration key names and source paths. It creates `AGENTS.md` only when absent and preserves existing project instructions. `--check` detects missing, altered or stale inventory without writing files. See [project context](https://rullst.github.io/Rullst/book/project-context.html) for limits, exclusions and legacy migration. ### `cargo rullst ai` (removed) The terminal AI assistant of the earlier 13.0 prereleases (`ai`, `ai connect`, `ai status`, `ai disconnect`, `ai upgrade`, `ai fix` and `ai review`) was removed in 13.0.0-alpha.4. `cargo rullst ai …`, with any arguments, prints what replaces it and exits with status 1; the command is hidden from `--help`, completions, the command palette and the home screen. Use your editor's or terminal's AI assistant instead, with the project's `AGENTS.md` and `.llms.txt` ([`generate:ai-context`](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-generateai-context---check) writes and checks them) and the framework's published [`llms.txt`](https://rullst.github.io/Rullst/llms.txt). The `rullst-ai` crate, which applications use to call models, is unchanged. In place of `ai fix <error-id>`, the development error page's "Ask your AI assistant" panel gives a prompt to copy into your assistant and the path of the recorded error context. Credentials that `ai connect` saved stay in `~/.config/rullst/credentials.toml` (`$XDG_CONFIG_HOME/rullst/`, or `%APPDATA%\rullst\` on Windows), and its git checkpoints under `refs/rullst/ai-checkpoints/`, until you delete them. See the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#upgrading-an-alpha3-application-to-alpha4). ### `cargo rullst eject [--force] [--output <path>]` Writes a reviewable Axum/Tokio entry-point template (`src/ejected_main.rs`). It is not a translation of the project's `main.rs`: it serves a placeholder route from `application_routes()`, wraps it in the configured Rullst security baseline (`apply_security_baseline` with `Rullst.toml`, as `Server` applies it) and reaches Axum through `rullst::web::axum`, so it compiles without a direct Axum dependency. Move the application's routes and module declarations into it before use. Static files, rate limiting, lifecycle probes, hot reload, sessions, authentication and authorization are not included. * **Flags:** * `--force`: Replaces `src/main.rs` with the template after copying the original to `src/main.rs.rullst-backup` (an existing backup stops the command). Application routes are not carried over. * `--output <path>`: Specifies a custom output path below `src/` for the ejected file. ### `cargo rullst deploy:doctor` (v13 candidate) Read-only inspection of a local deployment configuration snapshot: ```bash cargo rullst deploy:doctor --env-file .env.production --json cargo rullst deploy:doctor --config Rullst.production.toml --process-env ``` Reuses Core environment/security validation, catches obvious key/configuration mistakes and identifies application-policy reviews without echoing values. `--target <production|staging>` (default `production`) names the expected environment; `--json` prints `rullst.deployment-diagnostic.v1`. Explicit environment sources remain separate. Exit zero covers only the inspected local profile; `deployment_verified` remains false. See the [input and output contract](https://rullst.github.io/Rullst/book/deployment-diagnostic.html) before using it in CI. ### `cargo rullst inspect [target]` Scans source files and prints structural summaries in the terminal without starting a server, expanding macros or connecting to a database. * **Arguments:** * `[target]`: The item or file to inspect (default `routes`): * `route` or `routes`: Lists `get`/`post`/`put`/`delete` declarations written as `method("path" => handler)` on one line in the regular `.rs` files under `src/`. Like `audit` and `generate:diagram`, the walk does not follow symlinks and is bounded in depth and entries; it reports when a bound left the table incomplete. * `model` or `models`: Lists the structs, enums and `pub` fields declared in `src/models`. * `schema`: Prints, as JSON, the table, fields, Rust types and optionality of every `#[derive(Orm)]` struct under `src/` (the extractor `make:migration:auto` uses). It describes the models, not the live database. A project-provided `rullst-schema.json` is printed instead when present; Rullst does not generate that file. * `<path/to/file.rs>`: Displays the first 40 lines of any target Rust file with line numbers. * **Optional Flags:** * `--json`: Prints `routes`, `models` or `schema` as one versioned document (`models` and `schema` since 13.0.0-alpha.4); another target with `--json` is refused. `inspect routes --json` prints `rullst.cli-routes.v1`: ```json { "schema": "rullst.cli-routes.v1", "schema_version": "rullst.cli-routes.v1", "complete": true, "incomplete_reason": null, "routes": [ { "method": "GET", "path": "/posts/{id}", "handler": "controllers::posts::show" } ] } ``` `complete` is `false` when the bounded source walk stopped early or `src/` is missing, with the reason in `incomplete_reason`. * `inspect models --json` prints `rullst.cli-models.v1`: `complete`, `incomplete_reason` and `files`, one entry per `src/models/*.rs` file (not `mod.rs`, no symlinks) with its public `struct` and `enum` items. A struct has `fields` (`name`, `type`); an enum has `variants`. A file that is not valid Rust is named in `incomplete_reason`. * `inspect schema --json` prints `rullst.cli-schema.v1`: the same `source` and `models` as the human output (`struct`, `table`, `fields` with `name`, `rust_type`, `optional`), or, when the project provides `rullst-schema.json`, `source: "rullst-schema.json"` with the parsed file under `snapshot`. --- ## 🚀 5. Development, Infrastructure, and Build ### `cargo rullst dash` Opens the Ratatui development control surface in an interactive terminal. The dashboard reports the probed application port, supervised auto-reload state, the child process exit state, and the configured database profile; it does not label a database as connected merely because a URL exists. Logs and input queues are bounded, ANSI control sequences are removed, terminal state is restored on error, and the owned application process is stopped and reaped when the dashboard exits. **Live metrics (v13).** Once a second the dashboard reads `GET /_rullst/dev-telemetry` from the application it started, on `127.0.0.1:<port>`, and shows only what the application reports: * requests per second over the last 10 s, from the application's exact request counters, and 4xx/5xx totals since the process started; * errors: 5xx responses and their share of all requests over the last 60 s; * p50/p95 latency over the last 60 s and a p95-per-poll sparkline (up to two minutes), computed from the individual requests the dashboard observed. They are marked `sampled` when more requests arrived between two polls, or before the first poll of a process, than the 64 newest the application returns; * the newest requests with status, method, duration and path (never the query string); * ORM queries since start and slow ORM operations (at least 100 ms); * possible N+1 queries (see [N+1 query warning](https://rullst.github.io/Rullst/book/cli_reference.html#n1-query-warning)); * the pending jobs of a queue. Counters, latency samples and slow operations restart with every new process (a rebuild or `r`); the request list and the sparkline keep earlier entries. Histories are bounded: 120 sparkline points, 50 requests, 16 slow operations and at most 4,096 latency samples. A response larger than 256 KiB, with another schema or with inconsistent counters is rejected, and control or bidirectional-formatting characters are replaced before anything reaches the terminal. A value the application does not report is shown as `not reported`, never as zero. Only the process the dashboard started is measured: a response carrying another `RULLST_DEV_GENERATION`, for example from a second `dev`/`dash` already listening on the port, replaces the metrics row with an **ANOTHER PROCESS ON THE PORT** panel instead of showing that process's figures. The endpoint exists only in a debug build running in Development under `cargo rullst dev`/`dash`; see the [telemetry guide](https://rullst.github.io/Rullst/book/telemetry-guide.html#development-dashboard-endpoint). When the application does not serve it, the metrics row is replaced by a **TELEMETRY NOT AVAILABLE** panel with these steps: 1. Serve the application with `rullst::Server` from Rullst 13 or newer (the generated starters do). 2. Run a debug build in Development: leave `RULLST_ENV`/`APP_ENV` unset or set `RULLST_ENV=development`. 3. Press `r` (or save a file) to restart. | Value | Reported when | To enable it | | :--- | :--- | :--- | | Requests, latency, errors | The endpoint answers. | The steps above. | | ORM queries and slow operations | Rullst's default tracing subscriber is installed (`Server::run` installs it when the application has none) and keeps `rullst_orm` INFO spans. | Do not install another global subscriber first, and do not filter out `rullst_orm=info` with `RUST_LOG`. | | Queue pending | The application passes its queue to the server. | `Server::new(router).with_dev_queue(queue)` (accepts a `Queue` or an `Arc<Queue>`). | ORM queries count the outermost `rullst.orm.query` span of each ORM operation (model queries, saves, deletes and `Orm::raw`). A `chunk`/`chunk_by_id` traversal is not counted itself: each page it fetches and each operation its handler runs counts separately. One operation can run several SQL statements, and SQL executed directly through SQLx is not counted. #### N+1 query warning When the application reports that one request ran the same ORM operation at least three times (the threshold Studio's trace view uses), a **POSSIBLE N+1 QUERIES** panel appears below the logs. Each line shows the request's method and matched route (`GET /posts/{id}`), the operation fingerprint (`Comment.select_many (comments)`, built from the ORM span's model, operation and table labels; Rullst never records SQL text or bound values) and the repetition count, followed by a hint to load the related rows eagerly or batch the lookups into one query. Inspect the handler before changing it: repeating an operation can be intentional. Raw statements (`Orm::raw`) and operations started on another task (`tokio::spawn`) are not attributed to a request, so they never produce a finding. The **VERIFIED PROJECT STATE** panel shows the detection state in its `N+1 check` line: the number of findings since the process started, `N+1 detection needs request-correlated ORM telemetry (Rullst Core 13)` when the application's telemetry has no request-correlated ORM data (an older Rullst Core), or `N+1 detection needs ORM query telemetry` when ORM queries themselves are not reported (see the table above). Performance note: a request that runs one ORM lookup per row (for example loading each post's comments inside a loop over posts) makes one database round trip per row. [Eager loading](https://rullst.github.io/Rullst/book/crates/orm.html#query-row-cap) runs one related-model query for all parents of a batch, and an `IN` query can fetch the rows of several keys at once; measure the route before and after the change. Terminals at least 26 rows tall show the metrics row and terminals at least 105 columns wide add the recent-requests panel; shorter terminals show a one-line summary in the header. The layout adapts to narrower terminals and provides these keyboard controls: * `r`: restart the application from the current build (no rebuild). It is refused, with a notice, while the application is still starting, while a migration runs and while a saved change is being rebuilt (a successful rebuild restarts the application itself). The notice clears once the restarted application is ready, or says that its readiness was not confirmed or that it exited. * `o`: open the application. * `s`: probe the loopback Studio endpoint and open it only when reachable. * `d`: open existing Scalar docs. Missing files produce explicit `cargo rullst make:scalar` guidance rather than silently modifying the project. * `m`: run `db:migrate` asynchronously and report its real exit result. * `/`: search both log panes; `f` cycles all/warning+error/error filtering. * `Tab`: switch the focused log pane; arrows and Page Up/Page Down scroll it. * `c`: clear dashboard logs. * `?`: keyboard and metrics help. Any key closes it; `q` still quits. * `q` or `Esc`: exit (`Esc` closes the help first). The footer lists `d` and `Tab` only on terminals at least 120 columns wide; the help always lists every key. The animated neon palette is enabled only for an interactive terminal. Set `RULLST_REDUCED_MOTION=1` (or `true`/`yes`) to stop the animation and keep the colors. `NO_COLOR` removes the colors only; set both for a color-free, static interface. Non-interactive automation should use `cargo rullst dev`; `dash` fails clearly when no terminal is attached. ### `cargo rullst dev` Builds and starts a directly linked application. Saving source, static assets, templates, `Cargo.toml`, `Cargo.lock`, `Rullst.toml` or `.env` schedules a coalesced rebuild. A failed build leaves the current application running. A successful build creates an owned executable snapshot, stops the previous process and starts its replacement. The snapshot avoids locking Cargo's build output on Windows. Initial migrations run before startup; later migrations remain an explicit command. The same-origin browser client polls an opaque process-generation marker and refreshes only when a different generation serves successfully. This is enabled only in debug/development. Readiness verifies that marker, not just an open port. Changing the configured port requires restarting the CLI. In-memory state and unsaved browser state reset during reload. The process receives a bounded shutdown interval before forced termination; this is a development facility. Ctrl+C, SIGTERM (an IDE stop button or `kill`) and SIGHUP (a closed terminal) end `dev` and `dash` the same way: the application's process group is stopped and its executable snapshot removed (on Windows, Ctrl+C and closing the console). No scaffold question is required: `dev` and `dash` enable auto-reload, while `cargo run` runs the application normally. The legacy `--hot-reload` scaffold flag is rejected (since v12) because DLLs can split ORM/Tokio globals. Existing legacy scaffolds can use their directly linked router; the supervisor removes `HOT_RELOAD` from its child's environment. See [Supervised Development Auto-Reload](https://rullst.github.io/Rullst/book/tutorials/51-authenticated-hot-reload.html) for limitations, failure recovery and the v13 architecture decision. `dev` keeps its plain output; at startup it prints one line suggesting `cargo rullst dash` for live requests, latency, errors and database metrics. * **Optional Flags:** * `--ts-sync`: Regenerates the TypeScript client SDK (`rullst-client.ts`, as `generate:ts` writes it from the routes in `src/main.rs` and `src/lib.rs`) after the initial build and after every successful rebuild. A failed generation is reported and the application keeps running. ### `cargo rullst build:client` Builds the library for `wasm32-unknown-unknown`, runs `wasm-bindgen`, and writes a separate `static/rullst-islands.js` hydrator that awaits binding initialization. It parses `Cargo.toml`, merges the required `cdylib` crate type without replacing existing library crate types, and honors an explicit `lib.name`. The command checks/installs the Rust target and `wasm-bindgen-cli`; any failed tool step aborts. Bundle size and browser performance depend on the generated application and must be measured. * **Flags:** `--debug` (builds the Wasm library with Cargo's debug profile instead of `--release`). ### `cargo rullst build` Creates the monolithic final Production binary of the backend and writes Brotli (`.br`) and Zstandard (`.zst`) siblings next to the `html`, `css`, `js`, `json`, `svg`, `wasm`, `xml` and `txt` files under `static/`. The server prefers a sibling over its source, so rerun the command after editing an asset and before building an image; `cargo rullst dev` removes siblings that are not newer than their source at startup and on every change (siblings without a source file are kept). * **Flags:** `--debug` (builds with Cargo's debug profile instead of `--release`). ### `cargo rullst green` **Maturity:** Experimental. It replaces `cargo rullst footprint` (13.0.0-alpha.1 to alpha.3) and is part of the main CLI, so it needs no separate install. Reports figures it measured for the app, each with its method: the release binary size, a local container image's size (`--image`, through `docker` or `podman image inspect`), the idle resident memory of the started app (Linux `VmRSS`), the third-party dependency count from `Cargo.lock` and, with `--bench`, the app process's CPU time during a bounded closed-loop loopback load and the CPU time per request. Anything unavailable is `NOT MEASURED` with the reason. It measures no energy: a Software Carbon Intensity (SCI, ISO/IEC 21031:2024) figure appears only as an estimate labelled `estimate`, computed from the measured CPU time, your `--cpu-watts` and your `--grid-intensity`, with every input shown. Without `--url` it builds the release binary (reusing an existing build), starts it with `RULLST_ENV=production` (or `--env staging`, which accepts the starters' mock billing credentials) on a free `127.0.0.1` port, measures it and stops it. With `--url` it measures an app already running on loopback and refuses any other host; it makes no network calls beyond loopback and the local container engine, and never fetches grid intensity. ```bash cargo rullst green --image my-app:latest cargo rullst green --url http://127.0.0.1:3000 --path /health --bench --json cargo rullst green --bench --cpu-watts 15 --grid-intensity 120 ``` * **Flags:** `--url <http://127.0.0.1:PORT>`, `--env <production|staging>` (default `production`; not with `--url`), `--path <PATH>` (default `/`), `--image <NAME>`, `--bench`, `--duration <10s|1m|1500ms>` (default `10s`, 1 s to 10 min; needs `--bench`), `--concurrency <N>` (default 4, at most 256; needs `--bench`), `--cpu-watts <W>` (needs `--bench`), `--grid-intensity <gCO2e/kWh>` (needs `--cpu-watts`), `--embodied <gCO2e>` (needs `--grid-intensity`), `--json` (versioned `rullst.green.v1`). * **Exit status:** `0` when the measurement ran, even with `NOT MEASURED` fields; `1` when the build failed or the app could not be started or reached; `2` for invalid arguments. See the [green guide](https://rullst.github.io/Rullst/book/green.html) for what each figure means and how to choose a grid intensity. ### `cargo rullst dockerize` Writes the `Dockerfile` (plus `.dockerignore` when absent) into an existing project, as `new --docker` does. It refuses to replace an existing `Dockerfile`; move a customized file aside to regenerate the template. The binary is named after `[package].name`, read with a TOML parser (`app` when `Cargo.toml` has no package name). As in `deploy`, a project whose `Rullst.toml` (or `.env.example`) names a SQLite database gets `DATABASE_URL=sqlite:///app/data/rullst.db?mode=rwc` in the image. `cargo rullst deploy` writes the same Dockerfile together with `compose.yaml` and the `Caddyfile`. **Removed in 13.0.0-alpha.4:** `nixify`, `generate:buildah`, `foundry:init`, `foundry:deploy` and `make:grpc`. Files they generated stay yours; see [Advanced deployment](https://rullst.github.io/Rullst/book/advanced-deployment.html) for Nix, Buildah, Podman Quadlet and Kubernetes. ## 🛡️ 6. Security, Compliance & System Diagnostics ### `cargo rullst audit` Executes bounded automated checks across recognized source, configuration, route, dependency, and local network patterns, and can invoke installed dependency scanners. Static findings require human review and are not a penetration test or compliance certification. * **Optional Flags:** * `--ai`: Prints fixed, rule-based remediation suggestions after the checks. It calls no AI model or network service; the flag keeps its legacy name. * `--compliance`: Writes `SECURITY_COMPLIANCE.md`, an evidence report. Each executed check is `NO FINDINGS`, `NO FINDINGS OUTSIDE EXCEPTIONS`, `FINDINGS`, `GENERATED`, `OBSERVED`, `NOT CHECKED`, or `ERROR`, and control families outside the command's scope are `NOT EVALUATED`. It never reports `PASS` and is not a SOC 2, ISO 27001, or transport certification. * `--idor`: Fails on parameterized routes without an explicit adjacent `// rullst-access: public|owner|role|admin — reason` classification. `owner` requires `RbacGuard::authorize_owner_or_role`; `role` requires a recognized role guard; `admin` requires `RequireRoleLayer` or `NexusAuthPolicy::protect_router`; `public` is restricted to recognized GET routes. The marker goes on the route's line or the line above it; in a multi-line `.route(` call, on the line above the path literal. The guard may appear anywhere in the same crate's `src` tree outside comments and `#[cfg(test)]` items, so the check does not prove that the route is mounted behind it. Manual review and runtime negative tests remain required. * `--geiger`: Inventories `unsafe` in the dependency tree. Unsafe may be justified and requires review; the command does not prove a zero-unsafe invariant. * `--sbom`: Generates a standardized **CycloneDX 1.5 JSON** Software Bill of Materials (`sbom-cyclonedx.json`) from `Cargo.lock`, with the SHA-256 checksums the lockfile records. It contains no license metadata. * `--audit-ignore RUSTSEC-YYYY-NNNN`: Passes one explicit, repeatable advisory exception to `cargo audit`. A successful run is reported as **NO FINDINGS OUTSIDE EXCEPTIONS**, not “no findings”; the caller must separately version, own, review, and expire every exception. * `--network`: Checks a bounded list of local ports/bindings for potentially exposed services; it is not a comprehensive network scan. The TCP listener inventory runs `ss -ltnH` (Linux iproute2). Where it cannot run, as on macOS, Windows or a Linux image without iproute2, the check is reported as `ERROR` and the command exits non-zero instead of reporting a clean scan. * `--report [md|html|json]` (new in 13.0): Runs the static report checks below (mapped to OWASP ASVS 5.0.0 Level 1, with a personal-data inventory and accessibility heuristics) and writes `SECURITY_REPORT.md` (the default), `SECURITY_REPORT.html` or `SECURITY_REPORT.json` in the current directory; `--output <path>` chooses another file (its directory must exist). It reuses the `cargo audit` step with its `--audit-ignore` exceptions and the `--idor` scanner, and cannot be combined with `--json`, `--compliance`, `--sbom`, `--network`, `--geiger` or `--ai`. See [the security report guide](https://rullst.github.io/Rullst/book/security-report.html). * `--json`: Moves the progress lines to standard error and prints one `rullst.cli-audit.v1` summary on standard output; the exit status is unchanged (non-zero for findings or incomplete requested checks): ```json { "schema": "rullst.cli-audit.v1", "schema_version": "rullst.cli-audit.v1", "issues_found": 0, "checks": [ { "id": "secret_scan", "status": "not_checked", "count": null, "exceptions": [], "detail": "no .env file was available to inspect" }, { "id": "dependency_audit", "status": "no_findings_outside_exceptions", "count": null, "exceptions": ["RUSTSEC-2099-0001"], "detail": "..." } ] } ``` Check ids are `secret_scan`, `dependency_audit`, `unsafe_scan`, `idor_scan`, `sbom` and `network_scan`, always in that order; `status` is `no_findings`, `no_findings_outside_exceptions`, `findings`, `generated`, `observed`, `not_checked` or `error`, and `count` is set for `findings`, `generated` and `observed`. Details are redacted like error reports. In a package directory, the unsafe and IDOR/BOLA scans cover its `src` and the `src` of every workspace member below it, as listed by `cargo metadata`; in a directory without `src`, such as a virtual workspace root, the IDOR/BOLA scan walks every `src` tree below it. The source scans (unsafe syntax, IDOR/BOLA routes and listener bindings) do not follow symlinked files or directories and skip `target/` and `.git/`. A walk stops at 64 directory levels or 250,000 entries; reaching either bound is reported as a finding, so the scan fails as incomplete instead of passing. The route and listener scans skip each top-level `#[cfg(test)]` item (such as `mod tests;` or an inline test module) on its own; code after it is still scanned. `cargo audit` and the SBOM read the project's own `Cargo.lock`. In a workspace member, which has none, they read the lockfile of the workspace root that `cargo metadata` reports, and `cargo audit` receives it as `--file`; the SBOM then lists every package of that workspace lockfile. SBOM components come from `Cargo.lock`. Only crates.io packages receive the plain `pkg:cargo/<name>@<version>` purl; a package from another registry adds a `repository_url` qualifier, a git package adds a `vcs_url` qualifier with the locked commit, and path or workspace packages (including the application) have no purl. Every component that is not from crates.io carries a `rullst:cargo:source` property with its lockfile source, or `local`. `SECURITY_COMPLIANCE.md` and `sbom-cyclonedx.json` are written in the current directory and replace a previous regular file. Because an audit may run on an untrusted checkout, the command refuses to write either file through a symlink. #### Security report (`--report`) The report maps each check to [OWASP ASVS 5.0.0](https://github.com/OWASP/ASVS/releases/tag/v5.0.0_release) (release `v5.0.0_release`) Level 1 requirement IDs and section names, and lists the Level 1 requirements no check relates to as `NOT EVALUATED`, by chapter. A header states that it does not replace a manual review or a penetration test; it never reports `PASS` or a certification. | Check id | What it reads | ASVS 5.0.0 | | :--- | :--- | :--- | | `security_headers` | `Server::new` or a header layer in `src`; `[security] csp`/`coep` in `Rullst.toml` | V3.4.1 (L1); V3.4.3, V3.4.4, V3.4.6 (L2) | | `csrf` | Single-line write routes; `Server`, `csrf_middleware`, `csrf_signed_webhook_paths` | V3.5.1 (L1) | | `cookies` | `Set-Cookie`-shaped string literals, cookie builders, `csrf_same_site` | V3.3.1 (L1); V3.3.2, V3.3.4 (L2) | | `auth_rate_limit` | Authentication write routes; a rate limiter or `LoginGuard` | V6.3.1 (L1) | | `committed_secrets` | Files from `git ls-files` (not `target/`, binaries, symlinks or files over 2 MiB) | V13.3.1 (L2), chapter V13 | | `dependency_audit` | `cargo audit` with the `--audit-ignore` exceptions | V15.2.1 (L1) | | `idor` | The `--idor` route-classification scan | V8.2.1, V8.2.2 (L1) | | `a11y_img_alt`, `a11y_form_labels`, `a11y_html_lang` | `html!` bodies and templates under `src/` and `templates/` | WCAG 2.2 1.1.1; 1.3.1, 3.3.2, 4.1.2; 3.1.1 | Each check reports `no_findings`, `no_findings_outside_exceptions`, `findings`, `not_checked` or `error` (shown as `NO FINDINGS`, `FINDINGS`, ... in Markdown and HTML) with a one-line fix and a link to the guide. A committed-secret finding shows only the file, the line and the first four characters of the value followed by `…`. The personal-data inventory (`#[privacy]`, `#[orm(encrypted)]`, `SecretString`, `#[orm(masked)]`, and `review` for unclassified fields named like email, phone, cpf, cnpj, ssn, birth, address, document or ip) is `observed` and never changes the exit status. **Exit status:** 1 when any check reports `findings` or `error`; 0 otherwise, including when checks are `not_checked` (no `src/`, no Git work tree, no `cargo-audit`). The terminal prints a grouped ✓/!/✗ summary, plain under `NO_COLOR`, `CI`, `TERM=dumb` or a non-terminal stdout. **Formats:** Markdown has one GitHub table per section; HTML is one self-contained file (inline CSS with light and dark schemes, no script, no external asset, every value escaped); JSON is the `rullst.cli-audit-report.v1` document below, without ANSI sequences: ```json { "schema_version": "rullst.cli-audit-report.v1", "generated_at": "2026-10-08T12:00:00+00:00", "tool": { "name": "cargo-rullst", "version": "13.0.0" }, "notice": "This report records bounded static checks for a reviewer. ...", "standard": { "name": "OWASP Application Security Verification Standard", "version": "5.0.0", "level": 1, "source": "https://github.com/OWASP/ASVS/releases/tag/v5.0.0_release", "level1_requirements": 70, "level1_mapped": 7, "mapping_note": "..." }, "accessibility_standard": "WCAG 2.2", "exit_non_zero": true, "checks": [ { "id": "csrf", "group": "security", "title": "CSRF", "status": "findings", "count": 1, "exceptions": [], "detail": "...", "fix": "...", "docs": "https://rullst.github.io/Rullst/book/security-report.html#csrf", "asvs": [{ "id": "V3.5.1", "section": "V3.5 Browser Origin Separation", "level": 1 }], "asvs_chapter": null, "wcag": [], "findings": [{ "file": "src/lib.rs", "line": 12, "message": "...", "preview": null }] } ], "personal_data": { "status": "observed", "count": 2, "review": 1, "detail": "...", "docs": "...", "asvs_chapter": "V14 Data Protection", "fields": [{ "model": "User", "field": "email", "file": "src/models/user.rs", "line": 8, "classification": "review", "encrypted": false }] }, "not_evaluated": [{ "chapter": "V1", "name": "Encoding and Sanitization", "status": "not_evaluated", "whole_chapter": true, "requirements": ["V1.2.1", "..."] }] } ``` `checks` always holds the ten ids of the table above, in that order. A new key or a changed meaning needs a new `schema_version`. **Limits:** every check is a bounded static heuristic. Routes are recognized only when declared on one line; limiter, guard and header evidence is project-wide and does not prove a route is mounted behind it; untracked files and Git history are not scanned for secrets; templates are not rendered and includes are not followed. The source walks share the `audit` bounds (no symlinks, 64 levels, 250,000 entries) and an incomplete walk turns the source checks into `error`. Files over 2 MiB are not read. ### `cargo rullst hook:install` Installs managed `pre-commit` and `commit-msg` wrappers. The first runs `cargo fmt --all -- --check`, strict workspace Clippy, and `cargo rullst audit --idor`; the second enforces Conventional Commits while accepting the subjects Git itself writes for merges (`Merge branch`, `Merge remote-tracking branch`, `Merge tag`, `Merge pull request`, ...), reverts (`Revert "..."`) and `fixup!`/`squash!`/`amend!` commits. Existing active hooks are moved to explicit `.rullst-original` backups and invoked first, while reinstalling the managed wrappers is idempotent. The command supports linked worktrees, fails clearly outside a Git worktree, and refuses a backup collision instead of overwriting it. When `core.hooksPath` (local, global or system Git configuration, as used by Husky or shared hook directories) selects another directory, it fails before writing, because Git would never run wrappers in the default hooks directory; call the checks from that hook manager instead. These local hooks are bypassable by design; protected CI remains authoritative. ### `cargo rullst doctor` Runs grouped health checks and shows each one as ✓ (pass), ! (warning), ✗ (failed) or · (information, optional or not applicable). Every warning and failure carries a fix (one line, or a short configuration snippet) and a link to its group below. The command exits with status `1` when any check failed; warnings do not fail it. `--fix` installs missing rustfmt/clippy components with `rustup component add rustfmt clippy`, then checks again. Outside a project only the toolchain, project, security (cargo-audit) and disk groups appear. Checks are local and bounded: tool probes are `--version` calls run in parallel, the database probe is a 2-second TCP connect (no credentials are sent) and a SQLite file is opened read-only. Kani is only looked up as a `cargo-kani` executable (in Cargo's `bin` directory or on `PATH`), never run: `cargo kani` starts its first-time download and toolchain installation when it is not set up yet. Values from `.env` are measured, never printed. #### Doctor: toolchain `rustc` (✗ when missing or older than the 1.96.0 MSRV), Cargo, rustfmt and clippy (fixable with `--fix`), Git, the `wasm32-unknown-unknown` target when the project has `src/islands`, and one informational line for the optional tools (cargo-deny, cargo-geiger, cargo-mutants, Kani, cargo-llvm-cov, Docker). On Linux, `toolchain.linker` (v13) is a `!` hint, never a failure, when no fast linker is selected: no `-fuse-ld=mold`, `-fuse-ld=lld` (or `--ld-path=` naming one of them) and no `linker` entry in `[build]`, the host's `[target.<triple>]` or a `[target.'cfg(..)']` table of any `.cargo/config.toml` (or legacy `.cargo/config`) from the working directory up, or of `$CARGO_HOME/config.toml` (`~/.cargo` by default), and none in `RUSTFLAGS`, `CARGO_ENCODED_RUSTFLAGS`, `CARGO_BUILD_RUSTFLAGS` or the host's `CARGO_TARGET_<TRIPLE>_RUSTFLAGS`/`_LINKER`. It says whether `mold` or `ld.lld` is on `PATH` and shows the snippet to add, for example: ```toml [target.aarch64-unknown-linux-gnu] rustflags = ["-C", "link-arg=-fuse-ld=mold"] ``` If the file already has that table, add the two flags to its `rustflags` instead of repeating the table. The check is skipped on other operating systems and on x86_64-unknown-linux-gnu with Rust 1.90 or newer, where rustc already links with its bundled LLD by default. #### Doctor: project The enclosing Rullst project (a `Cargo.toml` that depends on `rullst`) and its `rullst` requirement compared with the CLI's major version; an older project suggests `cargo rullst upgrade --dry-run`. #### Doctor: config and .env Whether `.env` parses, which keys listed in `.env.example` are set neither in `.env` nor in the process environment (names only), whether `RULLST_ENV` (or `APP_ENV`) names a known environment, and whether `Rullst.toml` is valid. #### Doctor: database The primary database from `DATABASE_URL` (environment, then `.env`, then `[database].url` in `Rullst.toml`): a SQLite file must exist or be creatable; PostgreSQL and MySQL/MariaDB must accept a TCP connection (✗ otherwise). `REDIS_URL` is probed the same way (! when unreachable). Turso/remote and `mock_*` endpoints are not contacted. #### Doctor: migrations The `src/migrations/m*.rs` files and, for a local SQLite database, which of them the ORM `migrations` table has not applied yet (`cargo rullst db:migrate`). For server databases run `cargo rullst db:status`. #### Doctor: security baseline `cargo-audit` installed, `.env` neither tracked nor unignored by Git (✗ when tracked), `APP_KEY` set to at least 32 characters and not a template placeholder, and a `Cargo.lock` for reproducible builds. #### Doctor: disk space Free space on the filesystem of the project (or the working directory): ✗ below 1 GiB, ! below 5 GiB. Not measured on Windows yet. `cargo rullst doctor --json` prints one document on standard output and keeps the exit status rules: ```json { "schema": "rullst.cli-doctor.v1", "schema_version": "rullst.cli-doctor.v1", "cli_version": "13.0.0", "ok": false, "summary": { "pass": 11, "warn": 5, "fail": 1, "info": 2 }, "groups": [ { "id": "database", "title": "Database", "checks": [ { "id": "database.primary", "title": "Primary database", "status": "fail", "detail": "PostgreSQL at 127.0.0.1:5432: connection refused", "fix": "Start the server (for example `docker compose up -d`) or fix the URL in .env", "docs": "https://rullst.github.io/Rullst/book/cli_reference.html#doctor-database" } ] } ] } ``` `status` is `pass`, `warn`, `fail` or `info`; `fix` and `docs` are `null` for passing checks. Group ids are `toolchain`, `project`, `config`, `database`, `migrations`, `security` and `disk`; check ids (`toolchain.rustc`, `config.env_keys`, `security.app_key`, ...) are stable, but a check appears only when it applies. ### `cargo rullst academy:doctor` Evaluates the twelve requirements of `ProductionPreset::academy()` (identity, school membership, entitlements, object authorization, tenant isolation, assessments, score events, durable automation and audit, content safety, privacy and distributed abuse controls) against the declarations in a `rullst.academy-evidence.v1` file passed with `--evidence <file>`. It reads only that file and never inspects the project, so it needs no Academy scaffold: v13 removed the complete Academy scaffold, and the LMS example (the former LMS starter) has no evidence file. Without `--evidence` every requirement is `NOT_EVALUATED`. Each requirement is reported as `PASS`, `FAIL`, `SKIPPED` or `NOT_EVALUATED`. Unknown fields or requirements, a repeated requirement and a `PASS` without a non-empty evidence reference are rejected. `--json` prints `rullst.academy-diagnostic.v1` with `certification` (always `false`), `contract_satisfied` and each requirement's `status` and `declared_evidence`. The command exits with status `1` until every requirement has an evidenced `PASS`. Evidence strings are caller declarations, not verified proof; see the [Academy production-boundary diagnostic](https://rullst.github.io/Rullst/book/security-architecture.html#academy-production-boundary-diagnostic). --- ## 🛠️ Quick CLI Cheat Sheet ```bash # Create a new project (the wizard asks for the starter, database and features) cargo rullst new my_app # Reverse-engineer ORM models from an existing database cargo rullst generate:models --driver postgres --url "postgres://user:pass@localhost:5432/mydb" # Statically inspect routes, models, or schemas in the terminal cargo rullst inspect route cargo rullst inspect model # Open the local Studio compatibility UI on 127.0.0.1:5555 cargo rullst studio # Map the project (AGENTS.md, .llms.txt) for your editor's or terminal's AI assistant cargo rullst generate:ai-context # Write the Dockerfile, compose.yaml and Caddyfile for a Linux VM (Docker or Podman) cargo rullst deploy ``` --- # Cargo Feature Matrix Source: https://rullst.github.io/Rullst/book/feature-matrix.html # Cargo feature matrix > [!IMPORTANT] > This page describes the v13 prerelease source, and its dependency examples > pin the published `13.0.0-alpha.4` exactly: Cargo never selects a prerelease > on its own, and a plain `"13.0.0-alpha.4"` would also accept later alphas > (see the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html)). The v12 line reached end > of life on 16 October 2026; see the [release record](https://rullst.github.io/Rullst/book/v12.html). Commit > Cargo.lock for reproducible builds, and use path dependencies only when > intentionally testing checkout-local changes. This page is the public feature contract for the 20 packages in the v13 [release inventory](https://github.com/Rullst/Rullst/blob/main/.github/release-order.json): the 16 published 12.x packages plus the `rullst-privacy`, `rullst-supervision`, `rullst-media` and `rullst-labs` candidates, first published in the 13.0.0-alpha.1 prerelease. The package manifests remain the machine-readable source of truth. The matrix explains the behavior those names select in this v13 source and makes the default build visible before an application adopts optional integrations. Cargo features are additive across a dependency graph. An application can disable a package's defaults at the dependency edge, but it cannot disable a feature enabled by another dependency. Inspect the final selection with: ```bash cargo tree -e features ``` The release gates compile every package with no default features, every public umbrella feature in isolation, representative domain-package boundaries, and the complete workspace with all features. The isolated umbrella list, and this page's umbrella table and default features, are checked automatically against `rullst/Cargo.toml`, so a newly added public feature cannot silently escape the matrix. See [`check-feature-boundaries.sh`](https://github.com/Rullst/Rullst/blob/main/.github/check-feature-boundaries.sh) for the exact individual checks. ## v13 prerelease privacy additions The v13 candidate adds `rullst-privacy` as an optional release package. These features are absent from published 12.x releases and first shipped in the 13.0.0-alpha.1 prerelease, which requires an explicit version opt-in. They do not change default dependencies or select application policies automatically. | Umbrella feature | Standalone privacy feature | Contract | | :--- | :--- | :--- | | `privacy` | None | Independent empty base | | `privacy-age` | `age-assurance` | Proportional age-policy contracts | | `privacy-challenge-tokens` | `challenge-tokens` | Authenticated server challenge transport | | `privacy-sqlite` | `sqlite` | Shared-local age replay protection | | `privacy-postgres` | `postgres` | Age replay protection on one authoritative PostgreSQL database | | `privacy-consent` | `consent` | Purpose/version choices and effective withdrawal | | `privacy-consent-sqlite` | `consent-sqlite` | Shared-local consent state | | `privacy-consent-postgres` | `consent-postgres` | New candidate for shared consent across application hosts; independent of age assurance | See the [privacy package guide](https://github.com/Rullst/Rullst/blob/main/rullst-privacy/README.md) for initialization, runtime roles, data minimization, backup/failover obligations and acceptance. The local email-login candidate adds Auth `email-login-sqlite` and `email-login-postgres`, with facade `auth-email-login-sqlite` / `auth-email-login-postgres`. These optional paths reuse authoritative recovery accounts and opaque sessions; they do not enable email login on accounts or replace tenant/MFA policy. Hosted source/package admission remains pending; see [the email-login contract](https://rullst.github.io/Rullst/book/email-login.html). The scoped API-token candidate adds Auth `api-tokens-sqlite` / `api-tokens-postgres` and facade `auth-api-tokens-sqlite` / `auth-api-tokens-postgres`. They enable the corresponding recovery backend only; email login, JWT and OAuth remain independent. See the [API-token contract](https://rullst.github.io/Rullst/book/api-tokens.html) for current admission and boundaries. ## Umbrella crate: `rullst` The v13 prerelease private-storage candidate adds Core/facade `storage-multipart`, which selects `storage-s3`, bounded XML parsing and checkpoint key zeroization. It enables no database, queue or default feature. See [private multipart uploads](https://rullst.github.io/Rullst/book/private-multipart-uploads.html) for server-mediated parts, encrypted resumable checkpoints, cleanup responsibilities and admission. The default `rullst` dependency enables no feature: since 13.0.0-alpha.3 it compiles the HTTP runtime without the ORM, a SQLx driver or Core's durable SQLite queue, so a database-free application builds no database code. Up to 13.0.0-alpha.2 the defaults were `orm`, `drivers-all` and `queue-sqlite`. Select what the application uses: ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["strict-sqlite", "queue-sqlite"] } ``` Each `strict-*` backend enables `orm`; `drivers-all` enables `orm` with all three drivers. `orm` itself adds no SQLx driver: it compiles, but `Orm::init` and `Server::with_db` then fail at startup for every URL scheme whose driver no other enabled feature happens to add (`queue-sqlite`, for example, adds only SQLite), with an error that names the feature to enable. `default-features = false` keeps the same graph on 13.0.0-alpha.2, whose defaults still included the drivers. | Feature | Default | Enables | | --- | :---: | --- | | `orm` | no | `rullst-orm` and Core's ORM integration, without a SQLx driver; pair it with `drivers-all` or one `strict-*` backend | | `drivers-all` | no | `orm` plus the SQLite, PostgreSQL and MySQL/MariaDB SQLx drivers in the ORM and Core, and in Studio and Nexus when `studio` or `nexus` is enabled | | `orm-mongodb` | no | `orm` plus the MongoDB document adapter | | `orm-duckdb` | no | `orm` plus the in-process DuckDB analytics adapter | | `orm-turso` | no | `orm` plus typed Turso-primary CRUD/query, parameterized remote libSQL SQL over Hrana HTTP v3, transactions, reversible checked migrations, and a persistent offline fallback | | `orm-surrealdb` | no | `orm` plus SurrealDB HTTP document and bounded graph adapters | | `orm-scout` | no | `orm` plus bounded Meilisearch, Elasticsearch and Algolia Scout HTTP adapters | | `orm-pgvector` | no | `orm` plus typed pgvector SQLx values; use with `strict-postgres` for the supported live query contract | | `orm-qdrant` | no | `orm` plus bounded dense-vector Qdrant HTTP operations and offline fallback | | `orm-redis` | no | `orm` plus namespaced Redis Hash, Set and Sorted Set operations | | `queue-sqlite` | no | Core's durable SQLite queue backend | | `storage-s3` | no | v13 candidate: Core's bounded AWS S3 and Cloudflare R2 object operations and signed GET URLs; see [private object storage](https://rullst.github.io/Rullst/book/private-object-storage.html) | | `storage-multipart` | no | v13 candidate: `storage-s3` plus server-mediated multipart uploads with bounded XML parsing and checkpoint key zeroization; see [private multipart uploads](https://rullst.github.io/Rullst/book/private-multipart-uploads.html) | | `nexus` | no | The generated Nexus administration interface | | `nexus-tz` | no | v13: `nexus` plus `NexusTimeZone::named`, so `DateTimeUtc` form values are shown and read in a named IANA time zone with its daylight-saving rules; adds `jiff` with the system zoneinfo database and a bundled fallback | | `studio` | no | Studio plus Core's Studio integration marker | | `auth` | no | Authentication, sessions, passkeys, and RBAC helpers from `rullst-auth` | | `auth-jwt` | no | `auth` plus the strict application-issued JWT policy | | `auth-sqlite` | no | `auth-jwt` plus bounded shared SQLite JWT revocation and passkey device lifecycle state | | `auth-passkey-postgres` | no | Optional v13 account/session-bound PostgreSQL passkey ceremony candidate; host credential-counter CAS remains required | | `auth-sessions-sqlite` | no | v13 candidate: `auth` plus the SQLite account registry, opaque sessions and session inventory (Auth `recovery-sqlite`); see [session management](https://rullst.github.io/Rullst/book/session-management.html) | | `auth-sessions-postgres` | no | v13 candidate: `auth` plus the PostgreSQL account registry, opaque sessions and session inventory (Auth `recovery-postgres`) | | `auth-email-login-sqlite` | no | v13 candidate: `auth` plus email login over the SQLite recovery accounts and sessions; see [the email-login contract](https://rullst.github.io/Rullst/book/email-login.html) | | `auth-email-login-postgres` | no | v13 candidate: `auth` plus email login over the PostgreSQL recovery accounts and sessions | | `auth-api-tokens-sqlite` | no | v13 candidate: `auth` plus scoped API tokens over the SQLite recovery backend; see [the API-token contract](https://rullst.github.io/Rullst/book/api-tokens.html) | | `auth-api-tokens-postgres` | no | v13 candidate: `auth` plus scoped API tokens over the PostgreSQL recovery backend | | `account-mail-sqlite` | no | `auth` and `mail` plus the SQLite recovery store and the `account_mail` delivery bridge; see [account mail](https://rullst.github.io/Rullst/book/account-mail-v12-1.html) | | `account-mail-postgres` | no | `auth` and `mail` plus the PostgreSQL recovery store and the `account_mail` delivery bridge | | `mail` | no | `rullst-mail` with HTTP/offline transports and no SMTP dependency | | `mail-sqlite` | no | `mail` plus bounded shared-local SQLite recipient suppression and provider-event replay evidence | | `mail-postgres` | no | v13 candidate: shared PostgreSQL suppression with keyed identifiers, authoritative dispatch checks and independent quotas | | `mail-smtp` | no | `mail` plus the optional SMTP transport | | `mail-aws-ses` | no | `mail` plus native SES v2 delivery signed by the official AWS SDK | | `messaging` | no | Native bounded broker-neutral messaging contracts and the deterministic process-local broker | | `messaging-sqlite` | no | `messaging` plus fixed-schema durable local SQLite publication, lease, retry/DLQ, ACK and idempotency state | | `messaging-schedules-postgres` | no | v13 candidate: encrypted PostgreSQL recurring-publication outbox and fenced relay into an explicitly selected broker; no default SQLite/ORM | | `messaging-webhooks` | no | v13 candidate: encrypted shared-local SQLite outgoing outbox, immutable HTTPS destination, signed exact bytes, fenced retries and minimized terminal inspection | | `messaging-orm-outbox` | no | `messaging` and `orm` plus the static relational outbox-to-broker relay; the publish/ACK crash window remains at-least-once | | `messaging-redis` | no | v13 candidate: `messaging` plus the Redis Streams broker with fenced delivery indexes; enables no ORM, mail, cache or Core queue; see [Redis Streams messaging](https://rullst.github.io/Rullst/book/redis-messaging.html) | | `privacy` | no | v13 candidate: the independent empty `rullst-privacy` base; the `privacy-*` features are listed in [the privacy additions](https://rullst.github.io/Rullst/book/feature-matrix.html#v13-prerelease-privacy-additions) | | `privacy-age` | no | v13 candidate: `privacy` plus proportional age-policy contracts | | `privacy-challenge-tokens` | no | v13 candidate: `privacy-age` plus authenticated server challenge transport | | `privacy-sqlite` | no | v13 candidate: `privacy-age` plus shared-local age replay protection | | `privacy-postgres` | no | v13 candidate: `privacy-age` plus age replay protection on one authoritative PostgreSQL database | | `privacy-consent` | no | v13 candidate: `privacy` plus purpose/version choices and effective withdrawal | | `privacy-consent-sqlite` | no | v13 candidate: `privacy-consent` plus shared-local consent state | | `privacy-consent-postgres` | no | v13 candidate: `privacy-consent` plus shared PostgreSQL consent state | | `mailer` | no | Compatibility alias for `mail-smtp`; prefer `mail-smtp` in new manifests | | `queue-redis` | no | Redis dependency and Core's Redis queue backend | | `cache-redis` | no | Redis dependency and Core's Redis cache backend | | `redis` | no | Convenience alias enabling `queue-redis`, `cache-redis` and `orm-redis` | | `scheduler-tz` | no | v13: `Scheduler::task_in_tz` and `Scheduler::deferrable_task_in_tz`, cron tasks (and deferral windows) in a named IANA time zone with a documented daylight-saving rule; adds `jiff` with the system zoneinfo database and a bundled fallback | | `oauth` | no | OAuth2/OIDC providers from `rullst-connect` with its Axum integration (`rullst-connect/rullst`): the `AuthCallback` extractor and the local mock IdP router | | `oauth-sqlite` | no | `oauth` plus bounded encrypted shared-local token-generation state with exact SQLite compare-and-swap, and the v13 SQLite login-challenge store (`SqliteChallengeStore`) | | `oauth-postgres` | no | v13: `oauth` plus the PostgreSQL login-challenge store (`PostgresChallengeStore`) over the application's pool | | `oauth-mysql` | no | v13: `oauth` plus the MySQL/MariaDB login-challenge store (`MySqlChallengeStore`) over the application's pool | | `oauth-mock` | no | v13: `oauth` plus `rullst-connect/mock`, so empty or `mock_*` provider credentials build an offline mock provider that signs in a fixed mock user; enable it only in development and test builds | | `ai` | no | Provider-agnostic AI clients and local safeguards from `rullst-ai` | | `ai-sql-memory` | no | `ai` plus tenant-aware durable chat memory for SQLite, PostgreSQL, MySQL, and MariaDB | | `capital` | no | Provider-neutral billing contracts, the Stripe and experimental InfinitePay adapters, webhooks and analytics from `rullst-capital` | | `capital-actix` | no | `capital` plus the Actix Web adapter for the canonical signed-webhook verifier | | `capital-quota-sql` | no | `capital` and `orm` plus atomic shared resource quotas for SQLite, PostgreSQL, MySQL, and MariaDB | | `capital-webhook-sql` | no | `capital` and `orm` plus bounded durable webhook replay/event claims for SQLite, PostgreSQL, MySQL, and MariaDB | | `capital-pdf` | no | `capital` plus bounded validated native invoice PDF rendering | | `capital-mail` | no | `capital-pdf` plus Mail's payment-bound HTML/PDF attachment delivery bridge | | `security` | no | RASP/WAF and application-security primitives from `rullst-security` | | `security-redis` | no | `security` plus the atomic Redis rate limiter | | `iot` | no | IoT models, frame helpers, and signed OTA verification from `rullst-iot` | | `telemetry` | no | OpenTelemetry dependencies and Core's OTLP integration | | `strict-postgres` | no | `orm` with the concrete PostgreSQL pool/backend selected | | `strict-mysql` | no | `orm` with the concrete MySQL pool/backend selected when PostgreSQL is not also selected | | `strict-sqlite` | no | `orm` with the concrete SQLite pool/backend selected when PostgreSQL and MySQL are not also selected | The three `strict-*` backend features are supported as single selections; each enables only its own SQLx driver. Feature unification can activate more than one; the current deterministic precedence is PostgreSQL, then MySQL, then SQLite. Do not depend on that precedence as backend negotiation. Select one strict backend in an application, or select none and keep `drivers-all` to use SQLx `Any` with all three drivers. ### Shared-local SQLite composition profile A bounded single-host application may compose the following umbrella features over one file-backed SQLite URL: ```toml [dependencies] rullst = { version = "=13.0.0-alpha.4", default-features = false, features = [ "auth-sqlite", "capital-quota-sql", "mail-sqlite", "messaging-sqlite", "oauth-sqlite", "queue-sqlite", ] } ``` The stores use distinct fixed table namespaces. Initialize/check them sequentially, keep application readiness false until every required component is healthy, and reuse exactly the same URL, quotas, namespaces, and keys after restart. Close every handle before file-level backup or restore. The dedicated [`facade_recovery` test](https://github.com/Rullst/Rullst/blob/main/rullst/tests/facade_recovery.rs) proves restart, idempotency, encrypted-secret plaintext absence, queue recovery, aggregate readiness, and isolated fail-closed corruption for this exact profile. Sharing the file is not a cross-domain transaction or a multi-host design. The host still owns permissions, encryption-key custody, a consistent whole-file backup procedure, recovery drills, contention policy, and domain authorization. Prefer separate databases when failure isolation or write throughput is more important than simple local operation. ## Runtime and data crates ### `rullst-core` Default features: none. | Feature | Enables | | --- | --- | | `orm` | Optional `rullst-orm` and SQLx support, including Artisan and database-backed feature flags; it adds no SQLx driver | | `drivers-all` | `orm` plus the ORM's SQLite, PostgreSQL and MySQL/MariaDB drivers | | `queue-sqlite` | SQLx-backed durable SQLite queues without enabling the full ORM facade | | `queue-redis` | Redis-backed queues | | `cache-redis` | Redis-backed cache storage | | `redis` | Convenience alias for both Redis queue and cache backends | | `scheduler-tz` | v13: `Scheduler::task_in_tz` and `Scheduler::deferrable_task_in_tz` for cron tasks (and deferral windows) in a named IANA time zone (system zoneinfo, bundled `jiff` database as fallback) | | `storage-s3` | v13 candidate: bounded AWS S3 and Cloudflare R2 object operations and signed GET URLs | | `storage-multipart` | v13 candidate: `storage-s3` plus server-mediated multipart uploads with bounded XML parsing and checkpoint key zeroization | | `studio` | Integration marker used by the umbrella Studio boundary; it adds no dependency by itself | | `telemetry` | OpenTelemetry tracing and OTLP export dependencies | | `strict-postgres` | `orm` plus the ORM PostgreSQL backend selection | | `strict-mysql` | `orm` plus the ORM MySQL backend selection | | `strict-sqlite` | `orm` plus the ORM SQLite backend selection | Core's process-local Radar and span collector do not require `telemetry`. That feature is specifically for OpenTelemetry/OTLP integration. The `rullst.client` v1 codec and bounded `#[server_function]` transport are available without a feature flag; only the explicit generated route exists on native targets, while the same annotated function becomes its Wasm caller. Identity, authorization and tenant policy remain application layers. ### `rullst-orm` No default feature (13.0.0-alpha.2 and earlier defaulted to `drivers-all`). With no `strict-*` feature, public pool and database aliases use SQLx `Any`, which opens only the drivers compiled in; without any, `Orm::init` fails with an error naming the feature to enable. The workspace and the umbrella crate depend on the ORM with `default-features = false` and forward `drivers-all` or a `strict-*` backend explicitly. | Feature | Enables | | --- | --- | | `drivers-all` | The SQLite, PostgreSQL and MySQL/MariaDB SQLx drivers | | `redis` | Redis query cache plus bounded namespaced Hash, Set and Sorted Set datastore operations | | `mongodb` | Official MongoDB driver plus typed document CRUD, identifier inventory, encrypted recovery participation and offline fallback | | `duckdb` | Bundled DuckDB client plus parameterized, bounded analytics queries | | `turso` | Direct official Hrana HTTP v3 transport, typed primary CRUD/query facade, parameterized SQL, atomic batches, reversible checksummed migrations, and a persistent SQLite-compatible offline fallback | | `surrealdb` | SurrealDB HTTP document CRUD, identifier inventory, encrypted recovery participation and bounded read-only ISO GQL; no embedded SDK | | `scout-http` | Bounded Meilisearch, Elasticsearch and Algolia adapters with deterministic offline fallbacks; Meilisearch also has a live container contract | | `pgvector` | Typed pgvector SQLx values and parameterized L2/cosine/inner-product helpers; the live contract also selects `strict-postgres` | | `qdrant` | Bounded dense-vector collection/upsert/delete/cosine query operations over HTTP with offline fallback | | `ai` | `pgvector` plus the generated `save_with_embedding` for `#[orm(embedding_for = "...")]` models; the application also depends on `rullst-ai` | | `strict-postgres` | The PostgreSQL driver plus concrete PostgreSQL pool, database, query-result, and query paths | | `strict-mysql` | The MySQL driver plus concrete MySQL paths when PostgreSQL is not also selected | | `strict-sqlite` | The SQLite driver plus concrete SQLite paths when PostgreSQL and MySQL are not also selected | The strict backend selection rules and precedence are the same as the umbrella crate. Each `strict-*` feature enables its own SQLx driver and selects concrete public types and query paths; on 13.0.0-alpha.2 and earlier, disable the ORM defaults to keep the other drivers out of the graph. The Polyglot features expose capability-specific APIs under `rullst_orm::polyglot`; they do not participate in a shared cross-backend transaction. The base deterministic document store and the MongoDB/SurrealDB adapters implement `DocumentInventory` for an application-operated bounded snapshot/restore contract; it does not supply online isolation or managed backup. Turso can additionally be selected explicitly by `#[orm(backend = "turso")]` and the blank/API scaffold. See the [Polyglot Persistence guide](https://rullst.github.io/Rullst/book/polyglot-persistence.html). ### `rullst-orm-macros` Default features: none. `#[derive(Orm)]` uses a fail-closed structured parser: unknown/duplicate options, missing persisted targets, conflicting relations, unsafe identifiers, and SQLx mappings that generated persistence cannot honor are compile errors. The exact derive grammar and its raw soft-delete-expression boundary are defined in the packaged crate README and the [SST](https://rullst.github.io/Rullst/book/spec.html#51-model-definition--crud). | Feature | Enables | | --- | --- | | `runtime-driver-codecs` | Enum codecs follow the ORM runtime's selected SQLx drivers; `rullst-orm` enables it | | `runtime-feature-gates` | v13 candidate: Redis and embedding APIs are emitted or omitted at expansion time from the forwarded `redis`/`ai` features instead of the application's own features; `rullst-orm` enables it | | `redis` | v13 candidate: forwarded by `rullst-orm/redis`; emits the Redis cache, hash and event code under `runtime-feature-gates` | | `ai` | v13 candidate: forwarded by `rullst-orm/ai`; emits `save_with_embedding` under `runtime-feature-gates` | | `strict-postgres` | Compatibility marker matching the ORM backend vocabulary; no macro expansion changes | | `strict-mysql` | Compatibility marker matching the ORM backend vocabulary; no macro expansion changes | | `strict-sqlite` | Compatibility marker matching the ORM backend vocabulary; no macro expansion changes | ### `rullst-connect` Default features: none. Provider clients and framework-independent OAuth/OIDC types remain available without a web-framework adapter. | Feature | Enables | | --- | --- | | `axum` | Axum callback extractors and the local mock IdP router | | `actix` | Actix Web callback extractors | | `leptos` | Framework-independent callback extractor module for Leptos integration; no Leptos runtime dependency | | `rullst` | Convenience integration boundary that enables `axum` | | `retry` | Retry-aware HTTP client behavior using `reqwest-middleware` and `reqwest-retry` | | `reqwest-middleware` | The optional middleware dependency alone; prefer `retry` for retry behavior | | `axum-session` | Axum plus a ten-minute, one-active-challenge `tower-sessions` state/PKCE/OIDC-nonce transaction and callback extractor | | `sqlite` | File-backed shared-local encrypted token snapshots with persisted quota, restart recovery and exact generation compare-and-swap; remote refresh leases, key custody and multi-host operation remain application concerns. v13: also the SQLite login-challenge store over the application's `SqlitePool` | | `postgres` | v13: the PostgreSQL login-challenge store over the application's `PgPool` (`DELETE … RETURNING` consumption) | | `mysql` | v13: the MySQL/MariaDB login-challenge store over the application's `MySqlPool` (`SELECT … FOR UPDATE` then `DELETE` in one transaction) | | `mock` | Deterministic offline provider modules outside test builds | ### `rullst-messaging` Default features: none. The deterministic process-local broker, versioned envelope, idempotency, consumer groups, leases, retry, dead-letter, and purge contracts are available without optional dependencies. The only remote broker adapter is the v13 Redis Streams candidate (13.0.0-alpha.1 prerelease); other remote brokers are not implemented and have no placeholder features. | Feature | Enables | | --- | --- | | `sqlite` | Fixed-schema durable local broker with serialized SQLite writes and immutable plaintext or explicit AES-256-GCM content profiles; restart/corruption/rotation/tamper/two-instance evidence is local, while metadata visibility, key custody and remote replication/failover remain explicit boundaries | | `orm-outbox` | Static bridge from the relational `rullst-orm` outbox to one configured broker topic, with exact replay after the publish-before-ACK crash window; worker operations and remote atomicity remain application boundaries | | `redis-streams` | v13 candidate: standalone Redis Streams broker with Rullst-owned fenced delivery indexes; see [Redis Streams messaging](https://rullst.github.io/Rullst/book/redis-messaging.html) | | `schedules-postgres` | v13 candidate: encrypted PostgreSQL recurring-publication outbox and fenced relay into a `MessageBroker`; enables neither SQLite nor ORM | | `webhooks` | v13 candidate: `sqlite` plus one immutable approved HTTPS destination, HMAC-SHA256 signing, bounded retry/dead-letter state, cancellation and terminal retention | ### `rullst-iot` Default feature: `std`. | Feature | Enables | | --- | --- | | `std` | Standard-library support in serialization and Ed25519 dependencies; disabling it makes the crate `no_std` + `alloc` | | `experimental-simulators` | Deterministic MQTT formatting, HSM, and PQC fixtures; not live transports, hardware-backed keys, or production PQC | ### `rullst-capital` Default feature: `axum`. | Feature | Enables | | --- | --- | | `axum` | Axum middleware for the canonical bounded signed-webhook verifier | | `actix` | Actix Web middleware for the same verifier; it does not enable Axum when selected directly | | `quota-sql` | Durable idempotent shared quota accounting over SQLite, PostgreSQL, MySQL, and MariaDB; schema setup/migrations and authoritative membership/tier state remain application-owned | | `webhook-sql` | Bounded durable provider-scoped payload/event claims over SQLite, PostgreSQL, MySQL, and MariaDB, including a caller-owned transaction path; cross-system effects and reconciliation remain application-owned | | `invoice-pdf` | Bounded paginated A4 invoice PDF with embedded WinAnsi or a validated caller-supplied TTF/OTF; payment/mail orchestration is separate | ### `rullst-mail` Default features: none. HTTP mail providers remain available without SMTP. | Feature | Enables | | --- | --- | | `mail-smtp` | Lettre-based SMTP transport | | `aws-ses` | Official AWS SES v2 SDK, regional SigV4, temporary/rotating credential providers and native attachments/CID; AWS account readiness and inbox delivery remain external | | `capital-invoice` | Capital's native invoice PDF plus the final-payment-bound delivery bridge; durable outbox claiming remains application-owned | | `sqlite` | File-backed shared-local suppression state with exact provider-event replay binding and immutable quotas; webhook authentication, encryption and multi-host replication remain application-owned | | `postgres` | v13 candidate: namespaced suppression on one authoritative writable PostgreSQL database; independent of SQLite. See [shared mail suppression](https://rullst.github.io/Rullst/book/shared-mail-suppression.html) for initialization, runtime grants, retention and pending admission | ### `rullst-auth` Default features: none. | Feature | Enables | | --- | --- | | `oauth` | Optional `rullst-connect` OAuth2/OIDC integration and re-exports | | `jwt` | Application-issued JWT claims, key rotation, and revocation-store policy | | `sqlite` | `jwt` plus bounded file-backed shared JWT revocation and passkey device lifecycle state | | `recovery-sqlite` | SQLite account registry, recovery and opaque sessions (`SqlRecoveryStore`); the v13 candidate adds session inventory/logout | | `recovery-postgres` | The same recovery store on PostgreSQL | | `passkey-postgres` | v13 candidate: shared account/session-bound PostgreSQL passkey ceremonies | | `email-login-sqlite` | v13 candidate: `recovery-sqlite` plus email login over its accounts and sessions; see [the email-login contract](https://rullst.github.io/Rullst/book/email-login.html) | | `email-login-postgres` | v13 candidate: `recovery-postgres` plus email login | | `api-tokens-sqlite` | v13 candidate: `recovery-sqlite` plus scoped API tokens; see [the API-token contract](https://rullst.github.io/Rullst/book/api-tokens.html) | | `api-tokens-postgres` | v13 candidate: `recovery-postgres` plus scoped API tokens | The umbrella crate exposes `jwt` and `sqlite` as `auth-jwt` and `auth-sqlite` (`auth-sqlite` also enables `auth-jwt`), `recovery-*` as `auth-sessions-*`, `passkey-postgres` as `auth-passkey-postgres`, and the email-login and API-token features as `auth-email-login-*` and `auth-api-tokens-*`; each enables `auth`. `account-mail-*` also selects `recovery-*`. The umbrella does not forward Auth's `oauth`; its own `oauth` feature adds `rullst-connect` directly. ### `rullst-security` Default features: none. | Feature | Enables | | --- | --- | | `redis-rate-limit` | Atomic namespaced Redis fixed-window limiter plus its explicit offline mock mode; CI/release run the independent-client contract against a digest-pinned Redis service | The umbrella crate exposes this as `security-redis`, which also enables `security`. ### `rullst-ai` Default features: none. Provider clients, prompt inspection and PII masking are always available. | Feature | Enables | | --- | --- | | `sql-memory` | `SqlChatMemory`: tenant-aware durable chat memory for SQLite, PostgreSQL, MySQL and MariaDB | The umbrella crate exposes this as `ai-sql-memory`, which also enables `ai`. ## Dashboard crates `rullst-nexus` enables `drivers-all` by default, and `rullst-studio` enables `drivers-all` and `queue-sqlite`. Disable their defaults to select one backend: | Crate | Feature | Enables | | --- | --- | --- | | `rullst-nexus` | `drivers-all` | All three SQLx drivers in Core and ORM (default) | | `rullst-nexus` | `strict-postgres` | PostgreSQL selection in Core and ORM | | `rullst-nexus` | `strict-mysql` | MySQL selection in Core and ORM | | `rullst-nexus` | `strict-sqlite` | SQLite selection in Core and ORM | | `rullst-nexus` | `tz` | v13: `NexusTimeZone::named` for `DateTimeUtc` form values in a named IANA time zone (system zoneinfo, bundled `jiff` database as fallback) | | `rullst-studio` | `drivers-all` | All three SQLx drivers in Core and ORM (default) | | `rullst-studio` | `queue-sqlite` | Core's durable SQLite queue backend (default) | | `rullst-studio` | `strict-postgres` | PostgreSQL selection in Core and ORM | | `rullst-studio` | `strict-mysql` | MySQL selection in Core and ORM | | `rullst-studio` | `strict-sqlite` | SQLite selection in Core and ORM | | `rullst-studio` | `remote` | The standalone `rullst-studio` binary's remote-projects mode (see [production monitoring](https://rullst.github.io/Rullst/book/production-monitoring.html#watch-your-projects-from-studio)); the binary requires it | Use the same single-selection rule described for `rullst-orm`. ## v13 prerelease candidate packages `rullst-supervision`, `rullst-media` and `rullst-labs` are in the v13 release inventory and were first published in the 13.0.0-alpha.1 prerelease. They have no umbrella features; depend on them directly. None has default features. | Crate | Feature | Enables | | --- | --- | --- | | `rullst-supervision` | `exam` | Typed exam sessions, collection categories and browser/capture observations | | `rullst-supervision` | `parental` | Course/window parental restriction policies | | `rullst-supervision` | `analysis` | `exam` plus bounded camera-presence/audio-activity adapter contracts | | `rullst-supervision` | `sqlite` | `exam` and `parental` plus the shared-local SQLite store | | `rullst-media` | `bunny` | Bunny Stream adapter, signatures, bounded HTTP and the browser upload module | | `rullst-media` | `sqlite` | Shared-local durable assets, leased operations and the application service | | `rullst-media` | `s3` | Experimental S3-compatible object storage (AWS S3, Cloudflare R2, MinIO): presigned upload and playback of the original, no transcoding | | `rullst-labs` | `sqlite` | Encrypted shared-local exercise and job storage with leases, cancellation and retention | | `rullst-labs` | `receipt-signing` | Receipt signing for the application-owned runner controller | ## Packages without optional features These packages have no public optional Cargo features: | Package | Always-available scope | | --- | --- | | `rullst-macros` | Core procedural macros | | `cargo-rullst` | CLI commands, generators, auditing, deployment helpers and the Experimental `cargo rullst green`; its internal `maintainer-tools` feature only builds the repository's `sync-badges` tool and is not part of the installed CLI | No optional feature does not mean that a provider is contacted automatically. External integrations still require explicit runtime configuration and use the documented deterministic offline behavior for empty or `mock_*` credentials. ## Selection recipes Minimal HTTP runtime (from 13.0.0-alpha.3 the defaults are empty, so `default-features = false` changes nothing there): ```toml rullst = { version = "=13.0.0-alpha.4", default-features = false } ``` SQLite application with the ORM, every SQLx driver and the SQLite queue, which were the defaults up to 13.0.0-alpha.2 and must be named since alpha.3: ```toml rullst = { version = "=13.0.0-alpha.4", features = ["orm", "drivers-all", "queue-sqlite"] } ``` PostgreSQL application with explicit domain integrations: ```toml rullst = { version = "=13.0.0-alpha.4", default-features = false, features = ["strict-postgres", "auth", "security", "telemetry"] } ``` Embedded IoT model without the standard library: ```toml rullst-iot = { version = "=13.0.0-alpha.4", default-features = false } ``` Experimental IoT fixtures are deliberately separate: ```toml rullst-iot = { version = "=13.0.0-alpha.4", default-features = false, features = ["experimental-simulators"] } ``` --- # Compatibility, MSRV & Support Policy Source: https://rullst.github.io/Rullst/book/compatibility-policy.html # Compatibility, MSRV, deprecation, and support policy This policy applies to the packages in the [release inventory](https://github.com/Rullst/Rullst/blob/main/.github/release-order.json): 16 packages in the v12 line and 20 in v13, which adds `rullst-privacy`, `rullst-supervision`, `rullst-media` and `rullst-labs`. They are released as one synchronized release train even when a user depends on only one crate. The current supported-version table in [`SECURITY.md`](https://github.com/Rullst/Rullst/blob/main/SECURITY.md) remains authoritative for releases that are actually available; a version in the workspace is not a supported release merely because its manifest exists. ## Source branches The permanent branches are `main` (v13 development and its 13.0.0 prereleases), `v12` (frozen; 12.x reached end of life on 16 October 2026) and `gh-pages` (generated site/benchmark data). Use short-lived feature, fix and Dependabot branches with a pull request to the appropriate protected base. Remove those temporary branches after their changes are integrated; a superseded PR must identify the replacement preserving its changes. The former `v13` integration branch is retired. Historical releases remain available through immutable tags and crates.io archives. Select a published version for application dependencies; a branch name is not a release. Stable and development branches have independent commit histories: ahead/behind counts are not a measure of security, freshness or missing fixes. Do not reset or merge whole release trains merely to make those counters zero. ### Carrying fixes between maintained lines Every stable maintenance fix must be assessed for `main`. Before closing its maintenance item, record the linked forward-port PR, the commit where the fix already exists, or a specific reason it does not apply. Carry the regression and relevant documentation with the fix, adapt it to the destination API and run that branch's required checks before integration. Conversely, evaluate confirmed v13 fixes for the existing v12 surface without importing new v13 features or breaking the stable API/MSRV. Versions, release records, feature inventories and branch-specific documentation stay appropriate to each release train. Dependency updates require the same assessment, but resolve and validate each branch's own graph; do not copy a whole lockfile across diverged workspaces. Prefer a linked, focused port over a blanket merge. A briefly pending port must stay explicitly tracked rather than being hidden by commit counts. This policy does not establish an indefinite LTS commitment or alter the support window below. ## Semantic Versioning contract Rullst follows Cargo Semantic Versioning for stable releases: - **Patch (`x.y.Z`)** releases contain compatible bug, security, documentation, and dependency fixes. They do not intentionally remove public APIs, public Cargo features, CLI commands, or accepted configuration fields. - **Minor (`x.Y.z`)** releases may add compatible APIs, opt-in features, diagnostics, and deprecations. Changes to defaults or generated projects must include migration notes and compatibility tests. - **Major (`X.y.z`)** releases may contain breaking changes. Each known break must be listed in the changelog and migration guide. - **Prereleases** such as `13.0.0-alpha.1` are public evaluation artifacts. Their APIs may still change in a later alpha, beta or RC, and users must opt in explicitly. Published prerelease archives are immutable, just like stable releases; fixes require a new version. The compatibility surface includes documented public Rust APIs, public Cargo feature names, CLI command and flag identifiers, supported configuration keys, serialized public data contracts, and generator output relied on by the packaged-distribution tests. Undocumented internals, exact dashboard HTML/CSS, test fixtures, deterministic mocks, and third-party provider behavior are not stable interfaces. ### Maturity tiers (v13) From `13.0.0`, each package has a [maturity tier](https://rullst.github.io/Rullst/book/maturity.html). Core and Extension packages are supported and follow the contract above for the whole 13.x line. Experimental packages, experimental CLI generators and facade features that re-export an experimental crate are the exception: their public APIs, features, schemas and generated output may change between 13.x releases. Every such break is still listed in the changelog and migration guide. Security handling is the same for every tier. ## Minimum Supported Rust Version The v12 release line and v13 both declare **Rust 1.96.0** in every publishable manifest. - Patch releases do not raise the MSRV. - A minor release may raise it only with an explicit changelog entry, updated manifests and documentation, and a green MSRV CI job for the release commit. - A major release may select a new baseline, which must be announced in its migration guide. The MSRV promise covers the supported feature boundaries exercised by CI. A nightly-only analysis tool or an experimental target does not change the crate MSRV and must be labelled separately. ## Deprecation and removal A stable public API scheduled for removal is marked with `#[deprecated]` and kept for at least one released minor version before removal in the next major release. Documentation must name the replacement when one exists. An API that is unsound, enables a security bypass, or cannot be made safe may be disabled or removed sooner. Such an exception requires a security advisory or changelog entry describing impact, affected versions, and the supported migration path. Compatibility never overrides safety or fail-closed behavior. Removing a public Cargo feature is a major change. Adding an opt-in feature is normally minor-compatible. Changing default features is treated as an operationally significant minor change and requires package and generated-app regression tests. ## Supported release window Rullst does not currently promise an LTS or multi-minor backport program. Routine fixes target the latest patch of the latest supported stable minor. Until 13.0.0 is published, only the most recent 13.0.0 prerelease is supported and fixes ship in a new prerelease. The exact versions receiving security triage are listed in `SECURITY.md`; older versions are end-of-life unless that table explicitly says otherwise. Release candidates receive fixes through a subsequent RC number rather than by overwriting a published artifact. Security reports follow the coordinated disclosure and response targets in `SECURITY.md` and the [advisory-exception policy](https://rullst.github.io/Rullst/book/security-advisory-exceptions.html). ## Release evidence Before publishing a stable or prerelease version, the release commit must prove the applicable compatibility contract with: 1. synchronized package versions and exact (`=`) internal requirements, so Cargo cannot pair packages from different releases; 2. the MSRV and multi-OS CI jobs; 3. full tests, strict Clippy, formatting, docs, and feature-boundary checks; 4. packaged crates and generated applications tested without monorepo paths; 5. a changelog and migration notes for every intentional behavior change. Passing these gates is evidence for the exact commit and declared matrix, not a guarantee about untested platforms, external providers, or downstream application code. --- # Capability Status & Vision Decisions Source: https://rullst.github.io/Rullst/book/capability-ledger.html # Capability status and vision decisions Rullst's ambitious ideas are not deleted when an implementation is incomplete. They are kept here with an explicit status and an engineering recommendation. User-facing guides describe what can be used today; this ledger preserves the larger vision without presenting roadmap work as a production capability. Status is evaluated against the current v13 development source tree (the 13.0.0 prereleases), not against an old changelog entry or marketing description: - **Implemented**: a real implementation and focused tests exist; deployment responsibilities may still apply. - **Partial**: useful foundations exist, but the advertised end-to-end contract is not complete. - **Experimental**: available only behind an explicit experimental/simulator boundary and carries no production guarantee. - **Not implemented**: absent or intentionally returns a typed `Unsupported` error instead of simulating success. - **Do not promise**: the absolute claim is not a technically honest product contract; a narrower engineering goal may still be worthwhile. The recommendation column is intentionally opinionated. "Worth implementing" does not mean "put it in Core": several excellent ideas belong in optional, independently tested crates. Ideas that additionally need continuous operations, homologation, or hardware can follow the [Maybe SaaS incubation strategy](https://rullst.github.io/Rullst/book/maybesaas.html). ## Architecture and framework contract | Capability or former claim | Current status | Recommendation and reason | |---|---|---| | Runtime-only Core with optional ORM | **Implemented in current hardening (keep validating)** | Bare `rullst-core` now defaults to the HTTP/runtime surface without SQLx/ORM; `orm` and `queue-sqlite` are independent opt-ins. Studio and Nexus request their database features explicitly, and since 13.0.0-alpha.3 the application umbrella has no default feature either: `orm`, a driver profile and `queue-sqlite` are opt-ins. | | Lifecycle-aware readiness and graceful request drain | **Implemented / bounded per process** | `ApplicationLifecycle` holds monotonic startup/ready/draining/stopped state, at most 32 immutable validated required-component bits, fail-closed request admission and a bounded wait for accepted requests. The lifecycle-aware `/ready` emits only phase/counts and becomes `503` for startup, dependency failure, lock corruption and drain while liveness remains process-only. `Server` marks ready after binding, begins drain before Axum's graceful wait, marks startup failure/termination stopped, and exposes a caller-supplied shutdown future for supervisors/tests. Applications still run timeout-protected dependency checks, establish identity/authorization and operate replica, load-balancer, `preStop` and termination-deadline behavior. | | One canonical security stack | **Partial (composition risk mitigated; high priority)** | Core and the extended crate now reuse one `CspNonce`, so nested header layers do not invalidate renderer output. Ownership is still split: the Server mounts Core CSRF/WAF/headers/PII while `rullst-security` supplies explicit RASP/DLP/abuse/telemetry layers. Introduce a Core stack contract implemented by the dedicated crate, then deprecate duplicates after parity tests; reversing the dependency directly would create a cycle. | | Umbrella features for every advertised crate | **Implemented in current hardening (keep tested)** | The umbrella exposes optional `security`, `iot`, base `mail` and other domain features, restores the `oauth`/Connect re-export, keeps SMTP separate through `mail-smtp`, and has focused facade tests including payment-bound Capital/Mail composition. The extended security suite remains nested under `security::runtime` because its surface intentionally overlaps the Core baseline; `CspNonce` itself is already one shared type. | | Static dispatch everywhere | **Partial (do not force absolutely)** | Preserve generic/static paths for common cases, but runtime provider registries legitimately need dynamic dispatch. Document each intentional `dyn Trait` boundary instead of pretending it does not exist. | | A universal environment/configuration policy | **Implemented (keep)** | The validated environment model and fail-closed production startup use exact `RULLST_ENV` → legacy `APP_ENV` → file precedence. New `.env`, Kubernetes (`make:k8s`) and billing scaffolds emit/read the canonical name first; retain the alias for existing applications and keep negative configuration tests. | | Every source file below 500 lines | **Partial (worth continuous refactoring)** | Keep 500 lines as a design target, not a release fiction. Split production modules by responsibility; large test fixtures can use a looser limit. | | Uniform `#[non_exhaustive]`, fallible builders, and `impl Into<String>` | **Partial (worth completing incrementally)** | Finish this during SemVer-reviewed API work. Mechanical mass changes without compatibility review are not worth the risk. | | “Zero lock-in” or complete automatic ejection | **Do not promise (migration tooling is worthwhile)** | Standard Axum/SQLx escape hatches and an inspected eject snapshot reduce coupling, but no full-stack framework can guarantee zero migration cost for every optional subsystem. | | “Zero panic/crash-free runtime” | **Do not promise (the scoped policy is worthwhile)** | Enforce typed errors and reject `panic!`/`unwrap`/`expect` in declared production paths. Dependencies, OOM, aborts, and host failures make an absolute runtime guarantee impossible. | | “Zero latency/zero allocation everywhere” | **Do not promise (benchmark instead)** | Bounded allocation and latency goals are excellent, but only reproducible per-operation benchmarks should carry numbers. | | ORM performance versus Diesel and SeaORM | **Implemented reproducible bounded harness; no superiority claim** | A pinned Criterion target runs five equivalent operations against typed SQLite with one connection per ORM, the same schema/index/100-row seed and identical SQLite policy. CI records Rullst, Diesel and SeaORM under the same commit/runner. The initial local smoke contradicted the historical “negligible overhead versus Diesel” claim, which is therefore rejected rather than marketed away. SQLite microbenchmarks do not establish networked database throughput, concurrency, memory, tail latency or full-application performance. | | “100% memory-safe/no `unsafe` anywhere” | **Do not promise (an unsafe allowlist is worthwhile)** | Rust dependencies and the development FFI/hot-swap boundary contain reviewed `unsafe`. Keep the allowlist tiny, documented with `SAFETY`, and CI-enforced instead of hiding it. | | “100% Pure-Rustls for every feature” | **Do not promise (a transport inventory is worthwhile)** | Prefer Rustls-backed first-party clients, then inspect the complete feature-specific lockfile. Transitive and optional stacks make a universal brand claim brittle. | | Static competitor matrix declaring other frameworks lack features | **Do not maintain without dated sources** | Comparative research can be useful, but ecosystems change quickly and absence claims are easy to get wrong. Prefer a Rullst capability/boundary matrix or a dated, sourced benchmark repository. | | Framework-wide “production-ready” label | **Do not promise as one boolean** | Publish stability per crate/capability. Routing can be stable while a live payment provider or hardware integration remains unvalidated. | ## ORM and persistence | Capability or former claim | Current status | Recommendation and reason | |---|---|---| | SQLx Active Record, transactions, migrations and relations | **Implemented / bounded** | Keep the PostgreSQL, MySQL/MariaDB and SQLite matrices. Executor-aware generated operations and explicit transaction handles have commit/rollback evidence; typed `morph_to` plus `morph_many`/`morph_one` cover declared SQLx polymorphic targets without a runtime type registry. Caller-owned raw SQL must use the supplied transaction rather than assuming pool calls are scoped. | | Compile-time query typing | **Implemented / bounded typed path** | Generated primitive-field filters require the persisted Rust value type and generated column enums reject unknown columns. Dynamic string-column builders, custom conversions and raw SQL intentionally remain runtime checked; do not describe them as SQLx `query!`-style live-schema verification. | | Native relational enum mapping | **Implemented / bounded per SQLx profile** | `#[derive(Enum)]` owns one validated label mapping for Serde, string/`RullstValue` conversion and SQLx encode/decode. `Blueprint::native_enum` creates and exact-drift-checks a named type on `strict-postgres`, emits inline `ENUM` on MySQL/MariaDB, and emits `TEXT CHECK` on SQLite. SQLx Any PostgreSQL is rejected before DDL because it cannot decode custom types. Adding, removing or reordering variants, deployment ordering and PostgreSQL type removal remain reviewed migration work. | | Fail-closed model tenant scopes | **Implemented / bounded** | `tenant_column` now requires a supported persisted field, denies queries/mutations without `with_tenant`, binds tenant predicates and rejects cross-tenant instance mutations. Keep `unscoped()` explicit and require application authorization/database RLS where appropriate. | | Stable large-table chunk traversal | **Implemented / bounded** | Use fallible `chunk_by_id` for ascending keyset traversal over generated `i32` IDs; deleting processed rows cannot shift later rows behind an offset. `chunk` remains offset-based, and neither API claims a server cursor, cross-shard snapshot or immunity to unbounded concurrent inserts. | | Automatic schema synchronization and destructive migration | **Partial** | The SQLite AST diff is a useful reviewed scaffold. Do not claim universal type-safe synchronization, autonomous DDL, backup or rollback; add backend-specific plans and approval gates before expanding it. | | Transactional typed partial updates | **v13 prerelease / source admitted in PR #236** | The [selected-field merge](https://rullst.github.io/Rullst/book/transactional-partial-updates.html) reads the current scoped row, runs the full save lifecycle in a savepoint, and updates the caller object after success. Audits and managed post-commit effects share that boundary. This changes v12 selected-column SQL behavior and does not provide expected-version conflict rejection. | | Strictly post-commit hooks/search/cache invalidation | **Implemented / bounded process-local contract; durable use is explicit** | `after_commit`, generated `committed` observers, Redis table-cache invalidation/pub-sub and Scout projections run after direct generated or `Orm::transaction` commit; rollback drops them and `PostCommit` distinguishes a failed effect after durable persistence. A raw caller-owned SQLx transaction cannot reveal its later commit decision. These hooks are intentionally not converted to durable events without an application-selected schema/payload/policy. | | Durable relational transactional outbox | **Implemented / bounded opt-in primitive** | `Outbox::enqueue` refuses independent commit and writes an idempotent event with the domain transaction; `OutboxMigration` supplies reviewed schema registration. PostgreSQL, MySQL, MariaDB and SQLite live matrices cover commit, rollback, duplicate reuse, claim and ACK. SQLite regressions additionally cover conflicting key reuse, stream isolation, exact/unexpired claim tokens, concurrent claim exclusion, lease recovery, bounded retry and dead-letter. Delivery is at least once: consumers must be idempotent, and automatic observer/webhook dispatch, tenant authorization, cleanup, cross-store transactions and distributed operations remain application boundaries. | | Native external search and Qdrant | **Implemented / bounded per adapter** | `scout-http` supplies Meilisearch, Elasticsearch and Algolia projection/search adapters; only Meilisearch has a live service lifecycle. Separately, `qdrant` supplies bounded dense-vector create/upsert/delete/cosine query operations, deterministic fallback, authenticated protocol fixtures and a digest-pinned live lifecycle. Generated Scout projection durability, hosted-provider certification, arbitrary Qdrant filters/vectors and ANN tuning remain explicit. | | Native Redis data structures | **Implemented / bounded Hash, Set and Sorted Set contract** | `redis` supplies an immutable application namespace, validated inputs, bounded reads, TLS-required remote configuration, redacted ACL credentials and deterministic fallback. A pinned live lifecycle proves hash increments, set membership/scan, sorted ranking, exact structure deletion and namespace isolation. Lists, Streams, Pub/Sub durability, cluster/failover, eviction and tenant authorization are not implied. | | Typed PostgreSQL pgvector queries | **Implemented / bounded feature-gated contract** | `pgvector` plus `strict-postgres` exposes SQLx-compatible `Vector` and parameterized L2/cosine/inner-product helpers. A digest-pinned live matrix installs the extension, inserts typed values and executes bounded similarity queries. Embedding policy, ANN index tuning, RAG orchestration, authorization and production topology remain application/deployment work. | | Polyglot MongoDB, DuckDB, Turso and SurrealDB boundary | **Implemented / bounded per adapter, with application-operated document recovery** | Keep document, OLAP, SQL and graph capabilities separate, with bounds and deterministic offline modes. MongoDB, SurrealDB and the deterministic store now retain IDs through `DocumentInventory`; an AES-256-GCM envelope binds key ID/application/collection, caps documents/bytes, compares two source observations, resumes only into an exact subset and verifies the final inventory. The live matrix crosses MongoDB→SurrealDB→MongoDB. Applications must quiesce writers and own key/snapshot durability; this is not online snapshot isolation, a universal Active Record facade, managed backup, replication or a cross-store transaction. | | Transparent edge replication and latency guarantees | **Not implemented** | SQLx read replicas can be configured, but Turso synchronization, health/failover and consistency remain vendor/deployment concerns. Benchmark measured topology; never promise “1 ms” universally. | | Historical ORM roadmap completeness | **Fully classified, not fully implemented** | All 45 unique former `[x]` claims were classified for v12: 28 bounded integral contracts/evidence items and 17 partial. The frozen IDs/states remain in [the compact historical ledger](https://github.com/Rullst/Rullst/blob/main/.github/historical-roadmap-ledger-v12.tsv), and the full claim text remains in the immutable [v12 source snapshot](https://github.com/Rullst/Rullst/blob/v12.0.0/docs/src/v12.md). The comparative benchmark is delivered evidence whose first result refuted, rather than confirmed, the historic performance wording. | ## Capital and billing vision | Capability or former claim | Current status | Recommendation and reason | |---|---|---| | Built-in provider set | **Narrowed in v13** | Capital keeps its provider-neutral base, Stripe (supported) and InfinitePay (experimental until validated live; live checkout and callbacks fail closed). The Paddle, Lemon Squeezy, Polar, Razorpay, Mercado Pago, Alipay, Coinbase Commerce and PicPay adapters and the Wise payout adapter were removed to keep the maintained surface reviewable; applications implement other gateways with the provider traits ([custom providers](https://rullst.github.io/Rullst/book/capital-custom-provider.html)). See the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html). | | NFS-e Nacional preparation and issuance | **Removed in v13 (possible separate product)** | The offline DPS preview and local NFS-e 1.01 preparation were never validated with a real municipality and carried a high maintenance cost. They left the framework in v13; the code remains in Git history and may return as a separate product outside Rullst. Never infer tax authorization from a Rullst receipt. | | Uniform live support across built-in adapters | **Partial (worth a capability matrix)** | Publish method-by-method support and provider contract tests. An adapter name must not imply checkout, subscriptions, portal, tax, and webhook support all exist; InfinitePay remains experimental. | | Shared outbound gateway failure policy | **Implemented / bounded** | Reviewed live methods use finite connect/request timeouts, disabled redirects and ambient proxies, one-MiB JSON decoding and credential-free HTTPS checkout URL validation. Redacted typed evidence distinguishes permanent, transient and rate-limited failures without automatic mutation retries. Durable provider-forwarded idempotency, backoff/attempt policy, reconciliation, explicit proxy configuration and live acceptance remain application or future adapter work. | | Axum and Actix signed-webhook middleware | **Implemented / bounded, with opt-in shared SQL replay** | Both adapters bound the raw body, call one provider-aware verifier before dispatch, restore the exact body, insert a normalized event and enforce the configured replay store. `webhook-sql` adds immutable bounded shared claims over SQLite/PostgreSQL/MySQL/MariaDB, with restart, contention, drift, capacity and four-protocol live evidence; a stable event claim can share the caller's relational domain transaction. Middleware payload admission happens before dispatch and therefore is not exactly-once processing. Cross-system effects still require outbox/idempotent-consumer/reconciliation contracts. | | Subscription handle and grace-period value | **Implemented / bounded** | `SubscriptionHandle<P>` validates and redacts its identifier and delegates cancel/pause through an explicit statically dispatched provider (with a global-provider compatibility path). `GracePeriod` is a fallible half-open Unix-time window capped at 366 days; `Billable` and its derive can carry a complete optional pair. The application still owns authoritative loading, authorization, trusted time, persistence, entitlement checks, scheduling/retry and provider-specific live behavior. | | Immediate `Billable` charge | **Implemented / bounded for reviewed Stripe Payment Intents** | `charge_with`/`charge` require integer minor units, normalized currency, explicit provider customer and tokenized payment-method IDs, validated model e-mail and a bounded idempotency key. Stripe forwards that key and confirms off-session; its response must match amount/currency and report `succeeded` or `processing`. Exact mock retries are deterministic but carry the distinct non-success `Mock` status; request/receipt debug views redact identity. Other providers return `UnsupportedOperation`; mandate/SCA setup, durable idempotency, webhook reconciliation, entitlement changes and live sandbox evidence remain application/deployment work. | | Stripe metered usage | **Implemented / bounded provider-specific API** | `MeteredBillingProvider` uses `StripeMeterEvent` for the current Stripe Meter Events form contract. It validates positive bounded usage, caps responses, binds provider echoes, redacts identities, forwards a rolling-window identifier and supplies deterministic non-live mocks. Custom adapters without a provider event key report `ApplicationOutboxRequired` so the application claims its key in a durable outbox. Live-account acceptance, retries, aggregation configuration, reconciliation and entitlements remain external. | | Coupon application and relative trial extension | **Implemented / bounded provider-specific API** | `CouponCode` is bounded/redacted. Stripe's current expanded-discount update is bound to subscription and coupon; InfinitePay and unreviewed live adapters fail explicitly. `TrialExtension` makes `extend_trial(15)` a 1–730-day operation; Stripe protocol fixtures bind the returned subscription/expiration, with an explicit-clock retry path. The host owns authorization, stable command time, serialization, webhook reconciliation, billing-cycle policy and live-account evidence. | | Shared team/workspace resource quotas | **Implemented / bounded, with opt-in durable SQL store** | `BillingSubject::from_tenant` binds shared accounting to trusted tenant context and `Billable::quota_request` derives the limit from the subscription owner. `QuotaGate` reserves before executing, suppresses exact replay and compensates ordinary operation failure. `quota-sql` uses a unique event claim plus conditional counter update; SQLite and live PostgreSQL/MySQL/MariaDB concurrency contracts stop exactly at the shared limit. Applications own membership/authentication, tier persistence and reconciliation, migrations, stale-reservation operations and non-relational adapters. Arbitrary domain writes are atomic only when performed through the same caller-owned SQL transaction. | | Payment-bound invoice PDF and mail delivery | **Partial historical automation; implemented bounded bridge** | `invoice-pdf` validates the compatibility invoice into exact minor units and renders bounded paginated PDF with embedded WinAnsi or a caller-supplied checked font. `PaidInvoice` requires final `Succeeded` evidence matching recipient, amount and currency; Mail's opt-in bridge attaches the PDF, runs pre-flight and sends through the facade or a static driver. Its stable non-secret key is intended for an application-owned outbox. No webhook is inferred, no key is claimed transactionally, provider acceptance/attachment parity vary, and exactly-once delivery is not promised. | | Durable cross-instance webhook idempotency/replay store | **Implemented / bounded relational contract** | The opt-in `webhook-sql` store persists bounded provider-scoped payload digests or stable event keys on SQLite, PostgreSQL, MySQL and MariaDB with immutable capacity/TTL, database-time serialized claims, expiry, restart, contention and fail-closed drift/storage/full-state evidence. Middleware admission is not exactly-once; atomic domain mutation requires the caller-transaction event-key path, and cross-system effects still require an outbox and reconciliation. | | Static fee, settlement, and tax promises in framework docs | **Do not promise** | Provider terms and regional availability change. Link to official current terms and document only what the adapter itself implements. | | “Zero-cost invoicing” | **Do not promise** | Removing an intermediary fee does not remove certificate, accounting, infrastructure, support, or compliance cost. Preserve cost transparency without advertising zero total cost. | ## IoT, edge, and cryptography vision | Capability or former claim | Current status | Recommendation and reason | |---|---|---| | `no_std` telemetry/frame helpers | **Implemented (keep focused)** | This is a credible small foundation. Preserve the lightweight, transport-neutral core. | | Ed25519-signed OTA manifest, firmware hash, and monotonic verification gate | **Implemented (foundation only)** | Keep negative vectors and anti-rollback tests. The gate is not a firmware installer. | | Persistent anti-rollback counter, download, flash, bootloader slot selection, rollback, and commit | **Partial: store contract implemented; platform path absent** | The `no_std` adapter requires exact monotonic compare-and-set and has restart/retry/conflict tests. A concrete store, download/flash path, bootloader coordination and power-loss/fault-injection evidence remain essential before calling OTA end-to-end. | | MQTT 5, CoAP, and Sparkplug B transport | **Packet helpers implemented; transport not implemented (worth a separate transport crate)** | The `no_std` crate now encodes one bounded MQTT 5 PUBLISH packet and RFC 7252 base requests with protocol/boundary vectors. Connections, TLS/DTLS, acknowledgement/retransmission state, broker/LwM2M/Sparkplug semantics and interoperability remain absent; network runtimes should not bloat the frame crate. | | Hardware HSM/secure-element backends | **Not implemented; simulators are experimental (worth adapter traits, later)** | Implement only against named hardware/PKCS#11 interfaces with device tests. Never create home-grown “HSM-like” hashing and call it secure. | | NIST ML-KEM/post-quantum encryption | **Not implemented; simulators are experimental (worth later, not home-grown)** | Adopt an audited implementation only when a concrete protocol and threat model justify it. A generic “quantum-safe” badge is not worthwhile. | | CAN/J1939, LoRaWAN, GPIO/I2C hardware integration | **Not implemented or helper-only (worth separate hardware packages)** | Preserve the idea, but require target boards, interoperability fixtures, and maintainers for each protocol. | | Embassy async executor integration | **Not implemented (worth implementing after the transport split)** | Valuable for embedded ergonomics once the `no_std` ownership and timer/network abstractions are stable. | | Actual QEMU/hardware-in-the-loop CI | **Not implemented (worth implementing with real runtime code)** | Merely compiling a target is not QEMU testing. Add it when there is boot/flash behavior to execute and assert. | | Aerospace/autonomous-vehicle/defense framework claims | **Not implemented (do not place in the web-framework Core)** | The idea is ambitious, but safety-critical systems need independent standards, certification, hardware, and governance. Consider a separate future project only after the IoT foundation is mature. | ## Connect, real-time, queues, storage, and data The optional v13 [distributed tracing candidate](https://rullst.github.io/Rullst/book/distributed-tracing.html) links approved operations across processes through explicit trusted parent context and minimized bounded OTLP export. A standard TLS collector and separate Messaging processes passed acceptance; hosted source/package admission passed in PR #236. Final release admission, collector operations and Studio's separate ingestion/viewer remain distinct responsibilities. | Capability or former claim | Current status | Recommendation and reason | |---|---|---| | OAuth2/OIDC/social-login providers | **Implemented (keep as Connect's current identity)** | Continue issuer, redirect, JWKS rotation, offline-fixture, and negative-token contract testing. | | Signed local OIDC identity-provider fixture | **Implemented / bounded for explicit Axum loopback tests** | A validated concrete router binds one registered client and callback, keeps authorization codes expiring/one-shot in a 64-record process-local store, verifies optional S256 PKCE, carries nonce into a deterministic-fixture EdDSA ID token, publishes discovery/JWKS and accepts only issued unexpired bearer tokens at userinfo. A real loopback test drives the production `OidcProvider` through discovery, authorization, token exchange, JWKS verification and replay rejection. The private signing material and credentials are intentionally public/predictable fixtures. It is not safe to expose, has no interactive login/consent, refresh/device/federation lifecycle, durable state, key rotation or OIDC certification. | | Server-bound OAuth/OIDC callback challenge | **Implemented / bounded for Axum + tower-sessions** | `begin_oauth_session` stores state + PKCE and `begin_oidc_session` additionally stores nonce for ten minutes. `AuthSession` reads the callback from the query string, removes and immediately saves the one active challenge before constant-time state validation and returns borrowed `ExchangeParams`; it does not complete Apple's `response_mode=form_post` callback, which the v13 `AuthSessionForm` reads from a bounded form POST body behind a `SameSite=None; Secure` challenge cookie; sequential replay, expiry, mismatch, replacement and redacted-debug negatives are tested. The host still owns a durable session store, secure cookie/TLS configuration, account linking, recovery and live-provider conformance. One active challenge per browser session deliberately means a second login invalidates the first. The generic store API is not a distributed compare-and-delete, so simultaneous already-loaded callbacks require idempotent effects or an application atomic-challenge adapter. | | Credential-safe normalized OAuth profile | **Implemented / bounded** | `ConnectUser::universal_profile()` emits only normalized identity fields. Serde on `ConnectUser` omits access and refresh tokens; applications must use a dedicated encrypted store for credential lifecycle rather than serializing the whole response as a session. | | Automatic OAuth token refresh | **Implemented / bounded process-local coordinator plus optional shared-local encrypted state** | `RefreshableTokenState` validates and redacts access/refresh credentials, lifetime, trusted receipt time and provider user identity. `AutoRefreshingSession<P>` uses static dispatch, a bounded early-refresh window and one async mutex to prevent overlapping refresh calls; callers waiting behind a successful refresh reuse it. The coordinator retains an unrotated refresh token, adopts a valid rotation, binds the returned user and swaps state only after validation. `EncryptedTokenSnapshot` supplies a bounded versioned AES-256-GCM envelope whose tag binds an explicit rotation key ID, provider and trusted local-account identity. The opt-in SQLite store persists only a pseudonymous binding digest, generation/key metadata and ciphertext under an immutable quota; serialized transactions and exact-successor CAS reject stale local writers, with restart/contention/corruption evidence. The application still owns the remote-provider lease and losing-call reconciliation, key/directory/backup operations, authorization, multi-host replication, retry/backoff, revocation and reauthentication. Providers without refresh fail explicitly. | | Explicit corporate proxy for OAuth/OIDC | **Implemented / bounded HTTP(S) transport** | `ReqwestClient` accepts an authority-only explicit proxy or separately supplied Basic credentials, disables ambient system-proxy lookup and requires HTTPS for authenticated non-loopback endpoints. A loopback protocol fixture proves routing and proxy authorization. PAC/WPAD, SOCKS, proxy mTLS and production network certification remain deployment/roadmap work. | | Brokered messaging / old Connect Phase 9 | **Partial bounded foundation in `rullst-messaging`** | The separate crate now provides a versioned envelope, bounded idempotent publication, groups, leases, retry/DLQ, explicit purge, deterministic in-memory broker, canonical bounded envelope codec, allowlisted W3C trace context and fixed-schema local SQLite. SQLite can immutably select plaintext or AES-256-GCM header/payload protection with row-bound AAD and bounded rotation; raw-state/restart/tamper/row-swap/two-instance tests are executable. An opt-in static ORM relay publishes committed outbox claims with their stable event key and proves exact replay across the publish-before-ACK crash window. Routing/idempotency metadata remains visible, delivery remains at least once, neither relay nor codec creates an atomic remote transport, and replication plus RabbitMQ, Kafka, NATS/JetStream, SQS/SNS, Google Pub/Sub and Pulsar adapters are not implemented. The optional [standalone Redis Streams profile](https://rullst.github.io/Rullst/book/redis-messaging.html) passed source admission in PR #236; it owns fenced group indexes and retains its documented deployment and final-release boundaries. | | WebSocket pub/sub and SSE | **Implemented in Core, not Connect (do not duplicate)** | Re-export through a coherent facade if desired; one runtime implementation is better than competing copies. | | Recoverable server-driven Live UI | **v13 prerelease / source admitted in PR #236** | `live::recovery` adds explicit authenticated scopes, mandatory revalidation, full snapshots, atomic expected-revision application contracts and bounded connection/action/timeout behavior. Actual WebSocket and Chromium tests prove conflict, revocation, disconnect and process-restart recovery without replaying uncertain actions. Applications own persistent domain state, transactional permission checks and HTML escaping. Legacy `LiveComponent`/`make:live` remain separate; DOM diffing, broadcast and offline mutation queues are outside this profile. See [the recovery guide](https://rullst.github.io/Rullst/book/live-recovery.html). | | Bounded Memory/SQLite/Redis queues with recoverable leases | **Implemented / bounded** | SQLite and Redis additionally persist `dispatch_at` timestamps for at most 366 days and never claim them early; Redis server-time promotion has a digest-pinned live contract. Execution is poll-dependent and at-least-once. Continue dead-letter observability, graceful shutdown, and multi-worker fault tests. | | NATS JetStream, SQS/SNS, and GCP Pub/Sub | **Not implemented (worth demand-driven optional adapters)** | Define one queue conformance suite first; add providers only when each can pass the same delivery/lease/idempotency semantics. | | Authenticated tenant-local storage namespace | **Implemented / bounded in Core** | `TenantStorage` is constructed from a validated `TenantContext`, prefixes keys with an immutable canonical tenant root and reuses traversal/symlink protections. An exact local test stores the same logical key under two authenticated tenants without interference. Attachment/media integration, remote bucket policy and cross-backend conformance remain open. | | Authenticated tenant cache namespace | **Implemented / bounded in Core** | `TenantCache` is constructed from a validated `TenantContext`, validates logical keys, applies an immutable namespace for memory or Redis-backed Core caches and deliberately exposes no global flush. Exact same-key non-interference is tested locally. Redis cluster/failover and application-wide conformance remain open. | | Authenticated tenant realtime namespace | **Implemented / bounded in Core** | `TenantRealtime` and `TenantPresence` bind shared in-process broadcast/presence managers to validated `TenantContext`, validate logical channel/event/identity names, cap payloads at 64 KiB and prove that the same room name in two tenants does not interfere. Broader room authorization, distributed transport/liveness and other application integrations remain application/roadmap work. | | S3 and Cloudflare R2 storage | **v13 prerelease / source admitted in PR #236** | Optional Core/facade `storage-s3` supports bounded private upload/download, metadata, deletion and temporary GET grants with maintained signing and explicit credentials. Disposable-service signature/restart and extracted-consumer checks pass. Owner-account interoperability, multipart streaming, IAM/bucket operations and final release admission remain separate. See [private storage](https://rullst.github.io/Rullst/book/private-object-storage.html). | | Upload admission and quarantine contract | **Implemented / bounded, storage-agnostic** | Core validates bounded in-memory bytes against tenant/name/size/type policy, recognized signatures, MIME/extensions and active-text denial (any leading markup after whitespace, byte-order marks or zero-width characters, or `javascript:`), produces a randomized tenant quarantine key plus SHA-256, and releases only after a clean scanner verdict. The bundled scanner is deterministic and mock-only. Multipart streaming, sandboxed deep parsing/transcoding, S3/R2 movement, archives and a production malware adapter remain open. | | Image resize pipeline | **Not implemented (worth an optional media crate, not Core)** | Media decoding expands attack surface and binary size. Isolate it with strict size/pixel limits and fuzzing. | | Transparent SQLite/Turso/database replication | **Not implemented (do not implement generically in Core)** | Integrate vendor-specific replication clients or sidecars. A generic timer that prints “syncing” cannot provide consistency semantics. | | Immutable/zero-copy distributed ledger engine | **Not implemented (interesting, but lower priority)** | First define the exact consistency, persistence, recovery, and audit use case. The current HMAC audit chain is not a distributed immutable ledger. | ## Security, authentication, Studio, and Nexus | Capability or former claim | Current status | Recommendation and reason | |---|---|---| | Versioned AES-256-GCM field encryption | **Implemented (keep)** | Keep envelope versioning, AAD, random nonces, key rotation, and round-trip/negative tests. Key custody remains external. | | Canonical browser security baseline | **Implemented / bounded in Core** | `apply_security_baseline` is the same composition used by `Server`: it installs the exact application config outside secure headers/CSP nonce, explicit-origin CORS with separate credential opt-in, WAF, double-submit CSRF and optional PII masking in a stable order. Configuration rejects wildcard/path/query/credential/duplicate CORS origins, malformed CSP/SameSite state and unknown COEP policies. COEP remains `require-corp` by default; an application can deliberately select validated `credentialless` or `unsafe-none` behavior after reviewing its media/isolation boundary. In-process HTTP tests prove matching renderer/header nonce, configured COEP, secure CSRF cookie and write denial/acceptance, preflight allowlist and no grant for a foreign origin. Tune policy per application and test the final browser/proxy/TLS deployment; never promise a universal third-party A+ score. | | “OWASP A+ guaranteed” | **Do not promise** | A scanner grade depends on the final page, proxy, cookies, TLS, and deployment. The worthwhile goal is a strict tested baseline, not a badge guarantee. | | Bounded body-aware WAF/RASP and DLP | **Implemented (defense-in-depth)** | Keep content-type, streaming, compression, overflow, and header-consistency tests. Never position heuristics as a replacement for parsers, binds, validation, or authorization. | | Distributed rate limiting | **Implemented foundation / external proof pending** | `rullst-security/redis-rate-limit` uses one atomic Redis script, namespace validation, hashed client keys, TTLs and typed fail-closed errors. Empty/`mock_*` configuration is explicitly process-local and `require_distributed()` rejects it. Real Redis cross-instance, eviction/failover and trusted-proxy deployment tests remain release work; the legacy no-argument selector stays `Unsupported`. | | Deterministic threat assessment and proof of work | **Implemented / bounded locally; autonomous AI claim remains partial** | Transparent thresholds classify three caller-supplied aggregate patterns and can issue an authenticated, subject-bound, expiring challenge with bounded difficulty/capacity and atomic one-shot verification. Rullst does not collect the aggregates, attribute botnets, block autonomously or share replay state across processes; accessibility, identity, distributed enforcement and DDoS architecture remain host concerns. | | Route-scoped JSON Schema/OpenAPI body enforcement | **Implemented / bounded** | `JsonSchemaPolicy` compiles one JSON Schema 2020-12 document or selected OpenAPI 3.1 component, caps schema bytes/nodes/depth, rejects external references, disables filesystem/network retrieval and uses linear-time regexes. The Axum layer composes exact JSON transport checks and returns a value-free `422` on mismatch. It does not infer schemas from arbitrary routes or replace auth, ownership, domain validation, or query/header/form contracts. | | Durable tamper-evident audit storage | **Partial (worth implementing as a sink interface)** | Canonical HMAC sequencing exists. Durable append-only storage, key protection, retention, and independent verification remain application/integration work. | | Versioned local security-event envelope and spool | **Implemented and bounded (v1/local)** | `LiveSecurityEvent` has a frozen six-field v1 contract, a packaged JSON Schema, normalized identifiers/IPs/timestamps, a 2 KiB UTF-8 detail limit, and CEF field escaping. `DurableSiemSpool` preserves the compatible unsigned SHA-256 format. The opt-in `AuthenticatedSiemSpool` adds a domain-separated HMAC-SHA256 sequence/predecessor chain, one active plus seven historical named zeroized keys, exact byte/record quotas, restart validation and fail-closed forgery, wrong/missing-key, interior deletion/reordering, symlink, external-change and uncertain-durability behavior. A separately trusted checkpoint is required to detect valid-tail rollback; trusted directory/key custody, permissions, rotation retirement, retention, backup, multi-process exclusion, delivery, retry, acknowledgement, dead-letter handling and external SIEM adapters remain application/operations work. | | Full normative WebAuthn server | **Partial; durable local device lifecycle implemented** | The implementation checks the documented ES256/`none`-attestation ceremony invariants. The opt-in SQLite store adds bounded credential inventory, rename/revocation, restart persistence and atomic counter CAS shared by local processes. Challenge state remains process-local, and an audited library or a full conformance suite is preferable to indefinite custom protocol ownership. | | TOTP recovery codes | **Implemented cryptographic foundation / workflow pending** | Security generates subject-bound 80-bit codes, returns plaintext only at enrollment, serializes salted HMAC verifiers, compares in constant time and removes a consumed verifier. The application must encrypt the TOTP secret, persist verifier consumption atomically, rate-limit attempts and own enrollment/recovery UX and audit. | | TOTP enrollment and Login Jail | **Implemented / bounded in-process** | MFA secrets contain 160 bits from the OS RNG, verification is constant-time and enrollment can emit a real bounded SVG QR. `LoginGuard::record_login_failure_and_wait` both records and awaits the progressive tarpit. The host must protect/encrypt the secret and persist/distribute abuse state where required. | | CLI security evidence tools | **Implemented with bounded analyses** | SBOM output parses `Cargo.lock` into CycloneDX 1.5 components with a valid UUID serial number, lockfile checksums and source-aware purls (no license metadata); the doctor parses the real Rust version against MSRV; requested Geiger/SBOM/network checks now fail on findings or incomplete execution, and hooks run all-feature Clippy plus unsafe/IDOR source checks. These are evidence producers, not certification, attestation or static-analysis completeness proofs. | | First-class application JWT service in `rullst-auth` | **Implemented / bounded shared local option** | The opt-in `jwt` feature centralizes issuer/audience, required versioned claims, bounded TTL/scopes, strong HS256 keys, `kid` rotation and sync/async revocation contracts. Production verification rejects the bundled process-local store. The `sqlite` profile adds a quota-bound shared local adapter with serialized JTI/session-version mutations, expiry pruning, configuration-drift checks and restart/two-instance evidence. Multi-host replication, database encryption/backup/availability and refresh-token workflows remain deployment/application work. | | Nexus open-by-default admin | **Removed; hardened Nexus is implemented** | Keep fail-closed construction, TLS boundary, role/field policy, ownership integration, and rate limits. Production readiness still depends on the host application identity model. | | Nexus registered-model CRUD and batch actions | **Implemented / bounded** | `#[derive(Nexus)]` generates tested metadata for named-field models; primitive widgets are inferred and enum/textarea semantics are explicit. `try_build()` bounds and validates model, field, primary-key, enum, relation and optional text-tenant metadata. Mutation forms accept at most 256 pairs, bound short/long values to 4/64 KiB, reject unknown/protected fields and parameter pollution, normalize Boolean controls and validate registered enum, JSON, number, date, e-mail and HTTP(S) URL semantics before bound SQL. Sort/search identifiers are registry-allowlisted, batch delete/deactivate is capped at 1,000 selected IDs, and metadata is escaped at HTML boundaries. Tenant-scoped models bind every built-in read/mutation/batch to a trusted Core `TenantContext`, inject it on create and deny missing context; models without the metadata remain global. `with_required_audit` couples minimized committed-mutation evidence to the same transaction and fails closed when its fixed schema is unavailable. Deactivation is available only for a writable Boolean `is_active`/`active`; external enum reflection, multiline inference, host identity/membership, within-tenant ownership, global/custom-route authorization, database schema/type compatibility and privileges remain host contracts. The same-database audit table is mutable, not append-only/tamper-evident, and host retention/backup/replication/immutable export remain external. | | Autonomous AI admin that can mutate production data | **Not implemented as a safe contract (do not enable by default)** | A read-only, explainable assistant is worthwhile. Mutations need explicit human approval, scoped capabilities, audit records, dry-run previews, and rollback. | | Studio with real telemetry | **Implemented with bounded authenticated ingestion and unavailable states** | Linux and Windows expose delta-based process CPU alongside RSS, Tokio and local span probes; KPI cards poll the local JSON source. Each separately mounted push-only router binds one producer name/key, accepts only bounded attribute-free v1 spans after exact-body/source HMAC, clock-window and atomic nonce-replay checks, then stores them in bounded process memory. The viewer derives 100 ms slow-label and three-repeat possible-N+1 heuristics without SQL text or bindings. This is not OTLP, durable trace storage, remote Studio access or proof of a defect. The local capability still verifies peer plus local `Host`, and same-origin `Origin` on mutations. Queue/revenue/security/AI panels expose only supplied state; unsupported operations, disconnected transports and the absent standalone migration registry fail explicitly. Keep the viewer local by default and never fabricate values for missing sources. | | Studio data/API/queue/cache/config tools | **Implemented with bounded partial surfaces** | The SQLx browser uses the application's pool or the shared `Server`/Artisan database resolver without a SQLite fallback, reads/filters and, behind the unforgeable verified-local request marker, mutates one primitive non-key value or deletes one complete-PK-selected row with exact confirmation. Tables/columns come from inspected allowlisted schema, values are typed/bound, bodies are capped, views cut cell text to 256 characters and search terms to 256 bytes, a key column outside the identifier boundary makes the table read-only, each write commits in a transaction only when exactly one row changed, and live SQLite/PostgreSQL/MySQL/MariaDB matrices cover the contract. Backend-specific types, tenant/RBAC, audit, rollback and shared production access remain outside it. The read-only API reference requires a supplied `OpenApi`; SSE records method/URI/status/latency without secret-bearing bodies/headers. SQLite removes successful jobs by default or explicitly retains 1–100,000 for the real queue snapshot with atomic pruning and purge; retained payload access/policy belongs to the host. Redis lists at most 1,000 jobs, retries failed jobs and purges failures and dead letters, and custom queue inspection remains capability-specific. Queue retry/purge and flag toggles require the same verified-local marker as database writes, and the flag page reports a missing table instead of creating it from a `GET`. An explicitly supplied memory or Redis cache returns metadata only; Studio limits display to 100 opaque HMAC identifiers with size/TTL and permits only individual verified-local invalidation. Values, exact keys and bulk flush remain absent; custom cache drivers fail explicitly until they implement inspection. A successful Studio flag toggle immediately invalidates warm `DbFeatureDriver` caches in the same process through a constant-size epoch; other processes/direct writers remain TTL-bound without application pub/sub. Relational ER metadata uses parameterized lookups and normalized Mermaid identifiers. Environment values are deny-by-default redacted and typed config omits URLs, paths and secrets. | | Studio automatically stripped from every release with zero overhead | **Do not promise** | Feature selection and route mounting determine inclusion. Explicit compile features are clearer than relying on a universal debug/release assumption. | | Threat Radar with external reputation and verified audit feeds | **Partial (worth pluggable connectors)** | Local counters exist. External intelligence, durable audit verification, and SIEM delivery should appear only when a source is connected and healthy. | | “100% OWASP coverage”, “tamper-proof”, or “zero-leak DLP” | **Do not promise** | These absolutes are not provable framework properties. Preserve the controls and publish exact threat-model/test scope. | | Automatic SOC 2/ISO/FedRAMP certification | **Do not implement as a PASS generator** | The evidence exporter is worthwhile; certification evaluates the whole organization and deployment. Report only the outcome of real checks: `audit --compliance` uses `NO FINDINGS`, `FINDINGS`, `GENERATED`, `OBSERVED`, `NOT CHECKED`, `ERROR` and `NOT EVALUATED`, never `PASS`. | ## AI and mail | Capability or former claim | Current status | Recommendation and reason | |---|---|---| | DeepSeek provider | **Implemented** | Keep it in the same provider contract and offline mock suite as the other cloud adapters. | | Mandatory prompt-injection/PII guardrail pipeline | **Implemented in the high-level client and built-in provider transports** | The versioned deterministic offline corpus fixes implemented injection/jailbreak/PII regressions across built-in transports. Custom low-level providers remain an explicit extension boundary; heuristics and this corpus cannot prove a prompt safe. `AdaptiveAiEvaluator<P>` now supplies bounded multi-turn evaluation orchestration, but operators still own the domain corpus and exact live-model execution/review. | | Adaptive AI evaluation | **Implemented / bounded runner** | A static-dispatch strategy can use one bounded response to choose its next prompt under turn, prompt/response, deadline and cancellation limits. Results distinguish pass, fail and inconclusive and serialize only low-cardinality metadata without raw prompts/responses/provider errors. Deterministic fixtures prove orchestration, not the safety or quality of a live model; the caller-supplied subject label and scenario assertions require operator governance. | | Tenant-bound RAG orchestration | **Implemented / bounded** | `RagPipeline::answer` composes guarded embedding, a static application retriever, Unicode-safe context budgets, guarded generation, source metadata and mandatory secret-minimized audit in one call. It requires trusted `TenantContext`, rejects cross-tenant tags and unsafe/empty context, and includes a bounded tenant-partitioned process-local cosine retriever. `DurableRagAuditTrail` adds a synchronized versioned local file with quotas and restart/corruption checks. The separate `AuditDeliveryClient` can export a caller-minimized event through a bounded HMAC-authenticated envelope with stable event identity, transient retry, cancellation and a bound acknowledgement. Authoritative datastore authorization, durable/external vector adapters, ingestion/deletion, output policy, receiver/outbox operation and live-model evaluation remain application/deployment work. | | Authenticated AI audit export | **Implemented / bounded transport** | Cloud delivery requires HTTPS and literal-loopback HTTP(S) is development-only. HMAC-SHA256 authenticates exact JSON bytes plus key/timestamp metadata; event and acknowledgement sizes, retry attempts and cancellation are bounded, and the ACK must repeat the stable event ID. The caller owns event minimization; the receiver owns freshness/signature verification, deduplication, authorization, persistence, retention, key rotation and SIEM availability. | | Offline mocks for chat, vision, embeddings, and mail transports | **Implemented** | Keep mocks deterministic and selected only by explicit empty/`mock_*` credentials; live endpoints must never silently fall back. | | Reusable and generated durable chat memory | **Implemented / bounded choices** | `StatefulChat<M>` and `ChatMemory` now live in `rullst-ai`; the bounded offline store is always available and opt-in `sql-memory` atomically persists tenant-bound exchanges with revision CAS on SQLite/PostgreSQL/MySQL/MariaDB. Live matrices prove the four protocols. `cargo rullst make:chat-session` remains an application-owned SQLx/Turso-primary model/migration path. Raw-text encryption, ownership inside a tenant, retention/erasure, provider audit, backups and conflict UX remain host contracts; CAS rejects rather than retries a billable provider request. | | Native JSON Schema structured output on every LLM | **Partial (worth capability-typed support)** | Separate parseable JSON from provider-enforced schema. Return `UnsupportedCapability` when native enforcement is unavailable. | | Machine-readable AI provider capabilities | **Implemented / bounded** | `AiProvider::capabilities()` and `AiClient::capabilities()` report transport support for text, chat, embeddings, vision, JSON/schema, streaming, tools, timeout, retry, and explicit cancellation. The public matrix states model-dependent and unsupported boundaries. | | Guarded local AI tool dispatch | **Implemented / bounded** | `ToolRegistry::execute` requires an exact allowlist, principal authorization, closed bounded JSON, a call budget and audit sink. Destructive/financial approvals are one-use and payload-bound. `DurableToolAuditTrail` adds bounded synchronized local persistence with restart/corruption checks; it is not a multi-process writer or authenticated external sink. Provider-native tool calling, approver authentication, domain authorization, rotation/retention and distributed delivery remain application/roadmap boundaries. | | Strict AI egress fetcher | **Implemented / opt-in bounded transport** | `EgressPolicy::strict()` denies all hosts until an exact allowlist is configured. `EgressFetcher` validates HTTPS URL/port and every DNS answer, pins all accepted addresses in a proxy-free client, verifies the connected peer, revalidates manual redirects and bounds deadline/declared and streamed bytes. It does not wrap arbitrary application/provider clients automatically; tenant-aware destination authorization, response schema/content handling and a deterministic successful live-origin contract remain integration work. | | OpenAI-compatible local/cloud LLM endpoint | **Implemented / bounded, capability-declared** | `OpenAiCompatibleProvider` sends the named chat/embedding/vision/response-format shapes. It defaults to chat-only; optional paths are explicitly declared for the configured model. Unauthenticated HTTP is restricted to a literal loopback IP, cloud requires HTTPS/Bearer, offline mode is explicit, responses/images are bounded, and redirects/environment proxies are disabled. This is not automatic model discovery or arbitrary-HTTP compatibility. | | “Any local LLM over any HTTP API” | **Do not promise** | Ollama and the bounded OpenAI-compatible adapter cover two explicit protocols. Arbitrary APIs still have incompatible authentication, streaming, schema, tool, and error semantics; applications implement `AiProvider` for those contracts. | | Autonomous AI self-healing/DevOps changes | **Not implemented; the CLI assistant was removed in 13.0.0-alpha.4 (do not auto-apply)** | `cargo rullst ai` (with `fix` and `review`) is gone; the development error page offers a read-only prompt and a loopback JSON context (`/_rullst/errors/{id}`) for the developer's own assistant. Diagnostics and patches can be valuable, but infrastructure/code mutation needs review, capability scopes, preview, audit, and rollback. | | Tenant-aware secure mail, typed failover, durable scheduling and expiring tracking tokens | **Implemented / bounded** | The mandatory pipeline and purpose-bound TTL tokens are real. `TenantMailResolver` accepts the trusted Core `TenantContext` directly, validates registration, fails closed on unavailable registry state, and proves context isolation with separate drivers. `FailoverDriver` distinguishes permanent, transient and rate-limited outcomes, sends only eligible failures to another provider, bounds/redacts HTTP error details and emits structured tracing; its state remains process-local. SQLite/Redis queues preserve tenant envelopes and bounded due times without early claims; unsupported real direct transports fail closed, while offline fixtures may retain schedule metadata. Credentials still live in an application-configured in-process registry; encrypted persistence, rotation and cross-process distribution are not implied. Token payloads are HMAC-authenticated but base64-readable rather than encrypted. | | Attachment inspection, recipient suppression and delivery observations | **Implemented / bounded opt-in controls** | `AttachmentInspectionGuard` fails closed before transport using a static scanner contract; its strict local implementation recognizes bounded safe types and rejects executable magic, spoofing, active PDF/SVG, secrets and unsafe links. `SuppressionGuard` checks process-local or `sqlite` shared-local manual/bounce/complaint state; SQLite binds verified provider/event payloads, preserves monotonic reasons and enforces exact quotas across restart/two instances. `ObservedMailDriver` emits only low-cardinality terminal metadata through a non-failing sink. Provider-specific webhook authentication, antivirus/sandbox/CDR, encrypted or multi-host suppression state, external telemetry and alert operations remain host/deployment work. | | Direct AWS SES v2 delivery | **Implemented / bounded protocol contract** | Behind `aws-ses`, the official AWS SES v2 SDK supplies SigV4 and supports temporary or caller-owned rotating credentials, HTML/text, RFC 8058 and attachments/CID. The adapter enforces field/encoded-size bounds and response binding, with a signed regional loopback contract. Offline and explicit bearer-proxy modes remain visibly distinct. Live-account acceptance, verified identities/domains, sandbox exit, IAM, quotas, reputation and inbox delivery remain external. | | SendGrid, Postmark, Mailjet, Mailtrap and Azure Communication Services transports | **Removed in 13.0 (kept in 12.x)** | v13 keeps Resend, AWS SES, SendPulse and SMTP. A configured removed driver fails resolution with a typed error instead of falling back; applications implement other providers with `MailDriver` ([Writing your own mail transport](https://rullst.github.io/Rullst/book/mail-custom-transport.html)). | | Safe generated mailables | **Implemented / bounded scaffold** | The five `make:mail` variants plus `make:mail-invoice` and `make:mail-dunning` validate names, reject traversal/collisions, enable the exact facade features, register modules and escape dynamic HTML. The payment-receipt template never claims tax authorization; dunning exposes D+1/D+3/D+7 without inferring billing state. A materialized project passes Clippy and adversarial rendering/link contracts; delivery credentials, scheduling, entitlement mutation and billing policy remain application/external boundaries. | | Mailgun, Brevo, MailerSend, Plunk, and Scaleway transports | **Not implemented (application-owned adapters)** | Applications implement `MailDriver` as in [Writing your own mail transport](https://rullst.github.io/Rullst/book/mail-custom-transport.html). An official adapter needs a maintainer and the shared offline/live transport conformance suite; provider count alone is not product quality. | | “Air-gapped/zero-leak AI” | **Do not promise automatically** | Local endpoints can avoid cloud LLM calls, but host networking, logging, model runtime, and telemetry determine the real data boundary. | ## CLI, generated applications, and release engineering | Capability or former claim | Current status | Recommendation and reason | |---|---|---| | Stable blueprint IDs and corrected Island/Resource flags | **Implemented in current hardening** | Keep snapshot tests because generated CLI identifiers are public compatibility surface. The Blank, API and SaaS IDs are stable; `--blueprint lms|blog|portfolio|erp` fails before writing and names the example that replaces it. The Nix and Buildah generators were removed in 13.0.0-alpha.4. | | Installable application (PWA) | **Implemented / bounded; Omni shells removed in 13.0.0-alpha.3** | `make:pwa` writes a web app manifest, placeholder icons, `/sw.js` and `/offline` (`src/pwa.rs`) and a registration script with an update prompt, wired into the starter router and layouts; the worker runs under the default nonce CSP and never stores non-GET, cross-origin, `no-store` or `private` responses. `--android` writes a Bubblewrap `twa-manifest.json`, and `rullst::pwa::AssetLinks` serves validated `/.well-known/assetlinks.json`. A Node contract executes the worker and registration script, and a generated Blank application is compiled and checked for the routes' headers. `rullst.client` v1 supplies bounded typed envelopes/version negotiation shared with Wasm, without client authority. The Tauri-based `make:omni`/`omni` commands and the native `offline-sync` feature were removed; Bubblewrap, PWABuilder, signing keys, store review and device testing remain application work. | | LMS/Academy starter domain foundation | **Removed in v13** | The complete Academy scaffold that generated school-scoped curriculum, publication, roles, completion and certificates was replaced by the smaller LMS starter, which became the `examples/lms` application in 13.0.0-alpha.4. The design and v12 implementation remain in the git history and the stable v12 branch; recommendation: rebuild it as an application or external example only with a maintainer and tests. | | Academy activity evaluation | **Removed in v13** | The complete Academy scaffold that generated server-authoritative activity evaluation was replaced by the smaller LMS starter, which became the `examples/lms` application in 13.0.0-alpha.4. The design and v12 implementation remain in the git history and the stable v12 branch; recommendation: rebuild it as an application or external example only with a maintainer and tests. | | Academy spaced-review scheduling | **Removed in v13** | The complete Academy scaffold that generated the durable spaced-review queue was replaced by the smaller LMS starter, which became the `examples/lms` application in 13.0.0-alpha.4. The design and v12 implementation remain in the git history and the stable v12 branch; recommendation: rebuild it as an application or external example only with a maintainer and tests. | | LMS example (former starter) | **Implemented / bounded** | Until 13.0.0-alpha.4 `--blueprint lms` generated it; it is now the `examples/lms` application, whose offline tests (`cargo test -p rullst-lms-example`) run in the workspace suite. It covers auth, catalog, course modules and lessons, an accessible player, enrollment, protected lesson playback and owner-bound monotonic/idempotent progress, with a Nexus admin. v13 replaced the earlier complete Academy scaffold and its `--lms-modules` profiles with this smaller application; quizzes, schools, roles, scoring, automation and notifications are application work. | | Academy privacy lifecycle foundation | **Removed in v13** | The complete Academy scaffold that generated the school-scoped age-band and retention lifecycle was replaced by the smaller LMS starter, which became the `examples/lms` application in 13.0.0-alpha.4. The design and v12 implementation remain in the git history and the stable v12 branch; recommendation: rebuild it as an application or external example only with a maintainer and tests. | | mdBook documentation hub | **Implemented in current hardening** | Keep mdBook build in CI pages deployment; retired legacy internal SSG in favor of industry standard mdBook. | | Parameterized browser RPC through `server_function` | **Implemented / bounded transport** | Concrete async free functions return `RpcResult<T>` and generate both a matching explicit Axum router and Wasm caller over `rullst.client` v1. The contract limits owned Serde arguments/results and same-origin paths, caps bodies at 256 KiB, correlates responses, rejects media/version/schema drift, forwards the CSRF cookie in a header and exposes only bounded failure codes. Native HTTP/security tests, compile-fail diagnostics, both Wasm compile targets and generated-project verification are executable. Authentication, authorization, tenant policy, application idempotency, actual browser engines and network/provider operation remain separate evidence; island hydration is not completed by this RPC transport. | | Every generator/blueprint combination compiled in a tempdir | **Implemented for the three starters (six blueprints in v12)** | Since 13.0.0-alpha.4 the structural matrix validates 10 shapes of the Blank, API and SaaS starters, four representative projects compile with their generated tests, and seven public-CLI profiles cross the database, AI, Redis and polyglot options; the LMS, Blog, ERP and Portfolio applications are workspace examples. The v12 record follows: a structural matrix validates the 18 public v12 shapes across six blueprints, materializes every blueprint, parses extracted templates, and inventories every public command. Eight representative source-tree projects cover all six blueprints, the fixed Active Record + SSR `html!`/HTMX profile, API, database, hot reload and release. The former 270-case matrix crossed incomplete frontend/ORM labels and is retained only as prerelease history. On approved candidate `a6b3bc8a`, the packaged-distribution gate installed the extracted CLI and compiled all six default blueprints without monorepo paths. Expand `fmt/smoke` only where it adds a distinct contract and do not treat provider/deploy commands as offline tests. | | Generated Auth/JWT/Billing production defaults | **Implemented for the audited regressions; still application code** | Async Argon2, issuer/audience/strong-secret checks, authenticated billing identity, CORS allowlists, and signed webhooks are foundations; generated apps still need deployment review. | | SQLx/Turso billing scaffold | **Implemented / bounded Stripe integration (the only generated provider)** | `make:billing --model` and SaaS share reversible persistence, authenticated customer/checkout intents, session IDs, account/test-live scope, signed Checkout/subscription events, current-state reconciliation, revision fencing and atomic event/subscription writes. Open sessions resume without new creation. Materialized SQLite/Turso contracts cover changed email, lost responses, ownership, replay/rollback, concurrent stale reads, replacement subscriptions and restart. Route mounting, provider setup, legacy data migration, retention/entitlement policy and live-account sandbox acceptance remain application/deployment work. | | Fully compliant automatic OpenAPI | **Partial (do not claim completeness)** | A syntax-derived draft is useful. Full fidelity needs a typed schema/route contract and validation, not regex confidence. | | TypeScript SDK that eliminates contract breaks | **Partial (worth contract tests)** | Generation reduces duplication; add serialization golden tests and API compatibility tests instead of promising elimination of drift. | | Total framework ejection | **Partial (keep as migration aid)** | Generate an inspectable entry point, list remaining Rullst dependencies, and run `cargo check`. Do not promise automatic removal of every subsystem. | | Opt-in Git pre-commit/commit-message installer | **Implemented / bounded (CI remains authoritative)** | `hook:install` writes managed format/Clippy/IDOR and Conventional Commit wrappers, finds repository roots from nested directories, supports linked-worktree common metadata, preserves and chains active hooks instead of overwriting them, stages replacements before activation, and fails with typed errors on missing worktrees or backup conflicts. Temporary-repository regressions prove idempotency, executable permissions, preservation and fail-before-mutation behavior. Client-side hooks remain deliberately bypassable and are not a substitute for protected CI. | | One-click, zero-downtime deployment | **Partial (guided deploy is worthwhile)** | `cargo rullst deploy` writes a Dockerfile, `compose.yaml` and `Caddyfile` for a Linux VM with Docker or Podman, and `deploy:doctor` inspects a configuration snapshot offline, but availability, secrets, migrations, rollback, DNS, and cloud credentials remain operator concerns. | | IDOR scanner that proves authorization | **Heuristic (keep as a warning tool)** | AST patterns can find omissions, not prove ownership semantics. Pair findings with route-level negative tests. | | Compliance report that prints unconditional PASS | **Removed; evidence-oriented report implemented** | This is the correct direction. Keep raw evidence, tool versions, skipped states, and commit digests. | | Release packaging in dependency order with preflight gates | **Implemented and exercised by stable v12** | Keep package-all-before-publish and topological publishing. The `v12.0.0` workflow at `eb11f892` admitted the protected exact SHA, reproduced and verified the archives, attested the artifacts, published all 16 packages with short-lived Trusted Publishing credentials, verified registry checksums, and created the GitHub release. The earlier redundant external SLSA job produced no evidence and was removed; no project-wide SLSA level is claimed. | | SLSA Level 3 certification | **Not established (do not claim)** | Build provenance is worth keeping. Pursue a named SLSA level only after every requirement is independently evaluated for the actual release platform. | | RustSec/OSV exception governance | **Implemented** | Keep only unavoidable exceptions, each with owner, compensating control, and expiry; patched findings must block regressions again. | | Full Kani/Miri/mutation proof of the framework | **Not implemented (do not promise)** | Scoped harnesses are valuable. Promote stable, deterministic subsets to blocking gates and label exploratory jobs honestly. | | “100% test coverage” | **Do not promise (measured coverage is worthwhile)** | Report the exact commit, features, targets, excluded/generated code, and coverage tool output. Line coverage is not behavioral completeness. | ## Suggested sequencing 1. **Finish 13.0:** fixes and documentation only in 13.0.0-rc.1, then the 13.0.0 release evidence (version 12 reached end of life on 16 October 2026). 2. **Architecture cycle:** preserve the new Core/ORM and umbrella feature gates, consolidate Security, and evolve the bounded `rullst-messaging` and remote-storage boundaries through provider-specific conformance evidence. 3. **Demand-backed integrations:** durable rate limits/idempotency, S3/R2, and selected message brokers with shared conformance suites. 4. **High-assurance programs:** WebAuthn conformance. Officially homologated NFS-e issuance, if pursued, belongs in a separate product outside Rullst. 5. **Hardware program:** only then expand OTA into real flash/boot lifecycle, MQTT/Embassy, named HSM devices, and audited PQC protocols. This ordering keeps the extraordinary vision while making the stable surface smaller, testable, and trustworthy. --- # Rullst v12 Stable Release Record Source: https://rullst.github.io/Rullst/book/v12.html # Rullst v12 Stable Release Record > [!NOTE] > **Historical record (v12).** The 12.x line reached end of life on 16 October > 2026 with 12.3.0 as its last release. The current line is v13; move a v12 > application with [Preparing an application for v13](https://rullst.github.io/Rullst/book/migration-v13.html). <a id="1212-unpublished-maintenance-candidate"></a> ## 12.1.2 maintenance release The `v12` maintenance line released 12.1.2 (tag [`v12.1.2`](https://github.com/Rullst/Rullst/releases/tag/v12.1.2), 28 September 2026 UTC, 27 September in Brasília) with Nexus stored-value escaping, HTML macro caller-binding hygiene, explicit OAuth authorization-code response types, a compatible Tauri family for generated Omni shells and additional Auth, HTTP lifecycle and SQLite messaging regressions. Compatible corrections are also prepared for v13 development; its package versions and admission remain separate. This `main` copy does not hold the 12.1.2 publication receipt. See the [12.1.2 review](https://github.com/Rullst/Rullst/blob/v12/docs/src/v12-1-2-review.md) and [maintained release record](https://github.com/Rullst/Rullst/blob/v12/docs/src/v12.md) for current stable status. The immutable 12.1.1 receipt follows below. <a id="1211-unpublished-maintenance-candidate"></a> <a id="1211-maintenance-preparation"></a> ## 12.1.1 published maintenance release All sixteen public packages were published as **12.1.1** on September 24, 2026 UTC (September 23 in Brasília). The protected release workflow completed successfully. Independent registry checks confirmed all versions were available, not yanked, and matched the reviewed SHA-256 checksums. The [registry observation receipt](https://github.com/Rullst/Rullst/blob/main/docs/src/evidence/v12-1-1-publication.json) records those checksums and timestamps; signed release attestations establish provenance. | Field | Value | | :--- | :--- | | Git tag and assets | [v12.1.1](https://github.com/Rullst/Rullst/releases/tag/v12.1.1) | | Source commit | [`d27db26c6089e06366ca01b67d741f3d037c076a`](https://github.com/Rullst/Rullst/commit/d27db26c6089e06366ca01b67d741f3d037c076a) | | Publication workflow | [Successful release run 35925968138](https://github.com/Rullst/Rullst/actions/runs/35925968138) | | Framework package | [rullst 12.1.1](https://crates.io/crates/rullst/12.1.1) | | CLI package | [cargo-rullst 12.1.1](https://crates.io/crates/cargo-rullst/12.1.1) | This compatible patch preserves the 12.1.0 API and Rust **1.96.0** MSRV. It fixes application-key/configuration validation, HTTP response-body drain accounting and SQLite lease timestamps under write-lock contention, and includes reviewed dependency, generated-code and documentation maintenance. Applications using public scaffold keys must replace them with unique secure secrets and renew sessions; updating dependencies does not rotate deployed secrets or rewrite application files. See the [migration guide](https://rullst.github.io/Rullst/book/migration-v12-1.html). The exact source passed all 28 required admission workflows. All 40 fuzz targets had eligible original campaign evidence under the reviewed equivalence policy; they were not all rerun for the final documentation change. Release packaging, crates-only consumers, installed CLI blueprints, reproducibility and four native CLI build/smoke jobs passed. Package/evidence digests and all four native artifact inventories matched the cryptographically verified tag/source/workflow/run-bound attestation. Packaged READMEs match the reviewed absolute-link documentation. Targeted mutations identified additional regression-test work; they do not establish a full-framework mutation score or absence of defects. Additional tests follow on maintained `v12` and development `main`, with a new patch version if another registry publication is needed. No real provider-account acceptance, physical-device acceptance or broad security certification is implied. The [maintenance and Daybreak review plan](https://github.com/Rullst/Rullst/blob/v12/docs/src/v12-1-1-review.md) retains scope and acceptance boundaries. The published tag, crate archives, attestations and the original 12.1.0 receipts remain immutable. ## 12.1.0 published maintenance release All sixteen public packages were published as **12.1.0** on September 20, 2026 UTC (September 19 in Brasília). The protected publication and GitHub release completed successfully; independent registry checks confirmed every version was available, not yanked, and matched the reviewed package SHA-256 checksum. The [registry observation receipt](https://github.com/Rullst/Rullst/blob/main/docs/src/evidence/v12-1-publication.json) retains the sixteen checksums and verification time; release attestations separately establish build provenance. | Field | Value | | :--- | :--- | | Git tag and assets | [v12.1.0](https://github.com/Rullst/Rullst/releases/tag/v12.1.0) | | Source commit | [`b62390b4fc61e5ac06d24eb31bb7c10d370d679d`](https://github.com/Rullst/Rullst/commit/b62390b4fc61e5ac06d24eb31bb7c10d370d679d) | | Publication workflow | [Successful release run 35475944059](https://github.com/Rullst/Rullst/actions/runs/35475944059) | | Framework package | [rullst 12.1.0](https://crates.io/crates/rullst/12.1.0) | | CLI package | [cargo-rullst 12.1.0](https://crates.io/crates/cargo-rullst/12.1.0) | The release admitted all 28 required workflows for that source, including the declared fuzz inventory, and produced attested crate archives and native CLI assets for four targets. The verified Linux binaries reported `rullst 12.1.0` through `rullst --version`, `cargo-rullst --version` and `cargo rullst --version`. The packaged README reflects the reviewed documentation and omits the staging SaaS URL. These receipts do not establish live acceptance for every external provider or physical-device compatibility. The [12.1 migration guide](https://rullst.github.io/Rullst/book/migration-v12-1.html) covers application changes when upgrading. Post-publication documentation and informational mutation campaigns do not replace the immutable tag, crate archives or release attestations. The record below preserves the completed **12.0.0** publication and its original evidence; its commit and checksums do not describe the 12.1 release. ## 12.0.0 published baseline Rullst **12.0.0** was published on September 15, 2026. This page is the concise record of that release; it is no longer a development backlog or launch checklist. ## Immutable release identity | Field | Value | | :--- | :--- | | Version | `12.0.0` | | Git tag | [`v12.0.0`](https://github.com/Rullst/Rullst/releases/tag/v12.0.0) | | Source commit | [`eb11f892ae28f076e7a83c38a635316c6ed89028`](https://github.com/Rullst/Rullst/commit/eb11f892ae28f076e7a83c38a635316c6ed89028) | | Publication workflow | [Successful release run 34955468167](https://github.com/Rullst/Rullst/actions/runs/34955468167) | | Registry | [Rullst packages on crates.io](https://crates.io/search?q=rullst) | | Previous evaluation release | [`v12.0.0-rc.1`](https://github.com/Rullst/Rullst/releases/tag/v12.0.0-rc.1) | The protected release workflow admitted the exact tag commit, reproduced the publication inputs, verified the artifacts, produced GitHub build-provenance attestations, published the packages in dependency order through short-lived Trusted Publishing credentials, and created the GitHub release. A release workflow pass is supply-chain evidence; it is not an independent security, provider, hardware, tax, accessibility, or store certification. ## Published packages All sixteen public packages use the same `12.0.0` version: - macros: `rullst-macros`, `rullst-orm-macros`; - foundations: `rullst-orm`, `rullst-core`, `rullst-messaging`; - domain services: `rullst-connect`, `rullst-iot`, `rullst-security`, `rullst-ai`, `rullst-capital`, `rullst-mail`, `rullst-auth`; - interfaces: `rullst-nexus`, `rullst-studio`; - entry points: `rullst`, `cargo-rullst`. The machine-readable publication order is [`.github/release-order.json`](https://github.com/Rullst/Rullst/blob/v12.0.0/.github/release-order.json). ## What v12 stabilizes Version 12 stabilizes the **bounded public contracts** described by the [Single Source of Truth](https://rullst.github.io/Rullst/book/spec.html). Major delivered areas include the Axum/Tokio runtime and CLI scaffolding, relational ORM foundations, typed Turso support, bounded optional persistence adapters, authentication and security controls, provider-oriented AI, payments and mail, broker-neutral local messaging, Studio and Nexus, and bounded Omni packaging foundations. “Stable” means the documented v12 APIs and stated boundaries are suitable for normal SemVer maintenance. It does not mean that every roadmap idea is complete or that every third-party service, deployment, mobile device, fiscal authority, or hardware platform has been homologated. Use these active sources instead of old development percentages: - [Specification](https://rullst.github.io/Rullst/book/spec.html) — normative architecture and capability boundaries; - [Capability status](https://rullst.github.io/Rullst/book/capability-status.html) — compact implemented/partial/ roadmap view; - [Capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html) — detailed evidence and limitations; - [Release audit](https://rullst.github.io/Rullst/book/v12-release-audit.html) — repository-owned verification record; - [Changelog](https://github.com/Rullst/Rullst/blob/main/CHANGELOG.md) — exact user-facing changes; - [Workflow guide](https://github.com/Rullst/Rullst/blob/main/WORKFLOWS.md) — current automated, scheduled, and manual checks. ## Verification summary The stable candidate passed the required exact-SHA hosted gates and the tag-only publication pipeline. The maintained assurance system covers multi-platform tests, strict linting and formatting, package consumers, documentation, coverage floors, dependency and source policy, code scanning, generated applications, DAST, property tests, sanitizers, Miri, Kani, fuzzing, Omni compile matrices, and release artifact reproduction. Every result is scoped to its workflow, target inventory, toolchain, commit, fixtures, and environment. For example, compilation of an unsigned mobile shell does not prove physical-device behavior or app-store acceptance, and protocol fixtures do not prove acceptance by a live provider. The [release audit](https://rullst.github.io/Rullst/book/v12-release-audit.html) records those evidence boundaries. ## Upgrading to v12 Existing applications should follow the [v12 migration hub](https://rullst.github.io/Rullst/book/migration-v12.html) and the guide for their starting line: - [v5 to v12](https://rullst.github.io/Rullst/book/migration-v5-to-v12.html); - [v6 to v12](https://rullst.github.io/Rullst/book/migration-v6-to-v12.html); - [v11-era dependencies to v12](https://rullst.github.io/Rullst/book/migration-v11-to-v12.html). `cargo rullst upgrade --dry-run` and `cargo rullst upgrade` from the v12 CLI provide an assisted, inspectable migration workflow; the v13 CLI accepts only v12 and v13 projects. They do not silently rewrite arbitrary application semantics; review the generated plan, back up application data, commit a clean baseline, run migrations in a safe environment, and execute the application's own tests. ## Maintenance after release The `12.x` line accepts compatible correctness, security, documentation, and dependency-maintenance releases. Breaking APIs and substantial new capabilities belong to v13. Security reports should follow [`SECURITY.md`](https://github.com/Rullst/Rullst/blob/main/SECURITY.md). <a id="1210-delivery-checkpoint-unreleased"></a> ### 12.1.0 historical delivery checkpoints The following pre-publication checkpoints preserve their original source and test boundaries. References below to a candidate, working source or pending release gates describe those earlier stages. The [published maintenance record](https://rullst.github.io/Rullst/book/v12.html#1210-published-maintenance-release) supersedes their release-readiness status. The maintenance candidate brought together: - Studio asset/cache routing and Nexus/Portfolio mobile fixes, with migration notes for application-owned workaround routes and generated views; - reviewed dependency and pinned Action updates, with synchronized fuzz locks; - [SaaS maintenance](https://rullst.github.io/Rullst/book/saas-v12-1-v13-triage.html), including provider configuration, Stripe signature rotation, machine-client WAF defaults and honest operation boundaries across the v12 gateway inventory; - bounded update notices, explicit advisory `cargo rullst update check`, the complete managed-package inventory and staged backup restoration; - the [API reference index](https://rullst.github.io/Rullst/book/api-reference.html), clearer [HTMX/CSRF guidance](https://rullst.github.io/Rullst/book/tutorials/07-forms-and-validation.html#step-3-send-the-csrf-token-with-browser-forms) and existing verification-efficiency improvements. The explicit checker has bounded private Unix caching and offline reuse. The Windows owner/DACL implementation cross-compiles with strict Clippy. Its three native cache contracts and the complete Windows CLI standard shard passed at `990ddf1cf790cd422c1770a87b3fa19d13ab9ecb` in [run 35259362060](https://github.com/Rullst/Rullst/actions/runs/35259362060/job/105331464048). That evidence covers private cache roundtrips/expiry/replacement and rejection of unsafe ACLs, locks, links and paths; the final candidate still requires all release gates on its exact source commit. Its source/age report grants no installation authority. Tauri materializes the Android Gradle configuration before the standalone Gradle check, and its tutorial is registered in the documentation inventory gate. The 2026-09-16 Linux check passed 318 CLI unit/process tests (including upgrade recovery and Omni tool boundaries), both documentation-inventory tests, CLI all-target/all-feature strict Clippy, formatting, Action pin validation and the mdBook build. These targeted local results are not a full workspace, Windows/macOS or native Android sign-off; the candidate's hosted checks remain required. Native Android evidence must include the actual signed APK and its certificate, not just process fixtures or a successful Rust compile. The [native Android check](https://github.com/Rullst/Rullst/actions/runs/35262089400) passed at `95225f5e`: a disposable key signed the generated release APK, and `apksigner` verification plus a DER certificate digest comparison passed. The certificate parser accepts both signer-label formats emitted by the supported toolchain and rejects substituted certificates or extra signers. This covers the generated CI shell, not physical devices or store acceptance. The [full Rust CI matrix](https://github.com/Rullst/Rullst/actions/runs/35262104284) passed at `95225f5e`, including all test shards on Linux, Windows and macOS, strict workspace Clippy/format, database/provider matrices and public feature boundaries. Subsequent checkout, billing, ORM and upgrade changes require a new exact-commit run; this checkpoint does not certify those later changes or replace the other publication gates. The [follow-up matrix](https://github.com/Rullst/Rullst/actions/runs/35267853511) at `e1d8fe4d` finished with 35 successful and seven failed jobs. The failures came from unconditional enum codecs in isolated ORM builds and an unexpanded billing persistence placeholder in the CLI template fixture. `98977d0b` and `c5ae541d` correct those causes with local regression evidence. Later customer and inbox additions still need a full candidate run; this failed checkpoint is not release admission. The sixteen public manifests and their internal minimum requirements now use the **unpublished 12.1.0 candidate**. The workspace and ten fuzz lockfiles are synchronized without changing external dependency versions. The metadata preflight validates all sixteen packages and their topological order. This version preparation creates neither a release tag nor a registry publication. Additional local gateway review passes 157 Capital tests with Actix and strict all-target Clippy. Lemon Squeezy now validates subscription lifecycle/state, store/mode/identities and expiry; InfinitePay's unreviewed live callback fails explicitly. The initial generated-billing containment is superseded by the customer-bound Stripe integration: durable intents, session recovery, atomic event completion and revision-fenced reconciliation. Polar has a typed current product checkout and external-customer webhook binding. The expanded SQLite/Turso materialized contracts cover lost responses, replay, rollback and ownership. All ten scaffold contracts and CLI all-feature/all-target Clippy pass. Existing SaaS controllers require an application-owned migration. Native artifact preparation at `6db7a2a6` passed actual release builds and version/inventory smoke checks on Windows x64 and both macOS architectures in [run 35276572044](https://github.com/Rullst/Rullst/actions/runs/35276572044). Linux built but its staging script failed because the runner's default Python lacked `tomllib`; `b97b02ef` selects Python explicitly. All four native artifact builds and version/inventory smoke checks then passed at `020d9a72` in [run 35278808660](https://github.com/Rullst/Rullst/actions/runs/35278808660), alongside Ubuntu/Windows standard CLI tests and workspace strict Clippy/format. The full candidate run at `64979b0e` exposed macOS cache-writer contention; explicit lock release and a duplicate-handle regression address it. The next macOS attempt found a nonportable FIFO test fixture, corrected with POSIX `mkfifo`. Native macOS standard CLI tests and workspace strict Clippy/format passed at `011d9437` in [run 35279884257](https://github.com/Rullst/Rullst/actions/runs/35279884257). These are bounded checkpoints, not an exact final candidate acceptance. Working-source `update verify` authenticates a private local manifest through a certificate-bound GitHub CLI policy, then hashes both native binaries without executing or installing them. Local policy, tampering, size, path, timeout and CLI composition tests pass, together with strict all-target CLI Clippy. The same certificate policy accepted the already-published v12.0.0 release-context artifact and rejected changed bytes and a mismatched source tag. That checks the existing verifier protocol, not unpublished 12.1.0 assets or installation. Working-source `update project prepare` now produces private before/candidate source copies and a bounded migration report without editing the original or executing builds/tests. Local process fixtures pass for dirty/untracked/deleted files, ignored/absent lockfiles, virtual workspaces, migration findings, downgrade rejection and unsafe inputs. Standard CLI tests and workspace strict Clippy/format passed at `88ddabad` on [Windows](https://github.com/Rullst/Rullst/actions/runs/35282333488) and [macOS](https://github.com/Rullst/Rullst/actions/runs/35282336539). The earlier full matrix at `64979b0e` finished with 42 successful jobs, including package-archive consumers and the isolated installed CLI, and only the subsequently corrected macOS cache contention failure. The full candidate matrix at `88ddabad` subsequently **passed**, including all native builds, package-archive consumers and the isolated installed CLI, in [run 35282705151](https://github.com/Rullst/Rullst/actions/runs/35282705151). It does not cover subsequent updater changes. Working-source `update project verify` adds exact command preview, source/plan revalidation, an exclusive lock and explicit trusted-code consent. Real locked checks and tests run in another private copy; 14 local preparation/verification process tests pass, alongside 300 CLI library tests, nine legacy upgrade fixtures and strict all-target CLI Clippy. Tests include stale inputs, missing consent, contention, timeout, application-test failure and unexpected source writes. Reports grant no application authority. Native Linux/macOS verification passed at `1c4ddd12`; Windows passed after path normalization at `1c4b3e60`. The native update diagnostic at `1c4ddd12` passed on Ubuntu but exposed a Windows lexical mismatch between Cargo paths and private-storage verbatim prefixes before project execution. Filesystem-resolved membership comparisons address that defect; Windows confirmation passed in [run 35284872349](https://github.com/Rullst/Rullst/actions/runs/35284872349). The working source also deduplicates those manifest identities. Explicit review now revalidates successful command logs and file inventories and returns the full dependency diff plus its review digest. All 16 local preparation, verification and review process tests pass with strict CLI Clippy and mdBook; these were focused review checks before application/recovery was added. At `9fb49b63`, explicit digest-approved application/recovery stages all reviewed manifest/root-lockfile replacements before original writes, binds access policy, persists an intent and refuses divergent edits. Local acceptance passed 306 CLI library tests, 22 project-flow process tests and nine legacy upgrade tests, strict all-target CLI Clippy and mdBook. Linux/macOS updater checks passed in [run 35287712905](https://github.com/Rullst/Rullst/actions/runs/35287712905); Windows found an SDDL buffer-capacity versus string-length defect before original writes; `e547746e` stops at the bounded terminator. Subsequent Windows tests exposed Git's handling of verbatim absolute paths in review diffs; `8b58cd12` uses relative paths from the canonical private cache root. At that checkpoint, the complete native updater diagnostic **passed** on Linux, Windows and macOS together with workspace strict Clippy/format in [run 35289409340](https://github.com/Rullst/Rullst/actions/runs/35289409340). Additional local tests kill a child after the first production replacement and recover the persisted before/after states; a Linux file-size limit terminates real CLI staging with originals unchanged. These tests do not prove power-loss behavior or replace the remaining native/final-candidate acceptance. The working source additionally stages bounded authenticated CLI downloads; it rechecks fresh registry eligibility, authenticates the manifest before fetching binaries and verifies exact bytes without executing or installing them. `86b6178f` adds a total body deadline, including a real slow-response regression. At `f2aa8055`, installation review authenticates a fresh eligible candidate and previews a new/empty private root without creating it or executing binaries. Local acceptance passed 319 CLI library, 7 discovery, 23 project-flow, 3 artifact/installation and 9 legacy-upgrade tests, strict all-target CLI Clippy and mdBook. These later changes still require native candidate acceptance. The latest examples report is pinned to `1af10ed4` and adds application retry quota/session resumption and Chromium CSP handoff findings. The SaaS generator now emits an exact Stripe `form-action` origin. A local Chromium regression reproduces the strict-default block, accepts the intended POST/303 handoff and blocks lookalike hosts, another provider, HTTP, another port and an unreviewed second redirect. Destinations are intercepted; this does not exercise payment, deployed SaaS or live provider sessions. The 319 CLI library tests, strict CLI Clippy, format, workflow lint and documentation build pass locally. The [full Rust matrix](https://github.com/Rullst/Rullst/actions/runs/35291343157) passed at `21ce347b`, including all three native hosts, generated applications, strict workspace Clippy/format, MSRV, four native CLI builds, sixteen package checks and archive-only consumers. The [managed installation native check](https://github.com/Rullst/Rullst/actions/runs/35296176849) passed at `ef0a8577` on Linux, Windows and macOS with workspace strict Clippy/format. It covers initial installation, updates, exact predecessor recovery, private receipts/locks, late replacement failures, an executing older binary and actual process termination. It does not claim power-loss safety or published 12.1.0 artifact acceptance. Working source also composes the checked commands through `update guided`, with separate default-no approvals and elapsed time per stage. Additional local regressions resume every injected recovery failure, reject oversized local files before hashing and preserve installed entries under an OS file-size limit. A real terminal exercises refused preparation/execution, failed project tests and approved manifest application followed by recovery. The new guided flow and additional faults must pass the final candidate's native matrix. The Stripe/Polar candidate passed the full 48-job Rust matrix at `4b5aca21`, including native CLI and package-consumer checks. Its source at `8c3c8391` also passed SemVer, coverage and CodeQL. Links and scope are retained in the [SaaS evidence record](https://rullst.github.io/Rullst/book/saas-v12-1-v13-triage.html#final-maintenance-consolidation). The subsequent Paddle customer/transaction and subscription work passed fresh SemVer, coverage and CodeQL checks; the full candidate Rust matrix remains in progress. See [12.1 migration](https://rullst.github.io/Rullst/book/migration-v12-1.html) for final API adoption. Stripe adapter acceptance now covers test-mode hosted Checkout, paid-invoice amount/currency/customer checks, provider-relayed signed events, current-state reads and cancellation at `0f15d70a`; see the redacted SaaS evidence record above. Fresh CLI-generated SQLite billing at `370a4170` also passed controller and signed-webhook persistence, process restart, duplicate delivery and altered signature rejection against real Stripe test events. Its isolated database contained the matching active and canceled subscriptions and event receipts. Release preparation now includes an exact dated `12.1.0` changelog section, successful release-note rendering, matching versions and topological internal dependencies for all sixteen packages, and a crates.io ownership check. None of the sixteen `12.1.0` versions was indexed during this check. Workflow validation now extracts notes for the current package version instead of only checking `12.0.0`; these checks do not create a tag or authorize registry publication. The next dependency candidate consolidates Dependabot PRs #207–#212 for 12.1, including their five Action updates and the four grouped Rust updates. The `tinyvec` resolver pin also requires refreshing four ORM-dependent fuzz locks. Earlier CI results remain evidence for their original commits; the combined candidate requires fresh CI and exact-main release admission before publication. **Release blockers still open:** latest-candidate CI, remaining provider-account acceptance and all exact-main release admission checks. Generated Stripe billing requires the account, recurring price, webhook and migration setup documented in generated BILLING.md. The existing SaaS staging's 12.0 one-time payment handoff remains separate from the candidate's isolated acceptance; deployed ingress, authentication and database migration have not been exercised here. The [acceptance plan](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#safe-update-experience) defines the bounded updater delivery. Future v13 migration rules remain v13 work. Do not regenerate an existing application over local changes. Updating the CLI or framework dependencies neither rewrites application-owned blueprint files nor redeploys the Azure demonstrations. New capability development should focus on v13 after this compatible minor is validated and released; v12 then receives necessary maintenance by exception. The maintenance candidate also adds default Rullst-branded Omni icons applied after native initialization, application-owned Android release signing and upgrade recovery fixes for macOS root aliases and Windows strict linting. See [Android signing](https://github.com/Rullst/Rullst/blob/v12/docs/src/tutorials/49-omni-android-signing.md). These changes are unreleased; native CI and cross-platform recovery acceptance remain required. ## Preserved development history The former v12 program contained 2,410 lines of planning notes, intermediate percentages, and a detailed audit of 190 historical roadmap claims. It remains available in the immutable [`v12.0.0` source snapshot](https://github.com/Rullst/Rullst/blob/v12.0.0/docs/src/v12.md) and Git history. The compact classification dataset used by documentation CI is [`.github/historical-roadmap-ledger-v12.tsv`](https://github.com/Rullst/Rullst/blob/main/.github/historical-roadmap-ledger-v12.tsv). Those historical labels describe what an earlier roadmap claimed and how the v12 audit classified it. They are not a substitute for the current specification, capability ledger, or executable evidence. --- # Migrating Applications to v12 Source: https://rullst.github.io/Rullst/book/migration-v12.html # Migrating an application to Rullst v12 > [!NOTE] > **Historical guide (v12).** The 12.x line reached end of life on 16 October > 2026. Use these pages only to bring an older application to v12, then follow > [Preparing an application for v13](https://rullst.github.io/Rullst/book/migration-v13.html). Rullst v12 is a coordinated release train of 16 packages. Upgrade all direct `rullst-*` dependencies together; mixing v12 facade/runtime crates with older domain crates is outside the supported compatibility contract. Choose the guide matching the application's source baseline: - [v5 to v12](https://rullst.github.io/Rullst/book/migration-v5-to-v12.html) - [v6 to v12](https://rullst.github.io/Rullst/book/migration-v6-to-v12.html) - [v11-era dependencies to v12](https://rullst.github.io/Rullst/book/migration-v11-to-v12.html) Only v5 has a repository release tag among those three baselines. The repository contains a v6 source snapshot, while “v11” principally identifies ecosystem dependencies such as `rullst-connect` rather than a tagged v11 umbrella release. The guides state these evidence limits instead of inventing release history. ## Safe upgrade procedure 1. Commit or stash the application and record its current `Cargo.lock`. 2. Back up every database and prove that the backup can be restored. 3. Run the old application's tests and save any known failures. 4. Install the published stable CLI with `cargo install cargo-rullst --version 12.1.0 --locked`. Pin an exact version for reproducible migrations. 5. Run `cargo rullst upgrade --dry-run` from the application root and resolve every `BLOCKER`; use `--dry-run --json` when CI or other tooling consumes the versioned plan. 6. Run `cargo rullst upgrade` to execute the backed-up transaction. 7. Review `Cargo.toml`, `Cargo.lock`, every compiler-provided edit, and the Markdown/JSON reports under `target/rullst-upgrades/`. 8. Apply the baseline-specific manual changes below. 9. Run migrations against a disposable copy of production-shaped data. 10. Execute the application's tests, authorization negatives, and deployment smoke tests before merging. The v12 upgrade command has deliberately bounded behavior: - it discovers exact Cargo workspace members and updates standard, inline, workspace, target-specific and renamed versioned Rullst dependencies while preserving TOML comments and relative order; - it leaves unversioned path/git dependencies untouched and reports them; - it never rewrites valid Axum, SQLx, or Tokio imports; - it selects source checks from a versioned migration-rule catalog and can emit the versioned `rullst.upgrade-plan.v1` JSON envelope; - it snapshots manifests, the root lockfile and Rust sources before applying compiler-provided `cargo fix`, then runs `cargo check` for the workspace's selected features; - it restores the snapshot when a gate fails unless `--keep-on-failure` was explicitly selected; an interrupted run can be recovered with `cargo rullst upgrade --restore <backup-directory>`; - it returns failure when any gate fails and never reports “100% stable” or production readiness. It does not install a new CLI globally, change application secrets, run database migrations, prove runtime behavior, or replace the full test suite. See the complete [assisted upgrade tutorial](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html) for the v5 workflow, recovery examples, JSON contract and future-major policy. ## Version placeholder The snippets in these guides target `12.1.0`; check [publication status](https://rullst.github.io/Rullst/book/v12.html). Keep the selected version while reproducing this migration, then review any later v12 maintenance update before changing the application's lockfile. ## Mandatory v12 review Every baseline must review these contracts: - [Cargo feature defaults and aliases](https://rullst.github.io/Rullst/book/feature-matrix.html); - [compatibility, MSRV, and support policy](https://rullst.github.io/Rullst/book/compatibility-policy.html); - explicit Nexus access policy and debug-only loopback convenience; - explicit Nexus tenant metadata for tenant-owned admin models, with a trusted `TenantContext`, plus the optional required audit schema/policy when mutation evidence is needed; - debug-only, loopback-bound Studio deployment; - ownership checks on parameterized data routes; - exact CSRF webhook exemptions, WAF/body limits, and trusted proxy identity; - deterministic offline provider credentials versus live provider validation; - Capital subscription calls: `Billable::extend_trial(15)` is a relative 15-day operation in v12; use `set_trial_end(unix_timestamp)` for an absolute provider timestamp and persist a command clock with `extend_trial_days_at` when a worker may retry; - the [AI provider capability matrix](https://rullst.github.io/Rullst/book/ai-provider-capabilities.html). Do not deploy v12 solely because `cargo check` succeeds. Compilation does not validate data conversion, authorization, provider credentials, reverse-proxy trust, rollback, or restore behavior. --- # 12.0 to 12.1 Source: https://rullst.github.io/Rullst/book/migration-v12-1.html # Migrating from 12.0 to 12.1 > [!NOTE] > **Historical guide (v12).** The 12.x line reached end of life on 16 October > 2026. A 12.x application moves to v13 with > [Preparing an application for v13](https://rullst.github.io/Rullst/book/migration-v13.html). Version [12.1.1 is published](https://rullst.github.io/Rullst/book/v12.html#1211-published-maintenance-release). Updating dependencies or the CLI does not rewrite generated application files, migrate databases or redeploy the Azure examples. Keep the existing application and lockfile, generate a separate comparison project, and review the changes below. <a id="preparing-for-the-1211-patch"></a> ## Upgrading to the 12.1.1 patch The [12.1.1 maintenance change set](https://github.com/Rullst/Rullst/blob/v12/docs/src/v12-1-1-review.md) preserves the 12.1.0 API, database schema and Rust 1.96.0 MSRV. Applications using public scaffold keys must replace them with unique secure secrets; changing a session-encryption key requires session renewal. HTTP drain now accounts for unfinished response bodies, and SQLite message leases use the time after acquiring the write lock. Existing generated LMS files are not rewritten by a dependency update. ## Account mail and machine requests The [account-mail guide](https://rullst.github.io/Rullst/book/account-mail-v12-1.html) describes the opt-in durable PostgreSQL/SQLite recovery registry and worker. Existing reset templates benefit from the mandatory-pipeline fix, but existing encrypted-cookie sessions and password tables do not gain revocation automatically. Review imports before switching the authoritative account store. Keep provider tracking disabled. `Server::with_machine_endpoints` accepts exact method/path registrations with mandatory bearer, signed-webhook or transport mTLS verification. Use it for trusted machine POST routes instead of disabling browser CSRF globally. It rejects browser-cookie/origin inputs; the application still owns domain permissions and ingress rate limits. Core with `orm` and disabled defaults now requires an explicit `strict-*` backend or `drivers-all`. Studio and Nexus retain their default convenience features; disable defaults to obtain an exclusive backend graph. Studio's SQLite queue is controlled by `queue-sqlite`, independent of PostgreSQL/MySQL. The facade forwards these choices, including optional Studio/Nexus consumers. ## Billing migration Generated real-mode Stripe billing now additionally requires `BILLING_LIVE_ACKNOWLEDGEMENT=I_UNDERSTAND_REAL_CHARGES`. This is a launch control, not a substitute for merchant activation and refund/dispute procedures. Test credentials do not need that acknowledgement. For a one-time purchase, use the opt-in `StripePaymentCheckoutRequest` and `create_one_time_checkout` API, with `StripeOneTimePrice` from a server-owned allowlist. It validates the current active provider price before creation and uses `mode=payment`, one card-funded item and a fixed amount/currency. It does not enable adaptive pricing, discounts or automatic tax. The existing generated subscription flow remains recurring; do not substitute a one-time price there. Persist owner/account/mode/attempt/session bindings. Process `verify_one_time_event` notifications through a durable inbox, then re-read `read_one_time_receipt` before an atomic entitlement transition. Refund and dispute hints require reconciliation too. Any partial refund/dispute prevents a `Paid` receipt; restoration after a resolved dispute needs an explicit reviewed application policy. Never grant access from a return URL or mock receipt. This additional one-time contract has protocol tests; the earlier subscription sandbox evidence does not validate it or the production certificate offer. | Existing integration | Required change | |---|---| | Generated SaaS/`make:billing` on 12.0 | Adopt the new billing modules and migrations in a reviewed application change. Configure the provider account, mode, recurring price allowlist and HTTPS URLs from generated `BILLING.md`. | | Stripe email-based owner lookup | Persist opaque local owner/customer/account/mode bindings. Use typed customer and checkout requests with durable immutable attempt keys; store session IDs before HTTP 303. | | Stripe normalized events alone | Adopt signed Checkout/subscription envelopes, current-state reads and atomic inbox/domain commits under database revision fencing. Never grant access from email or a return-page visit. | | Stripe portal by email | Use the typed customer-ID-bound portal operation after authorizing the persisted binding. Other legacy uniform live portal calls remain unsupported. | | Paddle legacy checkout | Migrate to `PaddleCustomerRequest`, `PaddleCheckoutRequest` and `create_transaction_checkout`. Configure the default approved Paddle.js payment page, select sandbox explicitly and persist all attempt/customer/transaction bindings. | | Polar price-based checkout | Migrate to `PolarCheckoutRequest` and `create_product_checkout` with a product UUID and opaque external customer identity. Supply client IP only from a reviewed trusted-proxy boundary. | | Lemon Squeezy checkout | Supply the merchant's positive numeric store ID through `with_store_id` and a variant belonging to that store. | | Wise email-based transfer | Do not invoke it with real credentials. Recipient, quote, transfer and funding need separate reviewed contracts; the legacy call fails before network dispatch. | Stripe's customer-ID portal requires an active default portal configuration in the selected account and test/live mode, plus permission to create portal sessions. Configure allowed subscription changes and cancellation explicitly and test the portal handoff separately. Successful Checkout acceptance does not validate portal configuration; generated `BILLING.md` records this prerequisite. The new generated durable real-provider integration is Stripe-specific. Paddle and Polar expose typed adapter operations; their application persistence and event orchestration remain explicit. Consult the [complete provider matrix](https://github.com/Rullst/Rullst/blob/v12.1.1/rullst-capital/README.md#-supported-providers) before enabling a method. Empty/`mock_*` credentials are deterministic local fixtures; mixed configuration must not silently produce a real-payment success. Do not replay a Paddle or Polar creation merely because it timed out. Their typed correlation metadata does not establish provider idempotency. Recover known objects and reconcile uncertain outcomes before another mutation. Stripe also needs durable attempt retention and reconciliation after its idempotency window. Generated `BILLING.md` describes recovery and replacement subscriptions. Existing customer rows cannot be adopted just by matching email. Back up the application database, review schema/unique constraints and migrate only bindings established from authorized provider evidence. Do not replace paid production tables with empty generated models. Test rollback and restart before rollout. ## Application and CLI changes - Replace `Redirect::temporary`/307 for hosted checkout with a 303 handoff. Add the exact provider checkout origin to application CSP `form-action`; Stripe's default SaaS policy permits `https://checkout.stripe.com`. - Commit application `Cargo.lock` and use locked container builds. Remove the generated unsupported MSVC `/DEBUG:FASTLINK` flag from existing projects. - Review strict ORM feature edges: backend-specific builds require disabled default features throughout the resolved dependency graph, not just one crate. - Nexus and AI share provider resolution, including explicitly configured Groq. Azure Basic authentication still requires the explicit trusted-TLS capability; raw forwarding headers do not establish trusted termination. - Core's default WAF no longer blocks ordinary HTTP libraries by user agent. Explicit application blocklists remain unchanged and may need local review. - Remove application workarounds for Studio embedded assets/cache navigation and the Nexus mobile drawer only after testing the updated native renderers. - Native CLI installation/update requires the admitted release artifacts and their verification evidence. Candidate CI binaries are not release assets. Application updates require a reviewed diff; installing the CLI alone does not update an application's dependencies or migrations. ## Acceptance and release evidence Run the provider's sandbox journey against the candidate, including signed events, owner isolation, retry/replay, cancellation and current-state recovery. Keep fixture tests, hosted CI and provider-account acceptance distinct. A Chromium navigation to hosted Checkout proves handoff only; it does not prove payment, webhook delivery or subscription reconciliation. A one-time payment application does not validate the framework's recurring subscription flow. The release additionally requires the full workspace all-feature tests, strict Clippy, formatting, package/consumer checks, coverage, SemVer/security checks and every workflow listed in `.github/release-required-workflows.json` at the exact `v12` commit. Only then may the protected release pipeline publish the sixteen crates in `.github/release-order.json` and attach verified native CLI binaries. See [release recovery](https://rullst.github.io/Rullst/book/release-recovery.html) for partial publication handling. --- # v5 to v12 Source: https://rullst.github.io/Rullst/book/migration-v5-to-v12.html # Migration guide: v5 to v12 > [!NOTE] > **Historical guide (v12).** The 12.x line reached end of life on 16 October > 2026. The v13 CLI upgrades only v12 and v13 projects: bring the application > to v12 with this guide and the v12 CLI, then follow > [Preparing an application for v13](https://rullst.github.io/Rullst/book/migration-v13.html). Baseline: the repository tag `v5.0.0` and the compatible v5 line. This is a multi-major migration. Use a branch and follow the common [safe upgrade procedure](https://rullst.github.io/Rullst/book/migration-v12.html) before applying these changes. The [assisted upgrade tutorial](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html) shows the complete CLI transaction and recovery flow. ## 1. Replace the dependency graph The v5 facade had no default features, kept several modules inside `rullst`, depended on `rullst-orm = 6.1.1`, and used `rullst-connect = 11`. V12 releases all framework packages in one version train and enables `orm` plus `queue-sqlite` by default. To preserve the old opt-in behavior, start explicitly: ```toml [dependencies] rullst = { version = "12.1.0", default-features = false, features = ["orm", "queue-sqlite"] } ``` Add only the domain features the application actually uses. Consult the [feature matrix](https://rullst.github.io/Rullst/book/feature-matrix.html); do not copy `--all-features` into a production manifest. Remove independently pinned old Rullst package versions or move every direct package to the same v12 version. Important feature changes include: - `auth`, `ai`, `capital`, `nexus`, `studio`, `security`, and `iot` now select real optional crates rather than empty facade markers; - `mail` selects HTTP/offline transports without SMTP; `mailer` remains a compatibility alias for `mail-smtp` when Lettre SMTP is required; - `redis` selects both queue and cache Redis boundaries; - `strict-postgres`, `strict-mysql`, and `strict-sqlite` select concrete ORM database types. Select at most one in an application. ## 2. Move from attribute-style routing The v5 README demonstrated attribute-style `#[routes]` registration. V12's supported registration surface is the explicit `routes!` macro. The deprecated `#[route]` compatibility marker does not register a route. ```rust,no_run use rullst::{Server, response::Html, routes}; async fn home() -> Html<&'static str> { Html("Hello from v12") } #[rullst::runtime::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let app = routes![get("/" => home)]; Server::new(app).run(3000).await?; Ok(()) } ``` Direct Axum escape hatches remain supported through `rullst::web::axum` or a normal direct dependency. The upgrade command does not rewrite those imports; its versioned scanner reports the known v5 routing/server markers for manual, semantic conversion. ## 3. Review ORM and migrations - Replace the old independently pinned ORM with v12. - Decide between SQLx `Any` and one concrete `strict-*` backend. - Run schema migrations on a disposable restored database first. - Recheck raw SQL parameterization, tenant predicates, ownership guards, query limits, pool timeouts, and rollback behavior. - Never assume that a successful compile proves an old migration is reversible. ## 4. Rebuild administrative and security boundaries Nexus no longer mounts as an implicitly open admin router. New code should use `Nexus::try_build()` with either validated production authentication or `LocalNexusAccess::loopback_only()` in a debug build. Basic Auth requires a verified TLS boundary in production. Tenant-owned Nexus models should add an explicit text tenant column to their derive metadata and install `TenantContext` only after authenticated membership resolution. Models without this metadata remain global administrator models. If `with_required_audit()` is enabled, run `create_nexus_audit_table()` as a deployment migration first; a missing table intentionally rolls mutations back. Studio must remain a separate debug-only loopback service. Rebuild the server middleware order using the v12 production baseline, then add application authorization and the extended `rullst-security` layers explicitly where used. Re-test CSRF, CORS, webhook signatures, request limits, IDOR, and cross-tenant denials. ## 5. Validate provider migrations Empty or `mock_*` credentials intentionally use deterministic offline behavior. They are not proof that mail, OAuth, billing, AI, or storage providers work live. Validate each configured provider in a non-production account and keep unsupported fiscal, transport, and hardware capabilities fail-closed. ## Completion gate Run at minimum: ```bash cargo fmt --all -- --check cargo clippy --workspace --all-targets --all-features -- -D warnings cargo test --workspace --all-features ``` Also test the application's actual feature selection without `--all-features`, database restore/migration/rollback, Nexus and Studio network exposure, and a production-profile smoke build. --- # v6 to v12 Source: https://rullst.github.io/Rullst/book/migration-v6-to-v12.html # Migration guide: v6 to v12 > [!NOTE] > **Historical guide (v12).** The 12.x line reached end of life on 16 October > 2026. The v13 CLI upgrades only v12 and v13 projects: bring the application > to v12 with this guide and the v12 CLI, then follow > [Preparing an application for v13](https://rullst.github.io/Rullst/book/migration-v13.html). Baseline: repository source commit `5229132f`, which bumped the facade to `6.0.0`. There is no repository v6 tag, so first verify the exact crate versions in the application's `Cargo.lock`; do not assume this snapshot matches every artifact an application may have consumed. Follow the common [safe upgrade procedure](https://rullst.github.io/Rullst/book/migration-v12.html). ## 1. Normalize the mixed-version ecosystem The v6 snapshot combined `rullst = 6.0.0`, `rullst-orm = 6.1.1`, `rullst-connect = 11.0.0`, path-based internal crates, and unversioned internal path requirements. V12 uses one synchronized version for all 16 publishable packages. For the old no-default behavior: ```toml [dependencies] rullst = { version = "12.1.0", default-features = false, features = ["orm", "queue-sqlite"] } ``` The explicit features above opt into v12's local database behavior. Omit them for a database-free HTTP service. If the application relies on v12 defaults, `rullst = "12.1.0"` enables both automatically. Remove obsolete direct `lettre` wiring from the facade migration and use `mail-smtp`. Review the new `security`, `iot`, `redis`, and `strict-*` boundaries in the [feature matrix](https://rullst.github.io/Rullst/book/feature-matrix.html). Path-only dependencies are not changed by `cargo rullst upgrade`. ## 2. Preserve valid ecosystem escape hatches V6 applications often imported Axum, SQLx, and Tokio directly. Those imports remain valid application choices. V12 exposes convenience paths, but migration does not require global substitutions such as `axum::` to `rullst::server::`. Review imports using compiler errors and API intent, not blind text replacement. Prefer explicit `routes!` registration for the central router and propagate startup errors from `Server::run`. Deprecated route attributes must not be treated as functional registration. ## 3. Revalidate modular crate boundaries V6 had already started splitting Core, Auth, Mail, AI, Nexus, Capital, and Studio. V12 makes those package and feature relationships explicit and adds the independently versioned ORM macros, Connect, Security, and IoT packages to the same release train. - Update direct imports only when the corresponding feature is enabled. - Remove code that depended on empty facade features compiling without their domain crate. - Use the umbrella `mail` feature for HTTP/offline transports. Add `mail-smtp` only when the SMTP dependency is required. - For OpenTelemetry export, enable `telemetry`; process-local Radar data does not require it. ## 4. Apply v12 security changes - Build Nexus with an explicit access policy and `try_build()`. - Keep Studio debug-only and loopback-bound. - Derive tenant identity from authenticated membership, never directly from a selector header, subdomain, or URL parameter. - Recheck exact CSRF exemptions, trusted proxy/TLS metadata, webhook replay protection, body limits, and parameterized-route ownership. - Treat local security telemetry events as unsigned unless a configured audit verifier proves HMAC integrity. ## 5. Revalidate data and providers Choose one strict database backend or SQLx `Any`, run migrations on restored data, and exercise real SQLite/PostgreSQL/MySQL behavior used by the application. Then validate live OAuth, mail, billing, and AI credentials separately from their deterministic offline modes. Finish with the full commands in the [common guide](https://rullst.github.io/Rullst/book/migration-v12.html), the application's exact feature build, a production-profile smoke test, and a documented rollback rehearsal. --- # v11-era Dependencies to v12 Source: https://rullst.github.io/Rullst/book/migration-v11-to-v12.html # Migration guide: v11-era dependencies to v12 > [!NOTE] > **Historical guide (v12).** The 12.x line reached end of life on 16 October > 2026. The v13 CLI upgrades only v12 and v13 projects: bring the application > to v12 with this guide and the v12 CLI, then follow > [Preparing an application for v13](https://rullst.github.io/Rullst/book/migration-v13.html). There is no repository tag for a `rullst` umbrella v11 release. The v5/v6 source line did, however, consume ecosystem packages such as `rullst-connect = 11`. This guide therefore targets applications whose manifests or lockfiles contain v11 Rullst ecosystem crates, and any untagged development snapshot described as “v11”. Record the exact starting commit and dependency graph before proceeding. Follow the common [safe upgrade procedure](https://rullst.github.io/Rullst/book/migration-v12.html). ## 1. Inventory before editing ```bash cargo tree -e features cargo metadata --format-version 1 ``` Save the output and list every direct or transitive `rullst-*` package. In v12, all 16 published packages must use the same release version. Do not update only `rullst-connect` or only the umbrella crate. Example explicit selection: ```toml [dependencies] rullst = { version = "12.1.0", default-features = false, features = ["orm", "queue-sqlite", "auth", "oauth", "security"] } ``` If the project uses renamed dependencies or local paths, update those entries manually; the upgrade command intentionally changes only standard versioned Rullst dependency keys. ## 2. Connect and authentication V12 `rullst-connect` keeps provider-neutral OAuth/OIDC types available without a web adapter. Select the adapter explicitly: - `axum` for Axum extractors and the local mock IdP router; - `actix` for Actix extractors; - `axum-session` for Axum plus `tower-sessions` integration; - `retry` only when the Connect retry client is intended; - `mock` only for deterministic offline provider modules. Empty credentials choose explicit offline behavior. Custom-provider configuration errors and live callback/state validation remain fallible. Re-test PKCE, state, redirect URL allowlists, session binding, logout, refresh, and unknown-key JWKS refresh behavior. `rullst-auth` enables Connect integration through its `oauth` feature, while the umbrella `rullst` facade exposes Connect through its own `oauth` feature. Review which layer the application imports instead of enabling both reflexively. ## 3. Adopt the v12 runtime contracts - Use explicit `routes!` registration; compatibility route attributes do not perform runtime registration. - Propagate startup, pool, migration, provider, and server errors. - Review the changed default features (`orm` and `queue-sqlite`). - Select at most one `strict-*` database backend. - Build Nexus with an explicit access policy and keep Studio on debug loopback. - Mark tenant-owned Nexus models with an explicit text tenant column and supply a membership-derived `TenantContext`; install the audit table before enabling `with_required_audit()`. - Re-run IDOR, tenant-isolation, CSRF/CORS, webhook, and trusted-proxy negative tests. ## 4. Review AI and tool assumptions V12 has a machine-readable [provider capability matrix](https://rullst.github.io/Rullst/book/ai-provider-capabilities.html). Built-in live transports have configurable request deadlines. The separate `StreamingAiClient` enforces bounded output and explicit cancellation, and an exact OpenAI-compatible configuration may opt into strict incremental SSE. Provider-native tool calling, automatic provider retries, ordinary non-streaming cancellation and streaming for other provider protocols are not uniform transport capabilities. `ToolRegistry` is guarded local dispatch infrastructure, not an authorized autonomous-agent boundary. ## Completion gate Compile both the application's exact feature set and the intended release profile. Run the full local trifecta, provider integration tests in non-production accounts, database restore/migration/rollback, and network-exposure checks. Keep the previous artifact and database restore procedure available until the v12 deployment has passed its observation window. --- # AI Provider Capability Matrix Source: https://rullst.github.io/Rullst/book/ai-provider-capabilities.html # AI provider capability matrix This matrix describes the transport paths implemented by the current `rullst-ai` source. Anthropic and Gemini streaming (with its explicit cancellation) and token usage were added in v13; the v12 crate does not have them. The matrix is not a claim that every model sold by a provider accepts every request. Model availability, account entitlements, regions, quotas, and upstream API behavior remain provider concerns. Applications can inspect the same contract in code through `AiProvider::capabilities()` or `AiClient::capabilities()`. Built-in provider tests assert every row so unsupported paths remain explicit instead of silently falling back to another operation. | Provider transport | Text | Chat | Embeddings | Vision | JSON | JSON Schema | Streaming | Provider tools | Rullst timeout | Automatic retry | Explicit cancellation | | --- | :---: | :---: | :---: | :---: | --- | :---: | :---: | :---: | :---: | :---: | :---: | | OpenAI | yes | yes | yes | yes | native mode | yes | no | no | yes | no | no | | Anthropic | yes | yes | no | yes | prompt only | no | native SSE | no | yes | no | yes (stream) | | Gemini | yes | yes | yes | yes | native mode | yes | native SSE | no | yes | no | yes (stream) | | DeepSeek | yes | yes | no | no | native mode | default model only | no | no | yes | no | no | | Ollama | yes | yes | yes | yes | native mode | yes | no | no | yes | no | no | | OpenAI-compatible | yes | yes | declared | declared | declared native mode | declared | declared SSE | no | yes | no | declared for SSE | `yes` means Rullst constructs and parses that provider request. A configured model can still reject vision, embeddings, or schema output. In particular: - DeepSeek JSON Schema is enabled only for the default `deepseek-v4-flash` transport contract. Selecting another model makes the capability false and returns `UnsupportedCapability`, including in deterministic offline mode. - Ollama vision, embeddings, JSON, and schema support depend on the installed local models. The transport can send those request shapes; it cannot prove that an arbitrary model implements them. - Anthropic JSON uses an instruction requesting one JSON value. It is labelled `prompt only` because the current transport does not request native JSON mode or schema enforcement. - Empty and `mock_*` credentials select deterministic offline behavior. They do not contact the configured endpoint and do not promote capabilities that the transport marks unsupported. - The OpenAI-compatible adapter defaults to text/chat only. Its exact endpoint/model configuration must explicitly declare embeddings, vision, native JSON mode, JSON Schema, and SSE streaming. This reports which request shapes Rullst will send; it does not discover or certify model behavior. ## Vision input sources The Vision column describes provider request-shape support, not unrestricted image acquisition. `AiClient::prompt_with_image` accepts application-admitted bytes. `prompt_with_image_file` additionally requires an exact canonical `LocalImagePolicy` root and byte budget. `prompt_with_image_url` accepts only an explicit `EgressFetcher`, so HTTPS host allowlisting, DNS pinning, peer and redirect validation, proxy bypass, timeout and streaming limits are mandatory. Both helpers verify bounded JPEG/PNG/WebP/GIF signatures; a supplied remote media type must match the bytes. Capability and prompt checks happen before file or network I/O. Local-directory trust, tenant/owner authorization, image decoding safety and model behavior remain application/provider boundaries. ## OpenAI-compatible local and cloud endpoints `OpenAiCompatibleProvider::try_local` accepts an unauthenticated OpenAI-shaped base URL only when its host is a literal loopback IP such as `127.0.0.1` or `::1`. It may use HTTP for local development and never probes localhost implicitly; requiring an IP literal avoids trusting host-name resolution. `try_local_with_bearer` supports an explicitly authenticated loopback server. `try_cloud` requires HTTPS and Bearer authentication; empty and `mock_*` keys select the offline fixture. All constructors reject URL credentials, query strings, and fragments, disable redirects and environment proxies, cap images at 10 MiB and JSON responses at 2 MiB, and retain the ordinary 30-second configurable request deadline. This adapter covers `/chat/completions`, optional `/embeddings`, OpenAI-shaped image content, the declared response-format modes, and opt-in strict `text/event-stream` chat deltas. It does not claim Azure query/header conventions, arbitrary authentication schemes, provider-native tools, retries, automatic model discovery, or compatibility with an unrelated HTTP protocol. Implement `AiProvider` for those explicit semantics. Local runtimes such as llama.cpp server, LocalAI, LM Studio, and vLLM are possible consumers only when their installed configuration exposes these exact shapes; Rullst does not certify a product name or infer capabilities from it. ## Operational boundaries ### Streaming `StreamingAiClient<P>` applies provider-independent output limits through static dispatch. For an exact OpenAI-compatible configuration that declares `with_streaming()`, Rullst parses incremental UTF-8 SSE deltas, requires the terminal `[DONE]` marker, rejects an incorrect media type, malformed/truncated events, a `finish_reason` of `length` or `content_filter` (the reply was cut short or withheld) and all configured byte/chunk overflows. The maximums are 4,096 chunks, 64 KiB per chunk and 2 MiB of raw response and delivered text. Anthropic and Gemini implement their native SSE protocols (v13) with the same bounds, guardrails and cancellation. Anthropic reads `message_start`, `content_block_delta` text deltas, `message_delta` and `message_stop`, ignores `ping` and unknown event types, maps an `error` event to its error type only, and requires `message_stop`. Gemini uses `streamGenerateContent?alt=sse`, skips thought parts, maps an `error` object or `promptFeedback.blockReason` to an error and requires a final `finishReason`. A truncated or withheld reply (Anthropic `max_tokens`, `model_context_window_exceeded` or `refusal`; Gemini `MAX_TOKENS` or a safety/recitation/blocklist reason) fails instead of being returned as complete, in both the streaming and the non-streaming paths. DeepSeek and Ollama ordinary payloads still select `stream: false`; DeepSeek streams through the OpenAI-compatible adapter. ### Token usage (v13) | Provider transport | Fields read | Streamed usage | | --- | --- | --- | | OpenAI | `usage.prompt_tokens`, `completion_tokens`, `total_tokens`, `prompt_tokens_details.cached_tokens` | through the OpenAI-compatible adapter with `with_stream_usage()` | | DeepSeek | as OpenAI, with `prompt_cache_hit_tokens` | as OpenAI | | Anthropic | `usage.input_tokens` + cache creation/read input tokens, `output_tokens` | `message_start` usage updated by cumulative `message_delta` usage | | Gemini | `usageMetadata.promptTokenCount`, `candidatesTokenCount` + `thoughtsTokenCount`, `totalTokenCount`, `cachedContentTokenCount` | the latest `usageMetadata` of the stream | | Ollama | `prompt_eval_count`, `eval_count` | no streaming transport | | OpenAI-compatible | `usage` when the server sends it | final usage chunk when the server sends one; requested only with `with_stream_usage()` | Counts come only from these fields. A response without them reports `None`; Rullst never estimates tokens or prices them. ### Timeouts and cancellation Every built-in live transport applies a 30-second request deadline by default. Each provider exposes `with_request_timeout(Duration)` to select a stricter or longer deadline, and a loopback regression proves timeout classification on the OpenAI-compatible transport. This bounds the local request future; it is not proof that an upstream provider stopped work or billing. The adapters still do not expose cancellation for ordinary `AiProvider` calls. `AiCancellation` provides an explicit cloneable signal for `StreamingAiClient`; the compatible transport and the Anthropic and Gemini streams race it against both the initial request and every streamed body read. Other provider protocols still use deadline/drop semantics. ### Retries The adapters make one transport attempt. There is no automatic retry, backoff, idempotency classification, or retry budget in `rullst-ai`. An application-level retry must classify operations carefully and must not assume that an interrupted provider request was never processed. These provider-call rules are distinct from `AuditDeliveryClient`. The latter supports one to five bounded attempts only for transport/deadline failures, HTTP 429 and HTTP 5xx, while preserving a caller-supplied event ID. Its receiver must deduplicate that ID because a timed-out request may already have been accepted. ### Authenticated audit export `AuditDeliveryClient` is an opt-in transport for application-minimized audit events, not an AI provider adapter. It signs the exact JSON body with HMAC-SHA256 plus a key ID and timestamp, caps an event at 16 KiB and an acknowledgement at 8 KiB, disables redirects and ambient proxies, and requires the acknowledgement to bind the original event ID. Cloud endpoints require HTTPS; literal-loopback HTTP(S) is reserved for development fixtures. Empty or `mock_*` keys never use the network. The receiver owns signature/freshness verification, event-ID deduplication, authorization, persistence, retention, key distribution/rotation and SIEM operations. The client does not automatically attach itself to RAG/tools or provider calls and does not inspect an arbitrary serialized event for secrets. ### Adaptive evaluation `AdaptiveAiEvaluator<P>` is also independent of the provider capability table. It wraps the configured text path with mandatory input guards, at most 32 strategy-directed turns, per-turn prompt/response/deadline limits and explicit cancellation. Strategies classify each bounded response or low-cardinality guardrail/provider/deadline outcome as passed, failed, inconclusive or another prompt. Reports are versioned JSON and retain no raw prompt, response or provider error. The caller supplies the exact suite and provider/model/configuration subject labels. Rullst neither discovers nor attests that identity. Repository tests prove deterministic orchestration and redaction, not a live provider's safety; operators must execute and review their versioned corpora against every exact model they deploy. ### Tools `ToolRegistry` is a separate [guarded local execution boundary](https://rullst.github.io/Rullst/book/ai-tool-security.html). Dispatch requires an exact policy allowlist, principal authorization, closed JSON validation, payload limits, a call budget and an audit sink. Destructive and financial calls additionally consume a one-use approval bound to the exact payload. Applications may select either the bounded process-local in-memory sink or `DurableToolAuditTrail`, whose local versioned file validates restart integrity and fails closed on quota, corruption, symlink targets and competing-writer growth. The durable sink is single-process and does not provide authenticity, rotation, retention, backup, external delivery or principal/approver authentication. The registry is not connected to any built-in provider transport. Consequently the provider tools column remains `no`, and local guarded execution must not be advertised as provider-native function calling or an autonomous safe agent. ## Portable custom-provider default A custom `AiProvider` receives a compatibility default of text, chat, embeddings, and prompt-only JSON because those first three methods are required by the trait and JSON has a guarded prompt fallback. Custom implementations that deliberately reject one of those methods, or implement additional native paths, must override `capabilities()`. `FallbackProvider` reports the union of its configured providers. The union means that at least one provider claims a path; it does not guarantee which provider will satisfy a particular model-specific request. Text, chat, vision and JSON requests try the next provider after any failure. Embeddings do not: vectors from different models are not comparable, so `embed` uses the first provider that supports embeddings and returns its error, skipping only providers that report `UnsupportedCapability`. --- # Guarded Local AI Tools Source: https://rullst.github.io/Rullst/book/ai-tool-security.html # Guarded local AI tools `rullst-ai` includes a local tool registry, not provider-native function calling or an autonomous agent runtime. Model-produced tool names and arguments are untrusted input. The only execution entry point requires all of these controls: - an exact registry-level allowlist (`ToolExecutionPolicy`); - a principal-specific authorization set (`ToolExecutionContext`); - a closed, bounded JSON object validated from `ToolParam` declarations; - non-zero serialized input/output limits and a per-context call budget; - a mandatory `ToolAuditSink` that fails execution closed when unavailable; and - a one-use, exact-payload approval for `Destructive` and `Financial` tools. The application authenticates the principal and approver. Rullst does not infer authorization from model output, a prompt, a role string, or tool registration. A call to an unregistered tool is denied with `ToolExecutionError::ToolNotFound` and audited. When the requested name is not a 1-64 byte `[A-Za-z0-9_-]` identifier, the denial and the error carry `invalid-tool-` plus the first 16 hex digits of the name's SHA-256 digest instead, so every sink accepts the record and the untrusted text is neither stored nor echoed. ## Minimal read-only dispatch ```rust use rullst_ai::ai::{ AiTool, InMemoryToolAuditTrail, ToolExecutionContext, ToolExecutionPolicy, ToolParam, ToolRegistry, ToolRisk, }; use serde_json::{Value, json}; struct AccountStatus; impl AiTool for AccountStatus { fn name(&self) -> &str { "account_status" } fn description(&self) -> &str { "Read an account status" } fn parameters(&self) -> Vec<ToolParam> { vec![ToolParam { name: "account_id".into(), param_type: "string".into(), description: "Authorized account identifier".into(), required: true, }] } fn risk(&self) -> ToolRisk { ToolRisk::ReadOnly } fn execute( &self, payload: Value, ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> { Ok(json!({ "account_id": payload["account_id"], "status": "active" })) } } let mut registry = ToolRegistry::new(); registry.register(AccountStatus)?; let policy = ToolExecutionPolicy::new(["account_status"])? .with_payload_limits(4 * 1024, 16 * 1024)?; let mut context = ToolExecutionContext::new( "authenticated-user-17", ["account_status"], 3, )?; let audit = InMemoryToolAuditTrail::new(128)?; let output = registry.execute( "account_status", json!({ "account_id": "acct-17" }), &mut context, &policy, &audit, )?; # Ok::<(), Box<dyn std::error::Error>>(()) ``` `additionalProperties: false` is emitted in the exported schema and enforced at dispatch. Supported parameter types are the bounded JSON primitives `string`, `number`, `integer`, `boolean`, `object`, `array`, and `null`. Applications must perform domain validation inside the tool as well: a JSON string is not proof that an account identifier belongs to the principal. ## Destructive and financial approval High-risk approval is bound to the exact serialized payload. Changing an amount, destination, identifier, or any other field produces `ToolExecutionError::ApprovalPayloadMismatch`. The fragment below continues with the registry, context, policy, audit sink and high-risk tool initialized by the application as described above, so it is contextual rather than a standalone program: ```rust,ignore use rullst_ai::ai::HumanApproval; use serde_json::json; let payload = json!({ "invoice_id": "inv-42", "amount_cents": 1500 }); let approval = HumanApproval::for_payload( "issue_refund", &payload, "authenticated-reviewer-9", "support ticket FIN-42", )?; context.approve(approval); let result = registry.execute( "issue_refund", payload, &mut context, &policy, &audit, )?; # Ok::<(), Box<dyn std::error::Error>>(()) ``` The approval is consumed once. Its approver and bounded reason are recorded in authorized/success/failure audit events, while the payload itself is omitted to avoid duplicating secrets or personal data in the audit trail. The sink is called before and after execution. If it rejects the `Authorized` record, the tool does not run, a matching approval stays in the context for a retry after the sink recovers, and the call returns `ToolExecutionError::AuditUnavailable`. If it rejects the outcome record after the tool ran, for example because a durable trail reached its record quota, the call returns `ToolExecutionError::OutcomeUnaudited` with the unrecorded outcome and discards the tool output. The side effect may have happened in that case: make destructive and financial tools idempotent, or reconcile with the tool's own records before approving the same operation again. ## Durable local evidence `InMemoryToolAuditTrail` is bounded and process-local; it is intended for local development and tests. A single-process service can instead open the built-in bounded local trail: ```rust,no_run use rullst_ai::ai::DurableToolAuditTrail; let audit = DurableToolAuditTrail::try_open("storage/audit/ai-tools.log")?; # Ok::<(), Box<dyn std::error::Error>>(()) ``` Each append is synchronized and followed by `sync_data`. The distinct versioned tool stream has 16 MiB and 4,096-record default ceilings, validates every frame and event on restart, and rejects corruption, unsafe file types, external length changes and quota exhaustion. `try_open_with_max_bytes` may set a smaller byte ceiling. This is a single-process local writer, not an external audit service. Its SHA-256 frames detect accidental or same-length record corruption but are not a signature or HMAC. The host owns trusted directory permissions, exclusive writer operation, rotation, retention, backup, incident export and deletion policy. Multi-instance applications should implement `ToolAuditSink` over an appropriate durable destination. ## Production boundary The registry deliberately does not provide network fetchers, shell execution, database ownership policy, distributed budgets, provider tool-call parsing, or human identity verification. Tools providing those capabilities must enforce their own domain authorization and egress policies. The [provider capability matrix](https://rullst.github.io/Rullst/book/ai-provider-capabilities.html) therefore continues to report provider tools as unsupported. --- # Security Architecture Source: https://rullst.github.io/Rullst/book/security-architecture.html # Security architecture and boundaries > **Vision preserved:** unfinished enterprise controls and absolute former claims > remain visible, with a recommendation for each, in the > [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html#security-authentication-studio-and-nexus). `rullst-security` contains composable controls for HTTP applications. The crate does not install every control automatically and does not replace secure domain logic, a trusted reverse proxy, operating-system hardening, or independent security testing. To choose between overlapping Core and `rullst-security` layers, and for one recommended production stack, see [which security layer to use, and when](https://rullst.github.io/Rullst/book/security-layers.html). The [security assurance case](https://rullst.github.io/Rullst/book/assurance-case.html) connects these boundaries to the threat models, secure design principles and tested countermeasures. ## Defense in depth ```mermaid flowchart LR Peer[Trusted peer identity] --> Edge[Rate limit and honeypots] Edge --> Request[CSRF, CSWSH and bounded RASP/WAF] Request --> Identity[Authentication, MFA and RBAC] Identity --> App[Application and parameterized data access] App --> Response[Secure headers and supported DLP filtering] App --> Audit[Tamper-evident audit records] ``` Each arrow is an application integration point. If a layer is not mounted, its counter and API being present in the crate do not protect traffic. ## Cloud and VPS deployments The application controls work in a compatible cloud container or VPS just as they do in a local HTTP server. Their effect depends on the configured routes, middleware, identity and persistence. `Server` mounts the Core baseline; additional Security controls require explicit composition. | Scope | Current boundary | | :--- | :--- | | Sites and API backends | Bounded WAF/RASP inspection, HTML sanitization, CSP/security headers, login abuse controls, ownership/role helpers, WebSocket-origin validation and audit primitives are available. Application policy and negative route tests remain necessary. | | Multiple application instances | The opt-in Redis limiter has an atomic shared-budget contract. Production must require distributed mode explicitly. Process-local jails, bans and counters do not become shared because the application has more replicas. | | Reverse proxy | Verified socket identity is the default. Core's `TrustedProxyLayer` (`Server::trusted_proxies` or `[security] trusted_proxies`) accepts a forwarded address only from listed proxy networks; an arbitrary client header cannot establish the rate-limit or ban identity. | | Host and cloud account | OS/SSH patching, firewall rules, IAM, secret custody, backup/restore and privileged container configuration belong to the deployment operator/provider. The crate does not administer these systems. | | Network availability | Application request limits can reduce specific abuse. Network saturation and upstream DDoS filtering require infrastructure controls outside the application process. | The [v13 deployment candidate](https://rullst.github.io/Rullst/book/deployment-acceptance.html) exercises two real application processes with an existing proxy, shared Redis budgets, forged forwarding metadata and HTTP/WebSocket controls. Local acceptance passes, and hosted workspace and installed-archive checks passed in PR #224; the final release campaign remains separate. It does not certify a cloud account, VPS or production topology. The separate v13 [local configuration diagnostic](https://rullst.github.io/Rullst/book/deployment-diagnostic.html), `cargo rullst deploy:doctor`, reuses Core validation with explicit environment-source selection and bounded, redacted reports. It identifies what was actually inspected and lists unobserved controls; its source merged in PR #225 after hosted checks, and its archive gate passed in a later exact-commit run. It does not label configuration as deployed evidence, modify firewall/SSH/cloud settings or certify a host. No new security crate is introduced. See the [delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html). ## Canonical production preset `ProductionPreset::middleware_order()` is the machine-readable ordering contract (introduced in v12). From the outermost inbound boundary to the handler, the order is: 1. trusted-proxy policy; 2. request-body limit; 3. request ID; 4. tracing; 5. secure headers; 6. explicit CORS allowlist; 7. bounded WAF/RASP; 8. CSRF; 9. session validation; 10. authentication; 11. tenant membership resolution; 12. role, permission and object-ownership authorization; 13. identity/direct-peer rate limit; 14. application handler. Tower response flow unwinds in reverse, so the secure-header layer still observes and protects responses returned by the inner guards and handler. `Server` mounts the framework-owned staging/production baseline. Session, authentication, tenant membership and authorization are deliberately application-owned: the framework cannot infer those policies without creating an insecure universal default. Generated applications must mount those layers in the declared slots, and protected parameterized routes must still perform an object-level ownership check. The direct socket peer is the default network identity. A deployment must not accept `Forwarded` or `X-Forwarded-For` until its exact proxy hops are configured and tested. Core implements the first stage as `TrustedProxyLayer`, which `Server::trusted_proxies` mounts outside the security baseline, lifecycle, Traffic Shield and rate limiter in both the static and hot-reload servers (see [identity and network trust](https://rullst.github.io/Rullst/book/security-architecture.html#identity-and-network-trust)). Body limits must be outer to any middleware that buffers content. Webhook routes may bypass browser CSRF only by exact path and only when their provider signature middleware is mandatory. ### Academy production-boundary diagnostic `ProductionPreset::academy()` layers twelve Academy-specific integration requirements over the canonical middleware order. Missing observations become `NOT_EVALUATED`, duplicate observations are rejected, and validation succeeds only when every requirement has one explicit `PASS`. The contract covers identity, school membership, entitlements, object authorization, tenant isolation, assessments, score events, durable automation/audit, content safety, privacy and distributed abuse controls. It cannot inspect or certify a deployed topology by itself. `cargo rullst academy:doctor` emits the normalized contract in text or JSON and returns a failing exit status until all requirements pass. An evidence document uses the versioned `rullst.academy-evidence.v1` schema: ```json { "schema_version": "rullst.academy-evidence.v1", "checks": [ { "requirement": "authenticated_identity", "status": "PASS", "evidence": ["test:session_rejects_forged_identity"] } ] } ``` Run it with `cargo rullst academy:doctor --evidence academy-evidence.json --json`. Omitted checks remain `NOT_EVALUATED`, and a `PASS` without a non-empty evidence reference is invalid. Evidence strings are caller declarations rather than independently verified proof. The output therefore always includes `certification: false`; satisfying this contract is not an audit, grade or production-readiness claim. Process-level CLI tests cover both the incomplete failing report and a complete declared-evidence report. ### Parameterized-route access contract `cargo rullst audit --idor` requires every recognized parameterized route to carry an adjacent `// rullst-access: public|owner|role|admin — reason` marker. `public` is accepted only for a recognized GET route. `owner` requires `RbacGuard::authorize_owner_or_role`; `role` requires a recognized role guard; and `admin` requires `RequireRoleLayer` or `NexusAuthPolicy::protect_router`. The latter lets application operational routes reuse the same fail-closed peer/credential and administrator boundary as Nexus. The guard is looked up in the same crate's `src` tree, outside comments and `#[cfg(test)]` items; it need not be in the route's file. The marker records intent and the scanner catches common omissions; neither is a proof of domain ownership. Protected object routes still need negative HTTP tests in which an authenticated subject requests another subject's resource and receives a denial before data or side effects are exposed. ## Control map | Risk area | Available primitives | Boundary | | --- | --- | --- | | Broken access control | `RbacGuard`, ownership helpers, authenticated user context. | The application must apply a check to every protected object and operation. | | Cryptographic storage | AES-256-GCM field encryption and zeroizing secret wrappers. | Operators own key generation, storage, rotation, separation, and recovery. | | Injection | SQLx binds, strict identifier validation, sanitizer and bounded RASP patterns. | Heuristics are not a complete language parser; domain validation and parameterization remain mandatory. | | Misconfiguration | Nonce-based CSP and a strict HTTP-header baseline. | Proxies and page content change the deployed policy; no scanner grade is guaranteed. | | Authentication abuse | Login jail, local limiter, optional atomic Redis limiter, timing helpers, TOTP, subject-bound recovery-code verifiers and WebAuthn integration. | Real Redis topology/failover, durable transactional recovery consumption and UX, RP/origin configuration, trusted peer identity, and capacity planning remain application concerns. | | Data integrity | HMAC audit records plus an opt-in bounded HMAC-chained local SIEM journal with named rotation keys. | Whole-tail rollback detection, independent checkpoints, multi-writer operation, key protection, retention and remote delivery require external storage and operations. | | Data leakage | Text-aware DLP, PII masking, and log redaction helpers. | Unsupported content types, encodings, streams, and oversize bodies follow explicit policy and must be tested. Core response masking buffers at most 2 MiB of identity-encoded text and scans it in linear time; it is a heuristic for emails and 13-19 digit card-like runs, not complete PII detection. JSON responses are masked only inside string values, so numbers such as millisecond timestamps and 64-bit IDs keep their digits and the body stays valid JSON; other textual responses, including JavaScript, are masked as plain text. A `206 Partial Content` response that masking would change is replaced by a `502` with `Cache-Control: no-store`, because a masked range no longer matches its `Content-Range`; a clean range passes through unchanged. Core masking and `DlpResponseLayer` split a `multipart/byteranges` response into its parts, check each textual identity-encoded part with the masker of its own media type and withhold the response the same way when a part would be masked or the body cannot be split exactly (an invalid boundary or delimiter line, more than 256 parts, a part header block over 8 KiB or no close delimiter). Each range is inspected on its own, so neither layer recognizes a value split across separately requested ranges, and Core does not recognize PII in a JSON range that starts inside a string literal; tower-http `ServeDir`, which serves Core static files, answers multi-range requests with `416` and never emits `multipart/byteranges`. An e-mail match needs an alphabetic top-level domain that is not a static-asset extension, so versioned package URLs (`htmx.org@2.0.4`) and density-suffixed asset names (`logo@2x.png`) are left unchanged. | | AI input risk | Prompt-injection heuristics and PII masking in the high-level AI client. | No heuristic can prove a prompt safe or guarantee detection of every secret. | This is a control mapping, not a claim of complete OWASP Top 10 coverage. ## Identity and network trust Rate limiting, bans, honeypots, and audit attribution use the direct peer address unless a trusted-proxy policy explicitly accepts forwarded metadata. Core's `RateLimiter` keys IPv4 peers per address and IPv6 peers per /64, so rotating addresses inside one delegated prefix shares a bucket. Its process-local map tracks at most 100,000 keys: fully refilled buckets are dropped and, beyond the cap, the least recently used buckets are evicted and restart with a full burst. Security's `rate_limit_middleware` groups peers the same way; its table tracks at most 16,384 keys and fails closed for new keys while it is full. Never trust `X-Forwarded-For`, tenant headers, or role headers directly from an arbitrary client. Tenant membership and roles must come from an authenticated session or a cryptographically trusted internal gateway. ### Trusted-proxy client resolution `rullst_core::security::TrustedProxyLayer` is the explicit policy for deployments behind reverse proxies. `TrustedProxyConfig::new` accepts at most 64 IPv4/IPv6 addresses or CIDR networks; it rejects `/0`, host bits and duplicates, and normalizes IPv4-mapped IPv6 entries. An empty policy never reads forwarding headers. List only the networks your own proxies connect from: any host inside a listed network can choose the client address. - Only when the socket peer is trusted does the layer read exactly one header: `X-Forwarded-For` (default) or RFC 7239 `Forwarded` (`with_header`). The two are never merged, and header lines form one list in their received order. - The list is walked right to left, skipping trusted proxies; the first untrusted address is the client, and if every hop is trusted the left-most is used. Text to the left of the client entry is never parsed, so a client cannot choose its identity or force a fallback by padding its own header. - Ports, IPv6 brackets and IPv4-mapped addresses are normalized. `unknown`, obfuscated `_identifiers`, unquoted RFC 7239 IPv6/ports, duplicate parameters and other malformed entries are rejected. At most 4,096 bytes and 32 entries are examined from the right. - A missing, malformed, oversized or over-long header from a trusted peer keeps the socket peer, never fails the request and emits one `tracing` event (`debug` when missing, otherwise `warn` with the reason) naming the header and peer but never the header contents. - On success the request's `ConnectInfo<SocketAddr>` becomes the client IP with port 0, so every existing consumer (Core and Security rate limiters, honeypots, the error console and Nexus lockout) uses it unchanged. Every request with peer metadata also receives a `ClientAddr` extension (client IP, original peer, `via_trusted_proxy`), which has no public constructor. - `trust_forwarded_proto(true)` additionally reports the scheme from that trusted peer as `ClientAddr::forwarded_proto`: one unambiguous `X-Forwarded-Proto` value, or the `proto=` of the selected `Forwarded` element. Nexus accepts an `https` report as TLS evidence for Basic Auth. Enable it only when every trusted proxy overwrites the value. The hot-reload development server now also supplies `ConnectInfo` from the accepted socket. The layer does not authenticate a proxy beyond its network address, cannot detect a trusted proxy that forwards client-supplied headers unchanged, and does not replace authenticated per-user limits or distributed rate-limit state. ## Request and response inspection RASP/WAF rules are bounded to avoid uncontrolled CPU or memory work. They can reject known suspicious patterns in supported URI, header, and bounded body data, but application queries must still use binds and access control. Body media types are classified at least as broadly as axum's extractors: any ASCII case, parameters, a `json`/`xml` subtype or `+json`/`+xml` suffix, and any type that starts with `application/x-www-form-urlencoded`. Core's WAF checks the percent-decoded request path for traversal signatures only (`../`, `..\`, `/etc/passwd`, `win.ini`), since routers decode path parameters; its SQL, XSS and command signatures apply to the query, the `Referer`, each cookie pair and bounded bodies (JSON key by key and string by string). SQL and shell signatures match injection structure, such as a quote breakout, a chained statement, `union select` or a metacharacter before a command name, never a keyword alone, so ordinary text like "select an option" passes. DLP modifies only supported textual responses whose body can be safely buffered within configured limits. Applications must test JSON, HTML, binary, compressed, SSE, streaming, and oversized responses so headers and bodies remain protocol-correct. Masking and log redaction run in time linear in the inspected length. Database URL passwords are masked only inside the URL authority, which ends at the first `/`, `?`, `#`, whitespace, quote, `<`, `>`, backtick or control character, or after 2,048 bytes; unencoded delimiters in a password therefore end detection early. ## Audit chains Audit records use an unambiguous canonical representation and an HMAC key of at least 32 bytes (`MIN_AUDIT_KEY_BYTES`) with at least eight distinct byte values. Verification must cover the ordered sequence, not just isolated records. A valid chain is tamper-evident; it cannot stop an attacker who can delete every record or steal the key. Store the log and key in separate protected systems. A new `AuditChain` starts at sequence 1 from the genesis predecessor, so a restarted writer must continue its persisted trail with `AuditChain::try_resume` (added in v13), passing the newest persisted record; that record's HMAC must verify. Exactly one writer may own a persisted chain, and deleting the newest records before a restart is detected only against an external checkpoint of the last sequence and hash. ## Supply-chain and compliance evidence Repository workflows can run dependency policy, RustSec, CodeQL, fuzzing, sanitizers, SemVer checks, SBOM generation, and release attestations. Their scope and blocking status are defined by the workflow files. Informational checks must not be described as formal gates. Neither those workflows nor this crate certify an application for SOC 2, ISO 27001, PCI DSS, FedRAMP, or any OWASP level. See [`AUDIT.md`](https://github.com/Rullst/Rullst/blob/main/AUDIT.md) and the [external audit kit](https://rullst.github.io/Rullst/book/external-audit-kit.html) for reproduction and evidence requirements. `cargo rullst audit --compliance` writes a bounded `SECURITY_COMPLIANCE.md` evidence report for one application; it is generated, not kept in the repository, and is not a certification. ## Deployment checklist - Mount middleware in a tested order and use exact webhook CSRF exemptions. - Configure secure session, webhook, audit, and encryption keys; reject weak or empty live credentials. - Define trusted proxies with `Server::trusted_proxies` (or `[security] trusted_proxies`) listing only the real proxy networks, and use the direct peer as the default identity. - Apply owner/tenant/role checks server-side for every data operation. - Validate per-response CSP nonces against rendered pages. - Export telemetry and audit records to durable, access-controlled storage. - Run negative integration tests and an independent security review for the application's actual configuration. --- # Which Security Layer to Use, and When Source: https://rullst.github.io/Rullst/book/security-layers.html # Which security layer to use, and when Rullst has two places for security code, and some of their controls look alike: - **`rullst-core`** (`rullst::security`) owns the **runtime baseline**: secure headers with a CSP nonce, CORS, a small WAF, double-submit CSRF, optional response PII masking, trusted-proxy client resolution, exact machine endpoints and tenant context. `Server` mounts the baseline for you in staging and production. - **`rullst-security`** (`rullst::security_runtime`, or the `rullst_security` crate that generated applications already depend on) is a toolbox of **opt-in** layers and helpers: RASP, response DLP, configurable headers, honeypots, CSWSH, JSON schema guards, rate limiters, the login jail, MFA, RBAC, audit chains, field encryption, SRI and more. Nothing in it protects traffic until you mount or call it. This page tells you which one to pick for each job, gives one recommended production stack and states what each layer does **not** protect against. It summarizes the code in `rullst-core/src/security/` and `rullst-security/src/`; the [security architecture](https://rullst.github.io/Rullst/book/security-architecture.html) page holds the deeper contracts, and both crates are in the Core [maturity tier](https://rullst.github.io/Rullst/book/maturity.html). ## The short answer 1. Run your application through `Server` and configure `[security]` in `Rullst.toml`. In staging and production that gives you headers, CORS, the WAF and CSRF without extra code. 2. Behind a reverse proxy, set `Server::trusted_proxies` (or `[security] trusted_proxies`) to your proxy networks only. 3. Add from `rullst-security` only what your application needs: the honeypot and response DLP for every app, the CSWSH guard on WebSocket routes, a JSON schema guard on JSON write routes, and the login jail, MFA, RBAC and audit chain inside your handlers. 4. Do not stack two layers that do the same job and expect double protection. The pairs below explain which one wins and what each one misses. ## What generated applications already have | Starter | Mounted for you | Not mounted | | :--- | :--- | :--- | | Every starter | `Server` applies the Core baseline in **staging and production**: secure headers, CORS (only when `cors_allow_origins` is set), WAF and CSRF. PII masking stays off unless `enable_pii_masking = true`. In **development** the baseline adds only CORS (and machine-endpoint authentication when configured) and a report-only WAF that logs what it would refuse. | No `rullst-security` middleware layer (RASP, DLP, `SecureHeadersLayer`, honeypot, CSWSH, schema guard) is mounted by any starter. No global rate limit or Traffic Shield is configured. Trusted proxies are configured only when `cargo rullst deploy` adds its Caddy proxy address to `[security] trusted_proxies`. | | Blank (full stack), SaaS (and the LMS, ERP and Blog examples) | Core `csrf_middleware` and `headers_middleware` on the router too, so development behaves like production. | — | | API (Blank JSON API) | Core `headers_middleware` on the router. The example write route `POST /api/messages` is an exact machine endpoint (`Server::with_machine_endpoints`): clients send `Authorization: Bearer <API_TOKEN>` (a random value in the generated `.env`), requests with cookies are refused, and startup fails without `API_TOKEN`. | No CSRF layer on the router. In staging and production the `Server` baseline requires CSRF on every other write route, so add each new JSON write route to `machine_endpoints()` in `src/main.rs` (a `{id}` segment covers routes such as `PUT /api/posts/{id}`), or send the double-submit token from a browser. `make:controller --api` prints these lines. | | Portfolio example | Only the `Server` baseline (staging and production). | No router-level layers in development. | | SaaS (and the LMS example) | A Core token-bucket limit on the credential routes; the SaaS `[security]` section exempts the exact signed billing webhook path from CSRF. The LMS example uses `rullst_security::{UserContext, RbacGuard}`. | Login jail, MFA (add it with `make:mfa`), audit chain. | Every generated `Cargo.toml` already lists `rullst-security`, so adding a layer needs no new dependency. Since 13.0 every generated project also has `src/security_tests.rs`, which `cargo test` runs offline against the project's own `router()` wrapped in the staging/production baseline (`apply_security_baseline`, as `Server` composes it). It checks the security headers, that a write without the CSRF token is refused and one with it passes, and that the WAF refuses an injection probe but accepts the prose "Please select an option". SaaS adds the sign-in rate limit and the API starter the bearer token of its machine endpoint; the LMS example also checks the owner of its lesson routes. Keep these tests passing as you change routes; they do not replace your own authorization tests. ## Recommended production composition From the outermost layer to your handler: | # | Layer | Owner | How you enable it | | :---: | :--- | :--- | :--- | | 1 | Trusted-proxy client resolution | Core | `Server::trusted_proxies` or `[security] trusted_proxies` | | 2 | Secure headers and CSP nonce | Core baseline | Automatic in staging/production; tune `[security] csp` and `coep` | | 3 | CORS allowlist | Core baseline | `[security] cors_allow_origins` | | 4 | WAF | Core baseline | Automatic in staging/production | | 5 | CSRF (and exact machine endpoints) | Core baseline | Automatic; `Server::with_machine_endpoints` for API, webhook and mTLS routes | | 6 | Readiness and drain, Traffic Shield, per-peer rate limit | Core `Server` | `with_lifecycle`, `shield`, `rate_limit` | | 7 | Honeypot trap paths and peer bans | `rullst-security` | `HoneypotLayer` | | 8 | Response secret masking | `rullst-security` | `DlpResponseLayer` | | 9 | Per-route guards | `rullst-security` | `cswsh_guard_middleware` on WebSocket routes, `json_schema_guard_middleware` on JSON write routes | | 10 | Session, authentication, tenant and authorization | Your application | Your auth middleware, `TenantContext`, `RbacGuard` | | 11 | Inside handlers | Both | `LoginGuard`, a `RedisRateLimiter` for credential routes, TOTP and recovery codes, `AuditChain`, `FieldEncryptor` | `Server` composes layers 1 to 6 itself (see `rullst-core/src/server/stack.rs`); you add 7 to 11 to your router. The [canonical production preset](https://rullst.github.io/Rullst/book/security-architecture.html#canonical-production-preset) is the full machine-readable ordering contract, including request-body limits, request IDs and tracing. ```rust,ignore use axum::{Extension, Router, middleware::from_fn}; use rullst::security::TrustedProxyConfig; use rullst::{RateLimitConfig, RateLimiter, Server}; use rullst_security::{ CswsPolicy, DlpResponseLayer, HoneypotLayer, HoneypotState, cswsh_guard_middleware, }; /// `pages` holds the application routes, with session, authentication and /// RBAC checks inside them; `live` holds the WebSocket upgrade routes. async fn serve(pages: Router, live: Router) -> Result<(), Box<dyn std::error::Error>> { // Layer 9: reject cross-site WebSocket upgrades. let live = live .route_layer(from_fn(cswsh_guard_middleware)) .layer(Extension(CswsPolicy::try_new(["https://app.example.com"])?)); let app = pages .merge(live) // Layer 8: mask private keys, AWS key IDs and database URL passwords. .layer(DlpResponseLayer) // Layer 7: refuse trap paths such as /.env and ban direct scanners. .layer(HoneypotLayer::new(HoneypotState::default())); Server::new(app.into()) // Layer 1: accept forwarded client addresses only from your proxies. .trusted_proxies(TrustedProxyConfig::new(["10.0.0.0/8"])?) // Layer 6: process-local token bucket per IPv4 address or IPv6 /64. .rate_limit(RateLimiter::new(RateLimitConfig::per_minute(600.0))) .run(3000) .await?; Ok(()) } ``` The snippet is marked `ignore`: it shows the shape and the real API names, but the book does not compile it. Replace the example origin and proxy network with your own. `rullst::RateLimiter` (Core token bucket) and `rullst_security::RateLimiter` (fixed window) are different types. The WAF, RASP and schema guards buffer at most 1–2 MiB of a body themselves; set an explicit limit on upload routes too (for example axum's `DefaultBodyLimit`). Leave these out unless you have a specific reason: - **`RaspSecurityLayer`** overlaps the Core WAF and buffers the same request body a second time. Add it when you need its extra signatures (cloud metadata hosts, JNDI lookups and every non-credential header) and accept more false positives. - **Core PII masking** rewrites e-mail addresses and long digit runs in every textual response, including ones your users are meant to see. Prefer redacting fields in your own serializers. - **`SecureHeadersLayer` inside `Server`** only when you need values that `[security]` cannot express: the Core baseline then keeps every header the layer chose or omitted (see below). ## Overlapping layers compared ### Secure headers: Core baseline vs `SecureHeadersLayer` | | Core `headers_middleware` | `rullst_security::SecureHeadersLayer` | | :--- | :--- | :--- | | Mounted by | `Server` in staging/production; most blueprints also mount it on the router | You, as a Tower layer | | Configuration | `[security] csp` (template with `{NONCE}`), `coep` and the `hsts_*` settings; other values fixed | `SecureHeadersConfig`: every header value replaceable or omitted (`None`), CSP template or static CSP | | Headers | HSTS (one year with `includeSubDomains`, no `preload`, by default), `X-Frame-Options: DENY`, `nosniff`, `X-XSS-Protection: 0`, `Referrer-Policy`, `Permissions-Policy`, COOP, CORP, COEP, CSP | The same set, from the config; the default HSTS value is the same | | Existing values | Adds each header only when the response has none; leaves all of them alone after an inner `SecureHeadersLayer` | Replaces the handler's values with its configured ones | | `Cache-Control` | Adds `no-store` when the handler set none | Not touched | | CSP nonce | `CspNonce` request extension | Same `CspNonce`, reused when one already exists | Both layers reuse one `CspNonce` per request, so templates render a nonce that matches the final header. The Core baseline is a fallback: it fills in a security header only when the handler or an inner layer has not set it, so an **explicit value set by the application wins over the baseline**. `SecureHeadersLayer` marks its responses with `rullst::security::SecurityHeadersApplied`; when the Core baseline (outermost inside `Server` in staging and production) sees that marker it leaves every security header to the layer, so the headers `SecureHeadersConfig` sets to `None` stay absent. It still adds `Cache-Control: no-store` when no cache policy was set. The trade-off: a weaker value that a handler sets on purpose or by mistake (for example `Referrer-Policy: unsafe-url` or a CSP with `'unsafe-inline'`) is no longer replaced, so review the headers your handlers set. Use `[security]` settings to tune a `Server` application, and `SecureHeadersLayer` when you need per-header values or omissions, or for a plain Axum router that you serve without `Server`. An exact `Referrer-Policy: no-referrer` from a handler survives both layers, normalized to one value. `CspSecurityLayer` (in `sanitizer`) is a smaller variant: default CSP, `X-Frame-Options`, `nosniff` and `Referrer-Policy` only; it does not set the marker, so the Core baseline keeps those four and adds the rest. #### Strict-Transport-Security The Core baseline sends `Strict-Transport-Security: max-age=31536000; includeSubDomains` unless the response already has the header. Three `[security]` settings choose another value: ```toml [security] hsts_max_age_seconds = 63072000 # default 31536000 (one year); 0 clears the policy hsts_include_subdomains = true # default true hsts_preload = true # default false ``` `includeSubDomains` makes browsers refuse plain HTTP on every subdomain of the host for the whole max-age, so leave it on only when all of them serve HTTPS. `preload` asks to be listed in the HSTS preload list built into browsers once the domain is submitted there, and leaving that list takes months, so it is opt-in. Startup fails when `hsts_preload` is set without `hsts_include_subdomains` or with a max-age below 31536000, the list's own requirements. A handler that sets the header, or `SecureHeadersConfig::hsts` in `SecureHeadersLayer`, still wins over these settings. 13.0.0-alpha.1 and earlier always sent `max-age=63072000; includeSubDomains; preload`. **Not covered:** headers reduce browser-side risks (framing, MIME sniffing, inline script injection); they do not fix an XSS bug in a page that allows `'unsafe-inline'` or a trusted script host that serves attacker content, and HSTS has no effect until the first HTTPS response reaches the browser. ### Request inspection: Core WAF vs RASP | | Core `waf_middleware` | `rullst_security::RaspSecurityLayer` | | :--- | :--- | :--- | | Mounted by | `Server` in staging/production; in development a report-only variant that logs instead of refusing | You | | Signatures | Injection structure, not keywords. SQL: a quote followed by `or`/`and` and a comparison or by a comment (`' or '1'='1`, `admin'--`), a `;` followed by a statement (`; drop table`, `; delete from`, `; select *`), `union [all] select`, and probes (`sleep(`, `benchmark(`, `waitfor delay`, `pg_sleep(`, `information_schema`, `@@version`, `xp_cmdshell`, `load_file(`, `into outfile`). Shell: `;`, `\|`, `&&`, a backtick or `$(` followed by a command name (`sh`, `bash`, `cat`, `ls`, `id`, `curl`, `wget`, `nc`, `rm`, `python`…). XSS (`<script`, `javascript:`, `onload=`, `onerror=`, `document.cookie`) and traversal (`../`, `..\`, `/etc/passwd`, `win.ini`) as substrings | More specific phrases: SQL (`union select`, `' or '1'='1`, `sleep(`, `information_schema`…), traversal, SSRF hosts (`169.254.169.254`, `metadata.google.internal`), shell (`cmd.exe`, `/bin/sh`…), JNDI (`${jndi:`…) | | Where it looks | Decoded path (traversal only), query, `Referer`, each cookie pair, `User-Agent` against a configurable blocklist (AI and SEO crawlers by default) | Full request target, every header except `Cookie` and `Authorization`, body; JSON keys and strings after decoding | | Bodies | `text/*`, JSON, XML, URL-encoded forms; identity encoding only (others get `415`); at most 1 MiB (`413`); invalid UTF-8 gets `400` | Same media types and limits | | Decoding | One percent-decoding pass, then lowercase with whitespace runs collapsed, checked with and without `/* … */` comments; JSON bodies are checked key by key and string by string | Raw text plus one percent-decoding pass | | Telemetry | None | `SecurityStore` interception counters and events | The Core WAF is a coarse baseline. Ordinary text that names SQL or shell words ("please select an option", "delete my account", "curl the API with your token") passes; only injection syntax around those words is refused. Parameterized SQL is the real defense against SQL injection, and shell-free process APIs against command injection. #### Seeing and accepting WAF false positives The WAF refuses requests only in staging and production. In development `Server` mounts the same rules in report-only mode: each request they would refuse passes, and a `tracing` warning from `rullst::security::waf` names the rule and the path, for example `rule="body-signature" path="/lessons/answers"`. The warning never includes the query, headers or body. The rules are `user-agent-blocklist`, `path-traversal`, `query-signature`, `referer-signature`, `cookie-signature`, `body-signature`, `body-encoding`, `body-too-large` and `body-not-utf8`. A route whose body must carry text the signatures refuse, such as code samples in a programming lesson (`../`, `javascript:`, `document.cookie`, `| grep`), can be exempted by exact path: ```toml [security] waf_body_exempt_paths = ["/lessons/answers"] ``` The WAF then neither reads nor inspects bodies sent to that exact path, so it also stops checking their encoding and size; set a body limit on the route (`DefaultBodyLimit`). Its path, query string, cookies, `Referer` and `User-Agent` stay inspected, and CSRF still applies. Entries must be exact absolute paths: wildcards, route parameters (`{id}`, `:id`), queries, fragments, dot segments and duplicates fail validation, so the server refuses to start. Each entry is a deliberate risk acceptance for that route: the handler alone (parameterized SQL, output encoding, validation) must make the body safe. **Not covered by either:** binary bodies, `multipart/form-data` file contents and file names, multipart text fields that are not read with `rullst::uploads::Multipart` (which applies the Core WAF signatures to each field it reads; RASP does not inspect multipart at all; see [form uploads](https://rullst.github.io/Rullst/book/form-uploads.html#waf)), double-encoded or otherwise obfuscated payloads (HTML entities, Unicode escapes outside JSON strings, and in RASP SQL comments between keywords), payloads split across fields, injections that need no quote, `;` or `union` (a bare numeric `1 or 1=1`), and every attack class without a listed signature. Both still produce false positives: the Core WAF refuses text in which a shell metacharacter precedes a command name (a Markdown cell `| id |`, inline code such as `` `ls` ``) or a quote precedes `or` and a comparison, and RASP refuses any field that contains `../` or `/bin/sh`. Parameterized SQL, output encoding, typed validation and URL allowlists remain the actual defenses. ### Response masking: Core PII masking vs DLP Both layers inspect **responses only**; neither looks at requests. (Prompt PII masking for LLM calls lives in `rullst-ai`.) | | Core `pii_masking_middleware` | `rullst_security::DlpResponseLayer` | | :--- | :--- | :--- | | Enabled by | `[security] enable_pii_masking = true` (staging/production) | You | | Finds | E-mail addresses; 13–19 digit card-like runs (digits with at most two spaces or hyphens between them; no Luhn check) | Complete PEM private-key blocks (PKCS#8, RSA, EC, DSA, OpenSSH, OpenPGP), `AKIA` access-key IDs, passwords in `postgres`, `postgresql`, `mysql`, `redis` and `rediss` URLs | | Media types | `text/*` except `text/event-stream`, JSON (masked only inside string values), XML, `application/javascript` | `text/*` except `text/event-stream`, JSON, XML, YAML, `application/javascript` and its aliases | | Bounds | Fixed-size, identity-encoded bodies up to 2 MiB; streams, unknown sizes, compressed and larger bodies pass unchanged; `HEAD`, `204` and `304` skipped | Same | | `206 Partial Content` | A range that would change becomes `502` with `no-store`; a clean range passes | Same | | `multipart/byteranges` | Split into parts (at most 256 parts, 8 KiB of headers per part); withheld with `502` when a textual part would change or the body cannot be split | Same | **Not covered:** a value split across separately requested ranges, any compressed or streamed response, other secret formats (temporary `ASIA` keys, AWS secret keys, API tokens, JWTs, passwords outside a database URL), PII other than e-mails and card-like numbers, and values encoded in a way the pattern does not match. Core masking also hides legitimate phone numbers, order numbers and e-mails. Neither layer stops a leak through logs, files or another channel; use `redact_secrets` for log fields and keep secrets out of responses in the first place. ### Browser and abuse controls | Control | Owner | Reduces | Mechanism | Does not protect against | | :--- | :--- | :--- | :--- | :--- | | Double-submit CSRF | Core | Cross-site form and fetch writes using ambient cookies | `rullst_csrf` cookie must match `X-CSRF-Token` or the `_token` form field, compared in constant time. A `GET` for a static-looking path (`/robots.txt`, `/sitemap.xml`, `/favicon.ico`, `/static/*`, or a `.txt`, `.xml`, `.ico`, `.json`, `.css`, `.js`, `.png`, `.jpg`, `.jpeg`, `.svg`, `.webp` or `.wasm` suffix) never mints the token; it only reuses an existing cookie's | State changes in `GET` handlers; a sibling subdomain that can set cookies; an XSS bug that reads the token; WebSocket upgrades | | Exact machine endpoints | Core | Webhook and API routes that must skip browser CSRF | Exact method and path, authenticated (bearer digest, signed-webhook or mTLS verifier) before the CSRF exemption applies | A weak verifier you supply; replay unless your verifier checks freshness | | Trusted-proxy resolution | Core | Spoofed `X-Forwarded-For` choosing the client identity | Forwarded headers read only from listed proxy networks, right to left | A listed proxy that forwards client headers unchanged; any host inside a listed network | | Tenant context | Core | Client-chosen tenant headers | Only tenant context inserted by your authentication middleware is used | Missing ownership checks in handlers | | Rate limit (`Server::rate_limit`) | Core | Per-peer request floods | In-memory token bucket per IPv4 address or IPv6 /64, at most 100,000 keys (LRU) | Multiple instances (each has its own budget), botnets, network-level DDoS | | `RedisRateLimiter` | `rullst-security` (`redis-rate-limit` feature; facade `security-redis`) | Budgets shared across instances | Atomic fixed-window Lua script in Redis | Redis outages and failover semantics; call `require_distributed()` so a mock URL fails at startup | | `rate_limit_middleware`, `RateLimiter` | `rullst-security` | Per-peer floods in one process | Fixed window, 16,384 keys, fails closed for new keys when full | Same as the Core limiter; prefer one limiter per policy | | `LoginGuard` | `rullst-security` | Password guessing on one identity | Progressive delay and an in-memory jail per hashed identity | Distributed guessing across identities; restarts and other instances (state is local) | | Honeypot (`HoneypotLayer`) | `rullst-security` | Automated scanners probing well-known paths | Exact trap paths; direct hits ban the socket peer for 15 minutes by default | Scanners that avoid the paths or rotate addresses; bans are per process and per exact IP | | `deception_trap_middleware` | `rullst-security` | The same, without bans | Global registry of exact trap paths; `403` plus telemetry | Anything beyond recording the probe | | CSWSH guard | `rullst-security` | Cross-site WebSocket hijacking with ambient cookies | Exact `Origin` allowlist, or same host/port/scheme when no allowlist is set; missing `Origin` rejected by default | Non-browser clients (they can send any `Origin`); authentication of the socket itself | | TOTP and recovery codes | `rullst-security` | Stolen passwords | RFC 6238 TOTP, ±1 step; `verify_totp_step_after` rejects replayed steps; HMAC-verified single-use recovery codes | Real-time phishing proxies; replay unless you persist the last accepted step | | `AuditChain` | `rullst-security` | Silent edits to stored audit records | HMAC-SHA256 chain over sequence, time, fields and predecessor | Deleting the newest records without an external checkpoint; a stolen HMAC key | | `FieldEncryptor` | `rullst-security` | Readable sensitive columns in a stolen database copy | AES-256-GCM with AAD and key IDs | An attacker with the key or with application access; key custody is yours | | SRI tags | `rullst-security` | A compromised CDN serving altered JS or CSS | SHA-384 `integrity` attributes | Assets that legitimately change without regenerating the tag | | JSON schema guard | `rullst-security` | Oversized, deep, duplicate-key or off-schema JSON | Size (2 MiB) and depth (32) limits, duplicate-key rejection, optional JSON Schema 2020-12 policy | Business-rule validation; the global guard passes non-JSON content types through | ## What none of these layers do - **Authorization.** No layer knows who owns a record. Check ownership and roles in every protected handler (`RbacGuard::authorize_owner_or_role`) and test the denial. - **Injection-proof data access.** Use SQLx parameters or `sanitize_identifier`; WAF and RASP signatures are a backstop, not a parser. - **Network-level protection.** Volumetric DDoS, TLS termination, firewalling and host hardening belong to your proxy, provider and operating system. - **Shared state.** Bans, jails and most counters are per process. Multiple instances need the Redis limiter or your own shared store. - **Certification.** Mounting these layers does not make an application OWASP-, PCI- or SOC 2-compliant. To collect static evidence of which of these layers a project mounts (headers and CSP, CSRF, cookie attributes, rate limiting on credential routes), run `cargo rullst audit --report`; the [security report guide](https://rullst.github.io/Rullst/book/security-report.html) explains each check and its OWASP ASVS 5.0 Level 1 mapping. The [external audit kit](https://rullst.github.io/Rullst/book/external-audit-kit.html) packages scope, threat models, a sample application and tooling for a third-party reviewer. See the [`rullst-security` crate page](https://rullst.github.io/Rullst/book/crates/security.html) for module details and the [security architecture](https://rullst.github.io/Rullst/book/security-architecture.html) for the deployment checklist. --- # Security Report (`audit --report`) Source: https://rullst.github.io/Rullst/book/security-report.html # Security report (`cargo rullst audit --report`) `cargo rullst audit --report [md|html|json]` (new in 13.0) writes an evidence report about one project: a set of bounded static checks mapped to [OWASP ASVS 5.0.0](https://github.com/OWASP/ASVS/releases/tag/v5.0.0_release) Level 1, a personal-data inventory and accessibility heuristics. It is written for a reviewer. The report **does not replace a manual security review or a penetration test**. It never states that a requirement is met, never reports `PASS` and is not a certification. A check that finds nothing reports `NO FINDINGS`: that check ran and saw nothing it recognizes, nothing more. ```bash cargo rullst audit --report # SECURITY_REPORT.md in the project root cargo rullst audit --report html # SECURITY_REPORT.html, one self-contained file cargo rullst audit --report json --output evidence/report.json ``` The terminal prints a short summary grouped as security checks, personal data and accessibility, with ✓ (no findings), ! (not checked, exceptions or review items) and ✗ (findings or error), a fix and a docs link per failing check. The command exits with status 1 when any check reports `FINDINGS` or `ERROR`, and 0 otherwise. Flags, formats and the JSON schema are in the [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-audit). ## Reading a result | Status | Meaning | | :--- | :--- | | `NO FINDINGS` | The check ran over the inputs it names and recognized no problem. | | `NO FINDINGS OUTSIDE EXCEPTIONS` | `cargo audit` passed only because of `--audit-ignore` exceptions; those advisories remain open. | | `FINDINGS` | The check reported items; each has a location and a message. The exit status is 1. | | `OBSERVED` | The personal-data inventory listed fields. It never changes the exit status. | | `NOT CHECKED` | The check could not run (for example no `src/`, no Git work tree or no `cargo-audit`). It says why. | | `ERROR` | The check started but could not complete (an unparsable `Rullst.toml`, an incomplete source walk, a failed `cargo audit`). The exit status is 1. | Each check lists the ASVS 5.0.0 requirements it relates to, with their level. Most are Level 1; a few related Level 2 requirements are shown for context (for example the CSP header, V3.4.3). A mapping names what the check relates to; a static scan only partially evidences any of them. ## Security checks ### Security headers and CSP Looks for `Server::new`/`Server::new_hot` (the Core baseline sends HSTS, `nosniff`, frame and CSP headers in staging and production) or a router header layer (`headers_middleware`, `SecureHeadersLayer`). A custom `[security] csp` in `Rullst.toml` is reported when it is empty, has no `script-src`/`default-src`, or allows `'unsafe-inline'`, `'unsafe-eval'` or a broad script source (`*`, `http:`, `https:`, `data:`). `coep = "unsafe-none"` or an unknown value is reported. Maps to V3.4.1 (L1) and V3.4.3, V3.4.4 and V3.4.6 (L2). Response-level tests against the deployment remain necessary; see [which security layer to use](https://rullst.github.io/Rullst/book/security-layers.html). ### CSRF Lists write routes (`post`, `put`, `patch`, `delete`) declared on one line as `method("/path" => handler)` or `.route("/path", method(handler))`. Without the `Server` baseline and without `csrf_middleware` on the router, each write route is a finding, except the exact paths in `[security] csrf_signed_webhook_paths`. Machine endpoints (`with_machine_endpoints`) are noted in the evidence. Maps to V3.5.1 (L1). ### Session and auth cookies Reads string literals shaped like a `Set-Cookie` value (`name=value; Path=/`): each needs `SameSite`, `Secure` (or a format placeholder that adds it outside development) and, for session, auth or token cookies, `HttpOnly`. Cookie builders with `.secure(false)`, `.http_only(false)` or `SameSite::None`, and `[security] csrf_same_site = "None"`, are findings. Maps to V3.3.1 (L1) and V3.3.2 and V3.3.4 (L2). ### Rate limiting Finds write routes whose path names an authentication action (`login`, `register`, `password`, `otp`, `session`, `token`, `auth`, ...). Each one is a finding when the project mentions no limiter or login jail (`RateLimiter`, `rate_limit_middleware`, `Server::rate_limit`, `RedisRateLimiter`, `LoginGuard`). The evidence is project-wide: it does not prove each route sits behind the limiter. Maps to V6.3.1 (L1). ### Committed secrets Scans the files `git ls-files` lists (`target/`, symlinks, binaries and files over 2 MiB are skipped) for private-key headers, AWS `AKIA` key IDs, Stripe `sk_live_`, GitHub `ghp_`/`github_pat_` and Slack `xoxb-`/`xoxp-` tokens, and `*_SECRET=`/`*_KEY=` assignments with values of 20 characters or more in a committed `.env` file (`.env.example`-style templates excluded). A finding shows the file, the line and a redacted preview: the first four characters and `…`. The report never contains the value. Untracked files and Git history are not scanned. A finding means: remove the value, rotate it at the provider and load it from the environment or a secret manager. ASVS 5.0.0 has no Level 1 requirement for this; it relates to V13.3.1 (L2) in chapter V13 Configuration. ### Vulnerable dependencies Runs the same `cargo audit` step as `cargo rullst audit`, with the same `--audit-ignore` exceptions, on the project's `Cargo.lock` or, in a workspace member, on the workspace root's lockfile. Without `cargo-audit` the check is `NOT CHECKED`; a run that reports advisories or fails is `ERROR`. Maps to V15.2.1 (L1). ### IDOR Reuses `cargo rullst audit --idor`: every parameterized route needs an adjacent `// rullst-access: public|owner|role|admin — reason` classification and the matching guard in the crate. Maps to V8.2.1 and V8.2.2 (L1). ## Personal-data inventory Lists model fields marked with the ORM attributes: `#[privacy]` (or `#[privacy(...)]`) in a `#[derive(PersonalData)]` model is `personal`, `#[orm(encrypted)]` or a `SecretString` type is `encrypted`, and `#[orm(masked)]` is `masked`. Each row says whether the column is stored encrypted. Fields of ORM models with likely personal names that carry none of these markers (`email`, `phone`, `cpf`, `cnpj`, `ssn`, `birth`, `address`, `document`, `ip`) are listed as `review`. That is a **name heuristic**: it neither proves nor rules out personal data. The inventory is `OBSERVED` and never changes the exit status. It relates to chapter V14 Data Protection (V14.1.1, L2). ## Accessibility A tag scanner reads `html!` invocations in production Rust sources and HTML templates (`.html`, `.htm`, `.tera`, `.hbs`, `.jinja`, `.j2`) under `src/` and `templates/`: | Check | Finding | WCAG 2.2 | | :--- | :--- | :--- | | Images have a text alternative | `<img>` without `alt` | 1.1.1 Non-text Content | | Form controls have a label | `<input>` (except hidden, submit, button, reset, image), `<select>` or `<textarea>` without `<label for>`, a wrapping `<label>`, `aria-label` or `aria-labelledby` | 1.3.1, 3.3.2, 4.1.2 | | Pages declare their language | `<html>` without `lang` | 3.1.1 Language of Page | The scanner does not render templates or follow includes, so a label supplied by an included partial is reported for review. Passing these checks does not make a page accessible; test with a screen reader and keyboard. ## NOT EVALUATED ASVS 5.0.0 has 70 Level 1 requirements. The checks relate to 7 of them. The report lists the other 63 as `NOT EVALUATED`, by chapter (whole chapters such as V1 Encoding and Sanitization or V11 Cryptography, and the remaining requirements of partly mapped chapters). `NOT EVALUATED` is not a failure and not a pass: a static source scan cannot establish those requirements, which need design review, dynamic testing or deployment evidence. ## Using the report with an auditor 1. Run the report from a clean checkout of the commit under review, with the same `--audit-ignore` exceptions your CI uses, and keep the JSON next to the commit hash. 2. Give the auditor the HTML or Markdown file together with the code. Treat every `FINDINGS` row as a work item and every `NOT CHECKED` or `NOT EVALUATED` row as scope the auditor still has to cover. 3. Use the JSON (`rullst.cli-audit-report.v1`) to track results across runs or to feed a ticketing system; its keys are stable within the schema version. 4. Do not present the report as a certification or as ASVS compliance. It is one input to the audit, not its result. --- # External Audit Kit Source: https://rullst.github.io/Rullst/book/external-audit-kit.html # External audit kit This page is for a third-party reviewer or penetration tester. It says what is in scope, where the threat models are, how to build and run a sample application with the production security baseline, which evidence the tooling produces, what to test first and how to report findings. Every command below runs from a checkout of the commit under review. ## Scope **In scope** | Area | What to review | | :--- | :--- | | `rullst-core` | The runtime baseline in `rullst-core/src/security/`: secure headers and CSP nonce, WAF, double-submit CSRF, exact machine endpoints, trusted-proxy client resolution, tenant context; the `Server` composition in `rullst-core/src/server/stack.rs`; rate limiting (`resilience_rate_limit.rs`); uploads (`uploads.rs`). | | `rullst-security` | RASP, response DLP, honeypot, CSWSH guard, JSON schema guard, login jail, MFA, RBAC (`RbacGuard`, `UserContext`), audit chain and field encryption. | | `rullst-auth`, `rullst-connect` | Password hashing, encrypted sessions, session registry, passkeys, OAuth2/OIDC flows. | | `rullst-orm` | Parameterized queries, `sanitize_identifier`, fail-closed tenant columns. | | `rullst-capital` | Payment webhook signature, freshness and replay verification. | | `rullst-nexus`, `rullst-studio` | The Nexus administrator boundary (loopback in debug builds, Basic Auth over verified TLS in release) and that Studio stays a debug-only, loopback-only tool. | | `rullst-macros` | Output escaping of `html!`. | | `cargo-rullst` starters | The code `cargo rullst new` generates for the Blank, API and SaaS starters, and the `audit` command. The LMS, ERP and Portfolio applications (former starters) are workspace examples under `examples/`. | Endpoints worth a direct look in the starters: SaaS `/login`, `/register`, `/logout`, `/dashboard`, `/billing/checkout`, `/billing/portal`, `/billing/webhook` and `/nexus`; LMS `/courses/{id}/enroll`, `/lessons/{id}/play` and `/lessons/{id}/progress`; ERP `/products`, `/products/{id}/add-stock` and `/orders`; Blank JSON API `POST /api/messages`. In the sample application: `POST /posts`, `/posts/repository`, `/checkout`, `/security-demo`, `/_live` (WebSocket), `/nexus` and the `/wp-admin` trap. **Out of scope** - Live third-party services: payment, mail, LLM and OAuth providers. The framework ships deterministic offline mocks for them; test the adapter boundary, not the provider. - Your deployment: reverse proxy, TLS termination, DNS, host and network hardening, and volumetric DDoS. - The deliberate mock credentials and offline fixtures of `examples/` (see `examples/blog/README.md`). - The Experimental-tier crates added in v13 (`rullst-privacy`, `rullst-labs`, `rullst-media`, `rullst-supervision`; see [maturity tiers](https://rullst.github.io/Rullst/book/maturity.html)) unless the engagement names them. ## Threat models - [Rullst threat models](https://rullst.github.io/Rullst/book/threat-models.html): one model per boundary. Start with TM-CORE-2 (client identity behind proxies), TM-AUTH-1 (sessions, passwords, OAuth and passkeys), TM-NEXUS-1, TM-STUDIO-1, TM-TENANT-1 (multi-tenant data access), TM-SEC-1 (HTTP payload contracts), TM-PAY-1 (webhooks and billing) and TM-DEPLOY-1 (CLI and release). - [Security architecture](https://rullst.github.io/Rullst/book/security-architecture.html): contracts and the canonical production middleware order. - [Which security layer to use](https://rullst.github.io/Rullst/book/security-layers.html): what each layer does and what it does **not** protect against. - [Session management](https://rullst.github.io/Rullst/book/session-management.html), [private multipart uploads](https://rullst.github.io/Rullst/book/private-multipart-uploads.html), [outgoing webhooks](https://rullst.github.io/Rullst/book/outgoing-webhooks.html) and [guarded local AI tools](https://rullst.github.io/Rullst/book/ai-tool-security.html) for those features. - [v12 security claims](https://rullst.github.io/Rullst/book/v12-security-claims.html) and `.github/threat-model-release-minimum.json`, the release gate's minimum threat-model evidence. ## Sample application ### Showcase app (`examples/blog`) The blog showcase mounts CSRF and the header baseline itself and exercises tenancy, forms, WebSockets, Nexus and the security primitives. Run it with `RULLST_ENV=staging`, which applies the same baseline as production (WAF, CSRF, secure cookies), and keep it on loopback: ```bash cd examples/blog printf 'RULLST_ENV=staging\nAPP_KEY=%s\n' "$(openssl rand -hex 32)" > .env HOST=127.0.0.1 cargo run -p rullst-blog-example ``` It listens on `http://127.0.0.1:3000` and creates `blog.db` on first start. Quick checks from another terminal: ```bash # Security headers and the CSRF cookie curl -si http://127.0.0.1:3000/ | grep -iE 'content-security-policy|x-frame-options|set-cookie' # A write without the CSRF token is refused (403) curl -si -X POST http://127.0.0.1:3000/posts --data 'title=t&body=b' | head -1 # The WAF refuses an injection probe (403) curl -si 'http://127.0.0.1:3000/?q=1%27%20union%20select%20pw%20from%20users--' | head -1 ``` A debug build keeps Nexus loopback-only. To review the release policy, build with `--release` and provide unique `NEXUS_ADMIN_USERNAME` and `NEXUS_ADMIN_PASSWORD` values behind TLS, as the example's README explains. ### Generated starters Build the CLI from the checkout and generate a starter next to it. Run the command from the repository root, so the project depends on this checkout's crates through path dependencies: ```bash cargo build -p cargo-rullst --bin rullst ./target/debug/rullst new ../audit-saas --default --blueprint saas --database sqlite --skip-initial-migration cd ../audit-saas cargo test ``` Use `--blueprint blank` or `api` for the other starters; the LMS, ERP and Portfolio examples run their tests with `cargo test -p rullst-lms-example` (or `-erp-`, `-portfolio-`) from the repository. `cargo test` runs the generated `src/security_tests.rs` offline against the starter's own router behind the staging/production baseline: security headers, CSRF, WAF, and where the starter has them, the sign-in rate limit (SaaS, LMS), the owner check of the lesson routes (LMS) and the bearer token of the JSON API (`--blueprint api`). Treat these as a floor, not as your test plan. ## Tooling Run the evidence report from the application directory. The CLI built above works in any project: ```bash cd examples/blog ../../target/debug/rullst audit --report json # writes SECURITY_REPORT.json ``` In a generated project, run `../Rullst/target/debug/rullst audit --report json` (adjust the path to your checkout), or `cargo rullst audit --report json` with an installed CLI. The [security report guide](https://rullst.github.io/Rullst/book/security-report.html) explains each check, its OWASP ASVS 5.0.0 Level 1 mapping and the `NOT EVALUATED` list. The command exits 1 when a check reports `FINDINGS` or `ERROR`. The SBOM and supply-chain evidence come from the repository root, as the release workflow (`.github/workflows/release.yml`) produces them: ```bash cargo run --locked -p cargo-rullst --bin rullst -- audit --idor --compliance --sbom cargo audit ``` The first command writes `sbom-cyclonedx.json` (CycloneDX 1.5, from `Cargo.lock`) and `SECURITY_COMPLIANCE.md`; `cargo audit` needs `cargo-audit` installed. Dependency policy is in `deny.toml`, which CI checks with cargo-deny (`.github/workflows/cargo-deny.yml`); locally, `cargo deny check`. ## What to test first 1. **Authentication and sessions:** login, registration and logout in the SaaS starter and the LMS example; session cookie attributes and fixation; the credential rate limit and its keying behind a proxy (`[security] trusted_proxies`); passkeys and OAuth state in `rullst-auth` and `rullst-connect`. 2. **CSRF:** every write route in staging, including multipart bodies (the token must precede file fields), HTMX requests and the exact `csrf_signed_webhook_paths` and machine-endpoint exemptions. 3. **Tenant isolation:** `TenantContext`, fail-closed `tenant_column` models (the showcase's `X-Tenant-ID` selector and `/posts/repository`) and Nexus tenant columns. 4. **IDOR:** parameterized routes such as the LMS lesson and enrollment routes and ERP stock updates. `cargo rullst audit --idor` fails on a route without a declared access class (`// rullst-access: ...`) and a recognized guard; verify that the guard actually protects the route. 5. **Webhooks:** signature, freshness and replay checks on `/billing/webhook` and in `rullst-capital`; the bearer machine endpoint of the JSON API. 6. **File upload:** body limits, content-type handling, file-name sanitizing and the private multipart upload flow. 7. **WAF and RASP bypass:** encodings, payloads split across fields, JSON versus form bodies, and false positives on ordinary text. The WAF is a coarse baseline; parameterized SQL and output encoding are the defenses. ## Reporting Report vulnerabilities privately; never in a public issue or pull request: - GitHub private vulnerability reporting: the **Report a vulnerability** button on the repository's [Security tab](https://github.com/Rullst/Rullst/security/advisories/new); or - e-mail to `officialrullst@gmail.com`, as the [security policy](https://github.com/Rullst/Rullst/blob/main/SECURITY.md) describes, with the affected crate and version, a proof of concept and the estimated impact. Include the commit hash you reviewed and, when relevant, the `SECURITY_REPORT.json` and the commands you ran. ## What a clean result means A run with no findings means that the checks you ran saw nothing they recognize on that commit and configuration. It does **not** mean: - that the application is secure, or that a requirement such as an OWASP ASVS control is met: `audit --report` never reports `PASS` and lists most Level 1 requirements as `NOT EVALUATED`; - that a deployment is safe: proxy, TLS, secrets and host configuration are outside the source; - that authorization is correct: the IDOR scan checks declarations and guards, not business rules; - that a later commit, another feature set or another starter behaves the same. Record the scope, commit, configuration and commands with every result, and treat each `NOT CHECKED` or `NOT EVALUATED` item as work the review still has to cover. --- # Rullst Threat Models Source: https://rullst.github.io/Rullst/book/threat-models.html # Rullst threat models > **Model version:** TM-13.0 > **Applies to:** the v13 development source and generated applications; the > frozen v12 branch (end of life since 16 October 2026) keeps TM-12.10 > **Last source review:** 2026-09-03 > **Status:** maintainer baseline; application owners must extend it for their > data, topology and providers. It is not a pentest or certification. These models turn security ambitions into named abuse cases. A control is not effective merely because its type exists: it must be mounted in the deployed application and its negative case must pass. The [security architecture](https://rullst.github.io/Rullst/book/security-architecture.html) defines the canonical HTTP boundary and the [v12 release audit](https://rullst.github.io/Rullst/book/v12-release-audit.html) records repository evidence. ## Method and common boundaries The method is a lightweight STRIDE review: spoofing, tampering, repudiation, information disclosure, denial of service and elevation of privilege. Every model distinguishes untrusted network input, authenticated application identity, process-local state, shared durable state and third-party/hardware trust. Process-local replay caches, counters, rate limits and audit buffers are useful single-instance controls. They are not distributed guarantees. Forwarded addresses are untrusted unless an explicit proxy policy (Core's `TrustedProxyLayer`, see `CORE-03`) establishes the direct peer as trusted. The machine-readable release minimum in `.github/threat-model-release-minimum.json` binds 52 distinct abuse-case IDs to 70 evidence rows and 69 exact test executions across fourteen packages (thirteen crates and the LMS example, `rullst-lms-example`). The gate rejects missing markers, missing tests and zero-test filters before executing Core (including trusted-proxy client resolution), ORM, Auth, Connect, Security, Messaging, Nexus, Studio, the production monitor, tenant ownership, Capital, AI, Mail, IoT, generated-default and Academy negatives. Passing that bounded minimum does not imply that every case below is closed. ## TM-CORE-1 — readiness and graceful request drain **Assets:** service availability, readiness state, accepted requests and dependency-health observations. **Trust boundaries:** orchestrator ↔ health endpoint; application dependency checks ↔ process-local readiness bits; new requests ↔ draining server. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `CORE-01` forged readiness or diagnostic disclosure | Readiness must fail during startup, any required-component failure, corrupted component state and drain; keep liveness process-only and return counts rather than component labels or error details. | The lifecycle-aware HTTP regression crosses startup → ready → draining, proves `503`/`200` transitions and verifies that the private component label is absent. Applications still own bounded dependency probes and determine which failures should gate traffic. | | `CORE-02` shutdown race admits new work or abandons accepted work | Change admission monotonically to draining before the server's graceful wait, reject later requests and bound the operator's wait for already accepted requests. | The concurrency regression holds one admitted request, begins drain, rejects a second request with `503`, observes a typed timeout, releases the first and reaches zero. A real ephemeral-listener test proves the Server ready → shutdown → stopped lifecycle. The v13 candidate additionally retains admission through response data/trailers and proves streaming drain with two real processes and an existing proxy; hosted admission is pending. Load-balancer propagation, client retry/idempotency, supervisor kill deadlines and multi-replica coordination remain deployment/application work. | **Release-negative minimum:** unready/draining response without private labels; request accepted before drain completes while a later request is denied. ## TM-CORE-2 — client identity behind reverse proxies **Assets:** rate-limit, lockout, ban and audit identity; the TLS evidence used by Nexus Basic Auth; availability of clients that share a proxy. **Trust boundaries:** arbitrary client ↔ forwarding headers; socket peer ↔ configured proxy networks; trusted proxy ↔ its appended entries. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `CORE-03` forged forwarding metadata chooses or collapses the client identity | Read `X-Forwarded-For` or RFC 7239 `Forwarded` (exactly one) only from a socket peer inside a bounded, validated list of trusted networks; never trust `/0` or an empty policy. Walk right to left, skip trusted hops, bound examined bytes/hops and keep the socket peer without failing the request when the trusted part is missing, malformed, unidentified or over-long. Report the forwarded scheme only after explicit opt-in. | `TrustedProxyLayer` unit tests cover direct-client spoofing, multi-hop chains, IPv6/brackets/ports/IPv4-mapped forms, `unknown`/obfuscated nodes, quoted-comma injection, byte/hop limits and client padding that is never parsed. Router tests prove Core `RateLimiter`, Security `rate_limit_middleware` and Nexus Basic Auth lockout bucket each client behind a trusted proxy while forged headers from a direct client are ignored, that only a trusted peer's `https` report satisfies Nexus TLS evidence, and that `Server` mounts the layer outside its rate limiter (static and hot-reload paths). A trusted proxy that forwards client-supplied headers unchanged, proxy address spoofing on a shared network, CDN/provider range maintenance and distributed limits remain deployment work. | **Release-negative minimum:** a direct client forging forwarding headers keeps its socket identity, and two clients behind one trusted proxy receive separate rate-limit buckets. ## TM-AUTH-1 — sessions, passwords, OAuth/OIDC and passkeys **Assets:** password verifiers, session keys/tokens, OAuth state and nonce, passkey challenges/credentials, recovery paths and account identity. **Trust boundaries:** browser ↔ application; application ↔ OAuth/OIDC provider; application ↔ session/challenge store; operator ↔ key store. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `AUTH-01` forged, truncated, expired or legacy session | Reject before constructing identity; authenticate encryption version, expiry and payload. | Negative session parsing/encryption tests in `rullst-auth`. | | `AUTH-02` weak/missing application key | Fail startup/configuration closed outside explicit deterministic mocks. | Weak-key and malformed-key tests. Malformed `.env` and `Rullst.toml` errors carry no file content (`.env` names an entry number, `Rullst.toml` a line and column), and cookie helpers skip `.env` when the process selects the environment; isolated-process regressions cover each. | | `AUTH-03` password timing/CPU starvation | Use Argon2id off the async executor, bound input and normalize failure behavior. | `spawn_blocking` implementation and password tests. The recovery registry applies the Argon2 72-byte bound before account lookup, so overlong input yields one error for registered and unknown emails; a regression covers login, registration and reset. Deployment capacity remains application work. | | `AUTH-04` OAuth login CSRF/code substitution | Bind state, nonce, redirect URI, issuer, audience and one-time callback context. | The optional Axum/tower-sessions path generates a ten-minute state/PKCE challenge plus OIDC nonce, stores verifier/nonce server-side, removes and immediately saves the one active challenge before validation, and tests sequential replay, mismatch, expiry, replacement and redaction. The v13 prerelease `AuthSessionForm` applies the same consumption to a bounded form POST body for `response_mode=form_post` providers such as Apple and tests mismatch, replay, method, media-type and size rejection. The store trait does not provide distributed compare-and-delete across already-loaded requests. The v13 prerelease `challenge` store keeps the same challenge in the application's database (or memory) under a digest of an opaque 256-bit id that is the only value in a short `HttpOnly`, `SameSite=Lax` cookie, and consumes it with one atomic removal: tests cover replay, expiry, wrong and missing state, missing and altered cookies, another provider's challenge, a provider error with a forged state, and concurrent callbacks with exactly one winner (in memory and on SQLite; PostgreSQL and MySQL run against owned servers in CI). `cargo rullst make:oauth google|github|microsoft|apple` builds on it and uses a provider email only when the provider verified it (GitHub: the primary address marked verified; Microsoft: only with `xms_edov: true`, the tenant chosen explicitly and the ID token's issuer bound to its `tid`), refusing by default to link an email that already belongs to an account; Apple's cross-site form POST callback uses a `SameSite=None; Secure` challenge cookie and is exempt from the CSRF token only by exact path, the challenge's state taking its place. A generated-project contract with all four providers in one project covers success, replay, state mismatch, unverified email, the linking policy across providers, a refused account and the session epoch. `Provider::get_user_from_token` is documented as unsuitable for signing in a client-supplied token, because a provider userinfo response does not bind the token to this `client_id`; the v13 prerelease `verify_id_token` on Google and `OidcProvider` binds a client-supplied ID token to this client ID and a server-issued nonce, with wrong-audience and wrong-nonce negatives against the local signed IdP. Redirect registration, durable session/cookie/TLS policy, idempotent account linking/recovery and live-provider conformance remain RC application/deployment tests. | | `AUTH-05` passkey origin/RP/challenge confusion | Require exact RP/origin, ceremony type, single-use bounded challenge, UV/UP flags and supported attestation/key formats. | Negative tests cover the bounded ES256/none scope. Challenge lifetimes are capped at one day and expiry arithmetic is checked, so configuration cannot panic and poison the challenge store. Normative WebAuthn conformance remains open. | | `AUTH-06` session fixation/replay/revocation gap | Rotate on privilege change, expire server-side and provide device/session revocation. | Versioned expiring cookie sessions exist. The optional JWT policy uses bounded expiry, `kid` rotation, token IDs and subject session versions; production rejects its process-local store. The SQLite profile adds shared-local JTI/session-version revocation and bounded passkey inventory/revocation with counter CAS. The optional v13 shared PostgreSQL ceremony candidate adds account/session/RP-bound single use with local database/browser evidence; its monotonic shared clock lets a host up to five seconds behind adopt the recorded time and fails closed beyond that. Hosted admission remains pending. Cookie-session inventory, refresh flow, credential-repository replication/failover and application device ownership remain open. | | `AUTH-07` account enumeration/recovery takeover | Normalize public responses, rate limit attempts, protect recovery factors and audit changes. | Login jail/timing helpers and subject-bound 80-bit recovery-code verifiers exist. Plaintext is returned only at enrollment and zeroized on drop; comparison is constant-time and consumption removes the verifier. Durable transactional consume, enrollment UX, step-up policy and full recovery workflow remain application-owned. | **Release-negative minimum:** malformed/expired session, wrong key, cross-origin passkey, replayed challenge, wrong RP, invalid callback state and repeated login failure. **TOTP replay (`AUTH-07`):** `verify_totp_code` is stateless and accepts the previous, current and next 30-second step, so an observed code verifies again for about 90 seconds. `verify_totp_step_after` accepts only a step newer than the supplied last accepted step (RFC 6238 section 5.2); persisting the returned step per secret atomically before admitting the login is application-owned. ## TM-CONNECT-1 — durable local OAuth token generations **Assets:** access and refresh tokens, provider identity, application-account binding, encryption keys and the latest accepted token generation. **Trust boundaries:** application authorization ↔ account binding; provider response ↔ refresh coordinator; local processes ↔ shared SQLite state; and operator secret manager ↔ encrypted snapshot key. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `CONNECT-24` stale or competing local writer rolls a rotated token generation back | Store only authenticated ciphertext plus non-secret key/generation metadata, serialize local writes and replace/delete only after an exact generation compare-and-swap. Reject configuration drift, quota exhaustion, malformed rows and unsafe existing file targets without exposing the database path or token material. | The opt-in SQLite store uses `BEGIN IMMEDIATE`, an immutable persisted row ceiling and exact successor CAS. Two independent pools race generation one; exactly one succeeds, the loser fails with `GenerationConflict`, restart recovers the winner, and a stale delete is rejected. Separate negatives cover key mismatch, ciphertext authentication, metadata corruption, quota/configuration, plaintext absence and symlink targets. Provider-call leases, recovery after a losing remote refresh, key-manager operation, trusted directory permissions, backup, multi-host replication and live-provider conformance remain application/deployment work. | **Release-negative minimum:** two local writers cannot both replace one observed generation, and restart cannot turn a stale generation into the winner. ## TM-NEXUS-1 — administrative CMS **Assets:** administrative session, model records, bulk actions, AI assistant tools, audit evidence and security telemetry. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `NEXUS-01` anonymous/default access | Fail closed without explicit production policy. Local shortcut requires debug build and verified loopback peer. | Router policy and loopback tests. Basic Auth counts only failed presented credentials per IPv4 address or IPv6 /64; credential-less challenges never lock. A locked bucket is not evaluated for unknown clients, while browsers holding the per-process known-client cookie keep access, and pruning is amortized. Behind a shared proxy a first login can still be locked out unless Core's `TrustedProxyLayer` (`CORE-03`) is configured with the proxy's network; a router test then proves per-client lockout isolation. | | `NEXUS-02` IDOR/BOLA on CRUD/batch routes | Resolve subject/tenant, then authorize object ownership or role for every ID and bulk member. | Models that explicitly register a text tenant column now scope every built-in read/mutation/batch predicate to a trusted `TenantContext`, inject it on create and fail closed without context. A real SQLite HTTP regression proves cross-tenant list/update/delete/batch denial and protected create input; pure SQL tests keep the all-feature release minimum portable. `#[derive(Nexus)]` keeps ORM-encrypted, `SecretString` and `#[orm(hidden)]` fields hidden and read-only, shows `#[orm(masked)]` fields only as write-only `Password` widgets by default and omits skipped fields, so the panel never lists, searches or overwrites them with plaintext; derive unit and facade regressions cover this. Global models, custom routes, identity/membership resolution, within-tenant object ownership and independent review remain host work. | | `NEXUS-03` stored/reflected XSS | Escape dynamic HTML; make raw HTML explicit; enforce nonce CSP. | Core proves renderer/header nonce identity. Generated LMS auth/catalog/course/player style elements consume the request nonce and remove remote shell dependencies/inline style attributes. Nexus serves its stylesheet, script and vendored htmx same-origin with no inline code, handler or style attributes; an HTTP regression checks every Nexus page and fragment against the default production CSP, and the Linux browser check loads the shell under that CSP and fails on any violation. A route-by-route review of application routes mounted next to Nexus remains open. | | `NEXUS-04` AI assistant privilege escalation | Treat model output as untrusted; allowlist typed tools and authorize each invocation as the human subject. | Prompt filtering exists; tool approval/audit policy remains open. | | `NEXUS-05` destructive CSRF | Require CSRF on cookie-authenticated mutations and exact signed-webhook exemptions only. | The exact Core baseline regression proves a production cookie write is denied without the matching double-submit value, accepted with it and retains the outer header/CORS policy on denial. A nested-layer regression proves an explicitly protected application router wrapped by the `Server` baseline emits one matching token/cookie pair and accepts the valid POST instead of applying the protocol twice. Exact signed-webhook exemptions have separate unit negatives; a full Nexus browser/proxy flow remains open. | | `NEXUS-06` audit repudiation | Record actor, tenant, object, operation, outcome and correlation ID in durable separate storage. | The opt-in required policy records the built-in authenticated actor, optional tenant, table/action, optional known key, count, committed outcome, bounded correlation ID, timestamp and format version in the same transaction; unavailable storage rolls the mutation back. A real SQLite regression covers commit and rollback, while schema tests cover all SQL dialects. It is same-database mutable evidence, not separate append-only or tamper-evident storage; denied attempts and automatically assigned create keys are not uniformly persisted. Host retention, backup, replication, immutable export and review remain open. | Trust boundaries are browser ↔ Nexus, Nexus ↔ application policy/database and Nexus ↔ LLM/provider. ## TM-STUDIO-1 — local developer control room **Assets:** environment/configuration, logs, traces, database browsing, job controls, prompts and source-error tooling. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `STUDIO-01` remote exposure | Compile/mount shortcuts only in debug development and bind loopback. `Server` mounts the panic console, `/_rullst/explain`, the retired `/_rullst/autofix` (`410 Gone`, never edits files) and `/_rullst/errors/{id}` only when the build has debug assertions and the environment is Development, so a release binary with an unset environment does not expose them; the panic console renders details only to loopback peers, and `/_rullst/errors/{id}` serves the bounded, expiring, header- and body-free panic context (whose path the page's "Ask your AI assistant" panel shows for the developer's own assistant) only to direct loopback requests with a loopback `Host` and no forwarding headers. Production exposure needs application-owned auth and TLS. | Generated startup and Studio boundary tests/docs; builder console-gate, panic-console peer and error-context endpoint tests. | | `STUDIO-02` secret leakage | Mask environment/log fields by key and value patterns; never render raw credentials. | Environment viewer/redactor tests cover key markers plus URL user information, bearer tokens and secret-named assignments in allowlisted values; novel formats remain residual risk. | | `STUDIO-03` SQL/table injection | Parameterize values and strictly validate dynamic identifiers. | Data-browser identifier/query-builder tests. | | `STUDIO-04` arbitrary source file access | Require debug loopback, canonical allowlisted paths/extensions and bounded files. | Traversal, sensitive-file, non-loopback and extension tests. | | `STUDIO-05` forged telemetry | Label local/unverified events accurately; never invent source IP, HMAC verification or provider status. | Telemetry integrity tests. | | `STUDIO-06` destructive job/database action | Require verified local policy and same-origin checks; remote production controls remain disabled until separately authenticated, authorized and reviewed. | Studio serves `Referrer-Policy: same-origin`, so browser form posts carry the real origin; `Origin: null` passes only with `Sec-Fetch-Site: same-origin`. A browser-modelled regression covers Studio's served policy, an imposed `no-referrer`, cross-site and same-site (other local port) documents and a bare `null` origin; a local Chrome run confirmed the header values. Data-browser writes additionally require an unforgeable middleware marker, inspected complete PK, typed binds, exactly one affected row and exact delete confirmation. A key column outside the identifier boundary or column cap makes the table read-only, and the write transaction is rolled back unless exactly one row changed; a four-backend composite-key regression and a multi-row rollback test cover this. Importing the raw router is denied by an exact release-negative test. Queue retry/purge and feature-flag toggles require the same marker; raw-router negatives prove that they return `403` and leave queue records and flag values unchanged. Because safe methods pass without an `Origin`, `GET` views perform no schema or data writes; a four-backend regression proves that the feature-flag page no longer creates `rullst_feature_flags`. Application tenant/RBAC, durable audit and rollback remain open. | | `STUDIO-07` forged or replayed remote span batch | Keep ingestion push-only; bind each endpoint to one producer name/key, authenticate the exact body and bounded source/timestamp/nonce fields, consume valid nonces atomically, reject stale/future requests and validate the complete batch before storage. | Wrong-source/key, HMAC tamper, stale timestamp, concurrent replay, schema/cardinality, deduplication and capacity regressions. TLS, key custody/rotation, clock synchronization and producer admission remain deployment work. | | `STUDIO-08` cache secret exposure or broad deletion | Return metadata only, replace logical keys with keyed opaque browser tokens, omit values, omit bulk flush and require the verified-local mutation marker for one-entry invalidation. | Memory plus live Redis metadata contracts, HTML non-disclosure tests and forged/missing-marker mutation negatives. Application cache-key classification and operator policy remain external. | | `STUDIO-09` database selection drift | Reuse the application's ORM pool; otherwise resolve the database only through the shared `Server`/Artisan resolver, never invent a fallback database and never echo configuration content in errors. | Resolver unit tests pin the process-environment, `.env` and `[database].url` precedence, full query strings and content-free errors; an isolated-process regression proves that an unconfigured `GET /studio` creates no pool and no `db.sqlite`. A Studio request that arrives before an application's own explicit `Orm::init` can still initialize the pool first; applications that choose their URL in code should initialize it before serving Studio. | | `STUDIO-10` resource exhaustion through table views | Bound rows, cell text and search terms before data leaves the database or is bound per column. | Pages load 25 rows; the SELECT cuts cell text to 256 characters (key columns to 16 KiB plus one character, making longer keys read-only) and search terms above 256 bytes return `422`. A four-backend regression proves that an 8 KiB cell is cut and the search bound is enforced. The per-column `LIKE` scan remains proportional to table size. | Trust boundaries are local browser ↔ loopback listener, authenticated trace producer ↔ push-only application endpoint, Studio ↔ telemetry/database/cache, and error console ↔ source filesystem. ## TM-STUDIO-2 — production monitoring endpoint Added for 13.0.0-alpha.3, before the endpoint shipped. Phase 1 of the [Studio production monitor plan](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#delivered-in-1300-alpha3) is a read-only JSON endpoint, `GET /_rullst/monitor/v1`, that an application mounts with `Server::with_monitor`. The standalone Studio that reads it for several projects from the developer's machine (phases 2 and 3) is modelled in `TM-STUDIO-3`. **Assets:** operational metrics of a production process (request counts, rates and latency per route pattern, status classes, queue depth and worker activity, possible N+1 query fingerprints, process radar); the monitor tokens; the availability of the application that serves the endpoint. **Actors:** an anonymous internet client; the holder of a leaked or replayed monitor token; a user signed in to the application without the monitoring permission; a compromised developer machine that holds tokens for several projects; a browser page on another origin using a signed-in administrator's cookies; a network observer between the client and the TLS terminator. **Trust boundaries:** internet client ↔ reverse proxy/TLS terminator ↔ application listener; monitor client ↔ token or application authorizer; request handling ↔ the bounded in-process collector; collector ↔ queue store. Test files named below are under `rullst-core/src/`. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `MONITOR-01` endpoint exposed by default or by accident | Off by default. The route and the recording layer exist only when the application calls `Server::with_monitor` **and** at least one credential source is configured (a token from `RULLST_MONITOR_TOKENS` or code, or an application authorizer); otherwise the path answers `404` like any unknown route and request handling is unchanged. Generated starters do not enable it: the Blank, SaaS and LMS starters carry the `with_monitor` call and the `RULLST_MONITOR_TOKENS` setting commented out. | `server/monitor_stack_tests.rs`: no monitor and a monitor without credentials both answer `404`, and an invalid token stops start-up without echoing it; `monitor/endpoint_tests.rs`: `Installed::prepare` mounts nothing (no route, no recording layer) without a credential, including an empty `RULLST_MONITOR_TOKENS` list. | | `MONITOR-02` token guessing, theft and replay | Accept tokens only in `Authorization: Bearer` (never in the query string, which access logs and proxies record). Reject at startup tokens below 32 bytes of entropy (43 base64 or 64 hex characters, a minimum character variety, no placeholders) without echoing them. Compare against every configured token in constant time; several tokens allow rotation without downtime. Count only presented, invalid tokens as failures; after a bounded number per client (IPv4 address, IPv6 /64, resolved through `CORE-03` trusted proxies) answer `429` without evaluating credentials until the bucket refills. Missing and invalid credentials get the same `401`. A bearer token is replayable by design: TLS (`MONITOR-08`), rotation and the read-only scope bound the damage; there is no nonce or expiry in phase 1. | `monitor/token_tests.rs` (length, alphabet, variety, placeholders, list size, position-only errors, constant-time set matching, `Authorization` parsing); `monitor/endpoint_tests.rs` for missing, malformed, wrong, other-scheme, rotated and valid tokens with one identical `401`, and a lockout that answers `429` even to a valid token while other clients are unaffected. Distributed (multi-replica) lockout and token expiry remain deployment work. | | `MONITOR-03` signed-in user without the monitoring permission | With an application authorizer, the application decides on every request: `Allow` serves, `SignIn` answers `401`, `Deny` answers `403`, and an authorizer error fails closed with `503` and a logged reason. A token never widens what the authorizer denies for other requests. | `monitor/endpoint_tests.rs`: authorizer allow, sign-in, deny and error, a bearer value that is not a token, and token or authorizer either sufficing. | | `MONITOR-04` information disclosure through the payload | Report only aggregates keyed by the matched **route pattern** (`/users/{id}`), never the raw path, query string, headers, cookies, client addresses, user identifiers, request or response bodies, SQL text or bound values, error messages, environment or configuration values. Requests without a matched route are counted as `(unmatched)`. N+1 findings carry the ORM's static `Model.operation (table)` fingerprint. Queue probe failures report a fixed reason, never a driver message. The schema is versioned (`rullst.monitor.v1`). | `monitor/payload_tests.rs` pins the exact set of JSON fields and values; `monitor/record_tests.rs` sends a path with an id, an email and a query string, a cookie, an `Authorization` value, a forwarded address and a user header, and finds only the route pattern in the body; `monitor/config_tests.rs` proves a driver error message never reaches the document. Application-chosen route templates and queue names are the application's responsibility. | | `MONITOR-05` denial of service through the endpoint or through collection | Bound collection: at most 256 route patterns (further ones share an `(other)` bucket), methods normalized to the standard set plus `OTHER`, a fixed latency histogram over a five-minute window, at most 64 N+1 entries, 16 queues and a 250 ms limit per queue probe. Collection is O(1) per request and lock-local to the route. The endpoint has its own per-client rate limit, and its requests are not counted in the statistics. The overhead per request is measured and published in the guide. | `monitor/collector_tests.rs` (route and method cardinality, `(other)`, N+1 table eviction), `monitor/endpoint_tests.rs` (per-client budget), `monitor/config_tests.rs` (concurrent queue probes bounded by the timeout), `monitor/record_tests.rs` (own requests not counted); the release-mode overhead measurement (`monitor/overhead_tests.rs`) is published in the production monitoring guide. | | `MONITOR-06` cache or proxy keeps a response | Every monitor response, including refusals, carries `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`; staging and production also receive the security-header baseline. | `monitor/endpoint_tests.rs` checks the headers on `200`, `401`, `403`, `405`, `426` and `429`; `server/monitor_stack_tests.rs` checks the production security headers. | | `MONITOR-07` cross-origin read with an administrator's ambient cookies | The endpoint is a machine contract: a request carrying an `Origin` header, or a `Sec-Fetch-Site` of `cross-site` or `same-site`, is refused with `403` before credentials are evaluated, so even an application CORS policy that allows the origin never releases monitoring data to a page. The endpoint itself adds no CORS headers and serves `application/json` with `nosniff`. | `monitor/endpoint_tests.rs`: `Origin` (including `null`) and cross-site or same-site `Sec-Fetch-Site` refused with an empty body and no `Access-Control-Allow-Origin` from the endpoint; `server/monitor_stack_tests.rs`: refused under an application CORS allowlist that includes the origin. | | `MONITOR-08` downgrade to plain HTTP | In staging and production, refuse (`426 Upgrade Required`, before credentials are evaluated) a request without TLS evidence: an `https` scheme reported by a trusted proxy with `trust_forwarded_proto` (`CORE-03`), or a `MonitorVerifiedTls` marker inserted by an application that terminates TLS itself. A forwarded header from an untrusted peer and an absolute-form `https://` request target are not evidence. An explicit opt-in admits plain HTTP from a direct loopback peer without forwarding headers (an SSH tunnel or a sidecar on the same host). A token already sent in cleartext must be rotated. | `monitor/endpoint_tests.rs`: staging and production plain HTTP and an absolute `https://` target refused; forged `X-Forwarded-Proto` from an untrusted peer refused; trusted proxy `https` admitted and `http` refused; marker admitted; the loopback opt-in admits direct loopback peers and refuses forwarding headers, a public `Host` and other peers; development and test admit plain HTTP. | | `MONITOR-09` compromised developer machine or monitoring client | A token grants read-only access to one application's aggregates and nothing else: no rows, no secrets, no mutation, no Studio development pages. Use one token per project and client, so revoking one machine is removing its token. | The read-only scope follows from `MONITOR-04`; `monitor/endpoint_tests.rs` proves other methods answer `405`. Tokens stored on a developer machine are covered by `REMOTE-03` (`TM-STUDIO-3`). | | `MONITOR-10` monitoring changes or breaks request handling | The recording layer never alters a response; it observes method, matched route, status and duration, tolerates poisoned locks and adds no work when the monitor is disabled. ORM attribution uses the passive query layer the telemetry subscriber installs; with a `RUST_LOG` that filters `rullst_orm` spans or an application-owned subscriber, the payload says so instead of reporting zero queries. | `monitor/record_tests.rs` compares responses with and without the layer and counts ORM operations only inside a recorded request; `monitor/payload_tests.rs` covers the unavailable reasons; the full Core suite passes with the passive layer. | **Release-negative minimum:** disabled → `404`; missing and invalid tokens → the same `401`; production plain HTTP → `426`; a raw path with an id and a query string never appears in the payload. ## TM-STUDIO-3 — Studio remote projects on the developer's machine Phase 2 of the [Studio production monitor plan](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#delivered-in-1300-alpha3): the standalone `rullst-studio` binary runs on a developer's machine without an application process, keeps a list of projects (name, base URL, monitor token) and renders each project's `rullst.monitor.v1` document (`TM-STUDIO-2`). It connects outwards only; the server collects nothing new. Studio's local pages (data browser, `.env` viewer, AI and API playgrounds, migrations) are not part of this mode. **Assets:** the monitor tokens of every configured project; the developer's local network position; the integrity of what Studio displays; the availability of the developer's Studio process. **Actors:** a malicious or compromised monitored application (or anyone able to answer for its origin); a web page open in the developer's browser; another local user on the same machine; malware running as the developer; a network attacker between Studio and the monitored application; someone who obtains a copy of the configuration file (backup, dotfile sync, screen share). **Trust boundaries:** browser ↔ Studio's loopback listener; Studio ↔ the configuration file and its separate key file; Studio ↔ each monitored origin over the network; Studio ↔ its local history database (phase 3); Studio ↔ the developer's webhook receiver (phase 3). Test files named below are under `rullst-studio/src/`. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `REMOTE-01` Studio reachable from other machines or pages | Bind `127.0.0.1` only; every route keeps Studio's loopback boundary (loopback peer, loopback `Host`, same-origin `Origin` on unsafe methods, the crate-private marker), which remote mode applies in release builds too because it serves no local data. | `remote/router_tests.rs`: a non-loopback peer, a public `Host` and cross-origin or `Origin`-less form posts are refused; `bin/rullst-studio.rs`: the only option is `--port` (a `--host` argument is rejected). | | `REMOTE-02` local CSRF against add/remove-project forms | Unsafe methods require a same-origin `Origin` (as for every Studio form) and a per-process random form token compared in constant time; `GET` pages change nothing. | `remote/router_tests.rs`: a post without `Origin`, with another origin, or with a missing or wrong form token answers `403` and leaves the project list unchanged; form tokens are compared whole. | | `REMOTE-03` token disclosure at rest | Store the project list in the user's configuration directory (`0600` file in a `0700` directory on Unix, never through a symlink, written atomically, bounded size). Encrypt each token with AES-256-GCM under a 256-bit key kept in a separate file in the user's data directory (`0600`), with the project name and base URL as associated data, so a token copied to another project or a URL edited in the file no longer decrypts and is never sent. Tokens never appear in pages, logs, errors or `Debug`. This protects a copied configuration file, not a compromised account: malware running as the developer can read both files. | `remote/store_tests.rs`: encryption round trip, the file never contains the token, swapped names and an edited URL fail to decrypt, a missing key makes tokens unreadable, `0600`/`0700` permissions and the readable-by-others warning on Unix, symlinked files refused, malformed and oversized files reported without contents. OS keychain integration is not provided. | | `REMOTE-04` token sent to the wrong party | Accept only `https` base URLs, or `http` to a loopback host (SSH tunnels). Reject user information, query and fragment. Send the token only in `Authorization: Bearer` to the configured origin's fixed monitor path; follow no redirects; validate certificates (no option to disable it); ignore proxy environment variables. | `remote/store_tests.rs`: `http` to a non-loopback host, user information, query strings, fragments and non-HTTP schemes are refused; `remote/client_tests.rs`: a redirect to another origin is reported and the second origin never receives the token, and the token goes as `Bearer` to the configured origin only. | | `REMOTE-05` hostile or compromised monitored endpoint | Treat the document as untrusted: bound the response (1 MiB, 5 s total, 3 s to connect), require `200`, JSON and `schema: "rullst.monitor.v1"`, bound every list and string, and HTML-escape every remote value; an error shows a fixed status, never the remote body. | `remote/client_tests.rs` against a local test server (`remote/server_test.rs`): oversized (declared and streamed), malformed, wrong-schema, non-JSON and slow responses become fixed statuses within the limits; `remote/router_tests.rs`: script and markup in labels are escaped on every page and unreachable projects show a status. | | `REMOTE-06` SSRF-like use of the project list | Projects are added only through the protected form; Studio requests only `GET <base>/_rullst/monitor/v1` and renders only the validated schema, so a project pointing at an internal service yields at most a refusal status. At most 32 projects. | Covered by `REMOTE-02`, `REMOTE-04` and `REMOTE-05` tests. Network policy on the developer's machine stays with the developer. | | `REMOTE-07` local surfaces leaking into remote mode | The remote router mounts only the remote pages; the data browser, `.env` viewer, AI and API playgrounds and migration pages are absent. | `remote/router_tests.rs`: those paths answer `404` in remote mode while the remote pages and assets answer. | | `REMOTE-08` history store disclosure or growth (phase 3) | Store only aggregates the document already exposes (reachability, request rate, 5xx rate, p95, queue pending and failed totals) per project and time, never tokens, routes or other labels, in an owner-only SQLite file in the user's data directory. Delete samples older than the retention age and beyond a per-project row limit on every write; keep a bounded number of alerts. | `remote/history_tests.rs`: retention by age and by row count, alerts bounded, file permissions on Unix, only numeric columns stored. | | `REMOTE-09` polling turned into load or a network scanner (phase 3) | Poll only projects the developer marked as watched, at an interval bounded to 15 s–1 h (default 60 s), one read per project per interval with the `REMOTE-04`/`REMOTE-05` bounds, so the monitored server's own budget (`MONITOR-05`) is never approached. | `remote/poller_tests.rs`: one read per watched project per round, unwatched projects untouched, interval clamped. | | `REMOTE-10` alert spam or hostile values in alerts (phase 3) | Evaluate rules with an explicit clock; a rule fires again only after its per-project cool-down; the 5xx-rate rule needs a minimum number of requests; restarts that reset counters never fire the failure rule. Alert text is built from Studio's own wording, the project name and numbers, never from remote labels. | `remote/alerts_tests.rs` with a fixed clock: thresholds, cool-down, minimum requests, counter resets, unreachable projects. | | `REMOTE-11` webhook secret disclosure or misuse (phase 3) | A webhook URL is a secret: stored sealed like tokens (bound to the project), shown only as configured or not, `https://` only, no user information or fragment. Delivery sends a small JSON body with the project name, rule, message and time, follows no redirect, ignores proxies, is bounded to 5 s and never retried in a loop; a failure is recorded on the alert as a fixed status. | `remote/webhook_tests.rs`: URL validation, sealing round trip, delivery to a local test receiver, failure and redirect statuses without following. | ## TM-TENANT-1 — multi-tenant data access **Assets:** membership, tenant-scoped records, billing/workspace IDs, cache keys, jobs, files, logs and exports. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `TENANT-01` trusted client header | Never accept tenant/role solely from an arbitrary header; bind to authenticated membership or a trusted gateway. | Strict tenant guard rejects absent context; gateway policy is application-owned. | | `TENANT-02` object ID crosses tenant | Query with tenant/owner predicate and authorize the returned object server-side. | `UserContext` carries a validated optional tenant and `RbacGuard::authorize_tenant[_owner_or_role]` requires an exact match that even admin cannot bypass. The materialized LMS starter test exercises its enrollment-owner boundary (`ACADEMY-02`); general route coverage remains open. | | `TENANT-03` bulk/list/export leakage | Apply tenant predicate before pagination/count/export and validate every bulk member. | Must be proven per generated/application route. | | `TENANT-04` cache/queue/storage collision | Include canonical tenant identity in keys, jobs and storage roots; validate again in workers. | Core's `TenantStorage`, `TenantCache`, `TenantRealtime` and `TenantPresence` can only be constructed from a validated `TenantContext`, apply immutable tenant namespaces and have exact same-key/channel/presence-room non-interference tests. Cache keys, realtime channels and presence rooms share one `tenants:<tenant>:<logical>` builder whose tenant segment encodes `%` as `%25` and `:` as `%3A`, so a logical name containing `:` cannot alias another tenant (identifiers without those characters keep their previous names); `("a", "b:c")` versus `("a:b", "c")` is tested for cache, broadcast and presence. Tenant identifiers made only of dots are rejected, and `TenantStorage` also refuses any tenant that is not exactly one normal path segment, with an exact tenant-`.` negative. On local storage it also refuses a tenant whose `tenants/<tenant>` directory the filesystem resolves under another name, so case-variant identifiers (`Acme`/`acme` on APFS or NTFS) and Windows trailing-dot stripping (`acme.`) cannot share a directory; the first tenant to create it keeps it. The cache wrapper exposes no cross-tenant flush; the realtime wrappers validate names and bound payloads. Generated ORM Redis model hashes bind the application namespace plus opaque tenant and table digests, require a typed tenant context for tenant models and reject another tenant's handle or decoded hash; an opt-in live Redis regression proves tenant/namespace separation, while the `orm:events:*` channels stay un-namespaced. Application room authorization, distributed realtime/liveness, search, metrics, exports, distributed cache/failover and remote bucket policy remain open. | | `TENANT-05` confused-deputy background work | Persist actor/tenant/authorization intent and revalidate sensitive execution. | Shared durable job authorization contract remains open. | Trust boundaries are identity ↔ membership, route ID ↔ object and application ↔ database/cache/queue/storage. ## TM-ORM-1 — portable document recovery **Assets:** document contents, portable identifiers, application/collection scope, snapshot encryption keys and the destination collection. **Trust boundaries:** application writer ↔ repository inventory; source store ↔ application-owned snapshot storage; operator key custody ↔ snapshot opener; and snapshot contents ↔ destination repository. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `ORM-01` copied, tampered or cross-scope snapshot discloses or substitutes documents | Encrypt and authenticate the complete versioned payload with a fresh nonce, explicit rotation key ID, length-delimited application/collection binding and fixed decode limits; authenticate before JSON decoding. | The exact negative changes ciphertext, key material and application binding and proves all three fail closed. Key bytes are consumed through a zeroizing temporary, opaque snapshot/key `Debug` output is redacted and plaintext labels are absent from the envelope. Key generation/custody/rotation, external snapshot permissions and deletion remain operator responsibilities. | | `ORM-02` partial retry overwrites conflicting destination data or silently accepts extras | Accept only an empty destination or an exact matching subset, insert without replacement, treat only an exact raced duplicate as replay and verify the final complete inventory. Never delete an extra row or claim transactionality across stores. | The exact negative proves a differing row and an extra row fail before mutation. Deterministic tests cover partial resume and idempotent replay; the live matrix exports MongoDB→SurrealDB→MongoDB and checks ordered IDs. Export performs two equal observations, but formal consistency still requires the application to quiesce writers; a failed restore can retain prior successful inserts for the documented retry path. | **Release-negative minimum:** ciphertext/key/scope substitution and conflicting or extra destination state. ## TM-SEC-1 — declared HTTP payload contracts **Assets:** route semantics, validated request data, schema configuration and security telemetry. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `SEC-16` malformed, ambiguous or schema-confused JSON body | Require a JSON media type on unsafe schema-bound requests, classified as the route's JSON extractor does (any ASCII case, parameters, `json` subtype or `+json` suffix) so an accepted spelling cannot skip the checks; reject malformed, duplicate-key, oversized or deeply nested input before applying a precompiled closed schema. Never fetch attacker-selected schema references. | The bounded route policy compiles JSON Schema 2020-12 or one OpenAPI 3.1 component with local references, no network/filesystem resolver and linear-time regexes. The exact negative covers shape/additional-property confusion and external references; middleware tests cover 415/400/422 and exact body preservation. Auth, ownership, domain rules and non-JSON parameters remain separate. | Trust boundaries are application schema configuration ↔ compiled validator and untrusted HTTP body ↔ route handler. ## TM-SEC-2 — anomaly assessment and proof-of-work admission **Assets:** service availability, canonical client subjects, challenge key, challenge capacity and replay state. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `SEC-07` forged, replayed, cross-subject or resource-exhausting challenge | Authenticate the complete challenge, bind it to one canonical subject, cap difficulty/TTL/cardinality, reject invalid work, expire it and atomically consume one successful proof. Classification must remain explainable and must not become authorization by itself. | The exact negative tampers with the token, changes the subject, submits invalid work, verifies concurrently and replays/expires the challenge; only one local verifier succeeds. Aggregate collection, proxy/device identity, accessible alternatives, distributed replay state, adaptive evaluation and enforcement remain application/deployment work. | Trust boundaries are host-supplied aggregates ↔ deterministic classifier, application subject ↔ challenge and process-local replay state ↔ distributed deployment. ## TM-SEC-3 — authenticated local security-event journal **Assets:** normalized security-event evidence, HMAC keys, key identifiers, record order and durable local file state. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `SEC-33` forged, substituted or rotation-confused local event | Authenticate a domain-separated canonical frame containing sequence, key identifier, predecessor tag, payload length and exact normalized event bytes; reject absent/wrong keys, forgery, reordering and removed interior records before returning an event as HMAC-verified. | The exact negative proves payload tampering, same-identifier wrong-key substitution and absent historical-key rotation fail closed. Additional deterministic tests cover restart across active-key rotation, predecessor/sequence attacks, quotas, symlink targets, external length changes and redacted/zeroizing key storage. This is one local writer and a maximum of eight keys/4,096 records/16 MiB; trusted key/path custody, whole-tail rollback checkpoints, multi-writer coordination, compaction, retention, remote delivery and acknowledgement remain operator/application work. | Trust boundaries are local event producer ↔ authenticated journal, key manager ↔ rotation ring and filesystem state ↔ restart verifier. ## TM-PAY-1 — webhooks and billing boundaries **Assets:** provider secrets, event IDs, subscription state, amount/currency, invoice identity and replay state. v13 keeps the provider-neutral base, Stripe and the experimental InfinitePay adapter. Payout and NFS-e fiscal code left the framework, so their cases below are retired and claim no current evidence; applications that add custom gateways, payouts or fiscal integrations own those threat models. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `PAY-01` forged webhook | Verify the provider's exact signed bytes and algorithm cryptographically before parsing side effects. | Axum and Actix call the same verifier; Stripe signature negatives plus Actix body/event preservation prove the bounded adapters. The experimental InfinitePay adapter rejects live deliveries with `UnsupportedOperation` because no reviewed authentication contract exists; its `mock_*` verification is offline-only. Custom adapters must declare their verification mode, and the trait default fails closed. | | `PAY-02` replay/stale event | Enforce timestamp window and unique event ID in durable shared state before effects. | Freshness and bounded local replay are enforced before dispatch. The opt-in SQL ledger persists provider-scoped payload/event claims across SQLite/PostgreSQL/MySQL/MariaDB processes, fails closed on capacity/storage/profile errors and has restart/contention/four-protocol live evidence. Middleware claims before dispatch and is not exactly-once delivery; stable event IDs can instead share the caller's relational domain transaction, and a live MySQL/MariaDB/PostgreSQL case proves a caller transaction whose snapshot predates a concurrently committed claim is rejected. A custom adapter for a provider that signs only the body checks no delivery timestamp, so an exact captured body verifies again once its replay claim expires (24 hours by default, at most 30 days) or, with the process-local store, after a restart; hosts must order and deduplicate their state changes durably. | | `PAY-03` amount/owner substitution | Derive product/currency/owner from authenticated server state, never arbitrary client values. | Materialized SQLx/Turso billing scaffolds bind checkout to authenticated identity and deny cross-owner subscription reuse before customer binding; route/plan/provider review remains open. | | `PAY-04` duplicate/partial transition | Use a transactional idempotent state machine and reconcile with the provider. | `check_and_record_event_key_with_transaction` can bind one stable provider event ID to one mutation in the same supported relational database. Cross-system atomicity, provider reconciliation, automatic state-machine policy, and middleware crash recovery remain open. | | `PAY-05` payout destination takeover | Retired in v13. | The Wise payout adapter and payout contracts were removed; no current evidence is claimed. Applications that add payouts own step-up authentication, destination allowlists/change delays and independent audit. | | `PAY-06` false fiscal authorization | Retired in v13. | NFS-e preparation was removed from the framework (it may return as a separate product); no current evidence is claimed. Rullst receipts never represent tax authorization. | | `PAY-07` forged or confused SEFIN issuance response | Retired in v13. | Removed with the NFS-e module; no current evidence is claimed. | | `PAY-08` replayed or substituted direct charge | Require integer minor units, currency, authoritative provider customer/payment-method identity and a durable order idempotency key; bind the accepted response before side effects. | `ChargeRequest` validates and redacts the bounded inputs. Stripe forwards `Idempotency-Key`, confirms off-session and rejects responses with mismatched amount/currency or non-accepted status. Offline receipts carry the distinct non-success `Mock` status and other adapters fail unsupported. The host still owns mandate/SCA setup, identity authorization, durable key uniqueness, webhook reconciliation and entitlement state. | | `PAY-09` substituted or replayed paid invoice delivery | Bind the recipient, exact minor-unit total and currency to final non-mock payment evidence; expose a stable delivery identity and require durable claiming before retryable effects. | `PaidInvoice` rejects `Processing`/`Mock` and any e-mail, amount or currency mismatch. The opt-in Mail bridge renders bounded HTML/PDF, runs mandatory pre-flight and retains a stable delivery key. The host still owns authenticated order construction, webhook reconciliation, atomic outbox claiming, retry policy and provider acceptance; delivery is not exactly once. | | `PAY-10` duplicated or response-confused metered usage | Bind every accepted response to provider-specific customer/item, metric identity, quantity, timestamp/action and retry evidence; never infer missing provider fields from a uniform subscription ID. | Stripe Meter Events forward a bounded identifier and recheck customer/event/value/timestamp/identifier, cap response bytes, redact identities and keep mocks visibly non-live. A custom adapter without a provider event key reports `ApplicationOutboxRequired` so the application claims the key durably. Provider-account acceptance, configured aggregation, durable storage/retry and invoice reconciliation remain application/operational work. | Trust boundaries are provider ↔ webhook, user ↔ checkout, application ↔ provider and application ↔ durable billing state. ## TM-MAIL-1 — outbound content, suppressions and delivery telemetry **Assets:** recipients, message content and attachments, provider event identities, suppression state, tenant identity and operational observations. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `MAIL-01` executable, active or type-confused attachment leaves the process | Validate bounded metadata and recognizable signatures, reject active content and fail closed when authoritative inspection is unavailable. | The opt-in static `AttachmentInspectionGuard` completes inspection before the wrapped transport. Its strict local heuristic rejects executable magic, spoofed known types, active PDF/SVG, secrets and unsafe text links. The declared MIME type alone never selects the checks: it is compared case-insensitively and combined with the filename extension and content signature, so an active PDF, SVG, HTML or script file labelled `text/plain` or `application/xml` is rejected (SVG and XHTML are recognized by root element after any prologue or DOCTYPE and namespace prefix, and by namespace URI, as browsers parse them), and the strict policy rejects a declared type it does not inspect (such as `text/html` on `invoice.txt`) unless it is `application/octet-stream`; executable/script-host extensions are rejected under both policies. PDF `#xx` name escapes and compressed streams are not decoded. It is not antivirus, sandboxing, recursive archive inspection or CDR; production risk policy may require an independently operated scanner adapter. | | `MAIL-02` forged, replayed or lost suppression event permits unwanted delivery | Authenticate the provider event before mutation, bind provider/event/payload exactly, persist suppression durably and check it before transport. | The opt-in SQLite store provides exact replay conflict detection, monotonic manual/bounce/complaint state, immutable quotas and shared-local restart/two-instance evidence; `SuppressionGuard` fails closed on suppressed recipients or unavailable state. One recipient parser reduces `Name <address>` forms to the bare address for the pipeline, disposable check, verified feedback and store lookup, so a display name cannot miss a suppression entry; unparsable recipients are rejected. Rullst does not authenticate provider webhooks in this API, and only already-verified events may be recorded. Replay-ID retention must cover the provider window. The optional v13 PostgreSQL store shares state across hosts behind a monotonic namespace clock that tolerates at most 300 s of backwards host skew and fails closed beyond it. Multi-host replication, webhook adapters and provider-account acceptance remain open. | | `MAIL-03` message or recipient leaks through delivery telemetry | Emit only bounded low-cardinality outcomes and never recipient, subject, body, filename or provider response content. | `ObservedMailDriver` records provider label, outcome, elapsed time, attachment count and two booleans through a non-failing static sink. The bounded default sink is process-local; externally operated metrics/tracing export, retention and alerting remain deployment work. | | `MAIL-04` oversized or adversarial message content exhausts the sending executor | Bound scanned text before inspection and keep every content scan linear in its input. | The mandatory pipeline rejects a subject over 2 KiB or an HTML/text body over 2 MiB before scanning; link extraction deduplicates through a hash set and the redactors and URL search are single forward passes. A regression test runs marker-, link- and PEM-dense bodies at the limit under a deadline. Scans still run on the calling task, so hosts should also cap request bodies. | | `MAIL-05` a production misconfiguration silently selects the offline mock and loses mail | Select offline delivery only from explicit mock configuration and reject partial credentials. | SMTP selects its offline fixture only for an empty/`mock_*` host or a `mock_*` username/password; a real host without credentials is a real relay and a lone username or password is a `ConfigError`. REST providers keep the documented empty/`mock_*` credential convention, so deployments should assert `delivery_mode()` at startup. Without `MAIL_DRIVER` (process environment, then `.env`) or `[mail] driver`, the facade falls back to the metadata-only `log` driver only in development/test, resolved with the `Server` environment precedence; staging and production fail with `ConfigError`. A `MAIL_DRIVER` or `[mail] driver` naming a transport removed in v13 (`sendgrid`, `postmark`, `mailjet`, `mailtrap`, their sandbox values or `azure-acs`) fails with a `ConfigError` naming it instead of falling back, and a queued job then fails visibly and stays retryable. | Trust boundaries are authenticated application state ↔ mail message, application ↔ inspection/suppression adapters, verified provider event ↔ local suppression state and transport result ↔ observation sink. ## TM-MESSAGING-1 — durable messages, wire frames and correlation **Assets:** message payloads and headers, idempotency state, ACK leases, consumer progress, storage keys, broker routing metadata and trace correlation. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `MESSAGING-01` protected message content is recovered from a copied SQLite file | Offer an explicit authenticated-encryption profile, encrypt header values and payload before persistence, and fail startup on profile/key mismatch. | `connect_encrypted` uses AES-256-GCM with random nonces and a bounded keyring. A raw-database/restart regression proves the selected header and payload are absent while delivery round-trips. Topic, event/content type, IDs, timestamps, idempotency key, fingerprint and delivery state remain visible metadata; host key custody, permissions, backup and erasure remain required. | | `MESSAGING-02` ciphertext or metadata is copied, reordered or altered | Bind namespace, topic, sequence, message ID, event/content type, timestamp and rotation key ID into AAD; reject authentication failure before claiming the message. | The exact negative swaps two valid ciphertext rows and receives `StorageAuthenticationFailed`; probe tamper and wrong-key tests cover startup. AES-GCM does not prevent deletion, rollback of the complete database or availability loss, so protected backup/rollback detection remains deployment work. | | `MESSAGING-03` unsafe rotation silently makes retained records unreadable | Track every retained record's non-secret key ID, require every referenced prior key at startup and open one retained record per referenced key ID to prove its bytes, use only the primary key for new writes and permit removal only after old records are purged. | The rotation regression opens old+new, writes with the new primary, rejects new-only or a prior key with the right ID but wrong bytes while an old record remains, then permits new-only after terminal purge. External secret-manager rotation, escrow, recovery rehearsal and multi-host rollout remain operator work. | | `MESSAGING-04` malformed, oversized, cross-namespace or future-version wire frame enters broker state | Bound the frame before payload allocation, validate every field and canonical ordering, require exact namespace and reject unknown versions/trailing bytes. | A fixed byte digest freezes v1; exact negatives cover every truncation, wrong magic, unknown version, trailing data and namespace mismatch. The codec is only an envelope primitive, not a remote transport or provider protocol. | | `MESSAGING-05` attacker-controlled trace metadata leaks baggage or creates ambiguous correlation | Propagate only strictly validated W3C version-00 `traceparent` and a conservative `tracestate` subset; exclude baggage and redact diagnostics. | Grammar, duplicate-key, zero-ID, uppercase and malformed-member negatives are exact; an in-memory delivery proves only the two allowlisted headers. Sampling, tenant authorization, exporter security and retention remain host work. | | `MESSAGING-06` process stops after broker publication but before acknowledging the relational outbox | Commit domain state plus outbox row together, claim with a lease, publish the stream-scoped outbox event key as broker idempotency, then ACK the exact claim. Never call the two systems one atomic transaction. | The opt-in static `OrmOutboxRelay` maps one configured stream/topic, validates the claimed JSON and keeps payload/event/claim keys out of `Debug`. An exact crash-window regression publishes, lets the claim expire, republishes through a new claim, observes the broker's duplicate receipt and only then ACKs; one message exists. Worker supervision, retention, tenant authorization and destination idempotency remain application work. | Trust boundaries are publisher/consumer ↔ broker contract, process ↔ SQLite file/key manager, local envelope ↔ future remote adapter and untrusted correlation headers ↔ tracing infrastructure. ## TM-AI-1 — prompts, RAG and tool execution **Assets:** system prompts, tenant content, provider keys, retrieved data, tool credentials, destructive operations and audit evidence. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `AI-01` direct/indirect prompt injection | Treat prompts/retrieval as untrusted and never equate heuristic pass with safety. | The versioned `rullst-ai-guardrails-v1` offline corpus runs injection/jailbreak cases across all built-in transports; adaptive and live-model evals remain open. | | `AI-02` PII/secret exfiltration | Minimize/redact before dispatch and enforce tenant-aware retrieval. | The versioned offline corpus fixes exact implemented email/card/CPF/CNPJ redactions across built-in transports; CPF/CNPJ masking requires valid check digits and does not recognize alphanumeric CNPJs. `RagPipeline` guards/masks every selected passage, requires trusted tenant context and rejects mismatched tags; the application retriever must still apply authoritative datastore tenant/ownership predicates, and raw provider calls remain an explicit bypass boundary. | | `AI-03` unauthorized tool/arguments | Allowlist typed tools, validate schema and authorize as the initiating subject after selection. | Local registry dispatch requires exact allowlist, principal authorization, closed bounded JSON, call budget and audit; destructive/financial approvals are one-use and payload-bound. Provider-native selection loops, domain authorization, approver authentication and durable audit remain open. | | `AI-04` destructive autonomous action | Require human approval and idempotency for finance, deploy, deletion or privilege change. | Not yet a general framework guarantee. | | `AI-05` SSRF/egress exfiltration | Validate destinations, block private/link-local/metadata networks and cap redirects/content/time. | `EgressPolicy::strict()` denies all hosts until an exact allowlist is configured. The opt-in `EgressFetcher` resolves under deadline, validates every answer, pins them into a proxy-free client, verifies the peer, revalidates manual redirects and bounds declared/streamed bytes; deterministic negatives stop private/mixed DNS before transport. It does not automatically wrap provider/application clients, and tenant-aware destination authorization, response validation plus a successful live-origin redirect/stream contract remain open. | | `AI-06` unbounded cost/availability | Enforce body/token/time/concurrency budgets, cancellation and circuit breaking. | Built-in transports have configurable request deadlines and local tools have call/payload budgets; provider-neutral cancellation, concurrency limits and circuit breaking remain open. | | `AI-07` hallucinated structured result | Require schema and authoritative server-side validation; prose is never authorization. | Structured-output foundations exist. | | `AI-08` cross-tenant, reordered or incompletely erased chat memory | Select storage only from trusted tenant/conversation context, commit complete exchanges atomically, reject stale writers and erase the exact key. | The reusable stores bind tenant plus validated conversation ID and enforce bounded consecutive user/assistant pairs. The SQL adapter uses transactional revision CAS; its exact SQLite negative proves isolation, a single winner, deletion with foreign keys disabled and no orphaned messages, while live matrices cover PostgreSQL/MySQL/MariaDB protocols. MySQL/MariaDB key columns use `ascii_bin` and every tenant-scoped statement compares the key byte-exactly; the MySQL 8.0 and MariaDB matrices prove that tenant and conversation IDs differing only by case stay separate, that a legacy case-insensitive table fails closed and that the documented migration restores separation. Authentication/ownership inside a tenant, encryption, retention deadlines and backup erasure remain host policy. | Trust boundaries are content ↔ model context, application ↔ provider, model output ↔ tool dispatcher and retriever ↔ network/data sources. ## TM-IOT-1 — OTA manifest and device lifecycle **Assets:** provisioned public key, firmware image/hash, target, monotonic version, boot state and telemetry. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `IOT-01` forged/tampered manifest | Verify Ed25519 over canonical bounded bytes with the provisioned key. | Signature/tamper tests. | | `IOT-02` rollback | Require version above a persisted monotonic counter before boot. | In-process anti-rollback exists; persistent integration remains open. | | `IOT-03` wrong target/image | Verify target, declared length and cryptographic hash before flashing. | Manifest checks exist; downloader/flasher is roadmap. | | `IOT-04` power loss/partial flash | Use a recoverable A/B boot flow and commit counter only after verified boot. | The target bank is always opposite the running bank that platform code reports; a commit does not move the running bank, and a regression proves a second update before reboot never targets it. Hardware/bootloader integration remains open. | | `IOT-05` signing-key compromise | Use offline protected signing plus rotation/revocation. | Operational/HSM program remains open; simulators are not HSMs. | | `IOT-06` telemetry spoof/replay | Authenticate channel/device and bind identity plus sequence/time. | Transport identity/MQTT remains open. | Trust boundaries are signer ↔ distribution, distribution ↔ device, verified manifest ↔ flasher/bootloader and device ↔ backend. ## TM-ACADEMY-1 — education, assessment and games **Assets:** school and tenant membership, learner identity, protected content, enrollment/entitlement, lesson progress, submissions and grades, score events, leaderboards, rewards, automations, payment state, minors' data and administrative evidence. **Actors and trust boundaries:** learner, guardian, instructor, evaluator, moderator, support operator, school owner and platform administrator; browser or game client ↔ application; application ↔ school membership/database/cache/ queue/storage; application ↔ billing/media/notification provider; automation or AI output ↔ authorized domain command. The client is never authoritative for identity, entitlement, grade, score, reward or payment state. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `ACADEMY-01` forged learner, role or school context | Derive the subject and memberships from an authenticated server-side session or reviewed gateway; never trust form/header identity. | The LMS example (`examples/lms`, the former LMS starter) derives its numeric learner ID from the encrypted session cookie, requires that user to exist and binds it with a fixed `student` role into a server-created `UserContext`; it has no schools, memberships or tenant selector. Invite/provisioning, roles and the separate production identity lifecycle remain application work. | | `ACADEMY-02` cross-user lesson or progress access | Resolve the lesson and active enrollment, authorize the enrollment owner before returning media or writing progress, and deny before side effects. | The LMS example's (`examples/lms`, the former LMS starter) learning service authorizes the enrollment owner from `UserContext` before any database access, and its routes sit behind the authenticated middleware. The example's own test `security_tests::another_learners_lessons_are_refused` (`rullst-lms-example`) executes that owner boundary. | | `ACADEMY-03` unenrolled or expired content access | Check a current server-side entitlement on every protected lesson, download and media request; signed URLs must be short-lived and subject-bound. | **Retired in v13 (TM-13.0).** The full Academy blueprint that implemented this was removed; the LMS example has no enrollment release rules, expiry or signed media URLs. An application that adds them must implement and test this disposition. | | `ACADEMY-04` cross-school data leak | Scope reads, counts, exports, cache keys, jobs, files, search and telemetry by authenticated school membership and test non-interference. | **Retired in v13 (TM-13.0).** The full Academy blueprint that implemented this was removed; the LMS example has no schools, memberships or cohorts. An application that adds them must implement and test this disposition. | | `ACADEMY-05` assessment or grade tampering | Version authoritative questions/rubrics, enforce attempt/time limits server-side, grade trusted inputs and audit every override. | **Retired in v13 (TM-13.0).** The full Academy blueprint that implemented this was removed; the LMS example has no quizzes, assignments or grades. An application that adds them must implement and test this disposition. | | `ACADEMY-06` replayed, impossible or reordered score | Authenticate a versioned `ScoreEvent`, recompute or validate results, enforce unique attempt/deduplication keys and deterministic leaderboard ordering. | **Retired in v13 (TM-13.0).** The full Academy blueprint that implemented this was removed; the LMS example has no score events or leaderboards. An application that adds them must implement and test this disposition. | | `ACADEMY-07` duplicate or confused-deputy automation | Commit a transactional outbox with domain state, deliver at least once, make handlers idempotent and reauthorize sensitive actions as the recorded actor/tenant. | **Retired in v13 (TM-13.0).** The full Academy blueprint that implemented this was removed; the LMS example has no outbox-driven automation. An application that adds them must implement and test this disposition. | | `ACADEMY-08` admin, support or AI privilege escalation | Apply least privilege and separation of duties, require step-up/approval for sensitive mutations and treat AI output only as untrusted input to guarded tools. | **Retired in v13 (TM-13.0).** The full Academy blueprint that implemented this was removed; the LMS example has no publication review, role grants or an AI admin. An application that adds them must implement and test this disposition. | | `ACADEMY-09` malicious upload or active content | Enforce real type/size/name limits, isolate parsing, quarantine/scanning and distinct SVG/HTML/archive/document/media policies. | Core now exposes a bounded in-memory admission contract with a hard size ceiling and application allowlist, canonical tenant/name checks, exact declared MIME/extension versus recognized signature, active-text rejection (text whose first significant character after whitespace, byte-order marks or zero-width characters opens markup, including XML prologs, comments and doctypes, or that contains `javascript:`), tenant-prefixed randomized quarantine keys, SHA-256 binding, a static-dispatch scanner contract and fail-closed release. The deterministic scanner is explicitly mock-only. The exact negatives reject active SVG/HTML text, including BOM-, prolog- and comment-prefixed markup, traversal, invalid tenant and MIME/extension spoofing. Multipart streaming, remote S3/R2 persistence, sandboxed parsers/transcoding, archives and a production malware adapter remain open. | | `ACADEMY-10` minors' privacy or unsafe retention | Minimize collection, separate guardian consent when required, implement export/deletion/retention/anonymization and keep PII out of logs/telemetry. | **Retired in v13 (TM-13.0).** The full Academy blueprint that implemented this was removed; the LMS example has no age bands, guardian consent or retention workers. An application that adds them must implement and test this disposition. | | `ACADEMY-11` payment-to-entitlement substitution | Derive product, school, learner and amount from server state; apply verified idempotent provider events before granting or revoking entitlement. | Capital webhook foundations exist, but the LMS example has no billing-to-entitlement integration and makes no paid-course claim. | | `ACADEMY-12` availability, farming or multi-account abuse | Apply identity and origin budgets, replay controls, anomaly review and accessible rules without using a manipulable cache as source of truth. | The optional `redis-rate-limit` adapter provides atomic shared counters, bounded windows and hashed client keys; empty/`mock_*` configuration is visibly process-local and fails `require_distributed()`. The exact negative gate proves that fail-closed boundary, while a separate CI/release contract proves two independent clients consume one budget against digest-pinned Redis. Composition of the shared limiter into generated HTTP routes, cluster/failover and domain-specific farming/multi-account tests remain open. | **Academy negative minimum before a comparative security claim:** anonymous, cross-user, cross-course and cross-school content/progress requests; expired entitlement; assessment replay/time manipulation; duplicate/impossible score; automation redelivery; unauthorized grade/admin mutation; hostile upload; payment replay; and multiple-account abuse. The exact release-negative gate now exercises the LMS example's owner/cross-user boundary (`ACADEMY-02`), hostile upload admission (`ACADEMY-09`) and the explicit distributed-rate-limit startup boundary (`ACADEMY-12`). Cross-school, assessment and score cases were retired with the Academy scaffold; tenant isolation, Redis failover and domain-specific multi-account behavior remain outside that bounded evidence. ## TM-LABS-1 — trusted grading and the host-owned runner boundary **Assets:** learner source, hidden grader cases and expected answers, job status and grades, the encrypted job database and content key, the receipt signing seed, and the hosts that execute submissions. **Actors and trust boundaries:** learner and instructor ↔ application; application ↔ `rullst-labs` shared-local job store; application-owned runner controller ↔ job store and signing seed; controller ↔ untrusted isolated worker. `rullst-labs` executes nothing. The `rullst-labs-runner` candidate was removed from the 13.0 workspace, so every execution, isolation and teardown control below the controller belongs to the application's own runner, described by the [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html). | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `LABS-01` learner code runs in the web process or beside application secrets | Never execute in the HTTP process. The worker receives only bounded `WorkerInput` data, without identity, expected answers, keys, database, network or control sockets. | `check-labs-boundary.py` rejects an executor binary, an execution-engine dependency in any workspace member and a reintroduced runner crate. `one_lease_and_exact_trusted_grading_without_worker_answers_or_authoritative_pass_flag` checks the worker input. Worker isolation is host-owned and not verified by Rullst. | | `LABS-02` forged or self-awarded grade | Accept results only under the pinned controller key; grade worker values against stored answers on the trusted side; never accept a pass flag. | `pinned_receipt_key_exact_binding_and_teardown_control_experimental_grades` and `signed_mixed_outcomes_award_only_exact_answers_and_preserve_minimized_feedback`. Signing-seed custody is host-owned. | | `LABS-03` stale, replayed, cancelled or cross-job result | Bind receipts to the current lease nonce, revision and request/profile/source digests; refuse expired, cancelled or withdrawn work; treat an exact replay as an idempotent read. | `cancellation_fences_late_results_and_retention_waits_for_confirmed_cleanup`, `withdrawal_and_expiry_prevent_a_validly_signed_late_grade` and `failed_controller_fences_immediately_and_old_attempt_cannot_stop_a_retry`. | | `LABS-04` lost worker or uncertain teardown treated as success | Fence first; require a signed `WorkerLost` cleanup attestation with confirmed teardown; allow at most one deliberate retry with a fresh nonce. | `lost_worker_requires_cleanup_and_a_fresh_nonce_before_one_bounded_retry` and the `byo_runner_controller` example tests. Whether teardown actually happened is host-owned. | | `LABS-05` hidden cases or source disclosed | Encrypt source and graders at rest, redact Debug output, return keyed exercise digests and erase source at terminal states. | `source_and_grader_are_encrypted_and_record_index_tampering_is_detected`, `untrusted_source_and_expected_answers_are_bounded_and_not_debug_output` and `returned_views_never_carry_the_unkeyed_exercise_digest`. Runner-side workspaces, logs and backups are host-owned. | | `LABS-06` resource exhaustion through submissions | Bound source, cases and limits; enforce store-wide and per-learner job budgets; rate-limit `Submit` in host authorization; enforce CPU, memory, process, disk and output limits in the runner. | `one_learner_cannot_fill_the_store_wide_job_budget`, `submissions_that_could_never_be_claimed_are_refused` and `malformed_identity_and_expanded_execution_policy_are_rejected`. Runtime limits are host-owned. | **Host-owned runner boundary:** the removed candidate's [first-profile threat model](https://rullst.github.io/Rullst/book/labs-first-profile.html) and hosted evidence describe software Rullst no longer ships. An operator deploying a runner must extend this model with its own platform threats (sandbox and kernel escape, network and cloud-metadata reachability, cross-tenant caches, toolchain provenance, resource exhaustion and incomplete teardown), test the negatives on the deployed host and obtain independent review before any production hostile-code claim. ## TM-DEPLOY-1 — CLI, artifacts and release/deployment **Assets:** source, generated projects, registry token, release tag, `.crate` archives, evidence, deployment credentials and production target. | Abuse case | Required disposition | Repository evidence or remaining work | | --- | --- | --- | | `DEPLOY-01` tag/version/DAG mismatch | Match every package/internal requirement to the tag and publish topologically. | Machine-readable order and metadata preflight. | | `DEPLOY-02` artifact/source substitution | Package from tag, inspect/checksum/attest, reproduce in publish job and compare bytes. | Workflow gates exist; real RC run remains open. | | `DEPLOY-03` secret/local artifact leakage | Deny `.env*`, key/certificate patterns, unsafe paths and unexpected archives. | `.crate` audit gate. | | `DEPLOY-04` partial irreversible publish | Wait for index/checksum after each crate and resume only if the existing checksum matches. Never reuse a version. | Workflow exists; recovery runbook remains open. | | `DEPLOY-05` CLI command/path injection | Validate identifiers, paths and image names; avoid shell interpolation and propagate failures. | Generator injection/path tests. | | `DEPLOY-06` generated insecure default | Compile materialized matrices and assert production fails closed while local shortcuts are loopback debug-only. | Structural/representative blueprint matrices plus materialized auth, mail, chat and SQLx/Turso billing contracts exist; final RC/multi-OS evidence remains open. | | `DEPLOY-07` compromised dependency/action | Pin actions, lock dependencies, audit advisories/licenses/sources and govern expiring exceptions. | Policy exists; tag-bound result remains open. | Trust boundaries are contributor/CI ↔ repository, tag ↔ artifact, artifact ↔ registry and CLI ↔ filesystem/process/cloud. ## Review and change control - Changes to authentication, authorization, tenant resolution, webhooks, AI tools, OTA or release flow must reference at least one abuse-case ID in their test or review description. - New boundaries receive new IDs; IDs are never silently reused. - Closing residual risk requires code/configuration, a negative test and commit-bound evidence. Documentation alone cannot claim a control is deployed. - For every v13 release, maintainers must review TM-13.0 against the exact candidate, applications must add topology/provider threats and an independent reviewer must cover the highest-impact paths. --- # Security Assurance Case Source: https://rullst.github.io/Rullst/book/assurance-case.html # Security Assurance Case This page argues why Rullst's security requirements are met, and where they are not. It ties together the threat models, the trust boundaries, the design principles and the countermeasures for common weaknesses, and points to the code and tests behind each argument. Source paths are relative to the [repository root](https://github.com/Rullst/Rullst/tree/main); tests are named as `file::test_function`. It covers the framework crates, the `cargo rullst` CLI and the release pipeline. It is not a certification, a penetration test or a guarantee for any application built with Rullst. Applications own their data model, identity policy, deployment and providers, and must extend the threat model for them. ## Top-level claim Used as documented, Rullst's framework-owned controls counter the abuse cases named in its threat models, fail closed when they are missing or misconfigured, and ship in releases that users can verify. Every narrower claim below cites its mechanism and at least one negative test. The [limits](https://rullst.github.io/Rullst/book/assurance-case.html#limits) section lists what is not claimed. ## Threat model The [threat models](https://rullst.github.io/Rullst/book/threat-models.html) (model version TM-13.0; v12 keeps TM-12.10) use a lightweight STRIDE review. Each model separates untrusted network input, authenticated application identity, process-local state, shared durable state and third-party or hardware trust, and names abuse cases with stable IDs such as `AUTH-06` or `STUDIO-01`. There are 21 models: | Area | Models | | --- | --- | | Runtime and network identity | `TM-CORE-1` (readiness and drain), `TM-CORE-2` (client identity behind proxies) | | Identity | `TM-AUTH-1` (sessions, passwords, OAuth/OIDC, passkeys), `TM-CONNECT-1` (OAuth token generations) | | Administration and tooling | `TM-NEXUS-1` (admin CMS), `TM-STUDIO-1` (local control room), `TM-STUDIO-2` (production monitoring endpoint), `TM-STUDIO-3` (Studio remote projects) | | Data | `TM-TENANT-1` (multi-tenant access), `TM-ORM-1` (document recovery) | | Request defenses | `TM-SEC-1` (payload contracts), `TM-SEC-2` (anomaly and proof-of-work admission), `TM-SEC-3` (security-event journal) | | Integrations | `TM-PAY-1` (webhooks and billing), `TM-MAIL-1` (outbound mail), `TM-MESSAGING-1` (messages and wire frames), `TM-AI-1` (prompts, RAG, tools) | | Devices and education | `TM-IOT-1` (OTA and devices), `TM-ACADEMY-1`, `TM-LABS-1` | | Supply chain | `TM-DEPLOY-1` (CLI, artifacts, release and deployment) | The machine-readable minimum in [`.github/threat-model-release-minimum.json`](https://github.com/Rullst/Rullst/blob/main/.github/threat-model-release-minimum.json) binds 52 abuse-case IDs to 70 evidence rows and 69 exact tests across thirteen crates and the LMS example. `check-threat-model-release-minimum.sh` rejects missing markers, missing tests and filters that match zero tests, then runs each test with `--exact`. It runs in four `ci.yml` shards after every merge to `main` and nightly (pull requests to `main` skip it), and release admission requires those four shards to have passed on the exact tagged commit. Changes to authentication, authorization, tenant resolution, webhooks, AI tools, OTA or the release flow must reference an abuse-case ID (see [review and change control](https://rullst.github.io/Rullst/book/threat-models.html#review-and-change-control)). Related evidence: the historical [v12 security claims ledger](https://rullst.github.io/Rullst/book/v12-security-claims.html) lists each narrow v12 claim with its tests and known limits (for v13 this page and the threat models take its place), and the [external audit kit](https://rullst.github.io/Rullst/book/external-audit-kit.html) gives third-party reviewers the scope, a sample application and tooling. ## Trust boundaries | Boundary | Untrusted side | Enforced by | Model | | --- | --- | --- | --- | | Browser or client ↔ application | Every request, header, cookie, WebSocket upgrade and body | The [canonical production order](https://rullst.github.io/Rullst/book/security-architecture.html#canonical-production-preset) (`rullst-core/src/production.rs`): trusted-proxy policy, body limit, secure headers, CORS allowlist, WAF/RASP, CSRF, then application-owned session, authentication, tenant and authorization slots | `TM-AUTH-1`, `TM-SEC-1` | | Reverse proxy ↔ application | `Forwarded` and `X-Forwarded-For` | `TrustedProxyLayer` (`rullst-core/src/security/trusted_proxy/`): the direct socket peer is the identity until exact proxy hops are configured | `TM-CORE-2` | | Identity ↔ tenant and object | Route IDs and tenant claims | `RbacGuard::authorize_owner_or_role` and `UserContext` (`rullst-security/src/rbac/guard.rs`), `TenantContext` (`rullst-core/src/security/tenant_guard.rs`), ORM `tenant_column` scoping | `TM-TENANT-1` | | Application ↔ database | Values and identifiers that reach SQL | Bind parameters; identifier validation and operator allowlists in the query builder | `TM-ORM-1`, `TM-TENANT-1` | | Application ↔ external providers | Webhooks, OAuth/OIDC responses, AI output, provider URLs | Signature checks with constant-time comparison, replay stores, HTTPS-only provider URLs, deny-by-default egress | `TM-PAY-1`, `TM-MAIL-1`, `TM-CONNECT-1`, `TM-AI-1` | | Content ↔ model; model output ↔ tools | Retrieved documents and model replies | Prompt-injection filter, PII masking and guarded tool dispatch ([guarded local AI tools](https://rullst.github.io/Rullst/book/ai-tool-security.html)) | `TM-AI-1` | | Developer tools ↔ network | Remote access to Studio and debug endpoints | Debug-only, Development-only, loopback-only mounting; same-origin checks for state changes | `TM-STUDIO-1`, `TM-NEXUS-1` | | Monitoring client ↔ application | Requests to the opt-in `/_rullst/monitor/v1` endpoint | Off by default; bearer tokens compared in constant time or the application's authorizer; TLS evidence in staging and production; per-client rate limit and lockout; aggregates keyed by route pattern only (`rullst-core/src/monitor/`) | `TM-STUDIO-2` | | Firmware signer ↔ device | OTA manifests | Ed25519 manifest signatures and a caller-provided rollback counter (`rullst-iot/src/ota.rs`) | `TM-IOT-1` | | Contributor and CI ↔ repository; tag ↔ artifact ↔ registry | Pull requests, dependencies, build runners | Protected branches with required status checks and no bypass actors, pinned Actions, release admission of the exact SHA, attestations and reproducible archives ([verifying a release](https://github.com/Rullst/Rullst/blob/main/SECURITY.md#verifying-a-release)) | `TM-DEPLOY-1` | | CLI ↔ filesystem and processes | Project names, paths, templates | Identifier and path validation in generators; fixed programs with argument arrays | `TM-DEPLOY-1` | ## Secure design principles The argument follows Saltzer and Schroeder's principles. | Principle | How it is applied | Evidence | | --- | --- | --- | | Least privilege | Workflow tokens default to read-only access (`contents: read`). In the release workflow only `attest` may write attestations, only `publish` gets a registry identity, and only `github_release` may write repository contents. API tokens carry an exact scope list without wildcards or implied parent scopes. | [`release.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/release.yml), [scoped API tokens](https://rullst.github.io/Rullst/book/api-tokens.html) | | Fail-safe defaults | Optional subsystems are off by default (`default = []` in Core, Security and Auth). Production webhook state rejects `mock_*` verifiers. Documented placeholder `APP_KEY` values and weak keys are refused. The CORS scaffold fails on a missing or `*` origin. Image fetches for AI use a deny-by-default egress policy. | `rullst-capital/src/webhook.rs` (`WebhookMiddlewareState::production`), `rullst-auth/src/auth/app_key.rs`, [CORS advisory](https://rullst.github.io/Rullst/book/cors-scaffold-security-advisory.html), `rullst-ai/src/ai/egress.rs` | | Complete mediation | `Server` applies the security baseline (headers, CORS, WAF, CSRF) to the whole router in staging and production, not per route. Every parameterized route must carry a `// rullst-access:` classification, and `cargo rullst audit --idor` fails when one is missing. | `rullst-core/src/server/stack.rs`, `rullst-core/src/security/baseline.rs`, [access contract](https://rullst.github.io/Rullst/book/security-architecture.html#parameterized-route-access-contract) | | Economy of mechanism | Twenty focused crates with feature-gated optional parts; one escaping path for `html!`; one canonical middleware order; source files target 500 lines. Maturity tiers keep experimental code out of the core promise. | [maturity tiers](https://rullst.github.io/Rullst/book/maturity.html), `.github/check-crate-architecture.sh` | | Separation of privilege | Releases pass through separate jobs: admit the exact SHA, verify, attest without executing source, reproduce, then publish from a protected `crates-io` environment that requires a reviewer. Branch rules have no bypass actors. | `release.yml`, [governance](https://github.com/Rullst/Rullst/blob/main/GOVERNANCE.md) | | Least common mechanism | Tenant data is scoped per request through `TenantContext` and ORM tenant columns, with fail-closed checks, instead of one shared unscoped query path. Studio and the debug consoles are separate mechanisms, mounted only in debug Development builds and reachable only from loopback. | [spec, tenant scope contract](https://rullst.github.io/Rullst/book/spec.html), [threat models](https://rullst.github.io/Rullst/book/threat-models.html) | | Psychological acceptability | Secure behavior is the default path: `cargo rullst new` scaffolds the baseline, `cargo rullst doctor` reports a security baseline, `cargo rullst audit` checks routes, and `html!` escapes without extra calls. | [CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html), [which security layer to use](https://rullst.github.io/Rullst/book/security-layers.html) | | Open design | The source, threat models, claims ledger and audit kit are public. Security relies on keys and verified controls, not on secret designs. | [threat models](https://rullst.github.io/Rullst/book/threat-models.html), [v12 claims ledger](https://rullst.github.io/Rullst/book/v12-security-claims.html), [external audit kit](https://rullst.github.io/Rullst/book/external-audit-kit.html) | ## Countering common implementation weaknesses The table maps OWASP Top 10 and CWE Top 25 style weaknesses to the mechanism and to tests that fail if the mechanism regresses. | Weakness | Mechanism | Tests | Limits | | --- | --- | --- | --- | | SQL injection (CWE-89) | The ORM binds values as parameters. Column names pass `validate_identifier` (`rullst-orm/src/schema/validation.rs`); operators come from an allowlist. Nexus and Studio use `sanitize_identifier`. | `rullst-orm/src/schema/tests.rs::test_validate_identifier`, `rullst-orm/tests/builder_validation_test.rs::joins_and_vector_helpers_reject_dynamic_sql_fragments_that_are_not_safe`, `rullst-studio/src/data_browser/tests.rs::test_sanitize_identifier`; Kani proofs for `sanitize_identifier` | `where_raw`, `or_where_raw` and `select_raw` take caller-written SQL. | | Path traversal (CWE-22) and OS command injection (CWE-78) | `validate_relative_path` (`rullst-core/src/storage.rs`) and cloud `validate_key` reject `..`, absolute paths and control characters; static files accept only plain paths. The CLI runs fixed programs with argument arrays and validates names as Rust identifiers. | `rullst-core/src/storage.rs::relative_path_validation_rejects_escape_attempts`, `rullst-core/src/storage.rs::local_storage_rejects_symlink_escape`, `rullst-core/src/storage/cloud/tests.rs::every_cloud_operation_rejects_unsafe_or_ambiguous_keys`, `rullst-core/src/server/server_middleware.rs::only_plain_static_paths_are_probed` | A tenant prefix does not authorize access to every object in that tenant. | | Cross-site scripting (CWE-79) | `html!` (`rullst-macros/src/html_parser.rs`, attributes in `html_attributes.rs`) escapes every interpolation, including conditional `name?={value}` attributes, neutralizes `javascript:` and `vbscript:` URLs and rejects dynamic or conditional event-handler attributes at compile time. Nexus escapes stored values. A per-response CSP nonce is set by the header layer. | `rullst-core/src/html_macro_tests.rs::dynamic_url_attributes_neutralize_script_schemes`, `rullst-core/src/html_syntax_tests.rs::conditional_url_attributes_keep_the_scheme_check`, compile-fail fixtures `rullst-macros/tests/ui/dynamic_event_handler.rs` and `conditional_event_handler.rs`, `rullst-nexus/tests/renderer_escaping.rs::stored_cells_remain_text_including_entity_prefixes`, `rullst-security/src/headers.rs::nonce_is_available_to_renderer_and_matches_header` | `RawHtml`, custom escaping, JavaScript contexts and dynamic `style` values stay the caller's responsibility. | | Cross-site request forgery (CWE-352) and cross-site WebSocket hijacking | Double-submit cookie middleware (`rullst-core/src/security/csrf.rs`) with constant-time comparison and `Secure` cookies in production; an Origin allowlist for WebSocket upgrades (`rullst-security/src/cswsh.rs`). | `rullst-core/src/security/csrf_tests.rs::empty_or_ambiguous_csrf_proofs_do_not_reach_the_handler`, `rullst-core/src/security/csrf_tests.rs::production_like_environment_sets_secure_cookie`, `rullst-security/src/cswsh.rs::middleware_rejects_a_deceptive_localhost_origin` | The unsigned double-submit scheme depends on cookie isolation between sites; no blueprint mounts the CSWSH layer by default. | | Broken authentication and session management (CWE-287, CWE-916, CWE-307) | Argon2id password hashing off the async runtime, with a dummy verification against user enumeration (`rullst-auth/src/auth/password.rs`). Cookie sessions are AES-256-GCM encrypted and `HttpOnly`. `LoginGuard` adds progressive delay and temporary bans. Passkeys check presence and monotonic counters. Webhook signatures use constant-time comparison. | `rullst-auth/tests/integration_test.rs::test_argon2_password_hashing_and_verification_async`, `rullst-security/src/login_guard/capacity_tests.rs::identity_flood_cannot_disable_the_jail_for_a_later_identity`, `rullst-auth/src/auth/passkey/invariant_tests.rs::assertions_require_presence_verification_and_monotonic_counters`, `rullst-capital/tests/stripe_event_envelope.rs::invalid_signatures_are_rejected_and_mock_events_cannot_pass_real_mode_gate` | Cookie-session inventory and refresh remain open (`AUTH-06`). The login jail is process-local. | | Broken access control and IDOR (CWE-639, CWE-862, CWE-863) | `RbacGuard::authorize_owner_or_role` and its tenant variant, `#[require_role]`, ORM tenant scoping, and the route classification enforced by `cargo rullst audit --idor` on pull requests and releases. | `rullst-security/tests/security_tests.rs::parameterized_route_denies_cross_owner_access_before_the_handler_succeeds`, `rullst-security/src/rbac/guard.rs::tenant_authorization_never_allows_role_bypass`, `rullst/tests/rbac_macro.rs::role_attribute_preserves_arguments_and_denies_before_the_handler`, `rullst-orm/tests/builder_safety_audit_test.rs::tenant_scope_cannot_be_bypassed_by_or_filters`, `cargo-rullst/src/generators/audit_idor.rs::owner_routes_require_the_owner_or_role_guard` | The IDOR scanner is a heuristic; it cannot prove domain authorization correct. | | Server-side request forgery (CWE-918) | `EgressPolicy` (`rullst-ai/src/ai/egress.rs`) and outgoing webhooks resolve DNS once, pin the addresses and refuse private, special and mixed results. AI provider URLs must use HTTPS, or a literal loopback address for local models. | `rullst-ai/src/ai/egress.rs::strict_policy_rejects_ssrf_destination_forms`, `rullst-ai/src/ai/egress_fetch.rs::fetcher_rejects_private_or_mixed_dns_before_transport`, `rullst-messaging/src/webhooks/transport.rs::private_special_transition_and_mixed_dns_addresses_are_denied` | OIDC discovery requires HTTPS but does not block private addresses. These checks do not replace a network firewall. | | Unsafe deserialization and resource exhaustion (CWE-502, CWE-400, CWE-770) | Input is parsed into typed serde structures, never into executable objects. `ValidatedJson` and `ValidatedForm` add validation (`rullst-core/src/validation.rs`). The JSON schema guard caps payloads at 2 MiB and nesting at depth 32 and rejects duplicate keys. 42 fuzz targets cover parsers and protocol inputs. | `rullst-security/src/schema_guard/tests.rs::middleware_rejects_deep_and_oversized_json`, `rullst-core/src/validation_tests.rs::extraction_failures_keep_their_413_and_415_status`; targets in [`.github/fuzz-targets.json`](https://github.com/Rullst/Rullst/blob/main/.github/fuzz-targets.json) | Upload routes need an explicit body limit; fuzz campaigns run on demand, not per pull request. | | Memory-safety errors (CWE-787, CWE-416, CWE-125) | Rust's ownership rules. `unsafe-policy.yml` compiles the workspace with `-D unsafe-code` and allows `unsafe` only in a fixed list of five OS-integration files (Windows ACLs, Unix permissions, the development dynamic-library loader and process telemetry). Four crates use `#![forbid(unsafe_code)]`. | Nightly AddressSanitizer and ThreadSanitizer (`sanitizers.yml`), Miri (`miri.yml`), 22 Kani harnesses (`kani.yml`) | Miri, Kani and fuzzing are started by hand. The allowlisted files hold about 50 `unsafe` blocks, mostly Windows FFI that Linux sanitizers do not exercise. | | Secrets exposure (CWE-798, CWE-312, CWE-532) | Keys come from the environment or configuration, never from source. `VaultSecret` zeroizes on drop and redacts `Debug`. `FieldEncryptor` and ORM field encryption use AES-256-GCM with key IDs for rotation. The DLP layer masks a bounded set of key formats in responses. TruffleHog scans the repository. | `rullst-security/src/vault/tests.rs::test_vault_secret_redaction`, `rullst-security/src/vault/tests.rs::supports_key_rotation_with_envelope_key_ids`, `rullst-auth/tests/app_key_resolution.rs::documented_placeholder_app_keys_are_rejected`, `rullst-auth/tests/scaffold_app_key.rs::public_env_example_key_cannot_encrypt_or_decrypt_sessions`, `rullst-core/src/validation_tests.rs::display_and_debug_never_contain_the_rejected_values` | DLP recognizes only listed formats (not JWTs or generic API tokens). Applications that used public example keys must rotate them. | | Weak cryptography and certificate validation (CWE-327, CWE-295) | AES-256-GCM, HMAC-SHA256, Ed25519, ECDSA P-256 and Argon2id. TLS uses rustls only (TLS 1.2 and 1.3); no first-party code disables certificate verification, and remote PostgreSQL requires `sslmode=verify-full`. | `rullst-auth/tests/recovery_contract.rs::postgres_urls_use_the_hardened_connection_policy_before_connecting`; CodeQL and `cargo audit` after every push to `main` | HMAC-SHA1 is used only for TOTP, as RFC 6238 and authenticator apps require. JWTs support HS256 only. | Security headers add defense in depth: HSTS, a nonce-based CSP, `X-Frame-Options: DENY`, `nosniff`, COOP, COEP, CORP and a Permissions-Policy (`rullst-core/src/security/headers.rs`, tested by `rullst-core/src/security/headers_tests.rs::unmarked_responses_get_every_default`). ## How the argument is kept true - **Per change:** formatting, Clippy with `-D warnings`, the Linux `workspace` and `cli-standard` test shards, `cargo audit`, `cargo deny`, the unsafe policy, the zero-panic Clippy gates, the crate-archive reproducibility check and a mutation check of the changed Rust lines (`mutants-pr.yml`) run on pull requests to `main`. The push after a merge to `main` adds every Linux shard, the threat-model release minimum, coverage, SemVer checks and CodeQL, and the nightly run covers Linux, macOS and Windows. CodeQL analyzes Rust and JavaScript/TypeScript after every push to `main` and weekly. - **Coverage:** line coverage must stay at or above 90% (`coverage.yml`, after every push to `main`); `main` measured 93.96% on 29 October 2026 ([run 40865935687](https://github.com/Rullst/Rullst/actions/runs/40865935687)). - **Scheduled and on demand:** nightly sanitizers, and manual fuzzing, Miri, Kani and mutation campaigns. See the [workflow guide](https://rullst.github.io/Rullst/book/workflows.html). - **Releases:** the exact tagged SHA must pass admission; artifacts are attested with Sigstore and archives are reproduced before publication. See [verifying a release](https://github.com/Rullst/Rullst/blob/main/SECURITY.md#verifying-a-release) and [reproducible crate archives](https://rullst.github.io/Rullst/book/reproducible-builds.html). ## Limits - **Application-owned controls.** Session validation, authentication, tenant membership and object authorization have slots in the canonical order, but the application must mount them. The framework cannot infer those policies. - **Open abuse cases.** The threat models mark items such as `AUTH-06` (cookie-session inventory and refresh) as open. A passing release minimum does not close every case. - **Heuristic layers.** WAF, RASP, DLP and the IDOR scanner reduce risk but can be bypassed or miss findings. They do not replace typed parsing, parameterized SQL or authorization. - **Single-instance state.** Login jails, replay caches, rate limits and audit buffers are process-local unless a shared backend is configured. - **Defaults depend on environment.** The Core security baseline applies in staging and production. Development builds and custom routers must not be exposed publicly. - **Analysis cadence.** Fuzzing, Miri and Kani run on demand, so a regression between campaigns is caught by tests and sanitizers, not by those tools. - **Review.** One active maintainer reviews changes. AI-assisted review and automated checks are not independent human review, and no external audit has been completed. - **Native binaries.** CLI executables are attested but not claimed to be bit-for-bit reproducible. --- # v12 Security Claims & Evidence Source: https://rullst.github.io/Rullst/book/v12-security-claims.html # v12 security claims and evidence ledger > **Historical (v12).** This ledger records the claims of the v12 release > line, which reached end of life on 16 October 2026; it is not updated for > v13. For v13, see the [security assurance case](https://rullst.github.io/Rullst/book/assurance-case.html), the > [threat models](https://rullst.github.io/Rullst/book/threat-models.html) (TM-13.0) and the machine-readable > `.github/threat-model-release-minimum.json`. This page is the canonical boundary for security claims made by the Rullst v12 release notes. Each row names the narrow behavior implemented in the repository, the tests that exercise it, and a known limit. A security statement that is not represented here is not a v12 release guarantee. The evidence links point to source-controlled tests; links to files that v13 removed point to the `v12.1.2` tag. The other links open the current `main` source, which may have changed since v12; the frozen `v12` branch holds the code these claims describe. A passing local run proves only that checkout and environment; the release workflow attaches the same checks to the immutable tag SHA. None of these rows is a certification, external audit, proof of complete OWASP coverage, or substitute for an application's own threat model. v13 removed the Paddle, Lemon Squeezy, Polar, Razorpay, Mercado Pago, Alipay, Coinbase Commerce, PicPay and Wise adapters, the payout contracts and the NFS-e preparation from `rullst-capital`, and `make:mail-invoice` now generates a receipt-only template. Rows that describe those surfaces remain v12 claims and link to the `v12.1.2` tag; see the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html). | Narrow v12 claim | Implementation | Verification | Known boundary | | --- | --- | --- | --- | | The optional Connect Axum session flow binds one authorization callback to ten-minute state + PKCE and, for OIDC, nonce held in `tower-sessions`; it removes and immediately saves the challenge before validation and does not print its URL, code, nonce or verifier through managed `Debug` implementations. | [Managed session flow](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/extractors/session.rs) and [constant-time callback state comparison](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/extractors.rs) | [Round-trip, sequential replay, expiry, mismatch, missing-state, replacement and redaction negatives](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/extractors/session/tests.rs), promoted to `TM-AUTH-04` in the release minimum | One challenge per browser session invalidates an earlier concurrent tab. The generic session-store contract is not a distributed compare-and-delete across requests that already loaded the same record. The host still owns durable/shared session storage, Secure/HttpOnly/SameSite cookie and TLS/proxy policy, redirect registration, post-login rotation, idempotent account linking/recovery and live-provider conformance. | | The explicitly mounted Connect Mock IdP supplies a bounded signed OIDC loopback fixture: exact configured HTTP-loopback issuer/client/callback, expiring one-shot code, optional S256 PKCE and nonce, EdDSA ID token/JWKS and issued-bearer userinfo. | [Concrete router/state](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/mock_idp/handlers.rs), [validated configuration](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/mock_idp/mod.rs) and [fixture signer](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/mock_idp/signing.rs) | [Real `OidcProvider` loopback discovery/exchange/signature/replay test and local negative matrix](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/mock_idp/tests.rs), plus [router integration](https://github.com/Rullst/Rullst/blob/main/rullst-connect/tests/integration_tests.rs) | The signing seed and credentials are public deterministic fixtures. The state is bounded and process-local. This is not safe for public exposure and does not implement interactive login/consent, refresh/device/federation lifecycle, durable state, key rotation or OIDC conformance. | | Nexus requires an explicit access policy and rejects an unverified peer; registered CRUD metadata and submitted semantic fields also fail closed before dynamic SQL. An opt-in registered text tenant column scopes every built-in read/mutation/batch path to a trusted Core `TenantContext`, and required mutation audit couples one minimized record to the data transaction. | [`NexusAuthPolicy`](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/src/nexus/access.rs), [registry validation](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/src/nexus/metadata.rs), [bounded form validation](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/src/nexus/crud/input.rs), [scoped mutations](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/src/nexus/crud/mutation.rs), [scoped batch](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/src/nexus/crud/batch.rs) and [audit schema/API](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/src/nexus/audit.rs) | [Access-policy unit tests](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/src/nexus/access/tests.rs), [metadata negatives](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/src/nexus/metadata.rs), [semantic validation negatives](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/src/nexus/crud/input.rs), [HTTP widget/parameter-pollution regression](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/tests/semantic_widgets.rs), and [materialized cross-tenant/audit atomicity regression](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/tests/tenant_audit.rs), promoted as `TM-NEXUS-02`/`TM-NEXUS-06` through portable release-minimum tests | Loopback convenience is debug-only and Basic Auth requires verified TLS. Unscoped models remain global. The application owns identity/membership/domain policy, within-tenant ownership, custom-route authorization, database schema/privileges and audit export. The built-in audit table is same-database mutable committed-event evidence, not denied-attempt, append-only, tamper-evident, retention, backup, replication or external-review evidence; generated create keys may be absent. | | Studio's built-in local capability accepts only a verified loopback peer and local `Host` in debug builds, and unsafe methods require a same-origin `Origin`. | [`LocalStudioAccess`](https://github.com/Rullst/Rullst/blob/main/rullst-studio/src/access.rs) | [Inline loopback, remote-peer, missing-peer, Host and Origin negative tests](https://github.com/Rullst/Rullst/blob/main/rullst-studio/src/access.rs), plus [router integration tests](https://github.com/Rullst/Rullst/blob/main/rullst-studio/tests/studio_integration_tests.rs) | This bounds the supported local browser surface against direct remote access, DNS rebinding and cross-origin mutation. Shared or release deployments still need application-owned authentication, administrator authorization, TLS and network policy; Studio does not supply them. | | Newly generated CORS middleware fails closed without a valid explicit origin allowlist. | [CORS scaffold template](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/cors_middleware.rs.template) | [Generated-middleware HTTP tests](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/tests/cors_scaffold_tests.rs) | Previously generated applications are copied code and are not rewritten by a CLI upgrade. Browser/proxy behavior must still be tested in the deployed application. | | Newly generated fiscal-receipt mail keeps an `OfflineMock` visibly unauthorized, rejects contradictory fiscal markers, and both fiscal/dunning action links require credential-free HTTP(S) URLs. | [Extended mail scaffold templates](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/generators/mail/extended.rs), [`validate_action_url`](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/pipeline.rs), and [CLI dispatch](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/cli/dispatch.rs) | [Materialized seven-template Clippy/runtime/provenance/link/collision contract](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/tests/mail_scaffold_cli.rs) and [action-URL unit negatives](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/pipeline.rs) | Generated source cannot prove an application-supplied response truthful, authorize tax documents, calculate due state, schedule delivery, pause access, or define billing policy. Previously generated source is not rewritten. | | The opt-in Mail inspection guard completes every configured static inspection before transport and fails closed on scanner unavailability. Its strict local policy rejects executable magic, spoofed recognized types, active PDF/SVG, recognized secrets and unsafe links. | [Inspector contract and guard](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/inspection.rs) | [Safe-shape and active/type-confusion negatives](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/inspection/tests.rs), promoted to `TM-MAIL-01` in the release minimum | The local policy is a bounded heuristic, not antivirus, sandbox execution, recursive archive inspection or CDR. Hosts that require authoritative scanning must provide and operate a production `AttachmentInspector`. | | The opt-in Mail suppression guard checks a recipient before transport and fails closed when suppression state is unavailable. The SQLite store binds provider/event/payload replay exactly, escalates manual/hard-bounce/spam-complaint state monotonically and enforces immutable quotas across restart and competing local instances. | [Suppression domain and guard](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/suppression/mod.rs) and [SQLite store](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/suppression/sqlite.rs) | [Process-local contracts](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/suppression/tests.rs) and [restart, two-instance quota, conflict, pruning, target and corruption negatives](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/suppression/sqlite/tests.rs), promoted to `TM-MAIL-02` in the release minimum | `SuppressionEvent` does not authenticate a webhook. Only already verified provider events may be recorded. Replay retention must cover provider redelivery; file protection/encryption/backups, multi-host replication and provider conformance remain external. | | Mail terminal observations exclude recipient, subject, body, filenames and provider response content. The default sink is bounded and exposes eviction count, while observer failure cannot turn an accepted delivery into a retryable error. | [Observation contract and wrapper](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/observability.rs) | [Outcome, minimization, capacity, tenant and label regressions](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/observability/tests.rs), promoted to `TM-MAIL-03` in the release minimum | The default sink is process-local. External tracing/metrics delivery, retention, correlation and alerting remain deployment work; a custom sink must not block or panic. | | Newly generated billing code selects SQLx or Turso-primary persistence, derives checkout/portal identity from an authenticated extension, requires an explicit production plan allowlist before persistence, accepts webhook events only through the mandatory verifier layer, refuses existing outputs and rejects cross-owner subscription reuse before binding the conflicting customer. | [Billing generator](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/billing.rs) and [generated controller](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/billing_controller.rs.template) | [Materialized SQLite/Turso Clippy, migration, persistence, ownership and collision contract](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/tests/billing_scaffold_cli.rs), promoted under `TM-DEPLOY-06` | Generated code is application code. The host must configure correct provider plan IDs, mount auth and exact webhook middleware, add durable provider-event idempotency/reconciliation, validate a provider sandbox and review previously generated source separately. | | Core CSRF middleware uses a double-submit check, only exact configured webhook exemptions, and request-idempotent nested composition so an application layer wrapped by the production `Server` baseline emits one matching token/cookie pair. | [CSRF middleware](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/security/csrf.rs) | [CSRF unit tests, including the nested-layer GET/POST regression](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/security/csrf.rs), and [application integration tests](https://github.com/Rullst/Rullst/blob/main/rullst/tests/security_tests.rs) | Safe deployment still depends on HTTPS, cookie configuration, application authentication, and a correct exact-path exemption list. Request-body inspection is bounded; idempotence does not make the double-submit cookie an authentication credential. | | Core WAF inspects and preserves supported bounded request bodies and rejects bodies outside its one-megabyte inspection envelope. | [WAF middleware](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/security/waf.rs) | [WAF body and query tests](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/security/tests.rs) | Detection is pattern-based, has false positives and negatives, and is not complete OWASP coverage. Streaming or other uninspected representations require application controls. | | Parameterized routes recognized by the CLI audit require an adjacent public/owner/role/admin classification; owner paths require the recognized ownership guard. | [Bounded route source audit](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/audit_idor.rs) and [`RbacGuard`](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/rbac/guard.rs) | [Audit negatives](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/audit_idor.rs), [scaffold contracts](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/tests/scaffold_contracts.rs), and [HTTP owner/cross-owner test](https://github.com/Rullst/Rullst/blob/main/rullst-security/tests/security_tests.rs) | Static analysis recognizes bounded syntax and guard names; it cannot prove the correctness of an application's identity source, object lookup, or business authorization. | | Encrypted session tokens are versioned, authenticated with AES-256-GCM, expire, and reject malformed or weak-key inputs. | [Session implementation](https://github.com/Rullst/Rullst/blob/main/rullst-auth/src/auth/session.rs) | [Session negative and round-trip tests](https://github.com/Rullst/Rullst/blob/main/rullst-auth/src/auth/tests.rs) and [integration tests](https://github.com/Rullst/Rullst/blob/main/rullst-auth/tests/integration_test.rs) | Application-key custody, rotation, session/device revocation, and distributed logout are application or roadmap responsibilities. | | The optional application JWT policy binds versioned access claims to issuer, audience, subject, times, token ID and session version; it bounds TTL/scopes and supports strong `kid` key rotation. The SQLite profile serializes bounded JTI/session-version revocation shared by local processes and verifies it asynchronously. | [JWT policy](https://github.com/Rullst/Rullst/blob/main/rullst-auth/src/jwt.rs) and [SQLite revocation store](https://github.com/Rullst/Rullst/blob/main/rullst-auth/src/jwt/sqlite.rs) | [Rotation, revocation, audience, configuration and production fail-closed tests](https://github.com/Rullst/Rullst/blob/main/rullst-auth/src/jwt/tests.rs), plus [restart, exact quota, two-instance and corruption evidence](https://github.com/Rullst/Rullst/blob/main/rullst-auth/tests/sqlite_jwt_revocation.rs) | The bundled in-memory store remains process-local and is rejected by production mode. SQLite is one shared local file, not multi-host replication, secret management, backup/availability or a refresh-token workflow. Third-party OIDC verification belongs to Connect. | | MFA recovery helpers generate subject-bound 80-bit codes and persist only salted HMAC-SHA256 verifiers; verification is constant-time and a successful in-memory consume removes one verifier. | [Recovery-code implementation](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/recovery_codes.rs) | [Subject binding, single-use, plaintext-storage, format, pepper and tamper tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/recovery_codes.rs) | The application must render plaintext once, protect the pepper, persist compare-and-delete transactionally, rate-limit attempts and audit enrollment/recovery. This is not a complete account-recovery workflow. | | TOTP enrollment uses 160 bits from the OS RNG, rejects weak/invalid inputs, compares exactly six ASCII digits in constant time and can render a bounded SVG QR. | [TOTP and QR implementation](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/mfa.rs) | [Secret, code, drift, invalid-input and real-SVG tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/mfa.rs) | The application must encrypt the TOTP secret, protect the enrollment ceremony, confirm a code before activation, rate-limit verification and own account recovery. | | The optional Redis rate limiter hashes client-derived keys and atomically shares a bounded fixed-window counter across independent clients. Empty or `mock_*` configuration is explicitly process-local and rejected by the production boundary. | [Redis rate limiter](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/rate_limit/redis.rs) | [Offline fail-closed regression](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/rate_limit/redis.rs), [live independent-client contract](https://github.com/Rullst/Rullst/blob/main/rullst-security/tests/redis_rate_limit_live.rs), and digest-pinned services in [CI](https://github.com/Rullst/Rullst/blob/main/.github/workflows/ci.yml), which [release](https://github.com/Rullst/Rullst/blob/main/.github/workflows/release.yml) admission requires at the exact SHA | A single Redis service does not prove cluster/failover behavior, Academy HTTP identity/origin composition, or an operational abuse policy. | | Core can bind its storage, cache and in-process realtime/presence facades to a tenant selected by authenticated membership. `TenantStorage` applies an immutable `tenants/<tenant>/` object root after strict path validation; `TenantCache` validates logical keys, applies an immutable backend namespace and exposes no global flush; `TenantRealtime`/`TenantPresence` validate logical names, cap message payloads at 64 KiB and namespace rooms. | [`TenantStorage`](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/storage/tenant.rs), [`TenantCache`](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/cache/tenant.rs), [realtime wrappers](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/realtime.rs) and [`TenantContext`](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/security/tenant_guard.rs) | Exact same-logical-name cross-tenant non-interference tests for [storage](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/storage/tenant.rs), [cache](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/cache/tenant.rs), broadcast and presence rooms in [realtime](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/realtime.rs) (`TM-TENANT-04`) | These prove local wrapper contracts, not room-level authorization, distributed realtime/presence liveness, remote bucket policy, search/metrics/export isolation or automatic use by Academy/application routes. Redis cluster/failover remains separate evidence. | | The LMS starter persists schools, active/default memberships, course scopes, cohorts and temporal entitlements. Authentication treats `X-School-ID` only as a selector against persisted membership, binds the validated tenant to `UserContext`, and loads roles for that school. Learning, publication/rollback, assignment grading/correction, score correction/leaderboard, completion/certificate mutation, role lifecycle and scheduled publication enforce the selected school. Outbox, derived automation and notification state preserve `school_id`; the leaderboard cache is tenant-scoped, payload-validated and invalidated after authoritative mutations. | [Tenancy models/migration/service](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/tenancy/mod.rs), [authentication and learning composition](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/access.rs), [leaderboard/cache service](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/score.rs), and [tenant-aware RBAC](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/rbac/guard.rs) | [Materialized arbitrary/ambiguous selection, entitlement, cache/invalidation, same-user notification/preference separation, foreign-rule isolation, cross-school HTTP enrollment/leaderboard and foreign-admin mutation/scheduler/role non-interference](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_tenancy_tests.rs), plus [exact tenant guard tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/rbac/guard.rs) | This is bounded SQLite database/HTTP/local-cache evidence, not complete multi-tenancy. Other caches, files, search, metrics, exports, Nexus, invite/provisioning, distributed cache/failover and PostgreSQL/MySQL non-interference remain open. | | The Academy scaffold provides a minimized, school-scoped privacy lifecycle foundation: age band instead of birth date, versioned bounded retention, purpose-specific guardian consent/revocation and idempotent export/delete request records. Minor authorization fails closed without matching active consent. A bounded admin/owner sweep schedules replay-safe delete requests; fulfillment adds exact leased claims, abandoned-claim recovery, delayed retry/dead-letter, a hard ten-attempt claim ceiling and actor/SHA-256-bound completion. A supervised static-dispatch executor applies an adapter timeout shorter than the lease, explicit shutdown and local metrics; its deterministic mock never exports, deletes or anonymizes data. | [Privacy migration and service templates](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/privacy/mod.rs), [retention scheduler](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/privacy/retention.rs), [request worker protocol](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/privacy/worker.rs), and [supervised executor boundary](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/privacy/executor.rs) | [Materialized consent, cross-school, stale-token, lease-recovery, retry/dead-letter, hard-limit, completion-digest, supervised-success and adapter-failure regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_privacy_tests.rs) (`TM-ACADEMY-10`) | No product adapter that performs cross-table export/deletion/anonymization, legal-guardian verification, PII-safe application telemetry proof or legal compliance claim is provided. | | Core's storage-agnostic upload admission contract enforces a hard in-memory size ceiling and kind allowlist, validates canonical tenant/name and exact declared MIME/extension against recognized signatures, denies recognized active text, emits randomized tenant-prefixed quarantine keys and a SHA-256 digest, and fails closed unless a scanner adapter supplies a valid clean verdict. | [Upload admission/quarantine contract](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/uploads.rs) | Exact unit negatives for active SVG/HTML text (including BOM-, XML-prolog- and comment-prefixed markup), traversal, tenant injection and MIME/extension spoofing, plus digest-bound clean/infected/unavailable mock scans in the same module | This is not an HTTP multipart implementation, deep parser, S3/R2 mover, transcoder or production malware scanner. PDF/media checks are bounded signatures, the bundled scanner is mock-only, and archives/SVG/HTML remain denied or outside the allowlist. | | The LMS outbox commits score, first lesson completion, enrollment, achievement, publication, editorial rollback, assignment submission/grading/correction, course completion and certificate revocation events with domain state, claims an available event under a bounded lease, binds acknowledgement/failure to an exact token, schedules bounded retry, recovers expired claims, counts attempts and dead-letters at a bound. Its supervised database worker validates each supported envelope, loads current rules, rederives/applies the sole achievement action and ACKs/fails the exact claim; configuration, shutdown, safe drop and local counters are explicit. | [Generated score/outbox templates](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/score.rs), [assignment services](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/assignments.rs), [progress/completion services](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/progress.rs), [claim service](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/outbox.rs), [worker](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/automation_worker.rs), [closed passive envelopes](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/automation_worker_events.rs), and [executor](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/automation_execution.rs) | [Materialized SQLite supervised worker/FIFO delivery, assignment/rollback/completion/revocation delivery, progress, score, backoff, expired-claim/stale-token, ACK-loss redelivery, transactional achievement and poison dead-letter regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_schema.rs) | No external transport or exported durable telemetry is built in. Only `score_recorded → award_achievement` currently drives an automation mutation; tenant policy and approval envelopes are required before adding sensitive actions. | | The LMS starter grades single-choice quizzes from server-side question/option state, binds replay to attempt/quiz/learner/ruleset, enforces the configured attempt count, and commits immutable answers, a `ScoreEvent`, the authoritative leaderboard update, `score_recorded` and `quiz_graded` in one transaction. Timed quizzes require a persisted server-time start/expiry session and server-random question/option presentation; replay cannot extend/change either, starting consumes an attempt and grading rejects an ID set changed under the same ruleset. Authenticated start/submit routes derive subject/quiz from session/path and never accept client points. | [Generated grading](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/assessment.rs), [timing ceremony](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/assessment_timing.rs), [HTTP boundary](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/assessment_controller.rs) and [schema/models](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_schema.rs) | [Materialized SQLite correct/incorrect grade, start/order replay/expiry, cross-user, unknown-option, attempt-limit, score-event/outbox counts and leaderboard regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_schema_tests.rs) | Quiz grade override, browser UI and multi-database contention proof remain open; distributed clock and concurrent-start behavior remain deployment/test boundaries. | | The complete LMS starter's owner-only activity routes accept only an idempotency key plus one selected option, a complete bounded pair-ID permutation or bounded typed text. Static-dispatch single-choice/matching/typed evaluators and opaque `ValidatedActivityResult` keep identity, answers, points, policy, evidence and time server-owned. Typed replay uses a policy-bound SHA-256 digest rather than raw text. A transaction-locked recheck binds exact evaluator configuration and atomically stores the exact-replay attempt, appends `ScoreEvent` v2, updates the leaderboard and emits strict `score_recorded` v2. Attempt identity is learner/activity-scoped and event identity is server-derived. | [Activity evaluation/HTTP templates](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/activity_contract.rs), [matching evaluator/route](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/activity_contract/matching.rs), [typed evaluator/route](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/activity_contract/typed.rs), [persisted bridge](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/score/activity.rs), [replay gate](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/score/replay.rs), [score service](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/score.rs) and [schema](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_schema.rs) | [Materialized SQLite HTTP owner/cross-user, actor/evidence/policy mismatch, malformed pair/text input, normalized/conflicting replay, raw-text absence, durable attempt, score/outbox/leaderboard and downstream automation regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_schema_tests.rs) | Typed comparison does not provide Unicode normalization/accent/fuzzy semantics and its digest may remain personal data. Listening/game evaluators, quiz-path unification and PostgreSQL/MySQL contention proof remain open. | | Enabled Academy activities update a versioned deterministic review schedule in the same transaction as a newly applied authoritative score. Policy/state validation and algorithm drift fail closed; exact replay returns before the schedule transition. The bounded owner-only due endpoint derives learner/time server-side and rechecks school membership, course scope and active enrollment. | [Review policy/state/service and HTTP templates](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/score/review.rs), [score transaction](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/score.rs), [routing](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/routes.rs) and [schema](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_schema.rs) | [Pure deterministic pass/lapse transition test](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/score/review.rs) and [materialized SQLite three-activity persistence, replay, future queue and cross-user boundary](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_completion_tests.rs) | `rullst-box-v1` is an inspectable bounded heuristic, not FSRS/SM-2 compatibility, educational efficacy, AI personalization or generated pedagogy. Algorithm migration/experimentation, complete privacy integration, visual UX and PostgreSQL/MySQL contention remain open. | | Text assignments persist versioned server policy, rubric criteria, owner-bound submissions, human grades and criterion feedback. Submission requires the authenticated learner's school-scoped enrollment, server deadline/attempt limit and exact idempotency request. Grading/correction derive the lesson scope from persisted assignment state, require the selected school and then apply evaluator/admin/rubric policy. Admin correction is append-only and records before/after, reason, actor, server time, canonical request and outbox. | [Assignment models](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/assignments_models.rs), [submission service](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/assignments_submit.rs), [grading service](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/assignments_grade.rs), [correction service](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/assignments_correction.rs), [schema](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/assignments_migration.rs), and [HTTP boundary](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/assignments_controller.rs) | [Materialized deadline, cross-user, replay, impossible-score and correction regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_assignment_tests.rs), plus [foreign-school grading denial](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_tenancy_tests.rs) | There is no attachment pipeline, visual authoring/review UI, tenant-scoped outbox or PostgreSQL/MySQL contention proof. Human evaluator identity is only as strong as the host authentication/role lifecycle. | | Protected LMS lesson, progress and assessment paths require the authenticated school, an active membership, a course scope, open-or-active entitlement policy, active enrollment and exactly one valid versioned availability rule; server time enforces release/expiry and same-course prerequisites. | [Generated learning guard](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/access.rs), [school service](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/tenancy/service.rs), and [availability schema/model](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/availability.rs) | [Materialized prerequisite/release/expiry regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_timed_tests.rs) and [cross-school HTTP/database denial plus entitlement regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_tenancy_tests.rs) | Entitlement issuance is not connected to billing, and signed media/download URLs, cross-subsystem tenant isolation and multi-database contention remain open. | | Academy course completion is derived only after school-scoped membership/entitlement checks and from the immutable version pinned at enrollment plus persisted lesson progress under a closed rule. The transaction writes immutable evidence, one opaque certificate and `course_completed`; public verification omits learner identity, while revocation requires an admin in the certificate's school, server time, reason and exact idempotency. | [Completion service](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/completion_service.rs), [models/schema](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/completion_models.rs), and [HTTP boundary](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/completion_controller.rs) | [Materialized completion/revocation regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_http_tests.rs) and [foreign-school revocation denial](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_tenancy_tests.rs) | Certificates are database-verifiable opaque records, not externally signed credentials. Portable signatures, full relational snapshots, tenant-scoped outbox, PostgreSQL/MySQL contention and an external revocation registry remain open. | | A newly applied Academy achievement emits school-bound `achievement_awarded` in the same transaction; the exact claimed event creates one localized in-app notification in that school, applies school/user opt-out/locale state, and authenticated owner/admin APIs control bounded school-filtered listing, preference updates and idempotent read transition without accepting subject identity from input. Its closed versioned template renders Portuguese, Spanish or English with deterministic English fallback and rejects unknown keys/payloads. A new unsuppressed row and the same rendered projection are sent after commit through `TenantRealtime` to an authenticated tenant/user subscription. | [Transactional achievement event](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/automation_execution.rs), [notification contract/schema/realtime projection](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/notifications.rs), [closed translation templates](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/notifications/templates.rs), [HTTP boundary](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/notification_controller.rs), and [worker dispatch](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/automation_worker.rs) | [Materialized database delivery, three-locale/fallback rendering, unknown-key denial, realtime receipt, owner/cross-user listing/read/preference/replay](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_schema_tests.rs) and [same-user cross-school notification/preference isolation](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_tenancy_tests.rs) | The database is authoritative; realtime is in-process and best-effort. Catalogs for other event kinds, Mail/push, distributed realtime/replay, tenant-scoped Nexus UI and PostgreSQL/MySQL contention remain open. | | Course versions are immutable snapshots with explicit draft/review/scheduled/published transitions, an admin reviewer distinct from the author, atomic archival/publication plus outbox, and an enrollment-time pin that does not move when a newer version publishes. Authenticated handlers derive the author/reviewer and current time from server state rather than payload fields. A supervised bounded scheduler acquires/renews/releases an exact shared lease, activates due versions with an admin context, preserves the independent reviewer, records the activation actor and fails closed on lease loss. Admin-only editorial rollback rejects the source author, requires reason/idempotency, creates a new published revision from historical content and atomically persists replaced/result version evidence plus outbox without moving old pins. | [Publication model/service/migration](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/publication.rs), [rollback service/model/migration](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/publication_rollback.rs), [HTTP boundaries](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/publication_controller.rs), [scheduler loop](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/publication_scheduler.rs) and [transactional enrollment pin](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/access.rs) | [Materialized service/pin regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_timed_tests.rs), [rollback authorization/replay/conflict/audit and old/new pin regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_rollback_tests.rs), and [handler plus supervised scheduler contention/activation/replay continuation](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_http_tests.rs) | The starter does not yet serve every module/lesson/assessment field from the snapshot, expose visual Nexus authoring, export scheduler telemetry to Studio, or prove PostgreSQL/MySQL contention. | | Education roles are durable, allowlisted and reconstructed from active assignments for the authenticated school; each grant stores `school_id`, actor, reason and validity window, support must expire, and only a distinct school owner can grant or revoke owner/admin. Exact grant/revocation replay is a no-op only for the same request; cross-school assignment lookup returns not-found. | [Role model/service/migration](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/roles.rs), [HTTP boundary](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/role_controller.rs) and [authentication composition](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/access.rs) | [Materialized expiry/grant/revoke/replay negatives](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_http_tests.rs) and [foreign-school revocation denial](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_tenancy_tests.rs) | There is no explicit step-up ceremony, external audit sink or AI tool integration. Provisioning/bootstrap and full Nexus isolation remain application-owned. | | Academy scheduler instances sharing a database coordinate on a named lease with exact holder/token renewal and release; another token can take over only at expiry. Scheduled publication mounts that lease around every bounded cycle, renews before each mutation and provides an awaited shutdown, safe drop and local counters. | [Generated scheduler lease model/service/migration](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/scheduler_lease.rs) and [supervised publication scheduler](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/publication_scheduler.rs) | [Materialized competing acquire, wrong-token renewal, pre/post-expiry takeover and stale-release regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_timed_tests.rs), plus [continuous scheduler standby, activation and empty replay](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_http_tests.rs) | Delay/failure metrics are not exported to Studio or a durable backend. PostgreSQL/MySQL contention and distributed clock-topology behavior remain unproved. | | Automatic OAuth refresh validates and redacts token state, binds every replacement to the original provider user, serializes concurrent process-local refresh and swaps credentials only after a complete response with a bounded lifetime. A storage-neutral AES-256-GCM snapshot preserves the validated generation and authenticates the version, key ID, provider and trusted local-account binding before restoration. The optional SQLite store adds a persisted quota, restart recovery and exact generation CAS for shared-local writers. | [Refresh coordinator and values](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/refresh.rs), [encrypted snapshot](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/refresh/snapshot.rs) and [SQLite store](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/refresh/sqlite.rs) | [Fresh, concurrent single-refresh, rotation, identity-confusion, malformed-response and rollback tests](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/refresh_tests.rs), [snapshot unit negatives](https://github.com/Rullst/Rullst/blob/main/rullst-connect/src/refresh/snapshot_tests.rs), [public storage/ownership contract](https://github.com/Rullst/Rullst/blob/main/rullst-connect/tests/token_snapshot_contract.rs) and the `TM-CONNECT-24` two-instance/restart/corruption matrix | The host must authorize the account, provide high-entropy key custody/rotation, lease the remote provider call, reconcile a losing rotation, protect and back up the directory, configure retry/backoff/revocation and decide when reauthentication is mandatory. SQLite CAS is local durability, not a secret manager, remote refresh lock or multi-host replication contract. | | The current passkey foundation binds ceremonies to relying-party/origin data, consumes bounded challenges once, verifies flags/signatures, and enforces monotonic counters. The SQLite profile persists bounded device registration/inventory/rename/revocation and advances counters with CAS shared by local processes. | [Passkey service](https://github.com/Rullst/Rullst/blob/main/rullst-auth/src/auth/passkey/service.rs), [ceremony store](https://github.com/Rullst/Rullst/blob/main/rullst-auth/src/auth/passkey/ceremony.rs) and [SQLite device lifecycle](https://github.com/Rullst/Rullst/blob/main/rullst-auth/src/auth/passkey/sqlite.rs) | [Invariant tests](https://github.com/Rullst/Rullst/blob/main/rullst-auth/src/auth/passkey/invariant_tests.rs), [negative-path tests](https://github.com/Rullst/Rullst/blob/main/rullst-auth/src/auth/passkey/service_negative_tests.rs), and [real ES256 lifecycle, restart, replay, quota and two-instance CAS tests](https://github.com/Rullst/Rullst/blob/main/rullst-auth/src/auth/passkey/sqlite/tests.rs) | Challenge state remains process-local, so multi-instance ceremonies require sticky routing or a host shared layer. It has not passed an external WebAuthn conformance suite or independent audit; SQLite file trust/encryption/backup/replication and device ownership remain deployment/application duties. | | Capital exposes Axum and opt-in Actix middleware over one canonical verifier. Both bound payloads at two megabytes, verify the selected provider before dispatch, restore the exact signed body, insert the normalized event, check protocol freshness where defined, and reject replay within the configured store. | [Canonical verifier and Axum adapter](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/webhook.rs) and [Actix adapter](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/webhook/actix.rs) | [Stripe/LemonSqueezy canonical, freshness, reconstruction, capacity, expiry, Actix preservation/rejection and replay tests](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/webhook.rs) | The default replay store is process-local and payload-keyed. Multi-instance deployments require durable shared provider-event idempotency before side effects, provider-specific operational validation, and an append-only audit trail. | | A `Billable` direct charge requires bounded integer minor units, currency, provider-owned customer and tokenized payment-method IDs, validated model e-mail and an application idempotency key. Stripe forwards the key, confirms off-session and accepts only amount/currency-bound `succeeded` or `processing`; deterministic offline receipts are explicitly non-success `Mock`. | [Charge contract](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/charge.rs), [Billable helpers](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/billable.rs), and [Stripe adapter](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/providers/stripe_charge.rs) | [Validation/redaction/provider-negative integration](https://github.com/Rullst/Rullst/blob/main/rullst-capital/tests/direct_charge.rs), [Stripe response-confusion negatives](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/providers/stripe_charge.rs), and [facade derive regression](https://github.com/Rullst/Rullst/blob/main/rullst/tests/billable_macro.rs) | The application must authorize provider IDs, establish mandate/SCA, persist unique keys/order state, reconcile signed events and drive entitlements. No other adapter or live sandbox is implied. | | Metered usage uses provider-specific identities and actions rather than guessing from one uniform subscription ID. Stripe forwards and binds customer/event/value/timestamp/identifier; Lemon Squeezy sends and binds the subscription-item relationship, quantity and action while preserving a separate application event key. | [Usage domain](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/usage.rs), [Stripe adapter](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/providers/stripe_usage.rs), and [Lemon Squeezy adapter](https://github.com/Rullst/Rullst/blob/v12.1.2/rullst-capital/src/providers/lemonsqueezy_usage.rs) | [Stripe exact HTTP/body-limit/mismatch/mock tests](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/providers/stripe_usage_tests.rs), [Lemon exact JSON:API/mismatch/mock tests](https://github.com/Rullst/Rullst/blob/v12.1.2/rullst-capital/src/providers/lemonsqueezy_usage_tests.rs), and [domain validation/redaction tests](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/usage_tests.rs) | The fixtures do not establish live-account acceptance. Stripe uniqueness is rolling, while Lemon's request has no equivalent provider event key; the application must durably claim events, configure the matching aggregation, retry/reconcile and drive quota/entitlement state. | | Coupon IDs and relative trial changes are bounded before provider dispatch. Stripe's discount response must bind the subscription and expanded coupon; Stripe and Lemon Squeezy trial responses must bind subscription and exact expiration. Unreviewed live operations fail explicitly. | [Subscription values](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/subscription.rs), [Stripe protocols](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/providers/stripe_subscription.rs), [Lemon Squeezy trial protocol](https://github.com/Rullst/Rullst/blob/v12.1.2/rullst-capital/src/providers/lemonsqueezy_subscription.rs), and [static handle](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/billable.rs) | Exact local HTTP fixtures assert form/JSON:API headers, body and response binding; [cross-provider fail-closed integration](https://github.com/Rullst/Rullst/blob/main/rullst-capital/tests/subscription_management.rs) covers unreviewed live paths. | The host must authorize ownership, persist one command clock for stable retries, serialize conflicting updates, understand provider billing-cycle effects and reconcile signed webhooks. Protocol fixtures are not live-account acceptance. | | Shared Team/Workspace quotas use a trusted billing subject, an application-stable event key and an atomic conditional counter. An over-limit or exact replay never executes `QuotaGate`'s callback; a conflicting replay fails closed. The SQL adapter can reserve in the same transaction as the domain insert. | [Quota domain and gate](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/quota/mod.rs), [SQL store](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/quota/sql.rs), and [Billable limit derivation](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/billable.rs) | [Local validation, isolation, replay, compensation and concurrency](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/quota/tests.rs) plus live [PostgreSQL](https://github.com/Rullst/Rullst/blob/main/rullst-capital/tests/quota_sql_postgres.rs), [MySQL](https://github.com/Rullst/Rullst/blob/main/rullst-capital/tests/quota_sql_mysql.rs), and [MariaDB](https://github.com/Rullst/Rullst/blob/main/rullst-capital/tests/quota_sql_mariadb.rs) contention contracts | Authentication must establish membership and the active tenant; webhook reconciliation must persist the authoritative tier. Schema migrations, abandoned standalone reservation policy and Turso/NoSQL adapters remain host work. Writes outside the explicit gate are not intercepted. | | Opt-in paid-invoice delivery rejects non-final/mock payment evidence and exact recipient, minor-unit amount or currency mismatch before producing bounded HTML/PDF; the Mail bridge runs pre-flight and retains a stable non-secret delivery key. | [`PaidInvoice` binding](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/invoice/paid.rs), [PDF renderer](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/invoice/pdf.rs), and [Mail bridge](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/paid_invoice.rs) | [Payment-status/substitution and key-stability negatives](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/invoice/paid.rs), [font/pagination/PDF parse tests](https://github.com/Rullst/Rullst/blob/main/rullst-capital/src/invoice/pdf.rs), and [prepared plus explicit-driver delivery tests](https://github.com/Rullst/Rullst/blob/main/rullst-mail/src/paid_invoice.rs) | A custom adapter is trusted to authenticate its own provider response. The application must construct the authoritative order, reconcile webhooks, atomically claim the key in a durable outbox and own retry/provider attachment policy. No automatic webhook hook, provider acceptance or exactly-once delivery is implied. | | Vault field envelopes use authenticated AES-256-GCM, random nonces, version/key identifiers, AAD binding, and explicit rotation keys. | [Vault implementation](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/vault.rs) | [Round-trip, tamper, wrong-key, malformed-envelope, and rotation tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/vault.rs) | The caller owns key custody and rotation. No real KMS/HSM adapter or hardware-backed key claim is made. | | `#[orm(encrypted)]` transparently encrypts `String` and `Option<String>` values on generated writes, decrypts generated model reads, authenticates table/column context, and reads prior keys from an explicit keyring. Generated audit rows, committed events, Scout documents and `save_to_redis` hashes never carry encrypted or `#[orm(masked)]` plaintext. | [ORM privacy implementation](https://github.com/Rullst/Rullst/blob/main/rullst-orm/src/privacy.rs), [derive integration](https://github.com/Rullst/Rullst/blob/main/rullst-orm-macros/src/models/mod.rs) and [generated projections](https://github.com/Rullst/Rullst/blob/main/rullst-orm-macros/src/models/json_ops.rs) | [End-to-end SQLite insert/read/update/rotation/tamper/query-boundary test](https://github.com/Rullst/Rullst/blob/main/rullst-orm/tests/encrypted_field_test.rs), [envelope unit tests](https://github.com/Rullst/Rullst/blob/main/rullst-orm/src/privacy.rs), [audit/event/Scout redaction test](https://github.com/Rullst/Rullst/blob/main/rullst-orm/tests/protected_audit_test.rs) and [projection/Redis hash test](https://github.com/Rullst/Rullst/blob/main/rullst-orm/tests/protected_projection_test.rs) | Randomized fields cannot support generated filtering, ordering, grouping, or explicit selection. Raw SQL is not transformed, blind indexes are application-owned, full key retirement is not automated, and key custody remains external. | | Portable document recovery encrypts and authenticates a versioned MongoDB/SurrealDB/deterministic-store snapshot with AES-256-GCM, fresh nonce, explicit rotation key and length-delimited application/collection binding. Export compares two bounded inventories; restore accepts only an empty or exact matching subset, never replaces/deletes and verifies the final collection. | [Recovery types and redacted key/envelope](https://github.com/Rullst/Rullst/blob/main/rullst-orm/src/polyglot/recovery.rs), [bounded codec](https://github.com/Rullst/Rullst/blob/main/rullst-orm/src/polyglot/recovery/codec.rs), and [restore algorithm](https://github.com/Rullst/Rullst/blob/main/rullst-orm/src/polyglot/recovery/operation.rs) | [Tamper/key/scope, conflict/extra-row, replay, capacity, ordering and concurrent-source negatives](https://github.com/Rullst/Rullst/blob/main/rullst-orm/src/polyglot/recovery/tests.rs), plus the live [MongoDB→SurrealDB→MongoDB matrix](https://github.com/Rullst/Rullst/blob/main/rullst-orm/tests/matrix_document_recovery.rs) | Applications must quiesce writers, provision destination schema and own high-entropy key custody, rotation, snapshot permissions, durability, retention and erasure. Two equal scans are not formal online snapshot isolation; partial successful inserts remain for exact retry, and no managed backup, point-in-time recovery, replication or cross-store transaction is claimed. | | OTA accepts firmware for commit only after Ed25519 verification binds target, version, rollback counter, length, and SHA-256 digest. The store-backed path additionally requires exact monotonic compare-and-set before local mutation. | [OTA manifest verifier](https://github.com/Rullst/Rullst/blob/main/rullst-iot/src/ota.rs) and [counter-store contract](https://github.com/Rullst/Rullst/blob/main/rullst-iot/src/ota/counter.rs) | [OTA integration negatives and state tests](https://github.com/Rullst/Rullst/blob/main/rullst-iot/tests/iot_integration_tests.rs), [restart/retry/conflict store tests](https://github.com/Rullst/Rullst/blob/main/rullst-iot/tests/ota_counter_store.rs), and [bounded fuzz target](https://github.com/Rullst/Rullst/blob/main/rullst-iot/fuzz/fuzz_targets/fuzz_ota.rs) | Rullst does not download, flash, boot, or implement/certify a physical durable counter. Platform bootloader, storage integrity, wear and power-loss integration remain external. | | Core's canonical browser baseline installs the exact application config outside a stable secure-header/CSP nonce → exact-origin CORS → WAF → CSRF → optional PII order; `Server` uses the same composition. Core and the optional extended secure-header layer reuse one per-request CSP nonce. Generated LMS/SaaS authentication plus LMS catalog/course/player style elements consume that nonce, and the audited LMS shell has no remote page dependency or inline style attribute. | [Core baseline](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/security/baseline.rs), [generated authentication controller](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/auth/auth_controller.rs.template), [LMS catalog](https://github.com/Rullst/Rullst/blob/main/examples/lms/src/controllers/lms_controller.rs) (the LMS example since 13.0.0-alpha.4), and [extended layer](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/headers.rs) | [Integrated Core baseline tests](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/security/baseline.rs), [generated catalog contracts](https://github.com/Rullst/Rullst/blob/v13.0.0-alpha.3/cargo-rullst/src/blueprints/lms_tests.rs), [materialized escaping/nonce regression](https://github.com/Rullst/Rullst/blob/v12.1.2/cargo-rullst/src/blueprints/lms/academy_catalog_tests.rs), and [extended nonce tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/headers.rs) | In-process/source evidence does not guarantee a scanner grade or real-browser behavior. TLS, proxy policy, every application-owned HTML surface and session/auth/tenant layers still require deployment testing. | | Response DLP masks recognized secrets only in supported, bounded textual payloads without corrupting invalid UTF-8 or incomplete values. | [DLP middleware](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/dlp.rs) | [Unit/integration tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/tests/rasp_waf_dlp_test.rs) and [bounded fuzz target](https://github.com/Rullst/Rullst/blob/main/rullst-security/fuzz/fuzz_targets/fuzz_dlp.rs) | It does not prove zero leakage and does not rewrite arbitrary binary, compressed, encrypted, or streaming bodies. Unknown secret formats can pass through. | | RASP rejects the implemented SQL injection, path traversal, JNDI, SSRF, RCE, header, and decoded JSON patterns within its inspection boundary. | [RASP inspector](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/rasp.rs) | [Middleware and pattern tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/rasp.rs), [integration tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/tests/rasp_waf_dlp_test.rs), and [bounded fuzz target](https://github.com/Rullst/Rullst/blob/main/rullst-security/fuzz/fuzz_targets/fuzz_rasp.rs) | It is a bounded detector, not a parser for every protocol or proof against every bypass; false positives and false negatives remain possible. | | Login Jail applies bounded progressive delay and temporary process-local jailing, with cleanup of expired state; its async API awaits the selected delay. | [Login guard](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/login_guard.rs) | [Applied-delay, jail, expiry, capacity, and concurrency tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/login_guard.rs) and [contention tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/tests/concurrency_tests.rs) | State is not distributed. Correct client identity depends on trusted peer/proxy handling, and deployments still need upstream abuse controls. | | The optional Redis limiter consumes one namespaced fixed-window budget atomically, hashes the caller-supplied client key and returns TTL-derived retry metadata. | [Redis limiter](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/rate_limit/redis.rs) | [Configuration, key-redaction, explicit-mock and shared-clone tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/rate_limit/redis.rs) | Offline mode is process-local and `require_distributed()` rejects it. Real Redis cross-instance, eviction/failover and trusted-proxy deployment tests remain required; the caller still owns the identity key policy. | | Audit-chain entries use HMAC verification and unambiguous length-prefixing; ordinary process-local telemetry is not presented as HMAC verified. | [Audit chain](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/audit/chain.rs), [telemetry store](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/telemetry.rs), [authenticated journal](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/siem/authenticated.rs), and [Nexus rendering](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/src/nexus/security.rs) | [Tamper/concurrency tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/tests/concurrency_tests.rs), [telemetry provenance tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/telemetry.rs), [journal forgery/rotation tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/siem/authenticated/tests.rs), and [Nexus badge test](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/src/nexus/security.rs) | The compatible `DurableSiemSpool` deliberately stores unsigned events. The separate opt-in journal sets the flag only after verifying its complete local HMAC chain; it does not authenticate the truth or identity behind a semantic event. Key management, trusted tail checkpoints, external storage/delivery and independent verification remain open. | | Local security telemetry uses an exact, versioned v1 JSON envelope with normalized identifiers, IPs and timestamps, bounded UTF-8 details, escaped CEF extension values and opt-in synchronous local journals. | [`LiveSecurityEvent`](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/telemetry/event.rs), [machine-readable schema](https://github.com/Rullst/Rullst/blob/main/rullst-security/schema/security-event-v1.schema.json), [CEF serializer](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/siem.rs), [compatible spool](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/siem/spool.rs), and [authenticated journal](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/siem/authenticated.rs) | [Schema/normalization tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/telemetry/event.rs), [unsigned spool tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/siem/spool_tests.rs), [authenticated restart/rotation/forgery/ordering tests](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/siem/authenticated/tests.rs), [public API restart test](https://github.com/Rullst/Rullst/blob/main/rullst-security/tests/siem_spool.rs), and [CEF injection test](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/siem.rs) | Both formats are bounded and single-process. The authenticated form supports explicit key rotation but not trusted whole-tail checkpoints, directory policy, compaction, delivery, acknowledgement, retention, correlation, retry, dead-letter handling, or an external SIEM. | | Declared JSON bodies fail closed on invalid syntax, recursive duplicate keys, excessive size/depth and ambiguous content types; an explicitly mounted policy additionally enforces one bounded JSON Schema 2020-12 document or OpenAPI 3.1 component without external retrieval; recognized log secrets and local SRI assets have explicit bounded helpers. | [Schema guard](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/schema_guard.rs), [compiled policy](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/schema_guard/policy.rs), [log redactor](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/log_redactor.rs), and [SRI helpers](https://github.com/Rullst/Rullst/blob/main/rullst-security/src/sri.rs) | Inline malformed/duplicate/nested JSON, schema shape/external-reference/OpenAPI component negatives, repeated secret and file-backed SRI tests in those modules, plus [DLP/RASP integration](https://github.com/Rullst/Rullst/blob/main/rullst-security/tests/rasp_waf_dlp_test.rs) | This validates JSON bodies only after explicit per-route mounting. It does not infer arbitrary route schemas, authorize a subject, enforce domain rules, install a global tracing filter, discover every secret, or rewrite every asset automatically. | | Security CLI evidence fails when an explicitly requested Geiger/SBOM/network check is incomplete or reports a finding; SBOM UUID/component fields and actual MSRV parsing are regression-tested. | [Audit orchestration](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/audit.rs), [SBOM/network evidence](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/audit_evidence.rs), [doctor](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/doctor/mod.rs), and [managed hook](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/hook.rs) | Unit tests in the linked modules and strict package Clippy | Source heuristics are not complete static analysis; the SBOM is not signed attestation; listener observations are not external reachability; tool presence and local output are not certification. | | AI prompt checks and PII masking run before built-in provider dispatch and report only a heuristic result. | [`GuardrailReport`](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/guardrails.rs), [provider dispatch](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/providers/mod.rs), [compatible adapter](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/providers/openai_compatible.rs), and [versioned corpus](https://github.com/Rullst/Rullst/blob/main/rullst-ai/evals/guardrails-v1.json) | [Versioned cross-provider eval runner](https://github.com/Rullst/Rullst/blob/main/rullst-ai/tests/versioned_evals.rs), [cross-provider guardrail pipeline](https://github.com/Rullst/Rullst/blob/main/rullst-ai/tests/guardrails_pipeline_test.rs), [compatible public/loopback contracts](https://github.com/Rullst/Rullst/blob/main/rullst-ai/tests/openai_compatible_contract.rs), and [provider capability tests](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/providers/mod.rs) | The corpus is a deterministic regression suite, not a safety benchmark. Compatible capabilities are caller declarations, not discovery. Passing heuristics or JSON schema does not make output trustworthy, stop adaptive injection, authorize tools, prevent egress, or prove an upstream request was cancelled. | | Every built-in live AI transport applies a configurable local request deadline, defaulting to 30 seconds. | [OpenAI](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/providers/openai.rs), [OpenAI-compatible](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/providers/openai_compatible.rs), [Anthropic](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/providers/anthropic.rs), [Gemini](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/providers/gemini.rs), [DeepSeek](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/providers/deepseek.rs), and [Ollama](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/providers/ollama.rs) adapters | [Capability contract and loopback timeout/shape regressions](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/providers/mod.rs) | A local timeout only drops the request future. It does not prove upstream cancellation, prevent billing, provide automatic retries, limit concurrency, or implement circuit breaking. | | The strict AI egress policy denies every host until an exact allowlist is configured and rejects insecure/credentialed URLs, local/private/metadata/reserved addresses, mixed DNS answers, disallowed redirects and oversized bodies. Its opt-in fetcher resolves under deadline, pins all validated answers into a proxy-free client, verifies the connected peer, disables automatic redirects and enforces the byte budget while streaming. | [`EgressPolicy`](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/egress.rs), [host normalization](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/egress_host.rs), and [`EgressFetcher`](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/egress_fetch.rs) | [IPv4, IPv6, allowlist, DNS, redirect, configuration and resource-budget negatives](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/egress.rs), plus [private/mixed DNS rejection before transport and streaming overflow tests](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/egress_fetch.rs) | The fetcher is not mounted around arbitrary application/provider traffic. Tenant-aware destination authorization, response content/schema validation, data minimization and a deterministic successful live-origin redirect/stream contract remain caller/integration work. | | Local AI tool dispatch requires an exact allowlist, principal authorization, closed bounded JSON, call budget and audit sink; destructive/financial approvals are one-use and exact-payload-bound. Bounded local RAG/tool trails synchronously persist distinct versioned streams and fail closed on quota, corruption, unsafe targets and competing-writer growth. | [Guarded tool registry](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/tools.rs), [policy](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/tools/policy.rs), [tool audit](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/tools/audit.rs), [RAG audit](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/rag/audit.rs), and [shared durable format](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/durable_audit.rs) | [Authorization, schema, budget, approval binding, failure, and audit tests](https://github.com/Rullst/Rullst/blob/main/rullst-ai/src/ai/tools/tests.rs), plus [restart, tamper, quota, concurrency and real-pipeline durable evidence](https://github.com/Rullst/Rullst/blob/main/rullst-ai/tests/durable_audit.rs) | Provider-native calling, principal/approver authentication, domain ownership and network egress policy remain application responsibilities. The local trail is single-process; SHA-256 detects corruption but does not authenticate events, and the host owns permissions, rotation, retention, backup and external delivery. | | The Academy production preset normalizes all twelve declared boundaries and fails unless each has one explicit evidenced `PASS`. | [`ProductionPreset::academy`](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/production.rs) and the [Academy CLI diagnostic](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/academy_doctor.rs) | [Preset negatives](https://github.com/Rullst/Rullst/blob/main/rullst-core/src/production.rs), [diagnostic normalization tests](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/academy_doctor.rs), [process-level CLI tests](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/tests/academy_doctor_cli.rs), and [CLI inventory contract](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/tests/scaffold_contracts.rs) | Evidence is caller-declared and the diagnostic always reports `certification: false`; it does not inspect a deployment, prove the declaration true, or replace independent review. | | Live Alipay RSA2 verification and NFS-e homologation/production fail closed instead of returning simulated success. NFS-e local preparation separately enforces pinned XSDs, PKCS#12 XMLDSig, deterministic issuance JSON, bounded signed-authorization and structured-rejection parsing and bounded mTLS construction. | [Alipay adapter](https://github.com/Rullst/Rullst/blob/v12.1.2/rullst-capital/src/providers/alipay.rs), [NFS-e signer](https://github.com/Rullst/Rullst/blob/v12.1.2/rullst-capital/src/fiscal/signer.rs), [protocol codec](https://github.com/Rullst/Rullst/blob/v12.1.2/rullst-capital/src/fiscal/protocol.rs), [schema boundary](https://github.com/Rullst/Rullst/blob/v12.1.2/rullst-capital/src/fiscal/schema.rs), and [NFS-e client](https://github.com/Rullst/Rullst/blob/v12.1.2/rullst-capital/src/fiscal/client.rs) | Alipay unsupported-path tests plus fiscal builder, signature verification, protocol binding/tamper/decompression negatives, official-artifact opt-in validation, and environment tests | Offline fixtures and local schema/cryptographic/protocol validity are not legal compliance, certificate trust, tax authorization, settlement, live response evidence, independent review, or provider homologation. | ## Release use (v12) Before a v12 RC or stable release, reviewers had to: 1. run the linked tests through the workspace trifecta on the candidate SHA; 2. attach multi-OS CI and packaged-distribution evidence to that same SHA; 3. confirm the changelog uses the narrow claim wording above; 4. keep every remaining boundary visible in the release notes; and 5. leave the release at `NO-GO` if a claimed control lacks matching evidence. The v12 threat assumptions and abuse cases are model version TM-12.10 on the frozen `v12` branch; the [current threat models](https://rullst.github.io/Rullst/book/threat-models.html) are TM-13.0. The broader implementation/roadmap boundary lives in the [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html). --- # Security Event Schema v1 Source: https://rullst.github.io/Rullst/book/security-event-schema.html # Security event schema v1 `rullst-security::LiveSecurityEvent` is the bounded event contract rendered by Studio/Nexus and accepted by the process-local security store. Its current schema version is exported as `SECURITY_EVENT_SCHEMA_VERSION = 1`. The package also embeds and exports the JSON Schema 2020-12 document as `LIVE_SECURITY_EVENT_V1_JSON_SCHEMA`; its packaged source is [`security-event-v1.schema.json`](https://github.com/Rullst/Rullst/blob/main/rullst-security/schema/security-event-v1.schema.json). This is an application telemetry envelope, not a remote SIEM transport. The optional local spools provide bounded persistence and one explicit authenticated-journal mode, but the schema itself does not provide delivery, retention, correlation, acknowledgement, retry, dead-letter handling, source identity, or regulatory evidence. ## JSON fields | Field | v1 contract | | --- | --- | | `schema_version` | Integer `1`. Legacy JSON without the field deserializes as v1; locally stored events are normalized to v1. | | `event_type` | Non-empty uppercase ASCII letters, digits, and underscores; maximum 64 bytes. Invalid values become `SECURITY_EVENT`. | | `details` | Unstructured human-readable UTF-8 text; maximum 2 KiB and truncated only at a valid character boundary. It must not be parsed as authorization data. | | `client_ip` | Canonical IPv4/IPv6 string or `unknown`. Forwarded headers are not implicitly trusted. | | `timestamp_str` | Absolute RFC 3339 timestamp. Invalid local timestamps are replaced at ingestion. | | `verified_hmac` | `true` only when a connected verifier validated an HMAC for that exact event. It does not prove the event's semantic claim or source identity. `push_local_event` always forces it to `false`. | Example: ```json { "schema_version": 1, "event_type": "RBAC_DENIAL", "details": "Authenticated principal denied access to the resource", "client_ip": "192.0.2.4", "timestamp_str": "2026-08-27T15:30:00.000Z", "verified_hmac": false } ``` ## Producer rules New local producers should use `LiveSecurityEvent::local(...)` and `SecurityStore::push_local_event(...)`. This path: 1. assigns schema v1 and an RFC 3339 timestamp; 2. validates/bounds the type and detail text; 3. canonicalizes the IP address; 4. removes any caller-provided local HMAC claim; and 5. stores the event in the bounded 50-entry process-local buffer. The buffer is deliberately a dashboard snapshot. Loss on restart, eviction at capacity, and absence of a consumer are expected properties, not successful external delivery. ## Compatibility rule Within a stable major line (v12, and v13 from 13.0.0; `rullst-security` is a Core crate): - fields cannot be removed, renamed, or change meaning; - a new required field or incompatible type requires a new schema version; - additive optional fields require tolerant consumers and a changelog entry; - event-type additions are compatible, so consumers need an unknown fallback; - consumers must not infer trust from `verified_hmac` alone; and - all compatibility claims apply to JSON, not field order in serialized text. The version-one contract has source-controlled serialization, legacy-input, normalization, size, UTF-8, and CEF-injection tests. `DurableSiemSpool` can persist normalized unsigned v1 values in a local single-process file with exact byte/record quotas, versioned length/digest frames and restart validation. `AuthenticatedSiemSpool` is a distinct opt-in file format that authenticates sequence, named rotation key, predecessor and exact payload before JSON decode; verified reads set `verified_hmac=true` only after the complete chain passes. Release evidence still belongs to the exact release tag SHA. ## CEF boundary `format_cef_event` is a serializer only. It escapes backslashes, equals signs, and CR/LF in extension values so event details cannot inject fields or records. Calling `dispatch_siem_alert` records a local SIEM-candidate event; it does not send or acknowledge an external alert. ## Durable local spool boundary `DurableSiemSpool` serializes one local event per bounded frame, calls `sync_data` before returning a receipt, validates every frame on reopen and rejects truncation, digest mismatch, invalid event JSON and external length changes. The SHA-256 digest detects corruption; it is not an authentication tag. The spool deliberately supplies no directory creation, multi-process lock, rotation, retention, backup, retry, dead-letter handling or remote adapter. Operators must provide a trusted, permissioned directory and one writer process per file. ## Authenticated local journal boundary `AuthenticatedSiemSpool` uses domain-separated HMAC-SHA256 frames and a `SiemKeyRing` containing one active write key plus at most seven historical verification keys. Key material is held in zeroizing storage and omitted from `Debug`. Reopen fails closed for an absent/wrong key, forged payload, non-canonical sequence, broken predecessor chain, removed/reordered interior frame, symlink target, quota violation or external file-length change. The chain does not identify whether the local producer's semantic assertion was true. Removal of a whole valid tail also needs a checkpoint retained in a separately trusted system. The journal does not rotate/compact itself or supply multi-process locking, remote delivery, retry, acknowledgement or dead-letter handling. A future operational sink should consume the versioned JSON envelope and define redaction, backpressure, authentication, durable spool/retry/dead-letter, delivery acknowledgement, retention, and multi-tenant access separately. --- # CORS Scaffold Security Advisory Source: https://rullst.github.io/Rullst/book/cors-scaffold-security-advisory.html # Security Advisory — CORS in Older Scaffolds ## Status - **Affected component:** applications generated by older versions of `cargo-rullst make:cors` or by blueprints that copied a permissive or reflected CORS policy. - **Current framework:** corrected. New scaffolds use an explicit allowlist and fail when `CORS_ALLOWED_ORIGINS` is missing, empty, contains `*`, or contains an invalid origin. - **Previously generated applications:** not modified automatically by a CLI update. Each repository must review the middleware already copied into its source tree. - **Severity:** application-dependent. Risk is higher when an untrusted origin can read responses authenticated by a cookie or bearer token. This is a scaffold migration advisory. It is not a CVE, penetration test, or claim that every final CORS configuration is secure. ## Find a potentially affected application Review the generated CORS middleware and search for these patterns. The snippet below is deliberately incomplete and unsafe; use it only as a search pattern. ```rust,ignore AllowOrigin::mirror_request() .allow_origin(Any) .allow_credentials(true) ``` Copying the `Origin` request header into `Access-Control-Allow-Origin` without comparing it against a server-managed allowlist is also unsafe. Reflected origins or wildcards combined with credentials require immediate review. Run an initial search from the application root: ```bash rg -n 'mirror_request|allow_origin\(Any\)|allow_credentials\(true\)|Access-Control-Allow-Origin' src ``` Review every match. `allow_credentials(true)` is not inherently vulnerable when it is combined with a correct closed origin allowlist. ## Recommended migration 1. Back up or review the existing middleware and generate the current version on a separate branch with `cargo rullst make:cors`. 2. Replace reflection or wildcards with exact origins including scheme, host, and port: ```text CORS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com ``` 3. Keep credentials disabled unless the application genuinely needs cookies or cross-origin authentication. If required, enable them only after validating the exact allowlist. 4. Restrict methods and headers to the API's actual contract. The current scaffold provides a baseline; it cannot infer every application endpoint. 5. Test at least one allowed origin and negative cases covering a similar host, unauthorized subdomain, different port, `null`, and no `Origin` header. Example manual check: an unauthorized origin must not receive an `Access-Control-Allow-Origin` response header. ```bash curl -i -X OPTIONS https://api.example.com/resource \ -H 'Origin: https://evil.example' \ -H 'Access-Control-Request-Method: POST' ``` ## Completion criteria The migration is complete when: - no origin is reflected or accepted by wildcard; - the process fails explicitly when required configuration is missing or invalid; - credentials are accepted only for exact authorized origins; - negative tests prove that deceptively similar origins receive no authorization; - caches and proxies receive `Vary: Origin` when the response depends on the request origin. The current template is [`cargo-rullst/src/generators/cors_middleware.rs.template`](https://github.com/Rullst/Rullst/blob/main/cargo-rullst/src/generators/cors_middleware.rs.template). --- # Security Advisory Exceptions Source: https://rullst.github.io/Rullst/book/security-advisory-exceptions.html # Security advisory exceptions Rullst does not treat an ignored scanner finding as remediated. Any temporary exception must have a narrow scope, a compensating control, an owner, and an expiry date. CI must fail for every advisory that is not actively governed here. Last reviewed: **2026-10-22**. There are **no active advisory exceptions** in the current audited dependency graph (`deny.toml` and the audit workflows ignore no advisory). ## Remediation history - On 2026-10-22, the time-bounded exception for `RUSTSEC-2026-0367`, `RUSTSEC-2026-0368` and `RUSTSEC-2026-0369` (`zip` 6.0.0 as a build dependency of `libduckdb-sys`, approved on 2026-10-17 until 2026-10-31) was removed: `duckdb`/`libduckdb-sys` 1.10506.1 build with `zip` 9, and `rullst-orm` now requires it. The same change moved `rustls-webpki` to 0.103.16 and `webpki-roots`/`webpki-root-certs` to 1.0.10 (`RUSTSEC-2026-0381` to `-0383`) and `ammonia` to 4.2.4 (`RUSTSEC-2026-0373`). - On 2026-09-08, `RUSTSEC-2023-0071` was removed from the production and fuzz dependency graphs by moving `jsonwebtoken` from its `rust_crypto` backend to `aws_lc_rs`. The CLI scaffolds the same backend for newly generated JWT middleware. Auth and Connect's RS256/OIDC tests remain enabled, and Cargo Audit runs without an exception. - On 2026-08-26, `RUSTSEC-2026-0173` (`proc-macro-error2`) and `RUSTSEC-2024-0436` (`paste` through Leptos) were removed from the resolved dependency graph. Their workflow ignores were removed in the same change. These are historical remediations, not active exceptions. ## Advisory response SLA The clock starts when a maintainer receives a credible private report or an automated advisory first appears on a protected branch. Severity uses the highest credible impact while triage is incomplete. | Severity | Acknowledge and assign owner | Mitigate or release target | Maximum temporary exception | | --- | --- | --- | --- | | Critical | 1 business day | 72 hours | 14 days | | High | 2 business days | 7 calendar days | 30 days | | Medium | 5 business days | 30 calendar days | 90 days | | Low / unmaintained without known vulnerability | 10 business days | 90 calendar days | 180 days | If the release target cannot be met, maintainers must disable or isolate the affected capability, yank an affected prerelease when appropriate, or create a time-bounded exception below. Critical/high exceptions require release-owner approval and a documented reason that disabling the capability creates greater risk. An exception is never evidence that the vulnerability is fixed. ## Review procedure At or before expiry, the owner must either remove the dependency, upgrade to a fixed dependency graph, or record a new review with fresh evidence and a new short deadline. The reviewer must inspect the dependency path with Cargo Tree and run the repository's Cargo Audit (`audit.yml`, `security-audit.yml`) and Cargo Deny (`cargo-deny.yml`) workflows. Patched `rustls-webpki`, `event-listener`, and `lru` findings and the removed HTTP/2 dependency from Actix's disabled default features are deliberately not ignored. This makes a regression in any of those versions block CI again. --- # CI/CD & Security Workflows Source: https://rullst.github.io/Rullst/book/workflows.html # Rullst CI/CD and Verification Contract This document describes what the repository's automation currently executes. It is not evidence that a workflow has passed for a particular commit. A green claim must always point to the GitHub Actions run, commit SHA, logs, and produced artifacts. Last source-level review: **2026-09-20 UTC**. ## Status language | Status | Meaning | | :--- | :--- | | **Blocking** | A failing command fails that workflow run. Branch protection still determines whether the check is required for merging. | | **Automated evidence** | The workflow runs automatically, but part of its result is external, uploaded, or deliberately non-blocking. | | **Informational** | The workflow is explicitly advisory and must not be described as a release gate. A manual trigger alone does not make a strict candidate check informational. | | **Roadmap** | The idea is preserved, but this repository does not yet provide reproducible evidence for it. | The distinction matters: Kani, Miri, mutation testing, or a scanner can be very valuable without proving that the entire framework is panic-free, race-free, memory-safe, or compliant with a regulation. ## Branch transition On 21 September 2026, `v12` was preserved from stable source `184bc1f7`. The hosted `main` and `v12` protections were read back with the same 43 required GitHub Actions checks as `v13`, an up-to-date base, administrator enforcement, conversation resolution and disabled force pushes/deletion. This is a settings observation, not a successful test receipt. Promotion of v13 source to `main` requires a normally admitted PR; existing tags, packages and running feature campaigns are retained. `v13` remains transitional while that admission runs. The default-branch Dependabot configuration targets Cargo and Actions on both `main` and `v12`. Minor/patch Cargo changes share a PR; Actions share one tools PR per line. On `v12`, Dependabot ignores SemVer-major Cargo updates (including Cargo's breaking `0.x` minors) and the held `aws-smithy-types` constraint: majors are evaluated on `main` first. Updates still require compatibility/MSRV and protected CI; group membership is not approval. Old Dependabot PRs are closed after equivalent updates have entered their destination source line, or with a comment when that line's policy excludes them. ## Mainline execution model The development dashboard and its automatic status badges follow `main`. The declared automatic release workflows accept pushes and pull requests targeting `main`, stable maintenance `v12`, and transitional integration `v13`, and expose `workflow_dispatch` where a safe rerun is useful. Superseded runs of these workflows are cancelled per workflow and ref so rapid development does not spend runner capacity proving an obsolete commit. Release policy schema 4 binds v12 to `v12` and v13 to `main`. Historical schemas 2 and 3 retain their original bindings. Before building release artifacts, the tag, every inventory package version and the current protected branch head must agree. Exact-source workflow admission and the crates.io deployment approval remain mandatory. Fuzz receipts are limited to the candidate's release line, including the source policy of reused runs. `ci.yml` deliberately treats the expensive operating-system matrix differently. Format and Clippy continue to give feedback on draft pull requests. A ready pull request to `main` runs the fast Linux gate: the `workspace` and `cli-standard` shards, MSRV, strict database features, Redis/S3 contracts, generated access, facade composition and AI evals. Line coverage, SemVer and CodeQL skip pull requests to `main`. The resulting `main` push runs every Linux shard, public feature boundaries, the threat-model minimum, coverage, SemVer and CodeQL. A nightly scheduled run (05:30 UTC) executes the complete Linux/macOS/Windows matrix on the default branch. Ready pull requests to the maintained `v12` line and transitional `v13` keep the complete matrix, blocking line coverage, SemVer fan-out and CodeQL before merge. Any matrix can be requested manually. Each operating system executes seven parallel shards: the non-CLI workspace (which includes the LMS, ERP and Portfolio examples), ordinary CLI targets, three public-profile groups and two generated-starter groups. The basic/relational/polyglot and foundation/product partitions retain every original case while bounding the longest Windows and macOS jobs. No test is omitted; this changes wall-clock scheduling rather than the assertions being executed. Hosted CI permits two nested compiler jobs for the otherwise serial generated profile/blueprint builds; constrained local runs retain their one-job default. Each CLI shard fetches the locked registry inventory before its generated applications prove that they compile without network access. After a reviewed commit is merged, the automatic `main` or `v13` push runs Linux only; for `main`, macOS and Windows evidence comes from the nightly run. Release candidates must use the manual full matrix when no successful full-matrix run points to the exact candidate tree. The SHA-bound quality scorecard is generated only by a ready pull request to a maintained line, the nightly run or a manual full-matrix run. It is deliberately skipped on Linux-only runs (automatic branch pushes and pull requests to `main`), because they cannot honestly award cross-platform verification credit. Run `ci.yml` manually on a final release-branch candidate to produce the exact-SHA release scorecard. Manual diagnostic runs may select one operating system and one test shard; those deliberately do not produce a full-matrix scorecard and do not replace final-candidate evidence. The manual `cli-updates` diagnostic selects discovery/cache/artifact tests, isolated project preparation/verification and legacy upgrade process fixtures. It does not replace the complete `cli-standard` shard in release matrices. Manual CLI-only shards also skip unrelated ORM/Redis/feature/threat/eval/facade and MSRV jobs, keeping correction runs bounded. Manual `workspace` and `all` selections, automatic runs and ready PRs retain those jobs. Strict workspace Clippy/format still runs for every diagnostic; release admission continues to require every job from the full `all`/`all` matrix. The manual `all`/`all` matrix additionally packages the 20 release-inventory crates, including `rullst-labs`, audits their contents and uses the release pipeline's archive-only consumer and isolated CLI installation/blueprint checks. `Packaged distribution and installed CLI` is a required exact-SHA release-admission job. It runs without registry publication credentials and never uploads to crates.io. Automatic development runs and other diagnostic subsets deliberately skip this expensive job. The explicit `platform=all`, `shard=packaged-distribution` diagnostic runs this same archive/installed-CLI check and workspace Clippy/format, without repeating the OS test matrix or native artifact builds. It emits no full-matrix scorecard and cannot satisfy complete release admission; the final `all`/`all` and tag pipelines retain their existing package verification. `Generated VM deployment (docker)` and `(podman)` run `.github/check-vm-deploy.py` (about ten minutes each): they install the CLI from the packaged archives and, for the Blank and SaaS starters, run `cargo rullst deploy`, copy the project as the [VM guide](https://rullst.github.io/Rullst/book/vm-deployment.html) does, check `.env.production` with `deploy:doctor`, build the image with the generated `Dockerfile` and start the generated `compose.yaml` and `Caddyfile`. They check `/health` over HTTPS through Caddy (verified against Caddy's local CA for `localhost`), the HTTP-to-HTTPS redirect, that the app port is not published, the read-only root filesystem, capabilities and `no-new-privileges` of both containers (read from `/proc`), Caddy's trusted-proxy address, `db:migrate`, a database-backed page (SaaS: registration and sign-in with a Secure session cookie), that the SQLite database stays in its volume across `docker compose restart` (Podman: `down` and `up -d`, because podman-compose's `restart` can leave Caddy stopped), the guide's backup command and that no container logged a panic. The release crates enter the build context as packaged archives through `[patch.crates-io]`; the generated deployment files run unmodified. Docker uses the runner's daemon; Podman runs rootless through `podman compose` with podman-compose and the `net.ipv4.ip_unprivileged_port_start=80` setting the guide documents. Full manual runs, the nightly run and the manual `vm-deployment` diagnostic run both jobs; a ready pull request runs them only when it changes `cargo-rullst/src/generators/deploy.rs`, `generators/project/docker.rs`, `blueprints/deploy.rs` or the check (otherwise the job succeeds without building), and branch pushes skip them, so the push inventory that site-only admission reads is unchanged. They are not listed in the release policy's required jobs, but a failure fails the full manual run that admission requires. Real DNS, Let's Encrypt, provider accounts, Arm images and reboots are not covered. Manual all-platform `all` and `cli-standard` selections also call the native CLI artifact builder for Linux x64, Windows x64, macOS ARM64 and macOS x64. The committed target inventory selects explicit runner labels. Both executable entry points must run and report the candidate version before bounded files, digests and source/platform metadata are retained. These ordinary CI artifacts have no release tag and no installation or publisher-verification authority. Exact full-candidate admission requires all four native jobs; a diagnostic CLI shard still does not replace the full matrix. The tag-only release also calls this builder after protected release-branch admission. The separate attestation job verifies downloaded checksums and includes native executables/manifests in build provenance without executing source or binaries. The GitHub release job adds those assets only after attestation and registry publication succeed. This prepares distribution, not an installer; no new installation command or platform recovery claim is implied. The existing Linux `workspace` shard also requires `RULLST_UI_BROWSER_TESTS=1` with Node 24 and Chromium. It feeds HTML from the real Nexus renderer and the Portfolio example's view (`examples/portfolio`) into the bounded `.github/mobile-ui-browser-smoke.mjs` contract: drawer dismissal/focus/no-JS behavior, responsive layout at 320–1440px, long content and reduced motion. Missing browser prerequisites fail that shard. Other platforms still run the render assertions and the complete existing project matrix; they do not claim Chromium evidence. Local runs opt in with the same flag and an absolute `RULLST_UI_BROWSER_SCRIPT` path. CDN resources are blocked in this UI fixture. The Nexus run serves the shell with the default production CSP and its real same-origin assets, and fails on a CSP violation; this is not live-provider, WebKit/Firefox, hardware-device, or WCAG certification. No extra Rust compilation matrix is added. GitHub executes `schedule` events from the repository's default branch, so scheduled checks follow v13 development on `main`. Stable `v12` receives its own push/PR checks and explicit manual campaigns; a scheduled `main` run is not stable-line evidence. Tag publication remains deliberately unavailable through a manual button. ## Manual and periodic execution map Every verification workflow except the PR-context-only `ai-sentinel-pr.yml` and `mutants-pr.yml`, reusable `cli-artifacts.yml` and tag-only `release.yml` can be started from **Actions → select workflow → Run workflow**. A manual run checks the selected branch's current SHA; record that SHA and the run URL before treating it as release evidence. The release workflow intentionally has no button because its publication authority begins only with an exact version tag. Fuzz target jobs have one bounded exception to repeating expensive work on an unchanged input surface: [verified evidence reuse](https://github.com/Rullst/Rullst/blob/main/docs/src/fuzz-evidence.md). The candidate still requires its own successful fuzz workflow; the admission checker independently verifies every reused original job and source digest. No other workflow gains cross-commit credit from this exception. The workflows below run **only when requested manually**: | Workflow | Evidence | RC interpretation | | :--- | :--- | :--- | | `dast-zap.yml` | OWASP ZAP baseline against a release blog showcase, a fresh generated REST API and the LMS example (`examples/lms`, release-built from the workspace) | REST/LMS warnings and failures block unless an exact rule ID is versioned as `INFO` with a local explanation in `.zap/`; those configs are passed explicitly to the pinned scanner and unlisted warnings remain live. The showcase is informational because it deliberately uses third-party presentation assets; reports and application logs are retained. This remains representative, not universal deployment coverage. | | `fuzzing.yml` | All 42 declared libFuzzer targets from the validated shared inventory | **Required release evidence:** release mode validates all 42 targets and independently verifies reusable input-equivalent evidence; packages with remaining targets run locked compilation preflights and new 5.5-hour campaigns; target-specific corpora are restored and saved, while failure reproducers are retained. Dependency-lock drift fails preflight, campaign and corpus jobs. The proc-macro parser uses strict processes of at most 30 minutes sharing one corpus, which bounds sanitizer RSS without weakening the total budget. Original reused results must be at most seven days old, come from an ancestor on the same release line, and prove the complete budget. The final candidate still runs the evidence boundary and publication independently recomputes all 42 results. [Reuse policy](https://github.com/Rullst/Rullst/blob/main/docs/src/fuzz-evidence.md). A strict five-minute single-target diagnostic accelerates correction feedback but is explicitly ineligible as release evidence. This is bounded evidence, not proof for every input. | | `kani.yml` | Twenty named bounded formal harnesses in ten supported runtime/library packages | **Required release evidence for the declared harnesses:** every proof has an isolated strict matrix job. Rullst itself stays on stable Rust 1.99.0 with a Rust 1.96 MSRV; only the separately built Kani verifier uses its pinned `nightly-2026-08-01` compiler (`rustc 1.99.0-nightly`) because the latest stable Kani bundle's Rust 1.93 compiler cannot compile the framework. The proc-macro-only `rullst-macros` target remains unsupported by Kani and is covered by compile-pass/fail and generated-project evidence instead. | | `miri.yml` | Randomized-layout Miri execution over 15 named pure-Rust/default-feature scopes | **Required release evidence for the declared scopes:** every selected scope is strict. This nightly-only interpreter uses pinned `nightly-2026-08-21` (`rustc 1.100.0-nightly`); it does not change the project's stable toolchain or MSRV. Native FFI, OS syscall, network/provider, umbrella re-export, and example-application boundaries are excluded explicitly rather than emitted as tolerated errors. | | `mutants.yml` | A source-bound discovered inventory, eighty lossless shards with at most four running concurrently, their artifacts and a strict aggregate | Informational: a cheap all-feature `--list --json` preflight records the selected source's complete unique inventory before runners start; every shard then uses that release surface, and aggregation requires every reviewed candidate to receive exactly one classification before reporting the conservative caught percentage. A targeted mode retests one validated production Rust file after a correction, with explicit per-mutant test/build timeouts and an optional library-only test scope; it does not replace the complete campaign. Every shard has a pinned Redis service, so the `RULLST_TEST_REDIS_URL` contracts run for each mutant. A contract mode runs one owned-server wrapper's mutation mode, which mutates only that wrapper's PostgreSQL or Redis adapter files and runs only its contract tests against the server it owns; it checks its own outcome count instead of the workspace inventory and aggregate. Recovery modes accept only the repository-reviewed campaign policy, bind the source/run/branch/tool/inventory digest, bisect explicitly authorized failed fragments, reuse immutable successful artifacts and emit a content-addressed aggregate. Missed/time-out exit codes remain findings, while a broken baseline, incomplete artifact set, preflight/classification mismatch, invalid invocation or cargo-mutants internal failure fails the workflow. The v12 campaign completed 14,391/14,391 classifications in run `34761010296`; “pass” does not mean every mutant was killed. | The v13 candidate also provides `verus.yml` as a manual-only pilot. Its source linkage/proof/negative-control checks are strict when invoked, while the pilot remains outside release admission. A new workflow must first be available in GitHub's default-branch workflow registry before its manual button/dispatch can be assumed available; prepared YAML is not hosted evidence. These workflows are **periodic and manually runnable**: | Cadence | Workflows | Mode | | :--- | :--- | :--- | | Daily | `audit.yml`, `ci.yml`, `sanitizers.yml` | Cargo Audit (default branch and `v12` locks) is blocking; the nightly `ci.yml` run executes the complete Linux/macOS/Windows matrix on `main`; TSan/ASan are blocking when executed. | | Weekly | `bench.yml`, `cargo-deny.yml`, `codeql.yml`, `corpus-sync.yml`, `coverage.yml`, `documentation.yml`, `pqc-compliance.yml`, `proptest.yml`, `scorecards.yml`, `security-audit.yml`, `trufflehog.yml`, `udeps.yml` | The inventory below identifies which results are blocking, automated evidence, or informational. Corpus sync warms and minimizes the same 42 validated target corpora with bounded parallelism. | All remaining test/build workflows run on the documented push, pull-request or path filters and also expose a manual rerun. For an RC or stable checkpoint, first use the automatic mainline suite, then run the exact-SHA manual admission set: full-platform `ci.yml`, release-mode `fuzzing.yml`, `dast-zap.yml`, `kani.yml`, `miri.yml`, `sanitizers.yml` and `proptest.yml`. The tag workflow checks those receipts through `.github/release-required-workflows.json`; the fuzz receipt is accepted only when all 42 named target jobs and its evidence boundary succeeded. Physical devices, store approval, live provider accounts, external security review and human release approval remain outside GitHub Actions. ## Required local and release baseline The contributor baseline from `AGENTS.md` is: ```bash cargo test --workspace --all-features cargo clippy --workspace --all-features -- -D warnings cargo fmt --all ``` The main CI uses the stricter all-target Clippy form and checks formatting without modifying files: ```bash cargo fmt --all -- --check cargo clippy --workspace --all-targets --all-features -- -D warnings cargo test --workspace --all-features ``` Rust CI disables Cargo incremental compilation and uses the pinned `sccache` Action and binary to store content-addressed compiler outputs in GitHub Actions cache. The current `cc` build dependency also honors the same Rust compiler wrapper, so compatible bundled DuckDB C++ objects can be reused. Same-repository pull requests can populate only their GitHub-isolated `refs/pull/.../merge` cache scope, making a failed-job rerun useful without modifying the trusted default-branch namespace. Fork pull requests remain read-only. Pushes and explicit manual runs on `main` populate entries reusable by later pull requests. Cache contents never substitute for a test result. The tag-only verifier uses the same namespace strictly read-only, so it can reuse an exact trusted compiler output but cannot alter the cache while creating release artifacts; every release command and assertion still runs. The setup action's job-scoped Cargo archive is disabled in these compiler-cache jobs, so neither the raw workspace `target` tree nor duplicate registry bundles compete with compiler objects. CLI integration tests create and remove nested Cargo targets, and archiving whole mutable trees previously produced false missing-directory annotations, duplicated roughly 9.56 GiB across twenty active main caches, and caused eviction churn at GitHub's default 10 GiB repository limit. Cargo may redownload registry sources on a fresh runner; this small network cost is preferable to storing the same registry/target archive under many job-specific keys. A first run on a new cache namespace is still a cold build; evaluate acceleration using the reported cache hit ratio and a later compatible run, never by weakening or omitting assertions. The same content-addressed approach accelerates LLVM coverage, benchmark, mutation/fuzz compilation and the scheduled release-mode regression suite. The weekly corpus workflow warms the same fuzz compiler namespace used by the manual campaign. Benchmarks remain sequential on one runner so comparisons do not mix host variance. Coverage deliberately remains one report job because splitting it without a reviewed profile-data merge could change the repository percentage. Its second default-feature pass is limited to ORM, Studio and the public facade: those are the packages with default-SQLite tests excluded by the mutually exclusive all-feature graph, so unrelated workspace tests are not repeated. The release-mode workspace is safe to split because every shard returns an ordinary test result, while the two source locations that actually use `proptest!` still receive their configured 10,000-case runs. SemVer checks fan out from the machine-readable release order and validate one published API per job, so adding or removing a release package cannot silently drift from the matrix. The all-feature coverage pass uses pinned cargo-nextest to schedule the same discovered unit and integration tests concurrently. It performs no retry and a flaky retry could not be normalized into success. A repository profile fixes four global slots and permits at most two integration tests that launch nested generated-application Cargo builds, preventing compiler fan-out from trading latency for memory or disk exhaustion. The exact per-test JUnit result and duration record is retained for 30 days. This runner is a coverage scheduler, not a replacement for Rust CI: the required multi-platform shards continue to execute the complete inventory with ordinary `cargo test`, preserving both traditional libtest process semantics and the materialized-project gates. CodeQL runs separate Rust and JavaScript/TypeScript analysis jobs. The Rust job's Cargo target cache is disabled so the extractor observes compilation for the exact SHA instead of inheriting a fresh artifact from another run; the analysis database itself is not interchangeable with ordinary test shards. Rust CodeQL's faster buildless mode is intentionally not used because manual compilation gives the extractor the stronger generated code boundary needed by this release. The JavaScript job uses buildless analysis and retains the `/language:javascript` category to compare against existing mainline results without leaving that language unexamined. `ci.yml` also compiles and exercises each ORM strict database feature in isolation (PostgreSQL, MySQL, and SQLite), exercises the runtime-only Core and all 70 public umbrella features in isolated additive graphs with automatic manifest-drift detection, runs the portable database matrix on Linux, and tests the complete all-feature workspace in seven parallel shards on Linux, macOS, and Windows. Feature-boundary rows and threat-model negative tests also fan out into four deterministic strict shards each; their matrix job remains a single blocking dependency for the quality scorecard. Each threat-model shard primes the reviewed lockfile before its deliberately offline generated-project checks, so it does not inherit a hidden source-cache dependency from another job. The umbrella's `cfg(doctest)` aggregation reads all 51 public tutorial files directly, so that same command discovers the versioned Rust blocks, compiles or executes complete examples, and records explicitly contextual fragments as ignored instead of pretending they are standalone programs. Its pinned live Redis job also proves that scheduled Core jobs are not claimed early, that Core cache inspection returns bounded metadata without values, plus ORM cache hit/TTL/recovery, tenant/table invalidation, rollback preservation, process-local post-commit observers and Scout commit ordering. A separate SQLite outbox contract runs on all three operating systems and covers atomicity, conflicting idempotency keys, claim races, lease expiry, retry and dead-letter; the relational matrix repeats the core outbox lifecycle against PostgreSQL, MySQL, MariaDB and strict SQLite. A dedicated job checks the declared MSRV, Rust 1.96.0. The Linux provider matrix also runs the feature-gated Scout adapter against a digest-pinned Meilisearch image; Algolia and Elasticsearch use bounded local protocol fixtures because no hosted provider account is part of CI. The same matrix runs typed, parameterized L2 and cosine queries against a digest-pinned PostgreSQL + pgvector image. It also runs Nexus's default Any/SQLite HTTP contract explicitly, because the global all-feature graph intentionally selects a strict database profile and excludes that materialized tenant/audit target. Coverage separately merges the default workspace pass, so those routes contribute real executed-line evidence. After a ready-PR or manual full-matrix Rust CI run finishes, an observational job emits a SHA-bound per-crate quality scorecard into the workflow summary and a 90-day artifact. The score combines versioned expert-audit ceilings with the actual gate results; a failed/skipped/cancelled gate can remove the dimensions it was meant to prove, while a green gate cannot inflate a crate beyond its audited ceiling. This is engineering-evidence reporting, not capability completion or certification. See the [scorecard methodology](https://rullst.github.io/Rullst/book/quality-scorecard.html). Rows with no feature selected compile every package target. Feature-selected rows compile the isolated library graph; feature-enabled tests, examples, and benchmarks remain covered by the workspace and specialist jobs. This avoids pulling unrelated development dependencies into every boundary while retaining real integration coverage. The tag-only packaged-distribution gate reads the complete feature set from the extracted `rullst` package manifest and compiles that crates-only consumer with defaults disabled and every public feature enabled. A partial hand-maintained feature allowlist therefore cannot make a monorepo-only integration appear release-ready. ## Recommended branch-protection profiles **`main` (v13 development)** requires the fast gate that every ready pull request emits: Code Quality & Format; Run Tests (ubuntu-latest / workspace) and (ubuntu-latest / cli-standard); Check MSRV (Rust 1.96.0); ORM strict-sqlite, strict-postgres and strict-mysql; Redis live contracts; Generated release access boundaries; Facade shared-local recovery composition; Versioned deterministic AI evals; Build book and validate local links; Validate workflow syntax and expressions; Enforce Zero Panics (published runtime targets); Enforce production unsafe boundary; Check Dependencies (Licenses, Vulnerabilities); Scan for leaked secrets; Enforce reviewed internal dependency graph; and Typo Check. It does not require an up-to-date base: the full `main` push run and the nightly matrix detect interactions between independently green pull requests, and each additional rebase would otherwise repeat the whole gate. The two Cargo Audit workflows emit an ambiguous `audit` context, so Cargo Deny's advisory check is the required vulnerability gate. The macOS/Windows and CLI-profile shards, public feature boundaries, threat-model minimum, coverage, SemVer and CodeQL run after merge, nightly or weekly. They remain release-admission requirements. **`v12` (stable maintenance)** keeps the complete profile with an up-to-date base: every Linux/macOS/Windows test shard, public feature boundaries, the threat-model minimum, CodeQL, documentation and workflow lint. Do not configure a path-filtered, scheduled, manual, deployment, or tag-only workflow as a universal required check: an intentionally skipped workflow may never create the check context. In particular, IoT Cryptography Containment is blocking when relevant paths change. Pages, benchmarks, fuzzing, sanitizers, Kani, Miri, mutation testing, udeps, ZAP, Scorecard, and release provenance belong to deeper evidence or release policy. GitHub repository rulesets remain the enforcement source; this document records the recommended profile and does not claim that the hosted setting is already enabled. ### Observed v13 protection On 20 September 2026 UTC, the hosted `v13` branch protection was enabled and read back from the GitHub API. It requires 43 existing GitHub Actions contexts: the 25 Linux runtime jobs, all sixteen corresponding macOS/Windows test shards, documentation and workflow lint. Contexts are bound to GitHub Actions, the base must be current, a pull request and resolved conversations are required, and administrators are included. Force pushes and branch deletion are disabled. No additional human approving review is required. These are hosted settings at that observation, not a guarantee that an administrator cannot change them. This initial merge profile does not replace the release policy's 28 complete workflow receipts, manual native/security jobs, package inspection or protected crates.io deployment approval. The additional automatic security workflows are enabled on v13 by this preparation change; the profile above names only checks already emitted by the integration candidate. ## Phase 4 release-engineering status | Release-engineering goal | Current status | Assessment | | :--- | :--- | :--- | | Trifecta with all features | **Implemented** | CI and tag release both run format, all-target/all-feature Clippy, and all-feature tests. | | Strict DB features in isolation | **Implemented in workflow** | `strict-postgres`, `strict-mysql`, and `strict-sqlite` compile independently and each runs a backend-specific CRUD test with only the selected strict feature enabled. | | Honest blocking/informational labels | **Implemented** | Unsafe and Wasm checks are blocking in continuous CI; the declared Kani, Miri and fuzzing scopes are strict release gates; mutation testing and udeps explicitly remain informational. | | Cover every fuzz target | **Implemented in workflow** | `.github/fuzz-targets.json` is the shared inventory for the manual campaign and corpus maintenance. A blocking validator compares it with all eleven fuzz manifests and their 42 source files. This records configuration, not a successful six-hour run. | | Package all crates before publishing | **Implemented in workflow** | The tag-only release validates versions, packages all publishable workspace crates, hashes and attests the archives, then publishes in dependency order. | | Unified evidence bundle per tag | **Implemented in workflow** | The tag-scoped bundle contains `Cargo.lock`, Cargo metadata, CycloneDX 1.5 SBOM, Cargo Audit JSON, `deny.toml`, bounded compliance evidence, governed advisory exceptions, commit/tag context, and checksums. The bundle and `.crate` archives are included in build-provenance attestation. | | Align manifest, changelog, tag, registry, and notes | **Implemented in workflow** | Preflight validates manifests and tag, extracts the exact dated changelog section into the GitHub release, and verifies registry checksums after publication. | ## Important evidence boundaries ### Zero-panics and unsafe Rust `zero-panics.yml` denies Clippy's unwrap, expect, panic, todo, and unimplemented lints for published runtime libraries, procedural-macro engines, CLI production targets, generated runtime templates, and the Wasm Core path. Tests are excluded where assertion panics are test semantics. `unsafe-policy.yml` compiles production libraries and binaries with `-Dunsafe-code`. The exact reviewed source allowlist contains the Radar OS probe, dynamic-library loader, the CLI's Windows cache/installation owner/DACL boundary, Windows project access-policy preservation and macOS extended-ACL inspection. Both inner and outer unsafe-lint attributes enter the source inventory. The macOS module borrows an open descriptor, inspects the first ACL entry and frees the returned allocation; it never edits an ACL. Its native regression adds an ACL and verifies that replacement rejects without discarding it. Windows descriptors are installed atomically and validated through owned handles. Each unsafe call documents pointer/handle ownership and lifetime; the workflow fails if the source allowlist changes. Windows ACL counts come from `GetAclInformation` into owned output structures; ACE pointers must be non-null before creating bounded borrowed views. SID lengths are checked before OS validation and no borrowed pointer outlives its descriptor. This is an enforced boundary, not a claim that all dependencies contain no unsafe Rust. ### Coverage `coverage.yml` runs LLVM coverage over workspace all-features and default profiles plus the live database matrix, then uploads LCOV to Codecov using GitHub OIDC rather than a long-lived upload secret. It also retains exact JSON and text line summaries for 30 days so a passing upload cannot be confused with the coverage percentage. The all-feature pass uses pinned cargo-nextest only to run the same discovered tests concurrently, with zero retries, bounded nested-Cargo concurrency and a retained JUnit execution record. Rust CI still runs ordinary `cargo test` across Linux, macOS and Windows. The default-profile pass explicitly includes ORM, Studio, Nexus and the umbrella facade so their real SQLite contracts are not hidden by mutually exclusive all-feature database profiles. Before upload, the workflow independently rejects an LLVM summary below 90% for either the whole repository or the governed framework-library paths. `codecov.yml` also requires at least 90%, with zero tolerance, for those views and changed lines; failure to upload LCOV also fails the workflow. The report filters examples, benchmarks, auxiliary test support, and separate test files. CLI and proc-macro code therefore remains part of the blocking repository aggregate and is additionally visible as informational components. Their stronger semantic evidence still comes from materialized scaffolds and compile-pass/compile-fail contracts. The README exposes both the public overall badge and the separate `framework_libraries` badge rather than substituting the higher component result for the repository total. ### Formal, dynamic, and stress analysis - Kani and Miri are manual research evidence scoped to the harnesses/packages that actually execute. Rullst itself remains pinned to stable Rust 1.99.1 and keeps Rust 1.96 as its declared MSRV. Kani builds reviewed upstream revision `8fcd6d90ed07b559e553ca8a92b95f2db69b2c78` with the verifier's own pinned `nightly-2026-08-01` compiler (`rustc 1.99.0-nightly`) into a bundle and installer, then treats proof failures in twenty isolated harness jobs across ten supported packages as real matrix failures. That revision intentionally predates a `compare_bytes` compiler crash reproduced with Kani's first `rustc 1.100.0-nightly` snapshot. The workflow does not patch manifests, bypass MSRV data, or change Rullst's stable toolchain. Kani cannot verify the proc-macro-only `rullst-macros` target. Miri, which only runs on nightly Rust, pins `nightly-2026-08-21` (`rustc 1.100.0-nightly`) and strictly executes 15 named pure-Rust/default-feature scopes. Its matrix excludes native `ring`, AWS-LC, SQLite, OS-syscall and network/provider execution that Miri cannot interpret; native CI, integration tests and sanitizers remain the applicable evidence for those paths. The `rullst` umbrella re-export facade and Blog example add no separate interpreter scope. A selected-scope failure fails the run. The Kani Security harnesses prove pure production decisions such as Vault key-ID character policy, DLP buffer admission, ASCII-folded RASP matching, SRI asset limits and bounded Login Guard delay; the IoT matrix also proves the complete CoAP option-component classification. They do not claim that Kani verifies `zeroize`'s unsupported inline assembly, cryptographic implementations or the entire concurrent middleware implementations. - Mutation testing is manual, split into 80 lossless shards with at most four running concurrently, and intentionally informational. A new campaign discovers its selected source's inventory; the historical 14,391-candidate receipt is not a permanent limit for later sources. Before the expensive matrix starts, a fail-fast `--list --json` preflight verifies the exact unique candidate set; the final aggregate must match that reviewed list, not merely its count. Full mode optionally accepts a complete `source_sha` that must be an ancestor of the workflow commit, allowing a repaired controller to measure an immutable published release. Provenance distinguishes the measured source from the workflow source. Targeted and recovery modes reject that override; recovery still enforces its reviewed historical digest. The hosted command makes `--all-features` explicit and `.cargo/mutants.toml` applies the same feature policy locally; the ignored legacy root configuration and its exclusions were not silently activated. Targeted mode accepts exactly one tracked production `.rs` path so a correction can be retested without restarting the complete workspace campaign. Each targeted run uses its own per-file concurrency group, an explicit per-mutant test timeout (`timeout_seconds`, default 300 s, which also bounds the unmutated baseline) and a 300 s build timeout, so one hanging mutant cannot consume the job budget. Its optional `test_scope=lib` runs only the package's library unit tests (`cargo test --lib`) to avoid rebuilding generated projects in slow integration suites; that narrower scope can report a mutant as missed when only an integration test would catch it, and such a mutant still gets a unit test. Each targeted test binary runs under an 8 GiB address-space limit (`prlimit` as the Cargo test runner), so a mutant that allocates without bound aborts its own tests instead of exhausting the runner. An optional `shard=k/n` slice (n at most 16) splits a file that is too slow for one job; the preflight inventory and the aggregate then cover exactly that slice. Every full and targeted shard has the pinned Redis service of `ci.yml`'s Redis job and sets `RULLST_TEST_REDIS_URL`, so the Security rate-limit and Core cache inspection Redis contracts run in the baseline and for every mutant (Core queue scheduling and the ORM Redis data matrix start their own testcontainer). The ORM query-cache and hash-scope Redis contracts need the non-strict pool, so the all-feature build compiles them out and no mutation run reaches them. [Contract mode](https://rullst.github.io/Rullst/book/workflows.html#owned-server-contract-mutation-runs) covers the adapters that only a wrapper-owned server reaches. Recovery is governed by a committed, versioned policy binding the originating and continuation runs, attempts, branches, workflow identity, measured source SHA, cargo-mutants version, exact shard set and inventory digest. It bisects only authorized incomplete fragments and combines them with successful immutable artifacts. The content-addressed mixed aggregate still has to match the complete reviewed inventory exactly once. Exit statuses for missed and timed-out mutants remain findings; baseline, usage and internal failures do not get normalized into green jobs. The aggregate also fails closed when an artifact is absent, a shard is incomplete or the reviewed full inventory drifts; its conservative percentage never treats a timeout as caught. - Fuzzing and corpus maintenance pin `nightly-2026-08-21` instead of following a moving nightly alias. This verifier-only toolchain does not change the framework's stable Rust 1.99.1 toolchain or its Rust 1.96 MSRV. Before a release campaign starts, eleven strict preflight jobs compile every target so a stale import or broken fuzz manifest fails in minutes rather than alongside hours of valid campaigns. A five-minute one-target diagnostic is correction feedback only; the evidence-boundary job refuses to call it release evidence. Each of the eleven fuzz packages has a checked-in dependency lock; inventory validation requires it, and locked metadata plus a post-command drift check prevents preflight, campaign or corpus maintenance from silently resolving a different dependency graph. The parser campaign restarts its ASan process every 30 minutes while retaining one corpus and the full 5.5-hour target budget, preventing instrumentation RSS accumulation from masquerading as a parser crash. - Branch coverage, `cargo-udeps`, TSan and ASan share the reviewed `nightly-2026-08-21` analysis snapshot instead of following a moving nightly alias. The first two remain observational/informational; sanitizer failures remain blocking whenever their daily/manual matrix executes. - `cargo-udeps` is weekly/manual and explicitly non-blocking. - TSan and ASan run daily/manual across twelve runtime/domain packages; Messaging runs its integration contract so its concurrent state is actually exercised rather than reporting a zero-test library pass. There is no MSan job in the current sanitizer workflow. - The manual ZAP workflow materializes, release-builds and migrates a fresh REST API through the real CLI, and release-builds and migrates the LMS example from the workspace (the LMS starter moved to `examples/lms` in 13.0.0-alpha.4). Both baselines fail on any warning/failure, preserve INFO observations and use no ignored rules. The release blog showcase is scanned separately but remains informational because its documented presentation boundary deliberately uses a relaxed CSP and third-party assets. Its rules retain those external-asset findings for review and reduce only evidenced token/state signals or escaped showcase reflections to INFO; they do not hide findings with `IGNORE`. These three targets are representative evidence, not coverage of every starter, authenticated role, browser, proxy or deployment. - Property tests and benchmarks are scheduled/manual evidence. The property workflow preserves the complete all-feature release-mode regression suite in seven parallel shards and separately runs the ORM and Connect property contracts with 10,000 generated cases. The eight published benchmark groups, backed by nine Criterion binaries, emit non-blocking alerts at a 20% regression and feed the [public benchmark hub](https://rullst.github.io/Rullst/benches/); they are not a promise against every nanosecond-level regression. ### Pull-request mutation gate `mutants-pr.yml` keeps the campaign's result from decaying between campaigns: on every pull request to `main` that changes Rust, it mutates only the changed lines and fails when a mutant survives. Resolve a red result before merging, with a test that fails with the mutant or, when the mutant is truly equivalent, with an entry in the list below. Branch protection does not require the check yet. `.github/mutants-pr-exclude.txt` lists the files it skips: the workspace example applications, test-only modules and adapters exercised only by wrappers that own a disposable PostgreSQL, MySQL or authenticated Redis server. Those files remain the manual campaign's responsibility, through its [contract mode](https://rullst.github.io/Rullst/book/workflows.html#owned-server-contract-mutation-runs). The CLI's integration suites build generated projects, which is too slow to repeat for every mutant, so `.github/mutants-pr.sh` runs two passes. The workspace pass tests each mutant with its package's whole test suite and leaves `cargo-rullst` out; the CLI pass mutates only the files listed in `.github/mutants-pr-cli.txt` and tests them with the CLI's library unit tests (`cargo test --lib`), the scope of a targeted `mutants.yml` run with `test_scope=lib`. A CLI file joins the list once such a run leaves no missed mutant apart from accepted equivalents; the other CLI files are not mutated by the gate. ### Accepted equivalent mutants The v13 focused campaign (targeted runs of security and correctness files before 13.0.0-alpha.1), the local whole-crate campaign that followed it (`rullst-iot`, `rullst-privacy`, `rullst-labs`, `rullst-supervision`), the security campaign (targeted runs of every `rullst-security` file), the `rullst-auth` campaign (targeted runs of every file outside the PostgreSQL adapters), the `rullst-core` campaign (targeted runs of its security and correctness files and, before 13.0.0-alpha.3, of every remaining file the first pass measured, with the Redis service), the `rullst-nexus` campaign (targeted runs of every file; the all-feature build selects the PostgreSQL pool, so the PostgreSQL SQL matrix test reaches the database paths wherever Docker is available), the `rullst-capital` campaign (targeted runs of every file; mutants that only a live Stripe API response can reach are left to provider sandbox testing), the `rullst-messaging` campaign (targeted runs of every file, with the recurring PostgreSQL and Redis Streams contract runs for those adapters), the service-backed campaign (the `passkey-postgres`, `auth-recovery-postgres`, `privacy-postgres` and `mail-postgres` contract runs, and targeted runs of the Redis rate limiter with the Redis service) and the sensitive-file campaign (targeted runs of the OAuth/OIDC, attachment, link-security, tracking, egress and guardrail files of `rullst-connect`, `rullst-mail` and `rullst-ai`; the peer-address, status and content-type checks of `EgressFetcher::fetch_bytes` run only after a connection to a public HTTPS origin, which the egress policy refuses to loopback test servers, and are left to live integration testing; in `rullst-core`, the `pool_ready` guard of `run_artisan_command` needs an already initialized ORM pool, which the all-feature build can only open against a PostgreSQL server: `rullst-core/tests/artisan_open_pool.rs` opens one in a PostgreSQL testcontainer and runs `db:status` in a child process, so runs with Docker measure the guard, and runs without it skip the test unless `RULLST_REQUIRE_TESTCONTAINERS=true`), the `cargo-rullst` campaign (targeted runs, with the CLI's library unit tests, of the self-update, audit, `add` and consumer-file, doctor, deploy and `upgrade` files; it covers the files `.github/mutants-pr-cli.txt` lists, and ROADMAP.md names the survivors left in the others) and the Studio remote-mode campaign (targeted runs of every `rullst-studio/src/remote` file and of the `rullst-studio` binary with the package's tests) gave every missed mutant in their files a test, except these. Each change has no observable effect in the all-feature Linux build the campaign measures, so a test cannot tell it apart. Keep this list short: a new entry needs the same one-line justification. The pull-request gate (`mutants-pr.yml`) skips an entry only when its exact cargo-mutants name is also listed in `.github/mutants-pr-equivalents.txt`. | Mutant | Why it is equivalent | | --- | --- | | `rullst-core/src/queue/deferral/window.rs`: `TimeWindow::length_ms`, `>` with `>=` | `TimeWindow::daily` rejects equal start and end times, so the two never compare equal. | | `rullst-core/src/queue/deferral/window.rs`: `TimeWindow::intervals`, `day < until_ms` with `<=` | The extra day starts at or after `until_ms`, so it never yields an interval. | | `rullst-core/src/testing/html_normalize.rs`: `mask_csrf_headers`, `found < copied` with `<=` | `copied` indexes the closing quote of a masked value, so no `x-csrf-token` key can start there. | | `rullst-media/src/s3/signing.rs`: `settings` with `Default::default()` | Adapter URLs hold only unreserved characters, `%XX` and `/` without empty or dot segments, so single or double encoding and path normalisation give the same canonical URI. | | `rullst-mail/src/facade/driver.rs`: `DigestHasher::finish` with `1` | `finish` is never called; the digest is read from the SHA-256 state. | | `rullst-mail/src/facade/driver.rs`: delete the `(Some(_), Some(_))` arm of `ses_credentials` | That arm exists only without the `aws-ses` feature, which the all-feature build does not compile. | | `cargo-rullst/src/generators/audit_report/a11y.rs`: `attributes`, the end `index + 1` after `>` with `index - 1` or `index * 1` | The tag scanner resumes at the next `<`; the byte before a closing `>` cannot open a tag, because a tag name never starts with `>`. | | `cargo-rullst/src/generators/green/procfs.rs`: `ticks_per_second`, `> 0` with `>= 0` | The clock tick rate is always positive. | | `cargo-rullst/src/generators/green/procfs.rs`: the non-Unix `ticks_per_second` with `Some(0)` or `Some(1)` | Not compiled on Linux. | | `cargo-rullst/src/generators/doctor/probe.rs`: `run`, `stdout.len() > OUTPUT_LIMIT` with `>=` | An output of exactly `OUTPUT_LIMIT` bytes is cut at its own length, which leaves it unchanged. | | `cargo-rullst/src/generators/doctor/database.rs`: `tcp_reachable`, delete the `TimedOut \| WouldBlock` arm | `TcpStream::connect_timeout` reports its timeout as a `TimedOut` error whose text is `connection timed out`, which the catch-all arm passes through unchanged, and never returns `WouldBlock` on Linux. | | `cargo-rullst/src/generators/output_guard.rs`: `write_output`, the `NotFound` guard of `fs::metadata` with `true` | `reject_symlink` has already returned every other `lstat` error, and for a path that is not a symlink `stat` fails exactly when `lstat` does. | | `cargo-rullst/src/generators/output_guard.rs`: `write_output`, the `!parent.as_os_str().is_empty()` guard with `true` | `tempfile` creates its file for the empty directory `""` in the current directory, as it does for `"."`. | | `cargo-rullst/src/generators/consumer_files.rs`: `reject_links`, the `NotFound` guard with `true` | Any other `lstat` failure of an ancestor (permission, not a directory, name too long) recurs with the same error when the path itself is read or created. | | `cargo-rullst/src/update/project/permissions_unix.rs`: `xattr_names`, `size > MAX_XATTR_NAMES` with `==` or `>=` | Linux never reports a name list longer than 64 KiB, and a list of exactly 64 KiB cannot hold only `security.selinux`, so `has_unreproduced_attribute` refuses it with the same error. | | `cargo-rullst/src/update/cache/unix.rs`: `options`, `NOFOLLOW \| NONBLOCK` with `^` | The two flags occupy disjoint bits, so OR and XOR give the same value. | | `cargo-rullst/src/update/cache/unix.rs`: `load_at`, `FILE_LIMIT + 1` with `-` or `*` | `validate_file` has already refused a file longer than `FILE_LIMIT`. Reading one byte less shortens only a file of exactly `FILE_LIMIT` bytes, whose body `decode` refuses either way because it allows 64 bytes less, and a file that grows during the read is refused either way. | | `cargo-rullst/src/generators/audit_source.rs`: `tokenized_views` and `mark_code_tokens`, `index < tokens.len()` with `<=`, and `tokenized_views`, `index = last + 1` with `last - 1` or `last * 1` | At `tokens.len()` neither attribute lookup finds a token, so the loop steps past the end or stops. Resuming on an item's last token or the one before it revisits its terminating `;` or `{ .. }`, which cannot start `#[cfg(test)]`; at the end of the input a re-found attribute only adds a range inside the item already blanked. | | `cargo-rullst/src/generators/build/upgrade/backup/restore.rs`: `is_link` with `false`, and its Windows reparse-point test with `&` replaced by `\|` or `^` or `!=` by `==` | On Linux `lstat` never reports a symlink as the regular file or directory that `validate_file` and `validate_directory_creation` also require, and the Windows test is not compiled. | | `cargo-rullst/src/generators/build/upgrade/backup/restore.rs`: `restore_from`, the index read's `MAX_INDEX_BYTES + 1` and the staging copy's `expected_size + 1` with `*` | The index and each snapshot have already been bounded by their metadata, so reading one byte more matters only for a file that grows between that check and the read, which a test cannot arrange. | | `cargo-rullst/src/generators/deploy_doctor/input.rs`: `linked` with `false` and its Windows reparse-point test; in `read`, the ancestor `linked(..) \|\| !is_dir()`, the file `!is_file() \|\| linked(..) \|\| len > MAX_FILE` and the opened-file re-check with `&&`, `NOFOLLOW \| NONBLOCK` with `&` or `^`, and `MAX_FILE + 1` with `*` | On Linux `lstat` never reports a symlink as a file or directory, and the Windows test is not compiled. A non-directory ancestor fails the final lookup with `NotADirectory`, and a directory, FIFO or oversized file that passes a weakened check is refused after opening or reading with the same `File` error. The open flags, the re-check and the extra byte matter only when the file is replaced or grows between `lstat`, `open` and the read, which a test cannot arrange. | | `cargo-rullst/src/update/project/snapshot.rs`: the non-Windows `linked` with `false`; in `read`, `NOFOLLOW \| NONBLOCK` with `&` or `^`, the opened-file re-check `!opened.is_file() \|\| linked(..) \|\| len != ...` with `&&`, and `MAX_FILE + 1` with `*` | `lstat` never reports a symlink as a file or directory, so the type checks refuse links either way. `read` has already refused links, special files and files over 64 MiB, so the open flags, the re-check after opening and the extra byte matter only when the file is replaced or grows between `lstat`, `open` and the read, which a test cannot arrange. | | `rullst-studio/src/remote/history.rs`: `Sample::from_outcome`, `window.requests > 0` with `>=` | With no request the rate is `0 / 0`, which `finite` drops, so the sample stores no rate either way. | | `rullst-iot/src/coap.rs` (`CoapRequest::encode` twice, `append_option`), `rullst-iot/src/mqtt.rs` (`MqttPublish::encode`) and `rullst-iot/src/i2c.rs` (`I2cHelper::try_build_read_frame`): `\|` with `^` | The operands occupy disjoint bits, so OR and XOR build the same byte. | | `rullst-iot/src/coap.rs`: `CoapRequest::encode`, `payload.len() > MAX_COAP_DATAGRAM_BYTES` with `==` or `>=` | `CoapRequest::payload` already bounds the payload, and `ensure_room` rejects the same lengths with the same error. | | `rullst-iot/src/mqtt.rs`: `MqttPublish::encode`, `remaining_length > MQTT_MAX_VARIABLE_BYTE_INTEGER` with `==` or `>=` | The builders keep topic and payload far below that limit, and `variable_byte_integer` repeats the check with the same error. | | `rullst-privacy/src/age_assurance/challenge_tokens.rs`: `ChallengeTokens::open_with_clock`, `token.len() > MAX_TOKEN_BYTES` and `payload.len() > MAX_PAYLOAD_BASE64` with `==` or `>=`, the `\|\|` before `payload.is_empty()`, the payload bound and `tag.len() != 43` with `&&`, and `MAX_PAYLOAD_BASE64` with its `+` or `/` replaced by `*` | These pre-authentication bounds only reject early what key lookup, tag verification, the 4096-byte decoded limit or JSON parsing reject with the same `InvalidChallengeToken`: a well-formed token is far below 8192 bytes, and 5463 base64 characters already decode to 4097 bytes. | | `rullst-privacy/src/age_assurance/sqlite.rs`: `SqliteReplayStore::open`, the `error.kind() != NotFound` guard with `false` | Every other metadata failure (permission, name length, symlink loop) also makes the SQLite open fail with the same `StoreUnavailable`. | | `rullst-privacy/src/consent/gate.rs`: `allows_with_clock`, `completed < record.valid_until()` with `<=` | A grant with `completed >= valid_until` has already returned `false`, so the two never compare equal there. | | `rullst-labs/src/sqlite/completion.rs`: `finish`, `lease.until <= tx.now \|\| record.view.expires_at <= tx.now` with `&&` | `narrow_deadline` with the earlier of the two returns the same `Expired`. | | `rullst-labs/src/sqlite/jobs.rs`: `cancel`, `revision <= 0 \|\| record.view.revision != revision` with `&&` | `save_job` updates only the row at that revision, so a stale revision still returns `Conflict`. | | `rullst-labs/src/sqlite/jobs.rs`: `load_job`, `body.len() > 32796`, and `seal_record`, `body.len() > 32768`, each with `==` or `>=` | Bounded record fields serialize far below 32 KiB, and a longer stored body fails authentication with the same `Integrity`. | | `rullst-labs/src/sqlite/jobs.rs`: `payload`, the `\|\|` before the scope and id comparisons with `&&` | The exercise digest covers scope, id and revision, so none of them can differ alone. | | `rullst-labs/src/sqlite/retention.rs`: `expire_queued`, either `\|\|` with `&&`, and `purge_terminal`, the `\|\|` after the state match with `&&` | The SQL selection, `load_job`'s column checks and `Record::validate` already guarantee a queued, expired, unleased record, or an unleased one that is neither queued, running nor `Uncertain` (which always holds its lease). | | `rullst-supervision/src/exam/observation.rs`: `Observation::from_code`, `(code - 10) % 4` with `+` | `(code + 10) % 4` equals `(code - 10) % 4`, because 20 is a multiple of 4. | | `rullst-supervision/src/sqlite/events.rs`: `events`, and `rullst-supervision/src/sqlite/observations.rs`: `observations`, `expires_at > MAX_TIME` with `==` or `>=` | An event is received before its session expires, at least an hour before the session's retention ends at or before `MAX_TIME`, so with its fixed retention it always expires before `MAX_TIME`. | | `rullst-supervision/src/sqlite/observations.rs`: `record_observation`, both source match guards with `true` | `record_browser` and `record_capture` pass only browser or capture observations with the browser source, and `analyze` only presence or audio findings with an adapter source. | | `rullst-supervision/src/sqlite/transaction.rs`: `Operation::begin`, `now < before \|\| now < previous_time` with `&&` | `finish` checks the time against the same high-water mark and returns the same `Clock`. | | `rullst-supervision/src/sqlite/mod.rs`: `resolved_path`, both metadata match guards with `false` | `open` checks for a regular file again, `initialize` creates the file exclusively and fails on any existing entry, and every other metadata error also fails the open with the same `Storage`. | | `rullst-auth/src/auth/app_key.rs`: `parse_dotenv`, delete the `dotenvy::Error::Io` arm | The parser reads an in-memory `&str`, and valid UTF-8 read line by line cannot raise an I/O error. | | `rullst-auth/src/auth/passkey/ceremony.rs`: `ChallengeStore::issue`, `challenge.expires_at > now` with `>=` | It keeps only a challenge whose expiry equals a later `Instant::now()` reading to the nanosecond, which `consume` already rejects as expired. | | `rullst-auth/src/auth/passkey/shared/manager.rs`: `SharedPasskeyAuth::start_authenticate`, `allowed.is_empty() \|\| allowed.len() > MAX_CREDENTIALS` with `&&` | `CeremonyIntent::new` rejects an empty or 33-entry authentication allowlist with the same `InvalidInput` before the store is called. | | `rullst-auth/src/auth/passkey/shared/manager.rs`: `validate_passkey`, the `\|\|` before `first() != Some(&4)`, and `rullst-auth/src/auth/passkey/sqlite.rs`: `validate_passkey`, `\|\|`, each with `&&` | A key that `from_sec1_bytes` accepts has either 65 bytes and tag 4 or neither, so a key failing only one check fails parsing with the same error. | | `rullst-auth/src/auth/passkey/verification.rs`: `verify_assertion`, `FLAG_ATTESTED_CREDENTIAL_DATA \| FLAG_EXTENSION_DATA` with `^` | The flags occupy disjoint bits, so OR and XOR build the same mask. | | `rullst-auth/src/recovery/connection.rs`: the fallback `configure_postgres` and `configure_sqlite` with `Ok(())` | They compile only without `recovery-postgres` or without both SQLite families, which the all-feature build does not compile. | | `rullst-auth/src/recovery/email_login/delivery.rs`: `<impl Drop for WireNotice>::drop` with `()` | Zeroizing a buffer that is being freed has no effect safe code can observe. | | `rullst-auth/src/recovery/email_login/types.rs`: `safe_path`, `part == "." \|\| part == ".."` with `&&` | The byte allowlist that follows already rejects every `.`. | | `rullst-auth/src/recovery/store.rs`: `SqlRecoveryStore::connect`, the `\|\|` before the `recovery-postgres` clause with `&&` | With every feature enabled both feature clauses are always false, so joining them with `&&` changes nothing. | | `rullst-security/src/telemetry.rs`: `SecurityStore::push_live_event`, `events.len() > MAX_LIVE_EVENTS` with `>=` | Truncating a buffer that holds exactly 50 events to 50 changes nothing. | | `rullst-security/src/telemetry/event.rs`: `security_event_schema_version` with `1` | `SECURITY_EVENT_SCHEMA_VERSION` is 1. | | `rullst-security/src/telemetry/event.rs`: `truncate_utf8`, `boundary > 0` with `>=` | `is_char_boundary(0)` is always true, so the loop stops at 0 either way. | | `rullst-security/src/sentinel/pow/codec.rs`: `decode_token`, `token.is_empty() \|\| token.len() > 128` with `&&`, and `>` with `==` or `>=` | Decoding rejects every token that does not decode to exactly 82 bytes with the same `InvalidToken`. | | `rullst-security/src/siem/spool.rs`: `decode_file`, and `rullst-security/src/siem/authenticated/codec.rs`: `read_and_verify`, `events.len() + 1` in the `invalid frame offset` error of `cursor.checked_add(relative_end)` with `-` or `*` | A line end found inside the buffer cannot overflow `usize`, so the closure never runs. | | `rullst-security/src/recovery_codes.rs`: `GeneratedRecoveryCodes::drop` with `()` | Zeroizing the plaintext codes changes only memory that is freed right after; safe code cannot read it. | | `rullst-security/src/mfa.rs`: `verify_totp_step_at`, `code.len() != 6 \|\| !all digits` with `&&` | Every other input fails the constant-time comparison with the six-digit expected code. | | `rullst-security/src/mfa.rs`: `decode_base32` and `generate_totp_at_counter` (three times), `\|` with `^` | The operands occupy disjoint bits, so OR and XOR build the same value. | | `rullst-security/src/schema_guard.rs`: `StrictJsonVisitor::expecting` with `Ok(Default::default())` | serde_json calls a visit method for every JSON value, so the message is never formatted, and `inspect_json_payload` maps every error to fixed text. | | `rullst-security/src/dlp/masking.rs`: `mask_database_passwords`, `pass_end + 1` with `-` or `*` | The next scheme search starts at or just before the `@`, where no scheme can begin. | | `rullst-security/src/log_redactor.rs`: `identifier_continuation`, `end + 1` with `-` | A `_` or `.` followed by a non-alphanumeric byte then yields an empty last component, which is skipped like the identifier without it: neither byte is a separator, quote or space. | | `rullst-security/src/timing_guard.rs`: `TimingScope::finish`, `elapsed < target_duration` with `<=` | Sleeping for a zero remainder returns immediately. | | `rullst-security/src/login_guard.rs`: `record_login_failure`, the window match guard with `true` | `prune_expired` has just removed every counter whose last attempt is a window or more old. | | `rullst-security/src/rate_limit.rs`: `is_rate_limited_in`, the retention `<` with `<=` | Two `Instant::now()` readings would have to differ by exactly the window, and an entry kept at that instant restarts its count on its next use. | | `rullst-core/src/security/pii.rs`: `decode_json_char`, delete the `"`, `\`, `/`, `b`, `f`, `n`, `r` or `t` arm | Each of these escapes decodes to a character that, like the opaque stand-in, is neither a digit, a card separator, an address character nor `@`, and both keep the escape's original spelling. | | `rullst-core/src/security/pii.rs`: `mask_chars`, `idx - start` with `+` and `username_len > 1` with `>=` | A local part of at most one character has nothing to mask after its first character, and skipping to the end of the domain passes only domain characters, never another `@`. | | `rullst-core/src/security/pii.rs`: `mask_chars`, the domain slice start `idx + 1` with `idx * 1` or `idx - 1` | The domain holds a dot, so the leading `@` never reaches the top-level domain; when its only dots are trailing ones, the text after the last dot starts with `@` and fails both domain tests. | | `rullst-core/src/security/csrf_multipart.rs`: `scan`, `available < first.len()` with `>` | A prefix shorter than the first delimiter reaches the loop's two-byte read past its end, which returns the same `Incomplete`. | | `rullst-core/src/security/csrf.rs`: `is_csrf_exempt_path`, the first two `\|\|` with `&&` | `/robots.txt`, `/sitemap.xml` and `/favicon.ico` stay exempt through their `.txt`, `.xml` and `.ico` suffixes. | | `rullst-core/src/queue/mod.rs`: the default `QueueDriver::push_at`, `available_at > SystemTime::now()`, and `Queue::dispatch_at`, `delay > MAX_SCHEDULE_DELAY`, each with `>=` | They differ only when the requested time equals a clock reading taken inside the call, which a caller cannot arrange. | | `rullst-core/src/queue/worker/execution.rs`: `record_outcome`, the `error.is_panic()` guard with `true` | `execute_job` handles every cancelled handler before it calls `record_outcome`, so the only join error left is a panic. | | `rullst-core/src/storage/tenant.rs`: `confine_local_root`, the `!create && NotFound` guard with `false` or without `!` | Below a missing tenant directory, `get`, `metadata` and `delete` then fail with the same `NotFound` that the operation itself reports; `put` creates the directory first. | | `rullst-core/src/cache/memory.rs`: `MemoStore::get`, `now < entry.expires_at`, and `MemoStore::set`, the expiry `retain`'s `<`, each with `<=` | An `Instant` reading would have to equal the entry's expiry to the nanosecond. | | `rullst-core/src/lifecycle.rs`: `begin_draining`, `in_flight_requests() == 0` with `!=`, and `release_request`, `&&` with `\|\|` | `wait_for_drain` waits only on a draining or stopped lifecycle and re-reads the count after every wake-up, so a wake-up that no waiter can receive, or an extra one, changes nothing. | | `rullst-core/src/storage/cloud/multipart/codec.rs`: `MultipartStorage::open`, `checkpoint.0.len() > MAX_TOKEN` with `==` or `>=`, and `bytes.len() < 28` with `<=` | No authentic checkpoint reaches 256 KiB (256 receipts and a 2,048-byte upload id stay under 180 KiB), and a 28-byte token has an empty plaintext; both fail with the same `InvalidCheckpoint`. | | `rullst-core/src/testing/snapshot.rs`: `line_diff`, the deletion test's `table[i * width + j + 1]` with `j * 1` | Without a match, `table[i][j]` is the larger of `table[i + 1][j]` and `table[i][j + 1]`, so `table[i + 1][j] >= table[i][j]` holds exactly when `table[i + 1][j] >= table[i][j + 1]`. | | `rullst-core/src/queue/deferral/planner.rs`: `lowest_intensity`, the tie-break `overlap_start < best_start` with `<=` | A tie at the same start stores the same start and value again. | | `rullst-core/src/live/recovery/types.rs`: the duplicate-free field visitor's `expecting` with `Ok(Default::default())` | `LiveCommand::decode` turns every deserialization error into the same `Invalid`, so the expectation text is never seen. | | `rullst-core/src/rpc.rs`: `request_contract_failure`, delete the `BodyTooLarge` arm | `to_bytes` already refuses a body over the same limit with the same 413 and code, so decoding never reports it. | | `rullst-core/src/rpc.rs`: `RpcFailure::from_detail` with `Default::default()` | Compiled only for `wasm32`. | | `rullst-core/src/rpc.rs`: the test helper `failure_for`, `64 * 1024` with `+` | Test code: every failure envelope it reads is far below 1,088 bytes. | | `rullst-core/src/storage/cloud/multipart/protocol.rs`: `parse`, delete the `allow_dtd` field | roxmltree's default `ParsingOptions` already rejects a DTD. | | `rullst-core/src/live/recovery/session.rs`: `run`, delete the `Ok(Ok(()))` arm | A session ends normally only when the peer closed: tungstenite answers a close frame itself, so the code chosen here never reaches the peer. | | `rullst-core/src/live/recovery/session.rs`: `send_snapshot`, `encoded.len() > 512 * 1024` with `==` or `>=` | A snapshot holds at most 64 KiB of HTML, which JSON escaping expands to under 400 KiB. | | `rullst-core/src/cache/memory/exact_json.rs`: `Display for Inexact` with `Ok(Default::default())` | `is_exact_json` only checks whether serialization succeeds; the error text is never formatted. | | `rullst-core/src/radar.rs`: `get_windows_memory_mb`, `get_windows_process_cpu_usage`, `filetime_100ns`, `calculate_windows_cpu_percent`, and the Windows and other-platform `get_process_cpu_usage` | Not compiled on Linux. | | `rullst-core/src/feature/db.rs`: `DbFeatureDriver::resolve_with`, the freshness test `Instant::now() < entry.expires_at`, and `remember`, the eviction `retain`'s `<`, each with `<=` | An `Instant` reading would have to equal the entry's expiry to the nanosecond. | | `rullst-core/src/cache.rs`: `MemoryDriver::cleanup_if_due`, the sweep's `now < expires_at`, and `inspect`, the filter's `expires_at > now`, with `<=` and `>=` | An `Instant` reading would have to equal the entry's expiry to the nanosecond. | | `rullst-core/src/storage/local_paths.rs`: `reject_symlink_components`, the `NotFound \| NotADirectory` guard with `true` | Any other `symlink_metadata` failure (permission, name length) recurs in the `canonicalize`, `create_dir_all` or write that follows, with the same `Io` text. | | `rullst-core/src/telemetry/distributed/processor.rs`: `MinimizedProcessor::on_start` with `()` | The wrapped `BatchSpanProcessor::on_start` ignores its arguments (opentelemetry_sdk 0.33). | | `rullst-core/src/server/dylib_loader.rs`: `is_expected_loaded_library_removal_error` with `true` or `false` | Not compiled on Linux. | | `rullst-core/src/config/project_setting.rs`: `parse_dotenv_entries`, delete the `dotenvy::Error::Io` arm | The entries are read from a `&str`, which never fails with an I/O error. | | `rullst-nexus/src/nexus/crud/form.rs`: `form_html`, the `Password` guard with `false` | `render_field_widget` renders a `Password` input empty and without a placeholder, whatever value or state it receives. | | `rullst-capital/src/invoice.rs`: `money_to_minor`, `(scaled - rounded).abs() > 1e-7` with `>=` | When `scaled` rounds to at least one minor unit, `scaled - rounded` is a multiple of 2^-53 and never equals the `f64` `1e-7`, so the two differ only for an amount that rounds to zero minor units, which `rounded < 1.0` rejects either way. | | `rullst-capital/src/invoice/pdf.rs`: `render_pdf`, `bytes.len() > MAX_PDF_BYTES` with `==` or `>=`, and the `\|\|` with `&&` | The embedded font is at most 8 MiB and a bounded invoice adds well under 1 MiB, so the document never reaches 16 MiB, and printpdf always writes the `%PDF-` header. | | `rullst-capital/src/providers/mod.rs`: `http_client` with a leaked default client | Adapters only build requests with it; `execute` always sends them through the bounded client from `http::client`. | | `rullst-capital/src/providers/stripe.rs`: `create_customer_portal`, `\|\|` with `&&` | `require_mock_operation` accepts the same empty and `mock_*` keys and the same fixture URL is returned; every other key fails there with the same error. | | `rullst-orm/src/value.rs`: the SQLx `Any` pool's `temporal::date_time`, `naive_date_time`, `date` and `time` with `String::new()` or `"xyzzy".into()` | They exist only without a strict driver feature, and the all-feature build selects the PostgreSQL pool; `the_any_pool_binds_dates_as_iso_text` checks their exact text in default-feature builds. | | `rullst-orm/src/schema/schema_builder.rs`: `prepare_postgres_enums`, delete `!` in the `!definitions.is_empty()` guard | That guard exists only without `strict-postgres`, which the all-feature build enables; `postgres_enum_preparation_validates_before_any_statement` covers it in default-feature builds. | | `rullst-orm/src/pool/dsn.rs`: `percent_decode_utf8`, `(high << 4) \| low` with `^` | The high and low hex digits occupy disjoint bits, so OR and XOR build the same byte. | | `rullst-orm/src/privacy.rs`: the default `ComplianceModel::personal_fields` with `vec![]` | The default already returns an empty vector. | | `rullst-orm/src/polyglot/turso/model.rs`: the default `TursoModel::opaque_columns` with `Vec::leak(Vec::new())` | The default already returns an empty slice. | | `rullst-orm-macros/src/feature_gates.rs`: `redis` and `ai` with `Default::default()` | With every feature the runtime gates are on and the feature is enabled, so both return an empty token stream, which is the default. | | `rullst-orm/src/polyglot/recovery/codec.rs`: `base64url_value`, the digit arm's `byte - b'0'` with `+` | The value is read only for its low four padding bits, and the two results differ by 96, a multiple of 16. | | `rullst-orm/src/polyglot/qdrant/mod.rs`: `LiveQdrant::send_json`, `bytes.len() > MAX_REQUEST_BYTES` with `==` or `>=` | No request reaches 4 MiB: vectors hold at most 65,536 values and payloads at most 1 MiB, so the body stays near 2 MiB; a test still sends a 1 MiB payload. | | `rullst-orm/src/outbox.rs`: `Outbox::install`, `case_insensitive_keys > 0` with `>=` | The MySQL key-collation conversion is idempotent: run on keys that are already `ascii_bin` it leaves the same columns and only costs a table rebuild. The MySQL matrix converts a legacy table, which kills the `==` and `<` mutants. | | `rullst-orm/src/schema/migration_lock.rs`: `MigrationLock::release` with `()` | The lock lives in the dedicated connection's session. The replaced body still drops `self`, which closes the socket and ends the session, freeing the lock as the graceful close does. | | `rullst-messaging/src/webhooks/signature.rs`: `WebhookSignature::from_headers`, the `\|\|` before `kind.len() > 128`, `timestamp.len() > 12` or `signature.len() != 46`, and `tag.len() != 32 \|\| …`, each with `&&` | `MessageId::from_stored`, `EventKind`, the canonical timestamp parse and the strict `v1=` decode repeat each length check with the same `InvalidSignature`, and 43 strict base64url characters always decode to 32 canonical bytes. | | `rullst-messaging/src/sqlite/storage/crypto.rs`: `open`, the `minimum` sum with a `+` replaced so that it shrinks, and `envelope.len() < minimum` with `<=` or `==` | The shortest authentic envelope is 38 bytes; a shorter one fails the key-ID, nonce or tag parse with the same `StorageAuthenticationFailed`. | | `rullst-messaging/src/schedules/crypto.rs`: `open`, `value.len() > MAX_CONTENT_BYTES + 128` with `==` or `>=` or its `+` with `*`, the `\|\|` after it with `&&`, and a `+` in `8 + 1 + 1 + 12 + 16` replaced so that the sum shrinks | Sealed values span 38 to 131,173 bytes, so each change only moves which later parse or authentication step rejects a value, with the same `Encryption`. | | `rullst-messaging/src/schedules/delivery.rs`: `verify`, `lease.namespace != … \|\| now >= lease.expires` with `&&` | Occurrence IDs digest their namespace, so another namespace's lease is not found by its ID, and an expired lease fails the commit deadline; both return the same `InvalidLease`. | | `rullst-messaging/src/wire.rs`: `FIXED_BYTES`, a `2 + 2` with `2 * 2`, and `Reader::is_finished` with `true` | `2 * 2` equals `2 + 2`; `decode` re-encodes the envelope and compares it with the frame, which rejects trailing bytes with the same `InvalidWireEnvelope`. | | `rullst-messaging/src/redis_streams/transport.rs`: `Remote::decode`, delete the `"missing" \| "corrupt"` arm | The catch-all arm returns the same `CorruptStorage`. | | `rullst-connect/src/provider/id_token.rs`: `validate_claims_with_presenters`, the `\|\|` before `nonce.is_empty()` with `&&` | `verify_nonce` compares SHA-256 digests, so a nonce empty on one side only already fails it, and two empty nonces still satisfy the joined condition. | | `rullst-mail/src/security.rs`: `redact_labelled_private_keys`, `MAX_PEM_LABEL_BYTES + 1` with `*` | Reading at most 64 label bytes cuts a longer label inside its run of label bytes, so the `-----` check that follows fails as the length check would. | | `rullst-mail/src/security.rs`: `href_values`, `value_start + length + 1` with `-` or `*` | The scan resumes on the closing quote or the byte before it (the opening quote for an empty value), and no `href` match can contain a quote. | | `rullst-mail/src/inspection/sniff.rs`: `root_element`, `open < close` with `<=` | Both are positions just after one byte, a `[` and a `>`, so they are never equal. | | `rullst-ai/src/ai/egress.rs`: `EgressPolicy::strict` with `Default::default()` | `strict` already returns `Self::default()`. | | `rullst-ai/src/ai/egress.rs`: `is_public_ipv6`, `is_unspecified() \|\| is_loopback()` with `&&` | `::` and `::1` lie outside `2000::/3`, which the next clause rejects. | | `rullst-ai/src/ai/guardrails/markdown_inline.rs`: `open_tag_end`, the self-closing `then_some(index + 1)` with `-` or `*` | The tag then ends on or just before its `/`; the byte before that `/` is whitespace, a quote or a name byte, never a bracket. | | `rullst-ai/src/ai/guardrails/markdown_images.rs`: `definition_destination`, the `<...>` end `end + 1` with `*` | It drops only the closing `>` or line ending, which neither the scheme nor the authority check reads. | | `rullst-ai/src/ai/guardrails/markdown_code.rs`: `indentation`, the tab step `4 - column % 4` with `4 + column % 4` | Indentation is only compared with 4, and either step takes a tab to at least four columns. | | `rullst-ai/src/ai/guardrails/tax_ids.rs`: `token_end`, `bytes.get(index + 1)` with `index - 1` | A separator then joins when a digit precedes it instead of follows it, but the end moves only on digits and a second separator stops both scans, so every token ends at the same digit. | | `rullst-ai/src/ai/guardrails/tax_ids.rs`: `is_valid_tax_id`, the CNPJ `all_digits && token.len() == 14` with `\|\|` | `digit_values` still requires exactly 14 digits, which only a 14-digit token or the CNPJ layout holds. | Timed-out mutants are not counted as caught, and cargo-mutants reports them separately. The campaign rewrote the scanning loops of the WAF detector, the RASP decoder, the HTML snapshot normaliser and the accessibility scanner so that every step consumes input, and bounded the WAF linear-time test, which removed their timeouts. The core campaign ran the queue worker, realtime, multipart CSRF, byteranges and long PII masking tests against a deadline instead, which turned their timeouts into caught mutants; the CLI campaign bounded the update cache's FIFO test the same way. The remaining ones turn forward progress into a loop that never ends without allocating, in code every test of the file reaches, so no single test can fail first: - `rullst-core/src/error_console/store.rs`: `Store::record` with `len() >= MAX_ENTRIES` replaced by `<` pops an empty queue forever. - `cargo-rullst/src/generators/build/upgrade/rules/tokens.rs`: the token scanner's `level` and `path` loops, where an index step (`+= 1`, `+= 2`, `next + 1`, `end + 1`, `end + 2`) becomes `-` or `*`, or `path` returns `0` or `1`, and the scan revisits the same token forever. - `cargo-rullst/src/generators/audit_source.rs`: the token scans of `tokenized_views`, `item_end`, `mark_code_tokens` and `doc_attribute`, where an index step (`+= 1`) becomes `-=` or `*=`, `item_end` returns `0` or `1`, or `doc_attribute` returns `Some(0)` or `Some(1)` or its `next + 1` becomes `-`, and the scan revisits the same token forever. - `rullst-security/src/log_redactor.rs`: the bearer, identifier, separator and escaped-quote scans, where an index step (`+= 1`, `+= 2`) becomes `-` or `*`, a key offset (`cursor + offset`, `key_start + key.len()`) becomes `-` or `*`, or `identifier_continuation` returns `0` or `1`, and the scan finds the same key or byte again forever. - `rullst-security/src/dlp/byteranges.rs`: `Lines::next`, where the non-empty filter is negated or the next-line offset (`start + offset + 1`) becomes `-` or `*`, and the reader returns the same line forever; `rullst-security/src/dlp/masking.rs`: the AWS-key and database scans, where `cursor + offset` becomes `*` or `-`, or `+ scheme.len()` becomes `-`, and the scan restarts before the same match. - `rullst-core/src/security/pii.rs`: `measure_digit_run` returning `(0, 0)`, `(0, 1)`, `(1, 0)` or `(1, 1)`, its `separators < 3` with `==` or `>`, or its `end += 1` with `*=` or `-=`, and the address and JSON-string scans of `mask_chars` and `mask_json_strings`, where `start -= 1` becomes `/=`, `end += 1` or `idx += 1` becomes `*=`, or the dotted-domain `end += 1` becomes `-=`, so the card, address or string scan never advances; and `rullst-core/src/security/pii_byteranges.rs`: `Lines::next`, where the next-line offset becomes `-` or `*`, as in the DLP reader above. - `rullst-core/src/server/dev_telemetry/recorder.rs`: `push_bounded` with `entries.len() >= capacity` replaced by `<` pops an empty queue forever, in code every recorded request or query reaches. - `rullst-core/src/scheduler.rs`: `shutdown_requested` returning `true` or `false`, and `wait_for_shutdown` returning at once or without its `!`, so a task loop never runs or never sees shutdown, and the scheduler, server and supervision tests that start and stop a scheduler wait forever. - `rullst-security/src/login_guard/state.rs`: `pop_earliest` returning `None` or `Some(Default::default())`, or `make_room` with `&&` replaced by `||`, so pruning or eviction never removes an entry; and `rullst-security/src/rasp/user_agent.rs`: the version scan's `index += 1` replaced by `*=`. - `rullst-mail/src/security.rs`: the redaction scans, where `start + relative_end` in `redact_values_after` becomes `-`, `offset + relative` in `redact_aws_access_keys` or `redact_labelled_private_keys` becomes `*`, or `start + prefix.len()` becomes `-` or `*`, and the `href_values` scan, where `index += 1` in `skip_whitespace` or the non-match `pos += 1` becomes `*=`, or the `pos += 4` after an `href` without `=` becomes `-=`; each scan restarts before the same match. - `rullst-ai/src/ai/guardrails/tax_ids.rs`: `mask_tax_ids` without the `!` of its digit test or with `start += 1` as `-=` or `*=`, `token_end` returning `0` or `1`, and its `index += 1` steps as `-=` or `*=`; `rullst-ai/src/ai/guardrails/markdown_inline.rs`: `code_span_hides_bracket` with either backtick test negated, `index += run` as `-=` or `*=` or `index += 1` as `*=`, and the whitespace steps of `open_tag_end` as `*=`; `rullst-ai/src/ai/guardrails/markdown_images.rs`: the `index += 1` steps of `unescaped_close` and `label_end` as `-=` or `*=`; and `rullst-ai/src/ai/guardrails/markdown_code.rs`: the index steps of `lines`, `mark_fenced` and `span_contents` as `*=`, and `blank_code` with `code[group_start] || is_blank(...)` as `&&`, which stops on a blank line. The guardrail tests reach these scans, which then never finish. `rullst-security/src/timing_guard.rs`: `TimingScope::finish` with the jitter `%` replaced by `+` does not loop: it sleeps for up to 72 minutes in the real-time padding tests. A targeted run sets each test binary's address space to 8 GiB, so a loop that does allocate aborts its tests and counts as caught instead. `rullst-ai/src/ai/guardrails/markdown_code.rs`: `blank_code` with `group_start += 1` replaced by `*=` does allocate, but it repeats one line forever; on a blank line the output grows one byte per turn and reaches that limit only after the timeout. ### Owned-server contract mutation runs Some adapters are reached only by ignored contract tests that a wrapper runs against a disposable Docker server it owns, so a full or targeted run reports their database-bound mutants as missed. `mode=contract` measures them: it runs `.github/check-<contract>.py --mutants REGEX [--shard k/n]` on a hosted runner and uploads `mutants.out` as `mutants-out-contract`. The wrapper starts its server, mutates only its adapter files and runs only the first (exercise) phase of its contract tests, one at a time, never the restart phase. `.github/mutation_contract.py` is the Cargo target runner for those tests: it resets the server before every test binary (a recreated database of the same name, no leftover runtime roles, `ALTER SYSTEM RESET ALL`, or `FLUSHALL` for Redis), because a mutant can stop a test halfway and leave a role, a disabled `fsync` or a full Redis behind. Missed and timed-out mutants are findings; a failed baseline, a baseline that did not run every contract test, a failed reset or an incomplete classification fails the run. Because only the contract tests run, a mutant that a unit test of the same file catches can still be reported as missed: compare it with a targeted run of that file. One build directory serves every mutant, so the job compiles incrementally, without sccache (which refuses incremental builds), and rebuilds a mutant in about a second. | `contract` | Mutated files | Contract tests | | --- | --- | --- | | `passkey-postgres` | `rullst-auth/src/auth/passkey/shared/manager.rs`, `shared/postgres/operations.rs` | `postgres_contract` | | `auth-recovery-postgres` | `rullst-auth/src/recovery/connection.rs`, `store.rs`, `email_login/guard.rs`, `email_login/storage.rs`, `api_tokens/storage.rs`: the files with PostgreSQL-only branches | recovery, sessions, email login, API token (with HTTP) and the exercise phase of the three restart journeys; not the browser journey | | `privacy-postgres` | `rullst-privacy/src/age_assurance/postgres.rs`, `consent/postgres.rs` | `shared_replay_contract`, `shared_consent_contract` | | `mail-postgres` | `rullst-mail/src/suppression/postgres/**` | suppression contract and restart exercise | | `recurring-postgres` | `rullst-messaging/src/schedules/**` (`schedules-postgres`) | recurring contract and restart exercise | | `messaging-redis` | `rullst-messaging/src/redis_streams/**` | the password, TLS, fault, ordering, outbox and bounds contracts and the AOF restart exercise | | `connect-sql` | `rullst-connect/src/challenge/sql/postgres.rs`, `sql/mysql.rs` (the wrapper also owns a MySQL server, which each contract resets by dropping its table) | `postgres_challenge_contract`, `mysql_challenge_contract` | | `orm-sqlite` | `rullst-orm/src/**` and `rullst-orm-macros/src/**`, test files excluded; select files with `mutants_regex` (for example `^rullst-orm/src/pool/savepoint\.rs:`) | every `rullst-orm` test target, built with `drivers-all` (the SQLx `Any` pool) | | `orm-polyglot` | the same files; select the optional-backend modules (`polyglot`, `scout`, `redis_data`, `query_cache`) with `mutants_regex` | every `rullst-orm` test target, built with `drivers-all` and the Redis, MongoDB, SurrealDB, Turso, Scout HTTP and Qdrant features, with an owned Redis server for the `RULLST_TEST_REDIS_URL` contracts | | `orm-sql-servers` | the same files; select the PostgreSQL and MySQL/MariaDB branches with `mutants_regex` | `matrix_postgres`, `matrix_mysql` and `matrix_mariadb`, built with `drivers-all`; their testcontainers start a disposable server per test binary, and a server that cannot start fails the run | | `orm-strict-postgres` | the same files | `matrix_postgres`, built with `strict-postgres` only, which also exercises the native enum types and typed binds of that pool | | `orm-strict-mysql` | the same files | `matrix_mysql`, built with `strict-mysql` only | | `orm-polyglot-servers` | the same files; select the optional backends' live paths with `mutants_regex` | the MongoDB, SurrealDB, document-recovery, Qdrant, Meilisearch, Redis data and Turso testcontainers matrices, built like `orm-polyglot` | `orm-sqlite` owns no server. Targeted and full runs build the workspace with every feature, which selects the strict PostgreSQL pool and compiles the SQLite behaviour tests of `rullst-orm` out, so they report the mutants that only a database statement reaches as missed. This contract builds the ORM with `drivers-all` instead and runs its whole suite on SQLite for every mutant, including the code `#[derive(Orm)]` generates; the testcontainers matrices are skipped. Code compiled only with every feature, such as the PostgreSQL-only branches, is measured by the targeted runs. `orm-polyglot` adds the optional backends, whose tests use mocks and loopback servers, and runs the query-cache and Redis hash-scope contracts against a Redis server it owns; DuckDB, whose bundled build is large, stays with targeted runs. `orm-sql-servers` measures the dialect branches (placeholders, migration locks, collations and payload types) against real servers; every mutant waits for three of them, so select only those branches. `orm-strict-postgres` and `orm-strict-mysql` run one of those matrices in the build of a strict pool, for code that the `Any` pool does not compile. The MySQL statement files `rullst-capital/src/quota/sql/mysql.rs` and `rullst-ai/src/ai/memory/sql/mysql.rs` hold only string constants, so cargo-mutants generates no mutant for them. The privacy URL policy in `postgres_connection.rs` and the capacity check have unit tests. So do the mail adapter's clock, event-time, stored-time and URL checks, the privacy choice-code decoding, and, in `passkey-postgres` and `auth-recovery-postgres`, most branches of `manager.rs` and the recovery files: a contract run reports those mutants as missed, and a targeted run of the file catches them. ### Fuzzing and OSS-Fuzz The v13 manual `fuzzing.yml` matrix covers all **42** declared libFuzzer targets (the immutable v12 line retains 40): Core 12, ORM 5, Security 7, Connect 3, Mail 4, AI 3, IoT 3, Capital 1, Nexus 1, Studio 1 and Privacy 2. The checked-in `.github/fuzz-targets.json` is validated against every `*/fuzz/Cargo.toml`, corresponding lockfile and source file before either the manual campaign or weekly corpus job can fan out. Release mode verifies input-equivalent historical results and compiles the remaining targets in package-level preflight jobs before starting any long campaign. Both jobs use versioned per-target corpora and one content-addressed compiler-cache namespace; campaign failures retain their exact reproducer, and the weekly job performs a bounded warm-up before minimizing and uploading each actual corpus. A clean run retains its original source SHA, target, corpus, toolchain and time budget; reuse adds a separately verified relationship to the candidate inputs and never claims a fresh execution. Diagnostic mode is limited to one exact inventory target for five minutes and is never counted as the complete release gate. The `oss-fuzz/projects/rullst` directory is a local integration draft. It is not proof of upstream acceptance, continuous ClusterFuzz execution, or coverage of all 42 targets; its helper build must be completed and validated against the official OSS-Fuzz repository before submission. **The integration is worth finishing, but a “100% first-pass acceptance” promise is not meaningful and should not be made.** ### Supply chain and release provenance All direct third-party GitHub Actions references in this repository's workflow files are pinned to full commit SHAs. A pinned composite action can still carry its own transitive downloads or references, so blocking integrations must also be reviewed for that behavior. RustSec exceptions are limited by `deny.toml` and documented with owners, controls, and expiry dates in `docs/src/security-advisory-exceptions.md`. `scorecards.yml` runs the pinned OpenSSF Scorecard action on `main` pushes and weekly, uploads SARIF to GitHub code scanning, and publishes OIDC-authenticated results to the public Scorecard API so the README badge follows the latest completed analysis. The numeric score is supply-chain evidence, not a security certification. `release.yml` is tag-only. It verifies source, validates the exact semantic tag against every publishable crate, packages before the first publish, and creates a tag-bound evidence bundle containing the lockfile, Cargo metadata, CycloneDX 1.5, Cargo Audit JSON, dependency policy, bounded compliance evidence, advisory exceptions, commit context, checksums, and release notes rendered from the exact matching changelog section: the section's introduction and "Known limitations", plus a link to the full section at the tag (the whole section when it has no introduction). The `.crate` archives and evidence receive GitHub's SHA-pinned build-provenance attestation. This does **not** by itself establish a project-wide SLSA level, Sigstore Cosign binary signing, independent review, or regulatory compliance. `workflow-lint.yml` validates all workflow syntax, GitHub expressions, and embedded shell with Actionlint 1.7.7. Its container is pinned to an immutable linux/amd64 digest, just like third-party GitHub Actions are pinned to full commit SHAs. `architecture.yml` is repository-owned and deterministic. It rejects any internal dependency edge or optionality change that is not reflected in the reviewed `crate-architecture-policy.json`. The earlier TangleGuard integration was removed because its composite action downloaded an unversioned `latest` binary without a repository-pinned checksum, which was unsuitable for a blocking supply-chain gate. ### Reviewed Action updates on the v12 maintenance line Keep CodeQL `init`, `analyze`, and `upload-sarif` on one reviewed Action revision. Dependabot groups these sub-actions, and the local pin validator rejects mixed CodeQL revisions inside a workflow. Review the scanner bundle change as well as the wrapper SHA; the 4.38.0 update selects CodeQL 2.27.0. `setup-rust-toolchain` 2.0.0 changes warning enforcement from `RUSTFLAGS` to `CARGO_BUILD_WARNINGS` (Cargo 1.97+). The v12 migration explicitly retains `rustflags: "-D warnings"` and `build-warnings: ""`, preserving the previous compiler flags, strict warning behavior and compiler-cache inputs. This does not disable Clippy's explicit `-D warnings`. The MSRV job still uses a separate toolchain installer and Rust 1.96.0. Adopting the new Cargo warning mechanism is a separate measured migration, not an implicit side effect of updating an Action. See the [upstream migration notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases/tag/v2.0.0). TruffleHog's composite Action pin does not pin its default `latest` scanner image. The workflow therefore also specifies the reviewed 3.97.4 multi-platform image digest. Future scanner updates must review and change that digest; Dependabot updating the wrapper alone is insufficient. The scan's existing scope and verified-secret failure policy remain unchanged. These automation updates do not change published crate versions or constitute a v12.0.1 release. ## Workflow inventory (37 definitions) Durations are intentionally omitted because runner load, cache state, and the dependency graph make static estimates unreliable. | Workflow | Trigger | Mode | Actual scope | | :--- | :--- | :--- | :--- | | [`ai-sentinel-pr.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/ai-sentinel-pr.yml) | pull requests | Automated evidence | Generates bounded CLI audit, compliance report, and CycloneDX SBOM artifacts; no certification claim. | | [`architecture.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/architecture.yml) | main/v12/v13 push and PR, manual | Blocking | Compares Cargo's publishable non-dev internal dependency graph with the reviewed `crate-architecture-policy.json`; unreviewed normal/build edges, removals, or optionality changes fail, while test-only dev-dependencies do not masquerade as production coupling. | | [`audit.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/audit.yml) | main/v12/v13 push and PR, daily, manual | Blocking | Cargo Audit over the production lock and all eleven fuzz-package locks with one advisory-database fetch. Scheduled and manual runs add a job that audits the protected `v12` locks, because scheduled workflows only check out the default branch. The v12 candidate applies no advisory exceptions; future exceptions must pass the separate owner/expiry governance check. | | [`bench.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/bench.yml) | main push, weekly, manual | Automated evidence | Eight published groups backed by nine Criterion binaries, with non-blocking 20% regression alerts and gh-pages data consumed by the benchmark hub. Scheduled runs use the repository default branch. | | [`cargo-deny.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/cargo-deny.yml) | main/v12/v13 push and PR, weekly, manual | Blocking | Advisory, license, ban, and source policy from `deny.toml`. | | [`ci.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/ci.yml) | main/v12/v13 push and PR (Linux gate on PRs to main), nightly full matrix, manual | Blocking plus observational report | Format, all-target/all-feature Clippy, eight-shard multi-OS tests including Cargo-aware doctests sourced from all 51 tutorials, four-way feature/threat partitions, the SQLite transactional outbox contract and Messaging concurrency suite, relational/polyglot live matrices, isolated strict-DB/feature boundaries, MSRV, the generated VM deployment built and started with Docker and rootless Podman (manual, nightly and deploy-changing pull requests), and a ready-PR/manual full-matrix SHA-bound per-crate quality scorecard artifact. A targeted manual OS/shard run is diagnostic and cannot emit the full scorecard. | | [`cli-artifacts.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/cli-artifacts.yml) | reusable call from full/CLI manual Rust CI or admitted tag release | Blocking caller job; required release evidence | Builds and runs both CLI entry points on four explicit native targets; stages bounded executables and source/version/platform/digest inventories before attestation. CI artifacts are diagnostic; only the tag pipeline adds separate provenance and release assets. No installation occurs. | | [`codeql.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/codeql.yml) | main/v12/v13 push, v12/v13 PR, weekly, manual | Blocking run | Rust CodeQL after an all-target/all-feature workspace check, plus JavaScript/TypeScript analysis. | | [`corpus-sync.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/corpus-sync.yml) | weekly, manual | Informational | Validates the shared 42-target inventory and eleven package lockfiles, restores each real target corpus, performs a bounded warm-up, minimizes it, uploads the result and warms the campaign's content-addressed compiler cache; individual target failures are retained but tolerated, while dependency-lock drift remains a hard failure. | | [`coverage.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/coverage.yml) | main/v12/v13 push, v12/v13 PR, weekly, manual | Blocking plus observational job | LLVM LCOV generation with a pinned, zero-retry, bounded-concurrency nextest scheduler and retained JUnit inventory; a focused default-SQLite pass for ORM/Studio/Nexus/the facade; exact local 90% floors; and blocking OIDC-authenticated Codecov upload. Scheduled/manual branch instrumentation is non-blocking and uses the pinned verifier-only nightly. | | [`dast-zap.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/dast-zap.yml) | manual | Blocking generated targets plus informational showcase | Pins the ZAP image by digest, scans fresh release/migrated REST API and LMS starter surfaces as blocking gates, scans the CDN-backed blog showcase informationally, and uploads separate reports plus application logs. | | [`documentation.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/documentation.yml) | main/v12/v13 push and PR, weekly, manual | Blocking plus informational external scan | Builds the mdBook; validates landing/benchmark templates, project identity, the README workflow count, local assets, pinned external chart scripts and all requested social links. Real Chromium checks desktop/390px/320px layout, keyboard/mobile navigation, clipboard success/denial, privacy disclosure, reduced motion, no-JS navigation, and absence of external landing requests/browser storage. This is a bounded browser contract, not WCAG certification. Also validates the 190-claim historical roadmap denominator and repository-local links. Scheduled/manual runs preserve an informational external-link report. | | [`e2e-smoke.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/e2e-smoke.yml) | main/v12/v13 push and PR, manual | Blocking | Boots the release Blog application and checks HTTP, headers, CSRF form flow, SQLite persistence, and the persisted page parsed by real headless Chromium. | | [`fuzzing.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/fuzzing.yml) | manual | Blocking release campaign or diagnostic | Release mode maintains all 42 targets through independently verified, at-most-seven-day input-equivalent evidence or fresh 5.5-hour jobs. Changed fuzz packages rerun their sibling targets; shared or unclassified input changes require all targets. Selected packages retain locked preflights, corpus caching and failure reproducers. `force_full` always requests a new full campaign; the current-commit boundary and independent tag-admission check remain mandatory. Lock drift is always rejected. The parser restarts its ASan process at most every 30 minutes while preserving the budget. Single-target diagnostic mode runs for five minutes and the evidence boundary marks it ineligible for release. | | [`iot-integration.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/iot-integration.yml) | main/v12/v13 push and PR, manual | Blocking | Host IoT tests, signed OTA invariants, and one Cortex-M no-std build; no hardware claim. | | [`kani.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/kani.yml) | manual | Blocking release scope; bounded evidence | Builds an immutable reviewed Kani snapshot with the verifier-only `nightly-2026-08-01` compiler, while Rullst stays on stable Rust 1.99.0 with a Rust 1.96 MSRV. It verifies twenty named bounded harnesses in isolated jobs across ten supported packages. Proof failures fail their matrix jobs; the proc-macro-only crate remains outside Kani's supported targets. | | [`machete.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/machete.yml) | main/v12/v13 push and PR, manual | Blocking | Unused dependency scan with configured exceptions. | | [`miri.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/miri.yml) | manual | Blocking release scope; bounded evidence | Pinned nightly-only Miri executes 15 named pure-Rust/default-feature scopes with randomized layouts without changing Rullst's stable toolchain or MSRV. Native FFI/syscall/network paths, the umbrella re-export facade, and the Blog example are explicit boundaries; selected-scope failures fail the workflow. | | [`mutants.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/mutants.yml) | manual | Informational | A source-bound inventory preflight followed by eighty lossless pinned cargo-mutants 27.1.0 shards with four concurrent jobs over that source's all-feature workspace, each shard with a pinned Redis service for the `RULLST_TEST_REDIS_URL` contracts, one validated production-file diagnostic, one owned-server contract (`mode=contract`: a PostgreSQL or Redis wrapper's adapter mutants against the server it owns, or the ORM crates against the `rullst-orm` SQLite suite), or policy-bound exact-SHA recovery of reviewed failed fragments. Successful immutable artifacts may be reused, but the content-addressed aggregate still requires every reviewed candidate exactly once and carries source/run/branch/tool/inventory provenance. Findings stay informational, while baseline/tool/invocation failures, missing artifacts, incomplete classification and reviewed-inventory drift fail the run. The completed v12 receipt is run `34761010296`: 14,391/14,391 classified and a conservative 70.57% caught. | | [`mutants-pr.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/mutants-pr.yml) | pull requests to main that change Rust | Blocking by policy; not a required check | Lists the pinned cargo-mutants 27.1.0 all-feature mutants on the Rust lines the pull request changes, skipping the globs in `.github/mutants-pr-exclude.txt` and every CLI file not listed in `.github/mutants-pr-cli.txt` (listed CLI files run against the CLI's library tests), and tests them in at most eight shards with a Redis service and the targeted-run timeouts and memory cap. A missed mutant fails the run; unviable and timed-out mutants do not. See [Pull-request mutation gate](https://rullst.github.io/Rullst/book/workflows.html#pull-request-mutation-gate). | | [`no_std-build.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/no_std-build.yml) | main/v12/v13 push and PR, manual | Blocking | Builds `rullst-iot` for three bare-metal targets; this is compile evidence, not hardware execution. | | [`pages.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/pages.yml) | main push, manual | Deploy | Validates and deploys the current development landing page, local visual assets, mdBook and benchmark hub/dashboards to GitHub Pages while preserving history data fetched from `gh-pages`. | | [`pqc-compliance.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/pqc-compliance.yml) | relevant main changes and PRs, weekly, manual | Blocking | Signed OTA and Vault tests, RustSec audit, and simulator-boundary checks; explicitly no PQC/HSM certification. | | [`proptest.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/proptest.yml) | weekly, manual | Blocking run | Eight parallel release-mode workspace shards plus dedicated ORM and Connect property contracts with configured case counts. | | [`release.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/release.yml) | exact-looking version tags | Release | Tag/changelog validation, exact-SHA admission of the full CI matrix (not repeated in the tag run), package-all, evidence bundle, checksums, GitHub build-provenance attestation, changelog-derived release notes, and dependency-order publication. | | [`sanitizers.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/sanitizers.yml) | daily, manual | Blocking run | TSan and ASan library matrices on pinned `nightly-2026-08-21`; this verifier toolchain does not change Rullst's stable compiler or MSRV. | | [`scorecards.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/scorecards.yml) | main push, weekly, manual | Automated evidence | OpenSSF Scorecard analysis and SARIF/artifact upload; not SLSA certification. | | [`security-audit.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/security-audit.yml) | main/v12/v13 push and PR, weekly, manual | Blocking | Cross-checks active advisory IDs and expiry metadata across the ledger, Cargo Deny, and scanner workflows, then independently reruns Cargo Audit. | | [`semver.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/semver.yml) | main/v12/v13 push, v12/v13 PR, manual | Blocking | Fans out one job per machine-readable release-order entry and compares each supported, already-published library API with its exact latest non-yanked crates.io baseline. Never-published packages and proc-macro/binary API surfaces unsupported by `cargo-semver-checks` are reported explicitly. | | [`spellcheck.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/spellcheck.yml) | main/v12/v13 push and PR, manual | Blocking | Repository typo scan. | | [`trufflehog.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/trufflehog.yml) | main/v12/v13 push and PR, weekly, manual | Blocking | Verified-secret scan over the configured Git history range. | | [`udeps.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/udeps.yml) | weekly, manual | Informational | `cargo-udeps` signal on pinned `nightly-2026-08-21`; command failures are tolerated. | | [`unsafe-policy.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/unsafe-policy.yml) | main/v12/v13 push and PR, manual | Blocking | Denies new production unsafe code and validates the reviewed exception allowlist. | | [`verus.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/verus.yml) | manual-only v13 candidate | Strict pilot, outside release admission | Verifies the source-linked age-policy method predicate with pinned Verus/Rust/Z3 and requires three postcondition-failing controls. Uploads fingerprints, tool identities and resource measurements; hosted acceptance/promotion pending. | | [`wasm-matrix.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/wasm-matrix.yml) | main/v12/v13 push and PR, manual | Blocking | Compiles Core, the public `rullst` facade (without features, with its defaults and with the CLI-generated application feature set) and macros for `wasm32-unknown-unknown` and `wasm32-wasip1`. | | [`workflow-lint.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/workflow-lint.yml) | main/v12/v13 push and PR, manual | Blocking | Validates the shared fuzz inventory, then Actionlint checks workflow syntax, GitHub expressions and embedded shell using an immutable container digest. | | [`zero-panics.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/zero-panics.yml) | main/v12/v13 push and PR, manual | Blocking | Panic-family Clippy lints plus generated-code regression checks for published runtime targets. | ## Verification efficiency — v12 maintenance and v13 **Priority: first, before the planned 12.1.0 updater and concentrated v13 product development.** Compatible policy/tooling can serve both release lines after review; it does not require a new framework capability release. The draft/ready split, seven OS shards, Linux-only post-merge repetition, mutation recovery and targeted diagnostic modes described above already exist. Measurement and change-impact **observation tooling now exists on v13**. A narrowly scoped development-only site admission path is being validated below; affected-crate execution and general cross-commit release-evidence reuse are **not enabled**. The `ci.yml`, `documentation.yml` and `workflow-lint.yml` development triggers include v13, as do those of the other branch-filtered workflows except the `main`-only benchmark, Pages, Scorecard and IoT cryptography workflows; these three jobs are not the whole release gate. The workflow-lint job retains a source/policy-bound `rullst.verification-plan.v1` observation. It uses committed Git objects and TOML metadata without executing Cargo, build scripts, macros or project tests. All normal/optional/target/build/dev dependency edges participate in its reverse closure. Renames, deletions, symlinks, unknown paths, policy/lockfile changes, critical crates, generators and potentially executable Markdown retain the full recommendation. Missing history or unsupported graph shapes also fall back to full. `may_skip_checks` is always false and no workflow consumes this report to skip a job. Candidate package lists are not a complete test/feature matrix. The separate `admit-site-only.py` / `plan-ci-scope.sh` path can avoid repeating Rust CI only for a **development `main` or transitional `v13` push** whose committed changes exclusively touch `docs/home_template.html`, `docs/site.css` or `docs/site.js`. It requires the immediately preceding commit to have a successful full Linux push CI in this repository within 72 hours, on the same branch, with all 24 expected runtime jobs completed successfully. Run/attempt/source/repository/branch identity and the entire job inventory are checked. A skipped runtime job, unknown job, failure, missing history, unavailable API, changed policy, symlink, mode change or stale receipt retains full runtime checks. The source-equivalence check also prevents an untested intermediate code change from being hidden by a later CSS-only push. The helpers are loaded from the preceding committed source and executed with isolated Python imports. A candidate cannot replace its own admission helper; the shell's error path and every runtime job guard also retain full execution if scope planning fails. Admitted pushes still build the book and run static site validation and real Chromium tests, plus the separate documentation and workflow checks. They do not produce a quality scorecard or qualify as release evidence. PRs, `v12`, manual matrices, fuzzing, mutation campaigns and release admission are unchanged. The first implementation deliberately does **not** chain presentation-only receipts or search arbitrarily old baselines; a missing immediate full baseline incurs a normal run. Hosted rollout evidence on September 15, 2026: [full Linux push CI 35000185080](https://github.com/Rullst/Rullst/actions/runs/35000185080) at `42dd0545` passed all 25 runtime jobs in 20m57s creation-to-completion. The following [presentation-only CI 35002518137](https://github.com/Rullst/Rullst/actions/runs/35002518137) at `3229b762` passed scope admission and fresh site validation in 43s, with runtime jobs skipped as intended. Separate documentation and workflow checks also passed. These are two observed Rust CI elapsed times, not a universal speedup, a sum of all workflow durations, or permission to reuse release evidence. Documentation and Pages builds install the same pinned mdBook version from its prebuilt release instead of recompiling mdBook on each runner. Book, link and browser checks remain enabled. Dependency changes still require runtime CI. Before workspace Clippy, `check-fuzz-locks.sh` resolves all eleven fuzz dependency graphs with `cargo metadata --locked` and the campaign's explicit Linux target. It does not compile or execute fuzzers. This catches stale fuzz lockfiles after workspace dependency updates without waiting for a long campaign build. Both campaign preflights use the same full graph resolution; `--no-deps` is forbidden because a real offline Cargo regression demonstrates that it accepts stale path-dependency locks. Five tests cover that failure, the complete inventory, early failure, argument rejection and CI wiring. Neither the forty-two-target inventory nor campaign duration or release admission is reduced. After formatting, `check-package-reproducibility.sh` packages `rullst-macros` and `rullst-orm-macros` twice with `--no-verify`: once from the CI checkout and once from a fresh worktree of the same commit at another path, with new file times and umask `077`, each into an empty target directory. Different SHA-256 digests fail the job. It compiles nothing; the release preflight still compares every admitted archive. See [reproducible crate archives](https://rullst.github.io/Rullst/book/reproducible-builds.html). The threat-model runner now validates all evidence rows before downloads or compilation and runs each unique exact test once, instead of first invoking Cargo again to list that target. The current 66 evidence mappings, 50 threat IDs and 65 unique tests therefore need one Cargo test invocation per unique test instead of two, excluding each shard's retained locked fetch. Every selected test still runs in a separate all-feature process on its deterministic hash shard. Both a successful process exit and a bounded libtest log showing exactly one named, passed, non-ignored test are required. An ignored or missing test can no longer produce a false pass from libtest's zero exit status. The LMS threat mapping (`ACADEMY-02`; v13 retired the other Academy mappings with the complete Academy scaffold) selects `security_tests::another_learners_lessons_are_refused` in the LMS example (`rullst-lms-example`, `--lib`). Since 13.0.0-alpha.4 the LMS starter is that workspace example, so the exact test needs no nested generated build; the workspace shard also runs it with the example's other tests. The normal foundation and product shards exercise the four reviewed generated configurations: Blank, the hot-reload JSON API, hot-reload SaaS and SaaS's release build. Before 13.0.0-alpha.4, the mapping materialized the generated LMS starter (`materialized_lms_executes_security_contracts`). That narrowing addressed an observed bottleneck: in [Rust CI 35007899011](https://github.com/Rullst/Rullst/actions/runs/35007899011), the old all-blueprint wrapper consumed 958.22 seconds inside threat shard 0, whose complete job took 26m12s. That gate needed LMS evidence, not another execution of the seven other project configurations. Hosted threat jobs now use the same bounded two-job nested compiler setting as the normal generated matrix. Removing warm listing calls alone saves overhead, not half the compilation time. The complete optimized Linux run passed all 25 required runtime jobs, plus documentation and workflow validation. | Measurement | [Before: 35011900532](https://github.com/Rullst/Rullst/actions/runs/35011900532) | [After: 35016405011](https://github.com/Rullst/Rullst/actions/runs/35016405011) | | :--- | ---: | ---: | | Required runtime jobs successful | 25/25 | 25/25 | | Threat-model shard 0 execution | 28m34s | 10m22s | | Generated-project threat wrapper | 1,063.37 seconds | 227.75 seconds | | Summed measured runner time | 196.68 minutes | 147.55 minutes | | Whole Rust CI elapsed time | 43m43s | 61m12s | These source revisions are `0b3588a4` and `d0898516`. Execution work decreased, but some jobs waited up to 51m40s between creation and start, so the whole workflow took longer. Waiting can include orchestration and runner availability. Cache warmth and overlapping workflows differ; this is not a controlled benchmark or a fixed speedup guarantee. Do not increase shard counts blindly: runner contention can outweigh shorter individual jobs. Complete and validate one feedback batch before pushing another when practical; no required check is cancelled merely to improve the displayed elapsed time. The timing reporter leaves skipped jobs visible but does not assign them an execution duration. GitHub can synthesize reversed timestamps for an unexecuted job. Malformed timestamps, contradictory executed steps and negative intervals in jobs that actually ran still fail. This observation-only correction does not change site admission or release-evidence rules. The compatible v12 backport must earn its own platform evidence; a v13 run is not a main acceptance receipt. Six parser tests include a real compiled Rust harness; five runner tests check the full exact inventory, shard partition and failure paths. Four generated scheduling tests compile the actual selectors and wrappers with a recorded verifier, proving the four-case partition without building applications locally. That recorder is policy evidence only: real generated application tests and full CI remain required. These changes do not enable general fuzz-result reuse or relax release gates. Single-target fuzz diagnostics now compile only the requested target in their package preflight, rather than every other target in that package. The exact target/package pair is checked again before Cargo; unknown, ambiguous or option-like targets fail before compilation. Release mode still compiles every target in all eleven packages before its forty-two 5.5-hour campaigns. Five scheduling tests cover all forty-two diagnostic selections, the complete package inventory, invalid inputs and compiler failure propagation. No sanitizer, instrumentation, corpus or campaign duration is weakened, and diagnostics remain ineligible as release evidence. Fuzzing, Miri, Kani and mutation campaigns remain manual; an ordinary push does not automatically dispatch them. Read-only local commands (the planner inspects commits, not uncommitted files): ```bash python3 .github/plan-verification.py --base main --head HEAD python3 .github/test-plan-verification.py python3 .github/test-report-ci-timings.py python3 .github/test-admit-site-only.py python3 .github/test-ci-scope.py python3 .github/test-exact-rust-test.py python3 .github/test-threat-model-runner.py python3 .github/test-generated-evidence.py python3 .github/test-fuzz-preflight.py gh api --paginate --slurp \ 'repos/Rullst/Rullst/actions/runs/34980693742/attempts/1/jobs?per_page=100' \ | python3 .github/report-ci-timings.py --top 10 ``` Initial timing observation: [Rust CI run 34980693742, attempt 1](https://github.com/Rullst/Rullst/actions/runs/34980693742) reported head `792c1d554465c016abfdc223cbcaf7d94ec8c0c5` and 42 completed jobs. The longest job was Windows/workspace at 30.05 minutes (27.83 in the combined build/test step); the longest creation-to-start wait was 17.50 minutes. Those waits can include orchestration, not just runner queues. The summed 386.72 runner-minutes are neither elapsed workflow time nor a billing estimate. This one run is a baseline, not a measured speedup, cold/warm comparison or evidence that compilation and test execution have been timed separately. | Order | Improvement | Acceptance evidence | | :--- | :--- | :--- | | 1 | Measure the existing bottlenecks before changing scheduling | Record queue, restore, native compilation, Rust compilation and test durations separately, by OS and cold/warm cache. Compare equivalent inventories and runner conditions. Do not promise a fixed speedup. | | 2 | Classify documentation, site, runtime, generator, dependency and verification-policy changes | Run the relevant book/link/browser/ledger tests for presentation changes. Markdown containing executable examples still needs its doctests. Unknown paths, renames, deletions and classifier failures fall back to broad verification; `.github/**` is never automatically a documentation-only exemption. | | 3 | Select affected crates and reverse dependencies for development feedback | Unit-test the selection policy against runtime, macros, manifests, lockfiles, build scripts, generated templates, shared fixtures and security controls. Prove no required test or feature combination disappears before enabling skips. Keep full/manual and broad scheduled/release modes. | | 4 | Reduce duplicate compilation and balance shards using measurements | Isolate trusted and untrusted caches; key compatible artifacts by toolchain, target, profile, features and dependency inputs. Preserve every case and assertion, report cache misses honestly and measure total runner time as well as elapsed time. | | 5 | Reuse expensive verification only under an explicit admission policy | Bind receipts to source inputs, dependency locks, tool versions, flags, inventory, artifacts and policy. Failed, partial, diagnostic, expired or mismatched evidence cannot become a release pass. Keep conservative reruns for security-relevant or uncertain impact. | The invariants remain unchanged: repository and library coverage floors stay at 90%; negative authorization and tenant-isolation tests remain enforced; no timeout/crash becomes a tolerated success merely to shorten a run. A reduction in redundant work must not be presented as stronger security proof. Until a reviewed implementation changes the relevant contract, the current `AGENTS.md` local baseline and exact-source release admission rules still apply. Update automation, policy tests and this document together; a roadmap paragraph alone must never authorize skipping a required check. Carry compatible fixes from v12 to v13, without importing unrelated next-major features into v12. ## Preserved next-generation roadmap These ideas remain valuable, but are not current guarantees: | Idea | Status and recommendation | | :--- | :--- | | Verus proofs for selected production contracts | **Locally verified v13 candidate; hosted acceptance pending.** `verus.yml` is manual-only, with a pinned archive/compiler/solver, syntax-linked `AgePolicy::permits` projection, one verified predicate, three mandatory postcondition-failing controls and source/resource receipts. Normal CLI CI checks the source linkage without installing Verus. The pilot remains outside release admission; Auth, Capital, age evidence/time/storage and promotion remain separate work. See the [measured scope and assumptions](https://rullst.github.io/Rullst/book/verus-roadmap.html). No additional v12.1 gate. | | Loom and Shuttle concurrency exploration | **Not implemented — worth implementing** for the small shared-state primitives that have explicit concurrency invariants. Do not apply them indiscriminately to the whole workspace. | | `cargo-vet` dependency review | **Not implemented — worth implementing** once review ownership and audit criteria are defined; an empty policy file would add ceremony without assurance. | | `cargo-careful` and zero-allocation assertions | **Not implemented — worth targeted experiments.** Allocation claims need stable benchmarks and explicit hot paths before becoming gates. | | PGO and BOLT | **Not implemented — defer until production profiles exist.** Fixed throughput-gain percentages must not be promised in advance. | | Chaos testing with `fail-rs` | **Not implemented — worth implementing** around queues, database retries, and provider timeouts after deterministic failure contracts exist. | | AFL.rs/honggfuzz differential fuzzing | **Not implemented — valuable after the 42 libFuzzer targets have healthy corpora and triage ownership.** | | Cross-platform CI acceleration | **Partial — first engineering priority, also applicable to compatible v12 maintenance.** The inherited v12 baseline has eight `cargo test` shards per OS and isolated fixture targets. Coverage separately uses nextest; the ordinary CI runner has not migrated to it. Preserve workspace, feature, generated-project, outbox, doctest and live-provider contracts while measuring further scheduling/cache improvements. Speed alone must never reduce assertions or supported-platform evidence. | | Differential database testing | **Not implemented — high-value v13 work.** Run equivalent generated ORM operations against the supported relational backends and compare normalized results, errors and transaction behavior; keep provider-specific semantics explicit instead of forcing false equivalence. | | Cross-browser and accessibility testing | **Not implemented — high-value v13 work.** Exercise generated applications with Playwright across Chromium, Firefox and WebKit, add keyboard and automated accessibility checks, and retain traces/screenshots for failures. This would complement, not replace, ZAP and server-level integration tests. | | Mobile physical-device farms | **Not implemented — requires external infrastructure.** Add Android and iOS device-farm execution, lifecycle/network interruption scenarios and signed-package evidence when accounts and secrets are governed. Simulator and compile checks must not be presented as physical-device or store-acceptance proof. | | Desktop GUI, installer and signing matrix | **Not implemented — requires platform signing operations.** Exercise real WebView sessions and packaged installers on supported Linux, Windows and macOS versions, then validate signatures/notarization without exposing signing credentials to untrusted pull requests. | | Live provider sandbox conformance | **Not implemented — requires governed provider accounts.** Add opt-in acceptance suites for payment, mail, OAuth, AI, search and storage providers, recording provider/version/region while preserving deterministic offline fixtures as the contribution baseline. | | Independent security review and penetration test | **External evidence — strongly recommended before broad production assurances.** Define scope, remediation ownership and retest criteria; repository automation cannot award this evidence to itself. | | Sigstore Cosign signing | **Not implemented.** Consider it for separately distributed binaries/containers; current `.crate` provenance and checksums should remain the immediate priority. | | Absolute “100% pure Rustls” mandate | **Not established and not recommended as a marketing absolute.** Enforce an audited TLS dependency policy based on supported platforms and threat model instead. | | Complete upstream OSS-Fuzz integration | **Partial draft — worth finishing.** Validate every intended target with `helper.py build_fuzzers` and `check_build`, then submit upstream; do not imply acceptance before merge. | ### Cross-platform CI acceleration acceptance plan The September 6 v12 candidate measurements record the pre-optimization baseline: the all-feature workspace and follow-up contracts took approximately 70 minutes on Linux, 59 minutes on macOS, and 107 minutes on Windows. Most time was spent in the clean `cargo test --workspace --all-features` build. At that checkpoint, only Cargo registry data was cached. Workspace `target/` caching was disabled after CLI integration tests reused and cleaned nested target paths while the post-job cache collector traversed them, producing false missing-file annotations and multi-gigabyte uploads. This is historical context, not the current configuration or expected duration. The stable v12 baseline now uses the sharded `cargo test` system documented above; pinned nextest currently schedules the coverage pass only. New experiments must measure that inherited configuration, not claim a speedup against obsolete commands or omit checks that moved into other jobs. An acceleration change is acceptable only when all of these conditions hold: 1. Give repository compilation and every generated-project family distinct, explicit target roots. A generated fixture may clean only its own disposable root and must never mutate a cached workspace target. 2. Record cold and warm wall time, cache size/hit data, discovered test counts, failures and doctest results on Linux, macOS and Windows. Compare equivalent commit content; queue time is reported separately from execution time. 3. Preserve the all-feature workspace test inventory, portable transactional outbox contract and Linux live-provider matrices until an alternative proves identical coverage. Package sharding must not weaken Cargo feature unification. 4. Evaluate a pinned compiled-artifact cache and `sccache` independently before composing them. Keys must include OS, compiler, lockfile and relevant profile inputs; caches are performance hints, never release artifacts or evidence that tests ran. Bound storage and prevent untrusted pull requests from replacing a protected default-branch cache. 5. Evaluate nextest for ordinary CI (or change its coverage configuration) only if tests, ignored-test policy, retries, process cleanup and failure reporting remain equivalent. Run Cargo doctests separately because nextest does not replace them. Keep plain `cargo test` as a documented recovery path. 6. Promote the experiment to blocking CI only after repeated green cold and warm runs on every supported OS show a material wall-time reduction without new flakes, lost tests, hidden failures or multi-gigabyte cache churn. Retain the previous workflow as a quick rollback during the observation window. For development-only presentation skips, comparing with the immediately preceding commit is insufficient: that commit might have an unfinished or failed runtime run which a new push cancels. Admission must bind a completed successful runtime inventory to its exact baseline commit, compare that baseline with the candidate, and reject incomplete, stale, diagnostic or presentation-only receipts. Manual/release runs remain full. Do not infer receipt validity from a green workflow badge, file extensions alone or the observation report. This work optimizes feedback latency, not the evidence boundary. Any v12 maintenance change must preserve the immutable v12.0.0 artifacts and carry its own applicable verification. Further scheduling/cache changes need comparable cross-platform A/B receipts before their claimed benefit is accepted. The goal of this roadmap is stronger, reproducible evidence—not a larger number of badges or absolute claims that no finite test suite can establish. --- # Reproducible Crate Archives Source: https://rullst.github.io/Rullst/book/reproducible-builds.html # Reproducible Crate Archives Rullst claims one specific kind of reproducibility: for the same commit and the toolchain pinned in that commit's `rust-toolchain.toml`, `cargo package` produces **bit-for-bit identical `.crate` archives**. The checkout path, file modification times, file ownership and the user's umask do not change the bytes. This page states the evidence, how to check it yourself and what is not claimed. ## Why the archives are stable Cargo writes the tar archive in a deterministic mode. In the archives Rullst publishes, every entry has the same fixed timestamp (2006-07-23 22:21 UTC), user and group 0, no user or group names, and normalized permissions: a checkout made under umask `077` still produces the same bytes. The gzip header carries no timestamp. `.cargo_vcs_info.json` records the commit and the crate's path inside the repository, not an absolute path. The embedded `Cargo.lock` comes from the workspace lockfile through `--locked`. ## Evidence | Check | When it runs | What it compares | | --- | --- | --- | | [`check-package-reproducibility.sh`](https://github.com/Rullst/Rullst/blob/main/.github/check-package-reproducibility.sh) in the `Code Quality & Format` job of [`ci.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/ci.yml) | Every pull request and push that runs Rust CI | `rullst-macros` and `rullst-orm-macros`, packaged once from the CI checkout and once from a fresh `git worktree` of the same commit at another path, with new file times and umask `077`. Each run uses its own empty target directory, and the SHA-256 digests must match. | | `Reproduce publication inputs without registry identity` in [`release.yml`](https://github.com/Rullst/Rullst/blob/main/.github/workflows/release.yml) | Every release tag, before publication | Every crate in [`release-order.json`](https://github.com/Rullst/Rullst/blob/main/.github/release-order.json) is repackaged from the tagged checkout on a separate runner and compared byte-for-byte (`cmp`) with the archives that were verified and attested. Any difference stops the release. For example, [v12.2.0 run 37128793097](https://github.com/Rullst/Rullst/actions/runs/37128793097) passed this job. | | Independent local rebuild, 9 October 2026 | One-off check | A fresh clone of tag `v12.2.0` (`565e222a3249f788a0eb560b7ecd6a2ce379ad47`), packaged with Rust 1.98.1 on a developer workstation. It reproduced `rullst-macros-12.2.0.crate` with SHA-256 `23f73c494af1e7d37084e7bce8bcef77bf5638f0f822d79c3334a2353e807bf5`, the same digest as the GitHub release asset, its `checksums.txt` and the crates.io checksum. | The release workflow's verification job packages with full build verification and the preflight job packages with `--no-verify`. Their archives are compared byte-for-byte, so build verification does not change the archive. ## Reproduce a published crate yourself Pick a published version and its tag. The tag's `rust-toolchain.toml` selects the pinned toolchain automatically through rustup. ```sh git clone https://github.com/Rullst/Rullst.git cd Rullst git checkout v12.2.0 target_dir="$(mktemp -d)" CARGO_TARGET_DIR="$target_dir" cargo package -p rullst-macros \ --all-features --locked --no-verify sha256sum "$target_dir/package/rullst-macros-12.2.0.crate" curl --silent --user-agent "rullst-reproduction-check" \ https://crates.io/api/v1/crates/rullst-macros/12.2.0 | jq -r .version.checksum ``` The two digests must be equal. They must also match the line for the same file in the release's `checksums.txt`. To check the signature on that release as well, see [Verifying a release](https://github.com/Rullst/Rullst/blob/main/SECURITY.md#verifying-a-release). ## Limits - **Only `.crate` source archives are claimed.** The native `rullst` and `cargo-rullst` executables attached to GitHub releases are built on GitHub-hosted runners and attested, but they are not claimed to be bit-for-bit reproducible. They embed platform, linker and toolchain details, and no job rebuilds and compares them. - **The same toolchain is required.** A different Cargo version may normalize manifests or lay out archives differently. Use the toolchain pinned by the tag. - **The same clean commit is required.** Packaging uncommitted changes with `--allow-dirty` records a dirty state and different content. - **The pull-request check is bounded.** It covers two small crates to keep the cost low. All admitted crates are compared once per release tag. - **Release evidence files are not claimed reproducible.** The SBOM, audit results, metadata and release context are generated at release time. The context records the workflow run, and the audit reflects the advisory database of that moment. They are attested, not reproduced. --- # rullst-orm Source: https://rullst.github.io/Rullst/book/crates/orm.html <div align="center"> <h1>Rullst ORM 🌟</h1> <p><strong>A beautiful, type-safe, Active Record ORM for Rust.</strong></p> <p> <a href="https://crates.io/crates/rullst-orm"><img src="https://img.shields.io/crates/v/rullst-orm?style=flat-square&color=orange" alt="Crates.io" /></a> <a href="https://crates.io/crates/rullst-orm"><img src="https://img.shields.io/crates/d/rullst-orm?style=flat-square&color=orange" alt="Downloads" /></a> <a href="https://docs.rs/rullst-orm"><img src="https://img.shields.io/docsrs/rullst-orm?style=flat-square&color=blue" alt="Docs.rs" /></a> <a href="https://github.com/Rullst/Rullst/actions"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/ci.yml?style=flat-square&label=Build" alt="Build Status" /></a> <img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square" alt="License: MIT" /> </p> </div> > [!IMPORTANT] > This page describes the v13 prerelease source, and its dependency examples > pin the published `13.0.0-alpha.4` exactly: Cargo never selects a prerelease > on its own, and a plain `"13.0.0-alpha.4"` would also accept later alphas > (see the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html)). The v12 line > (`rullst-orm@12`) reached end of life on 16 October 2026; see the > [release record](https://rullst.github.io/Rullst/book/v12.html). Use a path dependency only for checkout-local > review. 🚀 **[Visit the Official Website & Documentation Hub](https://rullst.github.io/Rullst/book/)** 🚀 Built on top of `sqlx` and procedural macros, **Rullst ORM** brings the delightful, fluent syntax of Active Record frameworks directly to the high-performance Rust ecosystem. <div align="center"> <h3>🛡️ Security Engineering</h3> <p>Rullst ORM uses SQLx bindings, validated identifiers, typed errors, and layered CI checks. Workflow badges are scoped test results, not a guarantee for an application or deployment.</p> | Security Audit | Status | Description | | :--- | :---: | :--- | | **OpenSSF Scorecard** | <a href="https://scorecard.dev/viewer/?uri=github.com/Rullst/Rullst"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.scorecard.dev%2Fprojects%2Fgithub.com%2FRullst%2FRullst&query=%24.score&label=OpenSSF%20Scorecard&style=flat-square" alt="OpenSSF Scorecard" /></a> | Current public supply-chain practice score; not a security certification | | **Release Provenance** | <a href="https://github.com/Rullst/Rullst/actions/workflows/release.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/release.yml?style=flat-square&label=" alt="Release provenance" /></a> | Provenance attestations for release artifacts; no SLSA level is claimed here | | **Codecov** | <a href="https://codecov.io/gh/Rullst/Rullst"><img src="https://codecov.io/github/Rullst/Rullst/branch/main/graph/badge.svg?component=framework_libraries" alt="Framework library coverage" /></a> | Blocking 90% target for the measured framework-library scope; the complete repository aggregate now also has its own 90% gate | | **Matrix DB Tests** | <a href="https://github.com/Rullst/Rullst/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/ci.yml?style=flat-square&label=" alt="Testcontainers" /></a> | Live PostgreSQL, MySQL, MariaDB, MongoDB, SurrealDB and libSQL contracts, plus in-process DuckDB tests | | **OpenSSF** | <a href="https://www.bestpractices.dev/projects/13359"><img src="https://img.shields.io/cii/level/13359?style=flat-square&label=" alt="OpenSSF Best Practices" /></a> | Open source security standards | | **Property tests** | <a href="https://github.com/Rullst/Rullst/actions/workflows/proptest.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/proptest.yml?branch=main&style=flat-square&label=Proptest" alt="Proptest" /></a> | Scheduled/manual bounded invariant evidence | | **Miri research matrix** | <a href="https://github.com/Rullst/Rullst/actions/workflows/miri.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/miri.yml?branch=main&style=flat-square&label=Miri" alt="Miri" /></a> | Manual bounded evidence; the selected pure-Rust privacy scope is strict, while native database FFI remains outside Miri | | **Kani research harnesses** | <a href="https://github.com/Rullst/Rullst/actions/workflows/kani.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/kani.yml?branch=main&style=flat-square&label=Kani" alt="Kani" /></a> | Manual, bounded formal evidence; not whole-ORM proof | | **CodeQL SAST** | <a href="https://github.com/Rullst/Rullst/actions/workflows/codeql.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/codeql.yml?style=flat-square&label=" alt="CodeQL SAST" /></a> | Advanced semantic code analysis | | **Cargo Deny** | <a href="https://github.com/Rullst/Rullst/actions/workflows/cargo-deny.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/cargo-deny.yml?style=flat-square&label=" alt="Cargo Deny" /></a> | Banning unmaintained/vulnerable crates | | **Cargo Audit** | <a href="https://github.com/Rullst/Rullst/actions/workflows/audit.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/audit.yml?style=flat-square&label=" alt="Auto-Audit" /></a> | Continuous scanning for crate vulnerabilities | | **Cargo SemVer** | <a href="https://github.com/Rullst/Rullst/actions/workflows/semver.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/semver.yml?style=flat-square&label=" alt="cargo-semver-checks" /></a> | Strict SemVer API breakage checks | | **Cargo Machete** | <a href="https://github.com/Rullst/Rullst/actions/workflows/machete.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/machete.yml?style=flat-square&label=" alt="Cargo Machete" /></a> | Detecting unused and bloated dependencies | | **On-demand fuzzing** | <a href="https://github.com/Rullst/Rullst/actions/workflows/fuzzing.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/fuzzing.yml?branch=main&style=flat-square&label=Fuzzing" alt="Fuzzing" /></a> | Manual time-bounded targets; no continuous OSS-Fuzz claim | | **Mutation Testing** | <a href="https://github.com/Rullst/Rullst/actions/workflows/mutants.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/mutants.yml?style=flat-square&label=" alt="Mutants" /></a> | Mutation testing for test suite robustness | | **Continuous Benchmarks** | <a href="https://github.com/Rullst/Rullst/actions/workflows/bench.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/bench.yml?style=flat-square&label=" alt="Benchmarks CI" /></a> | Continuous performance regression testing & live dashboard | | **Unsafe Policy** | <a href="https://github.com/Rullst/Rullst/actions/workflows/unsafe-policy.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/unsafe-policy.yml?style=flat-square&label=" alt="Unsafe Policy" /></a> | Audits unsafe usage within the workflow's declared scope | | **Panic Policy** | <a href="https://github.com/Rullst/Rullst/actions/workflows/zero-panics.yml"><img src="https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/zero-panics.yml?style=flat-square&label=" alt="Panic Policy" /></a> | Graceful error handling across the framework | </div> ## 🚀 Why Rullst ORM? Rullst ORM generates Active Record operations and a fluent query builder from `#[derive(Orm)]`. SQLx remains available for queries that do not fit the generated API. Both work with SQLite, PostgreSQL and MySQL/MariaDB; Turso/libSQL and the side stores have their own APIs, as [Database Support](https://rullst.github.io/Rullst/book/database-support.html) lists. **Key Features:** - **Generated CRUD:** Insert, update, delete, restore, and find operations for supported model shapes. `restore()` and `force_delete()` use the same savepoint, hook/observer, audit and post-commit cache/event/Scout pipeline as `delete()`/`save()`; see [Active Record CRUD](https://rullst.github.io/Rullst/book/tutorials/03-active-record-crud.html). - **Fluent Query Builder:** Chain methods such as `.where_eq()`, `.limit()`, and `.order_by()`; values are bound and structural identifiers are validated. - **Relationships and eager loading:** `has_many`, `has_one`, `belongs_to`, and polymorphic relationship helpers, with explicit eager-load methods. - **Opt-in tenant scope:** `#[orm(tenant_column = "account_id")]` adds the configured task-local tenant to generated model queries. Applications must establish the tenant context at their authenticated boundary. - **Actor-bound audit revisions:** `#[orm(auditable)]` requires a validated user/service/system `AuditContext`; the active tenant and optional correlation ID are recorded with recursively redacted bounded changes. Generated instance saves/deletes/restores/force-deletes and their audit entry share a savepoint and fail together. Eligible v2 updates expose guarded revision restoration, which rejects stale, cross-tenant, redacted, malformed, legacy, create/delete, and oversized revisions and records a compensating audit entry. The host still derives authenticated principal/tenant authority, while bulk per-row history and durable export remain explicit. See [Auditable Revisions](https://rullst.github.io/Rullst/book/tutorials/50-auditable-revisions.html). - **Field privacy:** `#[orm(encrypted)]` transparently encrypts supported `String` fields with a versioned AES-256-GCM envelope. Randomized ciphertext cannot be filtered or sorted; use a separate keyed blind index where needed. Encrypted and `#[orm(masked)]` values appear as `"***"` in generated `to_json()`, audit rows and committed events, are omitted from Scout documents, and stay encrypted in `save_to_redis` hashes. `SecretString` fields are redacted the same way, and `SecretString` serializes as an encrypted envelope rather than plaintext. `RULLST_ENCRYPTION_KEY` (and its `_KEY_ID`/`_KEYRING` companions) is read from the process environment, then from `./.env`; see [field encryption](https://rullst.github.io/Rullst/book/tutorials/15-rullst-vault-encryption.html). - **Native relational enums:** `#[derive(Enum)]` owns one closed label mapping for SQLx, Serde and ORM values. `Blueprint::native_enum` emits a named, drift-checked PostgreSQL type with `strict-postgres`, inline MySQL/MariaDB `ENUM`, or a SQLite `TEXT CHECK` constraint. - **Scout hooks and providers:** `#[orm(searchable)]` calls a configured `SearchEngine` after generated writes/deletes. `scout-http` supplies bounded Meilisearch, Elasticsearch and Algolia adapters; the generated effect is process-local unless the application composes the transactional outbox. See [Scout Search Providers](https://rullst.github.io/Rullst/book/tutorials/39-scout-search.html). - **Typed pgvector queries:** `pgvector` re-exports `Vector` with SQLx support; vector/distance values in L2, cosine and inner-product helpers are bound, not interpolated. The strict PostgreSQL matrix creates the extension and runs a typed live lifecycle. See [RAG Systems & Vector Search](https://rullst.github.io/Rullst/book/tutorials/22-rag-vector-search.html). - **Generated embeddings:** `ai` implies `pgvector` and adds `save_with_embedding(&rullst_ai::AiClient)` to models with an `#[orm(embedding_for = "...")]` field; the application must also depend on `rullst-ai`. - **Bounded Qdrant vectors:** `qdrant` keeps specialized dense-cosine collection/upsert/delete/query semantics separate from SQL Active Record, with resource/transport bounds, deterministic fallback, authenticated protocol fixtures and a pinned live lifecycle. - **Native Redis structures:** `redis` adds an immutable namespace and bounded Hash, Set and Sorted Set operations in addition to `.remember`; remote endpoints require TLS and live evidence covers isolation and native commands. - **Portable document recovery:** MongoDB, SurrealDB and the deterministic store expose identifier-preserving inventory. An application-operated, AES-256-GCM snapshot binds application/collection scope, compares two bounded source observations, resumes only into an exact destination subset and verifies the final inventory. Writers, schema provisioning, key custody and durable backup storage remain explicit operator responsibilities. See [Polyglot Persistence](https://rullst.github.io/Rullst/book/polyglot-persistence.html). - **Structured telemetry:** generated/raw query and stream spans expose only static model/table/operation metadata, managed transactions record bounded outcomes, and Rullst-created pools emit checkout timing. Core's opt-in OpenTelemetry layer can export the standard tracing signals; subscriber, sampling, collector and separately configured SQLx logs remain host policy. - **Comparative SQLite evidence:** a lockfile-pinned Criterion harness gives Rullst, Diesel and SeaORM one typed connection, the same indexed schema, 100-row seed, SQLite policy and five logical operations. The CI history is scoped comparison evidence; it does not claim universal or negligible overhead, networked-database throughput or complete-application performance. - **Durable opt-in outbox:** `Outbox::enqueue` commits a stream-scoped, idempotent event with relational domain state. Exact lease tokens, bounded retry and dead-letter are shared by SQLite, PostgreSQL, MySQL and MariaDB, and streams and event keys are case-sensitive on all of them. Delivery is at least once, so the application dispatcher and consumer remain idempotent; generated observers are not silently converted into events. A nested `Orm::transaction` joins the active transaction through a savepoint, so a helper that enqueues inside its own transaction stays atomic with its caller. Concurrent sibling nested transactions take turns on the shared connection, and a savepoint left open makes the enclosing transaction roll back instead of committing. See the [transactional outbox tutorial](https://rullst.github.io/Rullst/book/tutorials/38-transactional-outbox.html). - **Database-first introspection:** `cargo rullst generate:models` reads SQLite, PostgreSQL, or MySQL metadata using bound schema/table parameters, normalizes table module identifiers, and rejects unsafe SQL identifiers, collisions, or columns requiring unsupported ORM remapping before writing files. - **Additive migration generation:** `make:migration:auto` compares supported model definitions and emits a migration for review. - **Cascading soft deletes:** Opt-in relationship metadata can cascade through generated delete methods; transaction-aware variants use the supplied transaction. The related model must also use soft deletes; a cascade into a model without them fails to compile instead of hard-deleting its rows. The children are selected within the tenant scope but outside the related model's `global_scope`. - **Transactional partial updates (v13 candidate):** `.update_partial()` merges selected values into a fresh row and performs its full save lifecycle, with explicit transaction support. Use it instead of `save()`, which writes the whole row, when requests may edit the same row at the same time; see [concurrent updates and counters](https://rullst.github.io/Rullst/book/crates/orm.html#concurrent-updates-and-counters) and [the contract and migration](https://rullst.github.io/Rullst/book/transactional-partial-updates.html). - **Model policies:** `#[orm(policy = "MyPolicy")]` invokes the configured policy on generated create/update/delete/restore operations. - **Strict lazy-loading prevention:** the global toggle makes generated lazy relationship methods return a validation error instead of performing the query. - **Explicit Capability Boundaries**: Unsupported replication paths fail closed instead of reporting simulated success. --- ## 🛠️ Quick Start ### Installation Install the v13 prerelease with an exact pin: ```bash cargo add rullst-orm@=13.0.0-alpha.4 -F strict-sqlite cargo add tokio -F full cargo add sqlx@0.9 --no-default-features cargo add tracing ``` The code that `#[derive(Orm)]` generates names `tokio` and `tracing`, and `#[derive(FromRow)]` names `sqlx`, so the application depends on all three directly. `rullst-orm` enables the SQLx features it needs; your own `sqlx` entry only has to be the same 0.9 release. A project generated by `cargo rullst new` already lists these dependencies. Select the driver of your database: `strict-sqlite`, `strict-postgres` or `strict-mysql`, or `drivers-all` for all three on SQLx's dynamic `Any` pool. Since 13.0.0-alpha.3 the crate enables no driver by default; without one, `Orm::init` fails at startup with an error that names the feature to enable. ### Zero-to-Hero Example ```rust,no_run use rullst_orm::schema::Schema; use rullst_orm::{FromRow, Orm}; // 1. Just add the Orm macro to your struct! #[derive(Debug, Clone, FromRow, Orm)] pub struct User { pub id: i32, // ID = 0 means it hasn't been saved yet pub name: String, pub email: String, #[orm(hidden)] // Excluded from generated to_json(), audit, event and search projections pub password: String, } #[tokio::main] async fn main() -> Result<(), rullst_orm::Error> { // 2. Initialize the connection pool (SQLite, Postgres, MySQL/MariaDB) Orm::init("sqlite::memory:").await?; // 3. Create the `users` table (a migration does this in an application) Schema::create("users", |table| { table.id(); table.string("name"); table.string("email"); table.string("password"); }) .await?; // 4. Create a new user through the generated Active Record API let mut user = User { id: 0, name: "Alice".to_string(), email: "alice@example.com".to_string(), password: "secret_password".to_string(), }; user.save().await?; // Runs INSERT and hydrates the generated ID. // 5. Fluent Queries let active_users = User::query() .where_like("email", "%@example.com") .order_by_desc("id") .limit(10) .get() .await?; println!("Found {} user(s)", active_users.len()); Ok(()) } ``` ### Concurrent updates and counters By default, `save()` on a loaded model writes every persisted column of that handle, so the last writer wins: when two requests load the same row, change different fields and save, the second `save()` silently reverts the first one's change. An insert also sends every field, so database column defaults (such as `DEFAULT CURRENT_TIMESTAMP`) do not apply to model fields, except to fields marked `#[orm(db_default)]`. **Dirty tracking (v13).** Declare one `#[sqlx(skip)]` field of type `RowSnapshot`, and `save()` writes only the columns that this handle changed: ```rust,no_run use rullst_orm::{FromRow, Orm, RowSnapshot}; #[derive(Debug, Clone, FromRow, Orm)] #[orm(table = "profiles")] pub struct Profile { pub id: i32, pub name: String, pub bio: String, #[sqlx(skip)] pub snapshot: RowSnapshot, } async fn rename(id: i32, name: String) -> Result<(), rullst_orm::Error> { let mut profile = Profile::find(id) .await? .ok_or(rullst_orm::Error::RecordNotFound)?; profile.name = name; // `profile.dirty_columns()` is now `["name"]`. profile.save().await // UPDATE profiles SET name = ? WHERE id = ? } ``` - Records read through the ORM (`find`, `first`, `get`, `all`, `paginate`, relations, streams, the query cache and the row `update_partial()` reads) remember a 64-bit fingerprint of every column that `save()` updates: 16 bytes per record plus 8 per column, never the values. `save()` compares the current values with them: `#[orm(encrypted)]` and `SecretString` fields by plaintext, so an unchanged secret is not encrypted again, JSON fields (`#[sqlx(json)]`, `Json<T>`, `serde_json::Value`) by their canonical document, and other fields by their Serde serialization. - The `UPDATE` sets only the changed columns, keeps the tenant predicate and returns the stored row (PostgreSQL and SQLite; MySQL/MariaDB read it again in the same transaction). The columns it did not write, `#[orm(db_default)]` columns and the soft-delete marker take their stored values, so the handle, the audit diff, model events and the search document describe the row as stored. A row deleted meanwhile still fails with `RecordNotFound`. - When nothing changed, `save()` runs no `UPDATE`, no `updating`, `updated` or `saved` observers, no `after_save` hook, audit entry or post-commit event. The `before_save` hook and the `saving` observers run first and may change fields; a model with neither, and without a policy, does not even open a transaction. - A record built in code holds `RowSnapshot::default()`: `save()` inserts it, or, with a non-zero `id`, writes every column. `mark_all_dirty()` makes a loaded record do the same, for code that relied on overwriting the row. `is_dirty()` tells whether `save()` has anything to write. - A failed `save()` keeps its changes pending. When a caller-owned transaction rolls back after a successful `save()`, reload the record or call `mark_all_dirty()`. - A `before_save` hook that stamps `updated_at` on every call makes every save write; stamp it only when `self.is_dirty()`. - Mark the field `#[serde(skip)]` when the model derives Serde traits. Any two snapshots compare equal and hash alike, so a derived `PartialEq` or `Hash` looks at the columns only. Since 13.0.0-alpha.3 every SQLx model that the CLI writes declares the field: `make:model` (and `make:resource`), `generate:models` (with `#[serde(skip)]`; a table with its own `snapshot` column keeps full-row saves), `auth`, `make:billing`, `make:chat-session` and the models of every `cargo rullst new` starter (Blank, API and SaaS) and of the LMS, ERP and Portfolio examples. Their struct literals set `snapshot: RowSnapshot::default()`. Turso models have no dirty tracking. Projects generated earlier keep their models until you add the field. Dirty tracking merges edits of different columns. Two writers of the same column still race, and the last one wins: - To change some fields of a row that other requests may edit, use `update_partial()`. It reads the current row in a transaction (with `SELECT ... FOR UPDATE` on PostgreSQL and MySQL/MariaDB), sets only the fields you chose and saves through the normal policy, hooks, audit and cache lifecycle. On SQLite a contended `update_partial()` can fail with a database error instead of waiting; retry it. It merges fields, but it does not detect two editors changing the same field: that needs a revision column checked by the application. - For a counter, run one atomic statement with `Orm::raw`. Its values are bound and it joins the active `Orm::transaction`, but it skips the model's tenant scope, policy, hooks, audit and cache invalidation: add the tenant condition yourself. Raw SQL uses the driver's own markers, `$1`, `$2` on PostgreSQL and `?` on SQLite and MySQL/MariaDB. ```rust,no_run use rullst_orm::{FromRow, Orm}; #[derive(Debug, Clone, FromRow, Orm)] #[orm(table = "students")] pub struct Student { pub id: i32, pub bio: String, pub xp: i32, } /// Changes only `bio`: an XP award committed meanwhile is kept. async fn edit_bio(student: &mut Student, bio: String) -> Result<(), rullst_orm::Error> { student.update_partial().bio(bio).save().await } /// Adds XP in one statement, so concurrent awards add up. async fn award_xp(student_id: i32, points: i32) -> Result<(), rullst_orm::Error> { let sql = if Orm::driver()? == "postgres" { "UPDATE students SET xp = xp + $1 WHERE id = $2" } else { "UPDATE students SET xp = xp + ? WHERE id = ?" }; let changed = Orm::raw(sql).bind(points).bind(student_id).execute().await?; if changed == 0 { return Err(rullst_orm::Error::RecordNotFound); } Ok(()) } ``` ### Database errors A unique or primary-key violation returns `Error::UniqueViolation`, and a foreign-key violation (a missing referenced row, or deleting a row that is still referenced) returns `Error::ForeignKeyViolation`. Generated writes, query builders and `Orm::raw` all map them; every other database failure stays `Error::DatabaseError(String)`. `constraint` holds the name PostgreSQL reports and is `None` on SQLite and MySQL/MariaDB. The message of these two errors names only the constraint, because the database's own text can quote the rejected value (MySQL reports `Duplicate entry 'alice@example.com'`). ```rust,no_run use rullst_orm::{Error, FromRow, Orm}; #[derive(Debug, Clone, FromRow, Orm)] #[orm(table = "members")] pub struct Member { pub id: i32, pub email: String, } /// `Ok(None)` when the email is already registered. async fn register(email: String) -> Result<Option<Member>, Error> { let mut member = Member { id: 0, email }; match member.save().await { Ok(()) => Ok(Some(member)), Err(Error::UniqueViolation { .. }) => Ok(None), Err(error) => Err(error), } } ``` `Orm::transaction` returns its closure's error unchanged after the rollback, so a violation inside it keeps its variant too. The closure may return the application's own error type when it implements `From<rullst_orm::Error>` and `Display`; failures of the transaction itself, such as a commit rejected by a deferred constraint, arrive through that conversion. ### Query values Filters such as `where_eq` and `where_in`, `Orm::raw(..).bind(..)` and `with_tenant` take any value that converts into `RullstValue`: `String`/`&str`, `i32`, `i64`, `u32`, `f64`, `bool`, the `chrono` types `DateTime<Utc>`, `NaiveDateTime`, `NaiveDate` and `NaiveTime` (see [date and time columns](https://rullst.github.io/Rullst/book/crates/orm.html#date-and-time-columns)), a `#[derive(Enum)]` type (see [native database enums](https://rullst.github.io/Rullst/book/crates/orm.html#native-database-enums)) and `Option` of those. `None` binds SQL `NULL`, typed as text: on PostgreSQL write `CAST($1 AS BIGINT)` to store it in a column of another type. A comparison with `NULL` never matches, so filter missing values with `where_null`. `RullstValue` is `#[non_exhaustive]`; give a `match` on it a wildcard arm. ```rust,no_run use rullst_orm::{FromRow, Orm}; #[derive(Debug, Clone, FromRow, Orm)] #[orm(table = "progress")] pub struct Progress { pub id: i32, pub lesson_id: Option<i64>, pub seconds: i64, } async fn for_lesson(lesson_id: Option<i64>) -> Result<Vec<Progress>, rullst_orm::Error> { let query = Progress::query(); match lesson_id { Some(lesson_id) => query.where_eq("lesson_id", lesson_id).get().await, None => query.where_null("lesson_id").get().await, } } ``` ### Query row cap Generated builders start with a global row cap (`Orm::set_max_query_limit`, 1,000 by default; `0` disables it). `limit()` clamps to that cap and `unsafe_unlimited()` removes it for one explicit query. `paginate(page, per_page)` clamps `per_page` to the same cap, because the value often comes from request input; `PaginationResult::per_page` and `last_page` report the effective page size. `count()` and the `paginate()` total count the rows the query returns: a `distinct()` or `group_by()` query is counted as a derived table, so the total is the number of distinct rows or groups. Eager loading runs one related-model query for all parents of a batch and never assigns relations from a result truncated by that cap: when the related rows exceed it, `get()` fails with a `Validation` error naming the relation. Load fewer parents per query, raise the cap, or choose explicitly with `with_<relation>_constrained(...)`: an explicit smaller `limit(n)` there applies to the whole batch, and `unsafe_unlimited()` loads every related row. Parents that share a related row or group all receive it: every child of one `belongs_to` parent, parents whose non-unique `local_key` matches the same `has_many`/`has_one` rows, and duplicated parent rows. The shared value is cloned for all but the last such parent, so a related model without `Clone` loads normally until a row must be shared, and then `get()` fails with a `Validation` error instead of leaving a parent without its relation. A relation key may be a nullable foreign key (`author_id: Option<i32>`) on either model. A `None` key matches no row, like SQL `NULL`: the lazy loader returns `None` or an empty list without a query, an eager load gives such a parent `None` (or `Some(vec![])` for a to-many relation), and a related row whose key is `None` belongs to no parent. ### Native database enums Generated applications should select a strict primary feature. PostgreSQL native enums specifically require `strict-postgres`, because SQLx's dynamic `Any` driver cannot decode custom PostgreSQL types: ```toml rullst-orm = { version = "=13.0.0-alpha.4", features = ["strict-postgres"] } ``` Derive one label contract and use it in schema code: ```rust use rullst_orm::schema::{Blueprint, Schema}; use rullst_orm::{Enum, Orm}; #[derive(Enum, Debug, Clone, Copy, PartialEq, Eq)] #[rullst_enum(type_name = "account_status", rename_all = "snake_case")] enum AccountStatus { AwaitingReview, Active, } # async fn create_schema() -> Result<(), rullst_orm::Error> { Orm::init("postgres://user:password@localhost/application").await?; Schema::create("accounts", |table: &mut Blueprint| { table.id(); table.native_enum::<AccountStatus>("status").not_null(); }).await?; # Ok(()) # } ``` The derive accepts 1–64 unique labels of at most 63 bytes using ASCII letters, digits, spaces, underscores or hyphens. An existing PostgreSQL type must have the exact same ordered labels or schema creation fails. MySQL/MariaDB store the labels in the table's inline `ENUM`; SQLite enforces them through `TEXT CHECK`. Adding, removing or reordering labels is an explicit reviewed migration. Drop every dependent table before calling `Schema::drop_native_enum::<T>()` on PostgreSQL; the method is a validated no-op on the other backends. The enum type creation, its label check and `drop_native_enum` use the active `Orm::transaction` or test sandbox like the table DDL, so they roll back with it and the type can be dropped right after its tables in the same transaction. Builder filters on a model field whose type derives `Enum` (or `Option<...>` of it) work on every backend: on PostgreSQL the comparison, `IN` and `BETWEEN` markers of that column become `CAST(? AS "<type_name>")`, because a text parameter has no operator against a named enum type. This covers `where_eq`, `where_in`, the generated `where_<column>` helpers and their `or_`/`not_` variants; `where_like` and raw SQL are unchanged. ### Constraints, indexes and foreign keys `Schema::create` declares unique constraints, indexes, foreign keys and CHECK constraints next to the columns. Every name is validated with the identifier grammar before it reaches DDL: ```rust,no_run use rullst_orm::schema::{Blueprint, ColumnDefault, ForeignKeyAction, Schema}; # async fn create_schema() -> Result<(), rullst_orm::Error> { Schema::create("enrollments", |table: &mut Blueprint| { table.id(); // An `INTEGER NOT NULL` column referencing `users(id)`. table .foreign_id("user_id", "users") .on_delete(ForeignKeyAction::Cascade); table.integer("course_id").not_null(); table .foreign("course_id") .references("courses", "id") .on_delete(ForeignKeyAction::Restrict); // MySQL/MariaDB cannot index a TEXT column: give indexed strings a length. table.string("code").not_null().unique().col_type = "VARCHAR(64)".to_string(); table.integer("progress").not_null().default(ColumnDefault::Integer(0)); table.unique(&["user_id", "course_id"]); table.index(&["course_id", "progress"]).name("enrollments_course_progress"); table.check("progress BETWEEN 0 AND 100"); table.timestamps(); }).await?; # Ok(()) # } ``` - `Column::unique()` adds a column-level `UNIQUE`; `unique(&[...])` a table constraint named `<table>_<columns>_unique` unless `.name(...)` sets one. - `index(&[...])` and `unique_index(&[...])` are `CREATE [UNIQUE] INDEX IF NOT EXISTS <table>_<columns>_index|_unique` statements after `CREATE TABLE` on PostgreSQL and SQLite, and `INDEX` clauses of `CREATE TABLE` on MySQL/MariaDB (which has no `IF NOT EXISTS` for indexes). Names are at most 63 bytes, so PostgreSQL never truncates them; a longer default name fails and asks for `.name(...)`. - `foreign(column).references(table, column)` adds `<table>_<column>_foreign` with optional `on_delete`/`on_update` (`Cascade`, `Restrict`, `SetNull`, `NoAction`). `foreign_id(column, table)` also adds the `INTEGER NOT NULL` column and references `table(id)`. The column type must match the referenced one (`id()` is `INTEGER`), `SetNull` needs a nullable column, and SQLite enforces foreign keys only with `PRAGMA foreign_keys = ON`, which SQLx sets on every connection it opens. - `check(expression)` adds `CHECK (expression)`. The expression is trusted developer SQL emitted as written: building fails when it is empty, longer than 512 bytes, or contains `;`, a comment, `\`, `$`, a backtick, a control character or unbalanced parentheses or quotes, but never build it from request data. MySQL enforces CHECK from 8.0.16 and MariaDB from 10.2.1. - On MySQL/MariaDB a unique, index or foreign-key column whose type is `TEXT`, `BLOB` or `JSON` (a plain `string()`, or `timestamps()` on the `Any` pool) fails before any DDL runs: set a bounded `col_type` such as `VARCHAR(255)`. A unique or foreign-key violation from a later write returns `Error::UniqueViolation`/`Error::ForeignKeyViolation`; PostgreSQL reports the constraint name. `Schema::table` alters an existing table with the same builders: it adds the blueprint's columns (`ALTER TABLE ... ADD COLUMN`), then its unique constraints, indexes, foreign keys and checks, all validated before the first statement runs: ```rust,no_run use rullst_orm::schema::{Blueprint, ForeignKeyAction, Schema}; # async fn add_reviewer() -> Result<(), rullst_orm::Error> { Schema::table("posts", |table: &mut Blueprint| { table.integer("reviewer_id"); table .foreign("reviewer_id") .references("users", "id") .on_delete(ForeignKeyAction::SetNull); table.index(&["reviewer_id"]); }).await?; # Ok(()) # } ``` SQLite cannot add a constraint to an existing table. There a unique constraint, or a `unique()` column, becomes the equivalent unique index, and a foreign key on a column added in the same call becomes that column's `REFERENCES` clause (the column must be nullable without a default). Any other foreign key (on an existing column, or a second one on the same new column), a CHECK, a primary-key column, a `NOT NULL` column without a default or a current-time default returns `Error::UnsupportedSchemaChange` before any statement runs; rebuild such a table with a new `Schema::create`. MySQL/MariaDB commit each DDL statement on its own, so a failure there can leave the earlier statements applied. ### Date and time columns `date()`, `time()`, `timestamp()` and `timestamp_tz()` hold `chrono` values (re-exported as `rullst_orm::chrono`, and in the facade as `rullst::orm::chrono`): | Helper | Rust field | PostgreSQL | MySQL/MariaDB | SQLite (ISO-8601 text) | | :--- | :--- | :--- | :--- | :--- | | `date` | `NaiveDate` | `DATE` | `DATE` | `2026-10-15` | | `time` | `NaiveTime` | `TIME` | `TIME(6)` | `09:30:00.500` | | `timestamp` | `NaiveDateTime` | `TIMESTAMP` | `DATETIME(6)` | `2026-10-15 09:30:00.500` | | `timestamp_tz` | `DateTime<Utc>` | `TIMESTAMPTZ` | `DATETIME(6)` (UTC) | `2026-10-15T09:30:00.500+00:00` | MySQL/MariaDB store a UTC instant in `DATETIME(6)` because their `TIMESTAMP` ends in 2038; SQLx sets the session time zone to `+00:00`, so values and `CURRENT_TIMESTAMP` defaults are UTC (other clients see the same UTC wall time). A `ColumnDefault::CurrentTimestamp` default is the current UTC time in the column's own form: `CURRENT_TIMESTAMP` on a PostgreSQL `TIMESTAMPTZ`, the UTC wall time (`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`) on the zone-less types, `CURRENT_TIMESTAMP(6)`, `(CURRENT_DATE)` or `(CURRENT_TIME(6))` on MySQL/MariaDB, and on SQLite the same text SQLx writes, so stored values of one column compare and sort as text (`strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now')` for `timestamp_tz`). `table.timestamps()` adds nullable `created_at`/`updated_at` `timestamp_tz()` columns that default to the current time. Model fields, filters and ordering use the `chrono` types; `RullstValue` converts them (`where_gt("starts_at", Utc::now())`), and `#[orm(db_default)]` leaves a column to its database default: ```rust,ignore // Needs a strict driver feature (see below). use rullst_orm::chrono::{DateTime, NaiveDate, Utc}; use rullst_orm::{FromRow, Orm}; #[derive(Debug, Clone, FromRow, Orm)] #[orm(table = "events")] pub struct Event { pub id: i32, pub starts_at: DateTime<Utc>, pub day: NaiveDate, pub ends_at: Option<DateTime<Utc>>, // Never written by `save()`: INSERT keeps the column DEFAULT and UPDATE // keeps the stored value. The handle keeps its own value (here `None`) // until the row is read again. #[orm(db_default)] pub created_at: Option<DateTime<Utc>>, pub updated_at: DateTime<Utc>, } ``` `#[orm(db_default)]` fields are left out of the generated `INSERT` and of every `UPDATE` of `save()`, `update_partial()` has no setter for them and `#[derive(Nexus)]` shows them read-only. Set an `updated_at` without it yourself (`Utc::now()`) or maintain it with a trigger. A model with a `RowSnapshot` field refreshes them from the stored row after each update. `#[orm(db_default)]` is also the option for any column that the database or your own SQL manages, such as the generated `users.is_admin` and `users.session_epoch`: queries can filter, order, group and select it, and ORM writes never touch it. `#[orm(skip)]` means something else: the field is not a database column for the ORM, so a filter, ordering, grouping or `select` on it fails with `Error::Validation`. A skipped field that the table does not have also needs `#[sqlx(skip)]` (or `#[sqlx(default)]`), because the application's SQLx `FromRow` still reads `#[orm(skip)]` fields. | Option | The column | Filters, ordering, grouping, `select` | ORM writes | | --- | --- | --- | --- | | `#[orm(skip)]` | is not a database column for the ORM | rejected | never | | `#[orm(db_default)]` | exists and is managed by the database | allowed | never | Date/time columns and `chrono` fields need a strict driver feature (`strict-postgres`, `strict-mysql` or `strict-sqlite`, which generated projects use). SQLx's dynamic `Any` pool, selected by `drivers-all` without a strict feature, carries only integer, float, text and blob values: it cannot decode a `chrono` field, and it cannot read a PostgreSQL or MySQL/MariaDB `DATE`, `TIME`, `TIMESTAMP` or `TIMESTAMPTZ` column at all. On that pool `Schema::create` and `Schema::table` reject those helpers on PostgreSQL and MySQL/MariaDB with `Error::Validation`, SQLite keeps its ISO-8601 text for `String` fields, chrono filter values bind as that same text, and `timestamps()` keeps the alpha.1 `TEXT DEFAULT CURRENT_TIMESTAMP` columns (`DEFAULT (CURRENT_TIMESTAMP)` on MySQL/MariaDB, which accept a default on `TEXT`, `BLOB`, `JSON` and `GEOMETRY` columns only as an expression; other non-`NULL` defaults on those types are wrapped the same way, MySQL 8.0.13+, MariaDB 10.2.1+). `soft_deletes()` stays a `TEXT` `deleted_at` column on every pool; declare `timestamp_tz("deleted_at")` for a native one. The Turso adapter does not use the Blueprint and keeps text timestamps. `table.float(...)` and `table.boolean(...)` map to `f64` and `bool` model fields. PostgreSQL receives `DOUBLE PRECISION` and `BOOLEAN` (an integer `ColumnDefault` of `0`/`1` becomes `FALSE`/`TRUE`); MySQL/MariaDB receive `DOUBLE` and an `INTEGER` 0/1 flag; SQLite keeps `REAL` and `INTEGER`. This applies to newly built DDL only: PostgreSQL columns created by earlier versions remain `REAL`/`INTEGER` until a reviewed migration alters them, and a model that paired `boolean()` with an integer field on PostgreSQL must switch to `bool` (or use `integer()`) for new tables. --- ## 📚 Documentation We recently launched a brand-new **Interactive Documentation Hub**! 👉 **[Explore the Full Documentation in the Rullst Book](https://rullst.github.io/Rullst/book/)** --- ## 🛡️ Security Rullst ORM uses SQLx prepared-statement bindings for values accepted by its query builders. Structural identifiers are restricted to a bounded ASCII identifier grammar before interpolation. Raw SQL and application authorization remain the caller's responsibility; these controls reduce injection risk but are not an absolute safety guarantee. ## 📄 License This project is licensed under the [MIT License](https://github.com/Rullst/Rullst/blob/main/LICENSE). --- # Transactional partial updates Source: https://rullst.github.io/Rullst/book/transactional-partial-updates.html # Transactional partial updates Since v13 (first released in 13.0.0-alpha.1), a SQLx model’s typed partial change participates in the model's normal save lifecycle. A lesson editor can submit a new title while preserving a note that another operation has already changed, applying its policy, recording the audit and refreshing projections after commit. Local SQLite, PostgreSQL and MySQL journeys, cancellation, Redis cache, Scout, MSRV 1.96, strict linting and an extracted-facade consumer passed. Hosted workspace/platform and extracted-package source admission passed in [PR #236](https://rullst.github.io/Rullst/book/v13-delivery-plan.html#six-increment-source-admission-on-september-21). Final release admission remains separate. ## Application use The consumer needs its selected `rullst` ORM/database features and direct `sqlx`, `tokio` and `tracing` dependencies, which the generated macros reference. The [extracted-package consumer](https://github.com/Rullst/Rullst/blob/main/.github/test-packaged-distribution.sh) records a compiled dependency profile (`rullst` with `strict-sqlite`, plus `tokio`, `sqlx` and `tracing`). ```rust,no_run # #[cfg(feature = "orm")] # mod example { use rullst::orm as rullst_orm; use rullst_orm::{FromRow, Orm}; #[derive(Debug, Clone, FromRow, Orm)] #[orm(table = "lessons", tenant_column = "tenant_id", auditable)] pub struct Lesson { pub id: i32, pub tenant_id: String, pub title: String, pub note: Option<String>, } // The host first authenticates the editor and authorizes this lesson. // Supply tenant/actor from trusted identity, not request-body assertions. pub async fn rename( lesson: &mut Lesson, trusted_tenant: String, trusted_editor: String, title: String, ) -> Result<(), rullst_orm::Error> { let actor = rullst_orm::audit::AuditContext::user(trusted_editor)?; rullst_orm::tenant::with_tenant(trusted_tenant, rullst_orm::audit::with_audit_context(actor, lesson.update_partial().title(title).save(), ), ).await } pub async fn rename_in_transaction( lesson: &mut Lesson, tx: &mut rullst_orm::db::Transaction<'_>, title: String, ) -> Result<(), rullst_orm::Error> { lesson.update_partial().title(title).save_with_tx(tx).await } # } ``` Create/migrate the application and audit tables before serving requests. An update on an auditable model now requires the same `AuditContext` as a full save. Tenant scoping does not establish ownership or grant an editor permission. Existing application routes keep their authentication, CSRF, security headers and ownership checks. `update_partial()` excludes the primary key and declared tenant column from its setters. The soft-delete column keeps its setter, but setting it fails with `Validation`; only `delete()`, `restore()` and `force_delete()` change that marker. `note(None)` explicitly clears a nullable field; omitting the setter preserves that field from the persisted row. An empty builder remains a no-op, without reading the database, running hooks or creating an audit entry. ## Database and lifecycle behavior A nonempty patch opens a savepoint, validates the model handle's active tenant, and reads the current row by bound primary key and tenant. Missing rows fail with `RecordNotFound`. PostgreSQL and MySQL use `SELECT ... FOR UPDATE` so a competing update cannot change that row between the read and write. SQLite retains its transaction semantics; a contended read-to-write upgrade can fail with a database error. There is no automatic retry. The row must decode through the selected SQLx profile before the patch can write. This differs from a bare selected-column update: incompatible database types now fail before mutation. For example, SQLx Any 0.9 rejects SQLite `BOOLEAN` column metadata; select and validate a typed backend profile for backend-specific types instead of assuming every Any schema can be read. Selected values are merged into the fresh model. The normal generated save then runs its policy, before/after hooks, observers, encryption, audit and post-commit registrations. A policy, hook, SQL or audit failure rolls the operation's savepoint back and discards its queued effects. Reentrant implicit ORM reads inside borrowed mutation callbacks fail validation; load required policy data explicitly before the mutation, as for full saves. The successful direct path commits its own transaction, replaces the caller's object with the fresh merged model, and runs committed observers, cache invalidation and Scout projections. A `PostCommit` error means that SQL has already committed; it does not authorize replaying the domain mutation. These callbacks are process-local. Use an explicit transactional outbox for effects that need durable recovery. This implementation saves the merged row through `save()`: a full-row SQL update, or, for a model with a `RowSnapshot` field (dirty tracking), an update of only the patched columns whose value differs from the locked row. The extra read and savepoint have a cost; row-read privileges, update triggers and privileges and encrypted-field re-encryption follow `save()` behavior. Model hooks may transform the candidate under their usual contract. The loaded row first runs the model's `after_fetch` hook, exactly as `find()` returns it, so a `before_save` mutator paired with an `after_fetch` accessor is applied once rather than to the stored representation again. The hook runs while the transaction is borrowed, so ORM access from inside it fails closed. Review hooks that assumed partial updates skipped them, and database triggers that react to the SQL column list. ## Caller-owned transactions The new `save_with_tx(&mut transaction)` terminal method supports an explicit borrowed transaction. Ordinary `.save()` also reuses the active `Orm::transaction` task scope. Neither commits the enclosing transaction. The `rename_in_transaction` example above uses that explicit terminal method. A successful scoped update replaces the object with the transaction's tentative row. If a later operation rolls that transaction back, discard or reload the object; the ORM cannot retroactively mutate an application reference after its borrow has ended. Catching a failed patch and continuing the managed transaction does not promote that failed savepoint's audit or effects. Strict post-commit timing requires the direct owned path or `Orm::transaction`. A raw SQLx transaction cannot report its eventual commit decision to these callbacks, so it retains the existing raw-transaction limitation. Do not treat savepoint release as proof that a caller-owned transaction is durable. ## Migration and evidence For existing `update_partial()` call sites, review: - Required audit identity, tenant context and the model's newly applied hooks. - Database update triggers/privileges (full-row, or only the changed columns for a model with a `RowSnapshot` field) and the extra read/savepoint. - Unsaved fields and loaded relations in the old object: successful updates refresh them from the database representation, rather than retaining stale local values. Only explicit setters submit field values. - Post-commit errors, enclosing rollback and ambiguous commit responses. A database commit error can require reconciliation; do not assume every error proves that no write occurred. The row lock merges current values; it does not reject a stale editor's expected revision. Conflicting edits to the same field still need an application revision or compare-and-set rule. This is also not a bulk update, multi-shard transaction, background repair service or automatic schema migration. The [shared database journey](https://github.com/Rullst/Rullst/blob/main/rullst-orm/tests/partial_update_contract/mod.rs) exercises rejected policy/hooks/observers, constraints, missing audit context, tenant isolation, fresh-row merging, nullable fields, concurrent patches, explicit/task-scoped rollback, cancellation after SQL, audit deltas and post-commit failures. The same journey is part of the existing SQLite, PostgreSQL and MySQL matrices. The separate Redis cache and Scout tests verify that committed patches refresh projections while rolled-back patches leave them intact. SQLite contention tests verify known rollback state before an explicit fresh retry. The linked source admission records the corresponding hosted and extracted-package results. --- # rullst-connect Source: https://rullst.github.io/Rullst/book/crates/connect.html # Rullst Connect 🦀 > [!IMPORTANT] > This page describes the v13 prerelease, published as `13.0.0-alpha.4`. Cargo > never selects a prerelease on its own, so its dependency examples pin that > version exactly. The v12 line (`rullst-connect@12`) reached end of life on > 16 October 2026; see the [release record](https://rullst.github.io/Rullst/book/v12.html). Use a path dependency only for checkout-local > review. Since **12.1.2** (and in v13), built-in authorization-code URLs explicitly request `response_type=code`. The [OAuth protocol](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1) and [Google request contract](https://developers.google.com/identity/openid-connect/openid-connect#sendauthrequest) require this field. Offline parsed-query tests cover all eleven providers; real account interoperability remains separate. Temporary workarounds for earlier 12.x releases should add the field only when absent. > **Vision preserved:** message brokers, additional queue transports, remote > storage, and media work are retained with explicit status and recommendations in > the [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html#connect-real-time-queues-storage-and-data). [![Crates.io](https://img.shields.io/crates/v/rullst-connect.svg?style=for-the-badge&logo=rust)](https://crates.io/crates/rullst-connect) [![Downloads](https://img.shields.io/crates/d/rullst-connect.svg?style=for-the-badge)](https://crates.io/crates/rullst-connect) [![Documentation](https://img.shields.io/docsrs/rullst-connect?style=for-the-badge&logo=docs.rs)](https://docs.rs/rullst-connect) [![Build](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/ci.yml?style=for-the-badge&logo=github)](https://github.com/Rullst/Rullst/actions/workflows/ci.yml) [![License](https://img.shields.io/crates/l/rullst-connect?style=for-the-badge)](https://opensource.org/licenses/MIT) **Rullst Connect** is an async OAuth2/OIDC client layer with a shared `Provider` interface and normalized `ConnectUser` output. Provider-specific protocols, scopes, claims, and application account-linking rules still require explicit review. ## 🛡️ Security Engineering Rullst Connect uses layered tests and repository security checks. CI badges report the state of those checks for the referenced commit; they are not an absolute security guarantee. | Security Audit | Status | Description | | :--- | :---: | :--- | | **OpenSSF Scorecard** | [![OpenSSF Scorecard](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.scorecard.dev%2Fprojects%2Fgithub.com%2FRullst%2FRullst&query=%24.score&label=OpenSSF%20Scorecard&style=flat-square)](https://scorecard.dev/viewer/?uri=github.com/Rullst/Rullst) | Current public supply-chain practice score; not a security certification | | **Codecov** | [![Framework library coverage](https://codecov.io/github/Rullst/Rullst/branch/main/graph/badge.svg?component=framework_libraries)](https://codecov.io/gh/Rullst/Rullst) | Blocking 90% target for the measured framework-library scope; the complete repository aggregate now also has its own 90% gate | | **OpenSSF Best Practices** | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13360/badge)](https://www.bestpractices.dev/projects/13360) | Current project badge from the official programme | | **Release Provenance** | [![Release](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/release.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/release.yml) | Provenance attestations for release artifacts; no SLSA level is claimed here | | **On-demand fuzzing** | [![Fuzzing](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/fuzzing.yml?branch=main&style=flat-square&label=Fuzzing)](https://github.com/Rullst/Rullst/actions/workflows/fuzzing.yml) | Manual time-bounded targets; no continuous OSS-Fuzz claim | | **Property tests** | [![Proptest](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/proptest.yml?branch=main&style=flat-square&label=Proptest)](https://github.com/Rullst/Rullst/actions/workflows/proptest.yml) | Scheduled/manual bounded invariant evidence | | **Miri research matrix** | [![Miri](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/miri.yml?branch=main&style=flat-square&label=Miri)](https://github.com/Rullst/Rullst/actions/workflows/miri.yml) | Manual bounded evidence; the selected pure-Rust client scope is strict, while network/provider execution remains outside Miri | | **Kani research harnesses** | [![Kani](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/kani.yml?branch=main&style=flat-square&label=Kani)](https://github.com/Rullst/Rullst/actions/workflows/kani.yml) | Manual bounded formal evidence, not proof of the complete protocol surface | | **CodeQL SAST** | [![CodeQL](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/codeql.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/codeql.yml) | Advanced semantic code analysis | | **Cargo Deny** | [![Cargo Deny](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/cargo-deny.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/cargo-deny.yml) | Banning unmaintained/vulnerable crates | | **Cargo Audit** | [![Cargo Audit](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/audit.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/audit.yml) | Continuous scanning for crate vulnerabilities | | **Benchmark CI** | [![Benchmark](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/bench.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/bench.yml) | Benchmarks after pushes to `main` and weekly; a regression past 120% posts an alert comment but does not fail the run | | **Cargo SemVer** | [![Semver Checks](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/semver.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/semver.yml) | Strict SemVer API breakage checks | | **Cargo Machete** | [![Machete](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/machete.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/machete.yml) | Detecting unused and bloated dependencies | | **Spellcheck CI** | [![Spellcheck](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/spellcheck.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/spellcheck.yml) | Automated typo detection across docs and code | | **Mutation Testing** | [![Mutants](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/mutants.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/mutants.yml) | Mutation testing for test suite robustness | | **Secret Scanning** | [![Trufflehog](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/trufflehog.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/trufflehog.yml) | Automated CI prevention of leaked credentials | | **Unsafe Policy** | [![Unsafe](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/unsafe-policy.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/unsafe-policy.yml) | Audits unsafe usage within the workflow's declared scope | | **Panic Policy** | [![Panics](https://img.shields.io/github/actions/workflow/status/Rullst/Rullst/zero-panics.yml?style=flat-square&label=%20)](https://github.com/Rullst/Rullst/actions/workflows/zero-panics.yml) | Graceful error handling across the framework | ## ✨ Features - 🚀 **Async HTTP**: Built on Tokio-compatible request paths and `reqwest`. - 🧩 **Standardized**: All providers return a unified `ConnectUser` struct. - 🛡️ **Type-Safe**: Robust error handling using `thiserror` (`ConnectError`). - 🔌 **Framework adapters**: Core provider APIs are framework-independent; optional extractor features cover the integrations declared in the crate. - 🔐 **Login challenge store** (v13, 13.0.0-alpha.3): `begin_login` and `finish_login` keep state + PKCE + OIDC nonce in a `ChallengeStore` (memory, or the application's SQLite, PostgreSQL or MySQL pool) behind an opaque id in a short `HttpOnly`, `SameSite=Lax` cookie, and consume each challenge exactly once. `cargo rullst make:oauth google|github|microsoft|apple` generates it for Rullst accounts. - 🔐 **Managed callback transaction**: The optional Axum/tower-sessions path generates, stores, expires, validates, and consumes state + PKCE + OIDC nonce. - 🔏 **Encrypted token snapshots**: Versioned AES-256-GCM envelopes bind a refresh generation to one trusted provider/account pair and an explicit key rotation ID before application-owned persistence. - 🔐 **OIDC Security**: Strict discovery validation plus isolated JWKS caches with TTL, single-flight refresh on unknown `kid` (at most once per 30 seconds per JWKS URL, after rejecting malformed `kid` values without I/O), and bounded stale-if-error behavior. `OidcProvider` verifies an ID token without `kid` only against a single-key JWK Set whose key fits the token's asymmetric `alg` (OIDC Core 10.1); other sets fail closed. - 🚪 **Typed remote revocation**: Access and refresh tokens are distinct API operations. Google, GitHub, Discord, Apple, Auth0 and Cognito have bounded protocol adapters; unsupported providers fail explicitly and offline credentials remain network-free. - 🏢 **Explicit Corporate Proxy**: First-class HTTP(S) proxy clients, including bounded Basic proxy authentication without credentials in the endpoint URL. - 📺 **Device Flow**: RFC 8628 device authorization for headless CLI and Smart TV auth through `Provider::request_device_code`/`poll_device_token`; the GitHub adapter implements it and other providers return an explicit error. - 🛠️ **Testing**: With the `mock` feature (`rullst/oauth-mock` through the facade), empty or `mock_*` credentials select a deterministic offline transport; without it, `try_new` rejects them. `mock_idp` supplies a loopback-only signed OIDC fixture with one-shot codes, PKCE, nonce, EdDSA ID tokens and JWKS. > 📚 **Important Documents:** > - [CHANGELOG.md](https://github.com/Rullst/Rullst/blob/main/CHANGELOG.md): See what's new. > - [ISSUES](https://github.com/Rullst/Rullst/issues): Any issue? Please report. > - [AUDIT.md](https://github.com/Rullst/Rullst/blob/main/AUDIT.md): Repository audit record; current workflow evidence remains authoritative. ## 📦 Supported Providers Official support for 11 core providers: 1. **Google** 2. **GitHub** (OAuth 2.0, not OpenID Connect: `ConnectUser::id` is the numeric account id of `GET /user`, which never changes, never the login name. From 13.0.0-alpha.3 `email` is the address `GET /user/emails` marks `primary` and `email_verified` is its `verified` flag; the default `user:email` scope grants the call. When it fails or lists no primary address, `email` is the public profile address and `email_verified` is `None`) 3. **Microsoft / Azure AD**, two adapters: - `MicrosoftOidcProvider` (v13, from 13.0.0-alpha.3) signs in with OpenID Connect for an explicit `MicrosoftTenant`: `Common` (any Entra ID organization and personal accounts), `Organizations`, `Consumers` or one directory ID (`MicrosoftTenant::parse` reads `common`, `organizations`, `consumers` or a GUID). It verifies the RS256 ID token against `https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys`, requires the issuer `https://login.microsoftonline.com/<tid>/v2.0` of the token's own `tid` and refuses a `tid` the tenant choice does not admit. `ConnectUser::id` is `<tid>:<oid>`, Microsoft's stable key for a user (`sub` is pairwise per application). `email` is the optional `email` claim, which Microsoft does not verify; `email_verified` is `Some(true)` only when the token also carries the optional claim `xms_edov: true` (request `email` and `xms_edov` in the app registration's Token configuration), otherwise `None`. It has no `get_user_from_token`: the identity comes only from the code exchange. - `MicrosoftProvider` reads Microsoft Graph `/me` through the multi-tenant `common` authority, so any Entra tenant and personal Microsoft accounts can sign in, and returns no tenant ID; `email` is Graph `mail` or, failing that, `userPrincipalName`, which tenant administrators control and which may not be a mailbox, so `email_verified` is `None`. Never link accounts or grant tenant access on it. 4. **Apple** (Sign in with Apple; `AppleProvider::verify_signing_key` checks at startup that the `.p8` key can sign the ES256 client secret, and `AppleProvider::name_from_user_field` reads the unsigned first-login name from the callback's `user` field, from 13.0.0-alpha.3) 5. **Auth0** 6. **AWS Cognito** 7. **Facebook** 8. **X (Twitter)** (Strict PKCE requirement; confidential clients authenticate to the token endpoint with HTTP Basic, `client_secret_basic`) 9. **Discord** 10. **LinkedIn** 11. **OIDC (OpenID Connect Custom Provider)** (sends the client secret in the token request body unless discovery lists `client_secret_basic` without `client_secret_post`, in which case it uses HTTP Basic) ### Tokens supplied by native or mobile clients `Provider::get_user_from_token` is **not** an authentication primitive for a token that a client (for example a native or mobile app) sends to your backend. Except for Apple, it sends the bearer token to the provider's userinfo or profile endpoint, which proves only that the token is valid for *some* client of the provider, not that it was issued to your `client_id`. Another app that obtained a user's token for its own client ID could replay it and be signed in as that user. Sign users in with your own authorization-code exchange (`get_user` with state, PKCE and, for OIDC, nonce), or with an ID token whose signature, issuer, audience (your `client_id`), expiry and nonce are verified for your client. Use `get_user_from_token` only for tokens your server obtained itself. Apple's adapter treats its argument as an ID token and checks signature, issuer, audience and expiry but no nonce, so a captured Apple ID token for your client can be replayed until it expires. v13 (since 13.0.0-alpha.1) adds that audience-bound entry point for Google and `OidcProvider`: `verify_id_token(id_token, expected_nonce)` verifies the signature through the provider's rotating JWKS and requires the exact issuer, `aud` equal to your `client_id` (and, when present, an `azp` equal to it or, for Google, to a configured native presenter), valid `exp`/`iat` and the nonce your server issued for that sign-in attempt. It never calls userinfo. The returned `ConnectUser` carries the verified ID token in `access_token`; there is no provider access or refresh token in this flow. `OidcProvider` treats profile claims as optional (OIDC Core 5.1) in ID tokens and userinfo alike: without `name`, `ConnectUser::name` falls back to `given_name` and `family_name`, then `preferred_username`, then `nickname`, and is empty when none is present. The email address and subject are never used as a display name. ```rust,no_run use rullst_connect::prelude::{ConnectError, ConnectUser, GoogleProvider}; async fn sign_in_native_google_user( google: &GoogleProvider, id_token: &str, nonce_issued_for_this_attempt: &str, ) -> Result<ConnectUser, ConnectError> { google .verify_id_token(id_token, nonce_issued_for_this_attempt) .await } ``` Generate the nonce on your server, give it to the client for the provider sign-in request, and consume it once, just like an OAuth `state`. Android Credential Manager and iOS Google Sign-In request the ID token for your server (web) client ID, so `aud` is that ID and `azp` is the Android or iOS client ID. Configure the server client ID on `GoogleProvider` and list the native client IDs with `try_with_authorized_presenters(["ANDROID_CLIENT_ID", "IOS_CLIENT_ID"])` (at most 16). `aud` must still equal the server client ID; any other `azp` is rejected, and the authorization-code flow keeps requiring `azp` to equal the server client ID. Remote token revocation is deliberately narrower than login support. Use `Provider::revoke_token` for an access token and `Provider::revoke_refresh_token` for a refresh token; Auth0/Cognito accept only the latter through these adapters, while GitHub accepts only the former. A successful provider response does not clear application cookies, sessions, cached identity, or durable token records—the host must commit that local logout lifecycle. Provider revocation endpoints are intentionally idempotent in several ecosystems, so HTTP success is protocol acceptance, not proof that the token was valid immediately before the call. ## 🛠️ Installation Inside a source checkout, a path dependency intentionally follows that tree. For a reproducible application, install the exact registry version instead. v13 prerelease command (the v12 line reached end of life on 16 October 2026): ```bash cargo add rullst-connect@=13.0.0-alpha.4 cargo add secrecy ``` A Rullst application can use the facade instead: the `rullst` feature `oauth` re-exports this crate as `rullst::connect` with its Axum integration (`rullst-connect/rullst`), so `rullst::connect::extractors::AuthCallback` works as a handler argument without a direct dependency. The login challenge store is `rullst::connect::challenge`; `oauth-sqlite`, `oauth-postgres` and `oauth-mysql` add its SQL store for that database. The managed `tower-sessions` transaction below (`axum-session`) still needs a direct dependency. For the recommended Axum session transaction, enable `axum-session` and add a `tower-sessions` store: ```toml rullst-connect = { version = "=13.0.0-alpha.4", features = ["axum-session"] } tower-sessions = "0.15" ``` Or manually add it to your `Cargo.toml`: ```toml [dependencies] rullst-connect = "=13.0.0-alpha.4" secrecy = "0.10" tokio = { version = "1.52", features = ["full"] } ``` ## 🚀 Quick Start ### 1. Initialize the Provider Choose your provider and pass your credentials and callback URL: ```rust use rullst_connect::prelude::*; # fn main() -> Result<(), ConnectError> { let github = GithubProvider::try_new( "YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET".to_string().into(), "http://localhost:3000/auth/github/callback", )?; # let _ = github; # Ok(()) # } ``` ### Credential modes and offline development Every provider exposes `credential_mode() -> CredentialMode`. An empty credential or a value beginning with `mock_` selects `CredentialMode::Mock`, a deterministic transport that never accesses the network and signs in a fixed mock user. That mode needs the Cargo feature `mock` (or `oauth-mock` on the `rullst` facade). Without it, `try_new`, `Connect::driver` and `OidcProvider::discover` return `ConnectError::InvalidConfiguration` for the field `credentials` when they receive such a value, so a missing production secret fails when the provider is built instead of at the login callback; the deprecated `new` constructors return a disabled provider. Enable the feature only in development and test builds, because it turns mock credentials into a working sign-in. In a facade application, run `cargo run --features dev-oauth` with: ```toml [features] dev-oauth = ["rullst/oauth-mock"] ``` With a direct `rullst-connect` dependency, name `rullst-connect/mock` instead, or enable it for tests only: ```toml [dev-dependencies] rullst-connect = { version = "=13.0.0-alpha.4", features = ["mock"] } ``` In mock mode the authorization URL is the configured `redirect_url` with the fixed `code=mock_authorization_code`, the `state` and any PKCE challenge, as a real provider redirects after consent. The browser flow, including the managed `axum-session` transaction, therefore runs locally without a login page; Apple's mock also uses a query string instead of a form post. Mock profiles use the reserved `example.invalid` email domain. For a real protocol exchange with discovery, one-shot codes and signed ID tokens, use the fixture below. ### Signed local OIDC fixture Enable `axum` and mount `mock_idp::mock_router_with_config` only on an exact loopback listener. `MockIdpConfig::try_new` also rejects non-loopback issuer and callback URLs. The resulting fixture exercises discovery, an exact registered client/callback, expiring one-shot authorization codes, optional nonce, S256 PKCE, EdDSA ID-token validation through JWKS and bearer-protected userinfo. Its signing seed and credentials are deterministic public test material. It is not a production identity provider, interactive login/consent UI, refresh-token service, federation implementation or OIDC conformance suite. Follow the [local OIDC testing tutorial](https://rullst.github.io/Rullst/book/tutorials/48-local-oidc-testing.html) for a complete loopback setup. ### Explicit corporate proxy Live providers can receive a first-class proxy-aware transport without relying on ambient `HTTP_PROXY` state: ```rust use rullst_connect::client::ReqwestClient; use rullst_connect::prelude::{ConnectError, GithubProvider}; use std::sync::Arc; fn github_through_corporate_proxy( proxy_password: impl Into<String>, ) -> Result<GithubProvider, ConnectError> { let proxy = ReqwestClient::try_with_proxy_basic_auth( "https://proxy.corp.example:8443", "proxy-user", proxy_password, )?; let github = GithubProvider::try_new( "YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET".to_string().into(), "http://localhost:3000/auth/github/callback", )?; Ok(github.with_http_client(Arc::new(proxy))) } ``` With the `retry` feature, a provider's `with_retry` replaces its transport with a new direct client, so call it before `with_http_client`, or not at all: the proxy constructors already apply the bounded retry policy. `OidcProvider::discover` fetches its metadata before `with_http_client` can apply, so pass the proxy client to `OidcProvider::discover_with_client` instead (added in v13); discovery, JWKS and token calls then share it. Proxy URLs are limited to an HTTP(S) scheme and authority, with no embedded credentials, path, query, or fragment. Authenticated non-loopback proxies must use HTTPS. The configured client uses only that explicit proxy; PAC/WPAD, SOCKS, proxy mTLS identity and deployment certification remain outside this bounded transport. ### 2. Start a Server-Bound Authorization A Rullst application signs users in with its own encrypted session cookie and has no `tower-sessions` store. For it, `rullst_connect::challenge` keeps each attempt in a `ChallengeStore` (v13, 13.0.0-alpha.3): `begin_login` stores the state, PKCE verifier, nonce and an optional same-site return page and returns the redirect plus an opaque id for a `ChallengeCookie`; `finish_login` consumes the challenge once, compares the state in constant time and exchanges the code with the stored verifier and nonce: ```rust,no_run use rullst_connect::challenge::{LoginOptions, LoginRedirect, MemoryChallengeStore, begin_login}; use rullst_connect::challenge::LoginError; use rullst_connect::providers::GoogleProvider; async fn start_google( store: &MemoryChallengeStore, google: &GoogleProvider, ) -> Result<LoginRedirect, LoginError> { // Redirect to `url()` and set `set_cookie(&ChallengeCookie::new("/auth/google", secure)?)`. begin_login(store, google, LoginOptions::oidc("google")).await } ``` `SqlChallengeStore::open(pool, max_entries)` stores them in the application's SQLite, PostgreSQL or MySQL database instead (features `sqlite`, `postgres`, `mysql`), consuming each row with one atomic statement or transaction. `cargo rullst make:oauth google|github|microsoft|apple` generates the handlers, account linking and sign-in for a Rullst application. A provider that POSTs the callback from its own site, such as Sign in with Apple, needs `ChallengeCookie::for_cross_site_post` (`SameSite=None; Secure`): browsers attach no `SameSite=Lax` cookie to that POST. See [Tutorial 42](https://rullst.github.io/Rullst/book/tutorials/42-server-bound-oauth-sessions.html#store-backed-login). The `axum-session` path generates state and PKCE, stores their private counterparts in `tower-sessions` for ten minutes, and returns only the redirect URL: ```rust,no_run use axum::response::Redirect; use rullst_connect::prelude::{ConnectError, GithubProvider}; use rullst_connect::extractors::begin_oauth_session; use tower_sessions::Session; async fn start_github_authorization( session: &Session, github: &GithubProvider, ) -> Result<Redirect, ConnectError> { let authorization = begin_oauth_session(session, github).await?; Ok(Redirect::temporary(authorization.url())) } ``` Use `begin_oidc_session` instead for Google or a custom OIDC provider. It adds and stores an OIDC nonce as well. Sign in with Apple is different: `AppleProvider` always requests `response_mode=form_post`, so Apple returns `code`, `state` and `id_token` in a cross-site POST body. `AuthSession` reads only the query string, and a `SameSite=Lax` or `Strict` session cookie is not sent on that POST, so the managed `begin_oidc_session` + `AuthSession` flow cannot complete an Apple login. In v13, use the store-backed login above with `ChallengeCookie::for_cross_site_post`, or `AuthSessionForm` below. The manual state/PKCE/nonce flow with a dedicated `SameSite=None; Secure; HttpOnly` challenge cookie, described in [Sign in with Apple: form POST callback](https://rullst.github.io/Rullst/book/tutorials/42-server-bound-oauth-sessions.html#sign-in-with-apple-form-post-callback), also works and is the only option on 12.x. v13 (since 13.0.0-alpha.1) adds `AuthSessionForm`, which consumes the `begin_oidc_session` challenge from the form POST body (at most 16 KiB). The session cookie that holds that challenge must still be `SameSite=None; Secure`, so give the Apple routes their own session layer. ### 3. Consume the Callback and Get the User `AuthSession` consumes the challenge before checking its expiry and state. It then supplies the exact PKCE verifier and optional OIDC nonce to the provider: ```rust,no_run use rullst_connect::extractors::AuthSession; use rullst_connect::prelude::{ ConnectError, GithubProvider, Provider as _, UniversalProfile, }; async fn finish_github_callback( auth_session: AuthSession, github: &GithubProvider, ) -> Result<UniversalProfile, ConnectError> { let params = auth_session.exchange_params()?; let user = github.get_user(params).await?; Ok(user.universal_profile()) } ``` Only one managed challenge is active per browser session. Starting another login deliberately invalidates the earlier tab. The application must configure a durable production session store, Secure/HttpOnly/SameSite cookies, TLS, registered redirect URLs, account-linking policy, and post-login session rotation. See [Server-Bound OAuth/OIDC Sessions](https://rullst.github.io/Rullst/book/tutorials/42-server-bound-oauth-sessions.html). ### 🛡️ Manual State Handling Non-Axum hosts may use the framework-neutral primitives directly. The host must atomically take the expected value from a short-lived server-side store before comparison; a reusable cookie value is not equivalent. The following fragment is host pseudocode because the durable one-time store and callback extractor are deliberately application-provided: ```rust,ignore use rullst_connect::pkce::generate_oauth_state; let state = generate_oauth_state(); store_one_time_state(&state).await?; // application-provided durable operation let url = github.redirect_url_with_state(&state); let expected = take_one_time_state().await?; // atomically removes it callback.verify_state(&expected)?; ``` ### 🔄 Refreshing Tokens If the provider returned `expires_in` plus a refresh token, bind the result to a statically dispatched, process-local coordinator when the callback receives it: ```rust,no_run use rullst_connect::{AutoRefreshingSession, ConnectError, ConnectUser}; use rullst_connect::prelude::ExposeSecret as _; # async fn send_token_to_the_authorized_api(_: &str) -> Result<(), ConnectError> { # Ok(()) # } async fn call_provider_api( github: &rullst_connect::providers::GithubProvider, user: &ConnectUser, token_received_at: u64, ) -> Result<(), ConnectError> { let session = AutoRefreshingSession::from_user_at( github, user, token_received_at, )?; let lease = session.access_token().await?; send_token_to_the_authorized_api(lease.access_token().expose_secret()).await?; Ok(()) } ``` The default checks 60 seconds before expiration. Refresh calls cannot overlap; callers waiting behind a successful refresh reuse its state. A response replaces the access token only after the lifetime and original provider user ID validate. If a response for the original user is otherwise rejected (for example it omits `expires_in`), its rotated refresh token is still kept and the generation advances, because the provider has already consumed the prior one; the call fails and the next call refreshes with the rotation. The same applies when the grant succeeds but the follow-up profile lookup or ID-token validation fails: provider adapters then return `ConnectError::RefreshIncomplete` (added in v13), whose `IssuedTokens` carry the new tokens to direct `Provider::refresh_token` callers, and the session keeps the rotation and returns the underlying error. Persist the snapshot after such a failure too. Do not cancel `access_token()` while it refreshes. The provider call runs inside the caller's future, so dropping that future (a client disconnect, a `tower` timeout layer or `tokio::select!`) after the provider accepted the grant discards its response: a provider that rotates or consumes refresh tokens has then spent the credential this session still holds, and the next refresh fails with `invalid_grant` (Auth0 rotation may also revoke the token family). Drive refreshes from a task that is not cancelled with the request, or treat such an `invalid_grant` as a reauthentication signal. Use `access_token_at` in deterministic workers/tests. Seal `state_snapshot()` with `EncryptedTokenSnapshot` before writing it to a dedicated application store: ```rust use rullst_connect::{ EncryptedTokenSnapshot, RefreshableTokenState, TokenSnapshotBinding, TokenSnapshotError, TokenSnapshotKey, }; fn seal_for_storage( state: &RefreshableTokenState, key_bytes: [u8; 32], local_account_id: &str, ) -> Result<EncryptedTokenSnapshot, TokenSnapshotError> { let binding = TokenSnapshotBinding::try_new("github", local_account_id)?; let key = TokenSnapshotKey::try_new("oauth-primary-2026", key_bytes)?; EncryptedTokenSnapshot::seal(state, &key, &binding) } ``` Persist only `snapshot.as_str()`. On restart, validate it with `try_from_envelope`, select the secret-manager key named by `key_id()`, and call `open` with the same trusted binding. Keys must be 32 bytes from a secret manager or CSPRNG; human passwords require an application-selected KDF. The envelope authenticates confidentiality, ownership binding and generation but does not provide a database transaction by itself. #### Durable shared-local SQLite state Enable `rullst-connect/sqlite` (or umbrella `rullst/oauth-sqlite`) to store the encrypted generations in one local SQLite file: ```rust,no_run use rullst_connect::{ RefreshableTokenState, SqliteTokenSnapshotStore, TokenSnapshotBinding, TokenSnapshotKey, TokenStoreError, }; async fn persist_initial_generation( state: &RefreshableTokenState, key: &TokenSnapshotKey, account_id: &str, ) -> Result<(), TokenStoreError> { let store = SqliteTokenSnapshotStore::connect( "sqlite:///var/lib/my-app/oauth-tokens.sqlite", 50_000, ) .await?; let binding = TokenSnapshotBinding::try_new("github", account_id)?; store.insert_initial(&binding, state, key).await?; store.close().await; Ok(()) } ``` After refreshing from generation `n`, call `compare_and_swap(&binding, n, &replacement, &key)`. `BEGIN IMMEDIATE` serializes local writers and only generation `n + 1` can replace the observed row. The fixed schema persists its configured 1–1,000,000 row ceiling and restart/key metadata; malformed rows, configuration drift, quota exhaustion and stale replacement/deletion fail with typed redacted errors. The account locator is a SHA-256 pseudonymous digest, not an anonymity guarantee; the provider/account binding is also authenticated inside the ciphertext. This local CAS does not serialize the earlier call to a remote OAuth provider. Applications must still authorize the account, lease that remote operation, reconcile a provider rotation lost to CAS, configure retry/backoff and local logout, keep keys in a secret manager, protect/backup the directory and provide multi-host replication when needed. A provider that does not support refresh continues to return a typed error. ### 🔒 Manual PKCE Support Provider adapters expose PKCE (Proof Key for Code Exchange) where supported by the provider protocol. Some providers such as **X (Twitter) v2** require it; applications must preserve and validate the verifier/state for the complete authorization transaction. The generic provider boundary accepts both values explicitly: ```rust,no_run use rullst_connect::{ ConnectError, ConnectUser, pkce::{generate_oauth_state, generate_pkce}, provider::{ExchangeParams, Provider}, }; async fn exchange_with_pkce<P: Provider>( provider: &P, authorization_code: &str, ) -> Result<ConnectUser, ConnectError> { let state = generate_oauth_state(); let (code_verifier, code_challenge) = generate_pkce(); // Persist state + verifier in a short-lived server-side record before redirecting. let authorization_url = provider.redirect_url_with_pkce_and_state(&code_challenge, &state); let _ = authorization_url; // After atomically consuming and validating that state in the callback: provider .get_user(ExchangeParams { auth_code: authorization_code, code_verifier: Some(&code_verifier), ..Default::default() }) .await } ``` ## 🧑‍💻 Full Example with Axum A complete Axum server is in `rullst-connect/examples/axum_server.rs`. From the repository root, without provider credentials (the `mock` feature makes the `mock_*` fallback credentials sign in offline): ```bash cargo run -p rullst-connect --example axum_server --features mock ``` ## 📦 Releasing a New Version Connect is released only through the repository-wide, topologically ordered release workflow. Do not publish this crate independently from a working tree. Follow the [release guide](https://github.com/Rullst/Rullst/blob/main/RELEASE_GUIDE.md) and require all candidate-SHA gates before creating a release tag. ## 🤝 Contributing Feel free to open Issues and submit Pull Requests! Want to add a new provider? It's easy! Just implement the `Provider` trait. ## 📄 License This project is licensed under the [MIT License](https://github.com/Rullst/Rullst/blob/main/LICENSE). --- # rullst-messaging Source: https://rullst.github.io/Rullst/book/crates/messaging.html # rullst-messaging `rullst-messaging` (Extension tier) is the broker-neutral event boundary for Rullst. It is kept separate from `rullst-connect` because OAuth/OIDC identity federation and message-broker delivery have different security, availability, and retry semantics. ## Current status The crate has an **implemented, bounded broker foundation**: - a versioned immutable envelope; - bounded identifiers, headers, payloads, batches, leases, and retention; - topic-scoped idempotent publication; - consumer-group fan-out and competing-consumer claims; - expiring single-use acknowledgement tokens; - bounded retry, dead-letter views, and explicit purge; - deterministic time injection and reusable contract tests; - a canonical bounded v1 envelope wire codec with a deterministic byte fixture; - allowlisted W3C `traceparent`/`tracestate` propagation without baggage; - a feature-gated SQLite adapter that transactionally retains publications, subscriptions, claims, ACK/retry/DLQ state and idempotency across restart; - an explicit SQLite AES-256-GCM profile for header values and payloads, with immutable profile selection and bounded primary/prior-key rotation; - an opt-in static relational ORM outbox relay with exact stream/topic binding and publish-before-ACK crash/replay evidence; - added in v13: `schedules-postgres` [recurring publications](https://rullst.github.io/Rullst/book/recurring-publications.html) and `webhooks` [outgoing webhooks](https://rullst.github.io/Rullst/book/outgoing-webhooks.html). The `InMemoryBroker` is suitable for offline tests, deterministic development, and explicitly process-local workloads. `SqliteBroker` uses a fixed schema and serialized SQLite write transactions; restart, two-instance contention, configuration drift and corrupt-row repair are tested. It is a durable local adapter, not a remote transport. The optional [Redis Streams transport](https://rullst.github.io/Rullst/book/redis-messaging.html) (`redis-streams` feature, added in v13) provides a standalone transport with Rullst-owned group and fenced-lease indexes; hosted source/package admission passed in PR #236 and final release admission remains separate. Kafka, RabbitMQ, NATS/JetStream, SQS/SNS, Google Pub/Sub and Pulsar adapters remain roadmap work. The wire codec is not a remote adapter: it neither opens broker connections nor maps a provider's publish/ACK/retention semantics. Trace sampling, exporting, retention and tenant-aware correlation also remain host policy. ## Security and correctness boundary Payloads, idempotency values, acknowledgement tokens, and header values are not included in debug output. Tracing emits only bounded routing and decision metadata. Applications must still authorize who may publish or consume each topic and must make external effects idempotent. Delivery is at least once. A valid acknowledgement consumes its lease exactly once, but no local ACK can make an arbitrary remote side effect atomic. Use the stable envelope ID at the side-effect boundary. `SqliteBroker::connect` keeps the compatible plaintext profile. `connect_encrypted` uses randomized AES-256-GCM and authenticates immutable row metadata. Raw-storage, restart, wrong-key, tamper, row-swap, rotation, symlink and two-instance regressions are executable. It protects header values and payloads, not routing/idempotency/delivery metadata or the complete database. The deployment still owns keys, database-file permissions, protected backups, rollback detection, retention, disk monitoring and topic/tenant authorization. Reopening a namespace with different limits or a different storage profile fails closed instead of silently changing retained semantics. The outbox relay does not make ORM and broker state one atomic transaction. It uses the committed event key for exact broker replay, publishes first and ACKs the exact ORM lease second. The application still supervises workers, retries, dead letters, cleanup, tenant/topic authorization and destination idempotency. Continue with the [brokered messaging tutorial](https://rullst.github.io/Rullst/book/tutorials/49-brokered-messaging.html) or inspect the [crate roadmap](https://github.com/Rullst/Rullst/blob/main/rullst-messaging/ROADMAP.md). --- # rullst-mail Source: https://rullst.github.io/Rullst/book/crates/mail.html # Rullst Mail 📬 > [!IMPORTANT] > This page describes the v13 prerelease, published as `13.0.0-alpha.4`. Cargo > never selects a prerelease on its own, so its dependency examples pin that > version exactly. The v12 line (`rullst-mail@12`) reached end of life on > 16 October 2026; see the [release record](https://rullst.github.io/Rullst/book/v12.html). Use a path dependency only for checkout-local > review. Items marked (v13) and other recent fixes are not in the published > 12.1.x crates. > **Vision preserved:** additional providers and air-gapped/zero-leak ambitions > were not silently removed; see their status and recommendation in the > [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html#ai-and-mail). `rullst-mail` is Rullst's transactional email and mailables engine. Official dispatch paths pass through a pre-flight pipeline for CRLF protection, recipient checks, content security scanning, and DLP sanitization before queueing or transport delivery. --- ## Delivery providers (v13) Version 13 ships four delivery transports: **Resend**, **AWS SES**, **SendPulse** and **SMTP**, plus the `log`, `memory` and offline `mock_*` drivers for development and tests. The SendGrid, Postmark, Mailjet, Mailtrap and Azure Communication Services transports were removed in 13.0 and remain in the 12.x line. A `MAIL_DRIVER` or `[mail] driver` that names one of them fails with `MailError::ConfigError` naming the driver, never falling back to another driver, and `cargo rullst upgrade` reports their names as `V13-MAIL-REMOVED`; see the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#changes-from-the-published-1210-source) row "Mail providers removed". Mail already queued by 12.x carries no provider name and is delivered by the driver the upgraded worker is configured with. Any other provider is an application-owned `MailDriver`: [Writing your own mail transport](https://rullst.github.io/Rullst/book/mail-custom-transport.html) walks through a complete offline example (`cargo run -p rullst-mail --example custom_transport`). SendPulse shipped in 12.1.0. Set `MAIL_DRIVER=sendpulse` with `SENDPULSE_API_KEY`; real delivery requires an explicit verified sender. The [crate guide](https://github.com/Rullst/Rullst/tree/main/rullst-mail#sendpulse) details authentication, attachment/inline limits, tracking configuration, unsubscribe headers and at-least-once retries. No extra feature is needed beyond Mail. Provider acceptance still requires the configured account/domain. ## 12.1: welcome and password recovery The [12.1 account-mail guide](https://rullst.github.io/Rullst/book/account-mail-v12-1.html) documents the opt-in PostgreSQL/SQLite account transaction, durable delivery bridge, action-link fix, localized lifecycle templates and signed feedback. These additions shipped in 12.1.0; its Azure Communication Services transport was removed in 13.0. Existing applications must adopt the new store/worker explicitly. ## Shared suppression (v13) Optional `postgres` (facade `mail-postgres`) adds `PostgresSuppressionStore`, a recipient suppression and replay store shared by hosts using one PostgreSQL database. Install the guard in every sending process and worker; it is not enabled automatically. See the [shared suppression contract](https://rullst.github.io/Rullst/book/shared-mail-suppression.html). ## ✨ Features - **🛡️ Typed failures:** production delivery paths return `MailError`; malformed messages and provider configuration fail closed. CI and formal checks remain scoped evidence, not an absolute guarantee. - **⚡ Delivery and Test Drivers:** - **Resend** (`ResendDriver`) — Native REST API with scheduled delivery & RFC 8058. - **SendPulse** (`SendPulseDriver`) — Native transactional REST adapter; see the section above. - **AWS SES v2** (`AwsSesDriver`, `aws-ses`) — official AWS SDK/SigV4 native transport with temporary/rotating credential support, plus offline fixture and an explicit legacy proxy boundary. - **Native SMTP** (`SmtpDriver`) — Pure async Lettre transport with implicit TLS on port 465 and mandatory STARTTLS on every other port. - **Memory & MailTrap** (`MemoryDriver`, `MailTrap`) — Zero-I/O in-memory harness with fluent assertions; it is not the hosted Mailtrap service, whose transport v13 removed. - **Your own transport** — implement `MailDriver`; see [Writing your own mail transport](https://rullst.github.io/Rullst/book/mail-custom-transport.html). - **Log** (`LogDriver`) — Metadata-only terminal and file logging (`storage/logs/mail.log`): a timestamp, attachment count and scheduling flag, never recipients, subject or body. - **🔀 Typed Circuit Breaker & Automatic Failover (`FailoverDriver`):** Fails over only for transport, HTTP 5xx, provider rate-limit, or transient SMTP failures; permanent message/configuration/provider rejection stays on the original error path. `SuppressionUnavailable` and `AttachmentInspectionUnavailable` are `Transient` (retry later) but never failover-eligible. A fallback that returns such an error ends the chain with it; when every driver fails transiently, the result is a `Transient` error, or `RateLimited` with the bounded `Retry-After` when the last driver was rate limited. Every attempt keeps the caller's tenant context or delivery ID, so a wrapped `TenantMailResolver` selects the tenant's driver. Structured tracing exposes bounded decision fields without provider bodies. - **🏢 Auth-bound Multi-Tenancy Resolver (`TenantMailResolver`):** Select isolated in-process drivers directly from a trusted Core `TenantContext`; registry failures and invalid IDs fail closed. - **📎 Bounded Attachments & Inline CID Assets:** The shared pre-flight contract caps count and byte size, validates safe basenames/MIME/CID metadata and requires every unique inline CID to be referenced by HTML. Resend, native SES, the SES bearer proxy and SMTP serialize the same owned-byte model, and SendPulse its non-inline attachments; transports copy or Base64-encode as required. - **🔬 Opt-in Attachment Inspection (`AttachmentInspectionGuard`):** A strict bounded local policy rejects executable magic, spoofed known types, active PDF/SVG, secrets and unsafe text links before transport. Checks follow the case-insensitive declared type, the filename extension and the content signature together, never the declared type alone. A static `AttachmentInspector` adapter boundary supports an independently operated production scanner. - **🚫 Durable Recipient Suppression (`sqlite`, v13 `postgres`):** `SuppressionGuard` checks manual, hard-bounce and spam-complaint state before transport. The SQLite store binds verified provider/event identities, detects conflicting replay, enforces immutable quotas transactionally and survives restart or multiple local processes; the v13 PostgreSQL store shares that state across hosts. - **📊 Secret-Minimized Delivery Observability:** `ObservedMailDriver` records only a bounded provider label, terminal outcome, latency, attachment count and scheduling/tenant booleans through a non-failing static observer. - **⏰ Durable Scheduling (`.send_at()`, `.send_in()`):** SQLite and Redis queues persist schedules for up to 366 days and never claim early; direct Resend delivery uses provider scheduling and rejects a schedule beyond its documented limit (30 days ahead) with `ConfigError` before any request. Real SMTP, SendPulse, Log and SES paths reject future direct delivery and must use a durable queue; offline fixtures may retain the timestamp for assertions. - **🕵️ Outbound Phishing & Homograph URL Interceptor (`.validate_security()`):** Pre-flight detection of mixed-script Unicode IDN spoofed domains (`pаypal.com` with Cyrillic characters), checked per DNS label of the link host and user-info only, as the text reads and as a browser resolves the link (tabs and newlines removed, `\` read as `/` for HTTP(S), `https:host` and scheme-relative `\\host` forms, percent-encoded and A-label hosts), so single-script IDNs such as `παράδειγμα.gr` or `пример.com` and non-Latin query text are allowed while all-lookalike Cyrillic labels under a non-Cyrillic TLD are rejected, and dangerous URI schemes (`javascript:`, `data:text/html`). - **📜 RFC 8058 One-Click List-Unsubscribe:** Automatic compliant header injection (`List-Unsubscribe`, plus `List-Unsubscribe-Post: List-Unsubscribe=One-Click` only for an HTTPS unsubscribe URL, as RFC 8058 requires). The unsubscribe email must be one bare address and the URL may not contain whitespace, `<`, `>` or `"`, so neither value can add another header entry. - **🔤 Automatic Plain-Text Fallback:** Automatic HTML-to-plain-text conversion without manual duplication; an absolute HTTP(S) link keeps its target as `label <URL>` unless the label already is the URL. - **🔒 Outbound DLP Secret Scanner:** Proactive credential masking (whole-token `AKIA`/`ASIA` AWS access key IDs, passwords, API tokens, bearer tokens and PEM private-key blocks of any `<label>PRIVATE KEY` type, including OpenSSH, EC and encrypted keys) before emails leave your server. - **📦 Async Background Worker Queues:** Native non-blocking dispatch via `rullst-core::queue`. - **🧪 Explicit offline provider mode:** empty or `mock_*` credentials select `DeliveryMode::OfflineMock`, never perform network I/O, and are inspectable through `OfflineMailMock`. - **🛠️ Safe CLI Scaffolding:** Generates registered facade-based Welcome, Password Reset, OTP, Invoice, custom, bounded payment-receipt, and explicit D+1/D+3/D+7 dunning mailables, refusing unsafe names/collisions and escaping dynamic HTML. - **🧾 Payment-Bound PDF Delivery:** The opt-in `capital-invoice` bridge accepts only Capital's final evidence-bound `PaidInvoice`, attaches bounded HTML/PDF, applies pre-flight and preserves a stable key for the application outbox. --- ## 🚀 Quickstart ### 1. Composing and Sending an Email ```rust,no_run use rullst_mail::{Mail, Message}; use chrono::{Utc, Duration}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let logo_bytes = b"replace with application-owned PNG bytes"; let message = Message::new() .to("alice@example.com") .from("noreply@rullst.dev") .subject("Welcome to Rullst!") .html(r#" <h1>Welcome, Alice!</h1> <p>Thanks for joining our platform.</p> <img src="cid:app_logo" alt="Logo" /> "#) .attach_cid("app_logo", "logo.png", logo_bytes.to_vec(), "image/png") .attach_bytes("welcome_guide.pdf", b"%PDF-1.4...".to_vec(), "application/pdf") .send_in(std::time::Duration::from_secs(60)) // Deliver in 1 minute .unsubscribe_url("https://rullst.dev/unsub/alice"); // The mandatory pipeline validates and sanitizes before queueing or delivery. Mail::send(message).await?; Ok(()) } ``` For a schedule that survives process restarts, operate a built-in queue and keep its worker handle alive: ```rust,no_run use rullst_core::queue::{Queue, Worker}; use rullst_mail::{register_mail_handler, Mail, Message}; # async fn schedule() -> Result<(), Box<dyn std::error::Error>> { let queue = Queue::sqlite("sqlite://storage/jobs.db").await?; let mut worker = Worker::new(&queue).poll_interval(100); register_mail_handler(&mut worker); let worker_handle = worker.run()?; let message = Message::new() .to("alice@example.com") .subject("Scheduled update") .text("Delivered after the durable due time") .send_in(std::time::Duration::from_secs(60)); Mail::enqueue(&queue, message).await?; // Keep `worker_handle` in application state; shut it down during graceful exit. worker_handle.shutdown().await?; # Ok(()) # } ``` Execution begins on the first worker poll after the UTC timestamp and remains at-least-once. Queue scheduling does not promise exact wall-clock execution, exactly-once provider delivery, or provider acceptance. Redis promotes scheduled jobs by its server clock, so the worker accepts a claimed job whose timestamp is at most 300 seconds ahead of the worker's own clock and fails a claim that is earlier than that. The queue has no handler-requested retry: any delivery error, including a `Transient` or `RateLimited` provider failure and its `Retry-After`, marks the mail job failed with the error text. Nothing is lost silently, but the job is sent again only after `Queue::retry_failed_job`. Automate that for transient failures, or deliver through an outbox with its own retry policy (as account mail does), when provider blips must be retried without an operator. `Mail::enqueue`, and `Mail::send` after `Mail::init_queue`, run the pre-flight pipeline and apply the default sender before writing a `rullst_mail_send` job whose JSON payload is a versioned envelope (schema version 1) holding the optional tenant ID and the prepared message. Queued jobs store attachment bytes as one base64 string per attachment. Workers still accept jobs written with the earlier integer-array encoding, but an older worker cannot read the base64 form, so upgrade workers before producers during a rolling deployment. --- ### 2. Resilient Multi-Driver Failover (Circuit Breaker) ```rust use rullst_mail::drivers::{FailoverDriver, ResendDriver, SendPulseDriver}; use std::time::Duration; # fn build_failover() -> Result<FailoverDriver, rullst_mail::MailError> { let primary = ResendDriver::try_new("re_...")?; let fallback_1 = SendPulseDriver::try_new("sendpulse_api_key...")?; let failover_driver = FailoverDriver::new(primary) .with_fallback(fallback_1) .with_threshold(3) // Trip circuit after 3 consecutive failures .with_cooldown(Duration::from_secs(60)); // Cooldown for 60s # Ok(failover_driver) # } ``` --- ### 3. Dynamic B2B Multi-Tenancy Routing ```rust,no_run use rullst_core::security::TenantMembership; use rullst_mail::{Message, ResendDriver, TenantMailResolver}; async fn send_tenant_message( message: &Message, ) -> Result<(), Box<dyn std::error::Error>> { let resolver = TenantMailResolver::new(); let membership = TenantMembership::try_new(["tenant_globex"])?; let context = membership.select("tenant_globex")?; // Register tenant-specific API credentials during application configuration. resolver.register_for_context( &context, ResendDriver::try_new("re_globex...")?, )?; // The context must be derived from trusted authentication/membership state. resolver.send_for_context(&context, &message).await?; Ok(()) } ``` The registry is intentionally process-local. Durable encrypted credential storage, rotation, and distribution between instances remain application/deployment concerns. --- ### 4. Fast Unit & Integration Testing with `MailTrap` ```rust use rullst_mail::{Mail, MailTrap, Message}; #[tokio::test] async fn test_user_registration_email() { Mail::set_driver(Box::new(MailTrap::driver())); MailTrap::clear(); let msg = Message::new() .to("alice@example.com") .subject("Welcome to Rullst!") .html("<p>Please verify your email address.</p>") .attach_bytes("terms.pdf", b"%PDF...".to_vec(), "application/pdf") .unsubscribe_url("https://example.com/unsub/alice"); Mail::send_now(msg).await.unwrap(); // Fluent assertions MailTrap::assert_sent_to("alice@example.com") .with_subject("Welcome to Rullst!") .with_body_contains("Please verify your email") .with_attachment_count(1) .with_attachment_named("terms.pdf") .with_unsubscribe_url("https://example.com/unsub/alice"); } ``` --- ### 5. Scaffolding Mailables with CLI ```bash # Generate Welcome & Onboarding email cargo rullst make:mail WelcomeEmail --welcome # Generate Time-limited Password Reset email cargo rullst make:mail PasswordReset --reset # Generate Two-Factor OTP code email cargo rullst make:mail OtpVerification --otp # Generate SaaS Invoice receipt email cargo rullst make:mail InvoiceReceipt --invoice # Generate a bounded commercial payment receipt (PaymentReceiptEmail) cargo rullst make:mail-invoice # Generate explicit D+1/D+3/D+7 payment-recovery stages cargo rullst make:mail-dunning ``` The receipt template enables only the `mailer` feature. Its `international_receipt` constructor records an application-confirmed payment and states that it is not a tax authorization; it has no NFS-e branch (v13 removed NFS-e from Rullst). The dunning template does not infer due dates, schedule itself, or mutate access. Both generated `build` paths run the mandatory mail pre-flight and reject unsafe links; payment evidence, billing state, scheduling, and policy remain application-owned. For native payment-bound PDF delivery, enable `rullst-mail/capital-invoice` (or umbrella `rullst/capital-mail`) and use `PaidInvoiceDelivery::prepare`, then set the verified sender with `.from(sender)?` (13.0), which re-runs pre-flight. It rejects non-final/mock evidence and recipient/amount/currency substitution. Applications still reconcile webhooks and atomically claim the stable delivery key; provider acceptance and exactly-once delivery are not promised. --- ### 6. Pre-Flight Deliverability & Disposable Email Filtering Prevent sender quota waste and fake user signups with built-in deliverability checks and blocked temporary domains: ```rust use rullst_mail::{is_disposable_email, validate_email_deliverability, Message}; // 1. Direct email address validation assert!(validate_email_deliverability("user@company.com").is_ok()); assert!(is_disposable_email("spammer@mailinator.com")); // 2. Pre-flight check before dispatching let msg = Message::new() .to("user@mailinator.com") .subject("Welcome!"); if msg.is_disposable() { eprintln!("Blocked disposable email address!"); } ``` --- ### 7. Composing inspection, suppression, and observability The wrappers use static dispatch and may be composed around any `MailDriver`. Enable `rullst-mail/sqlite` (or umbrella `rullst/mail-sqlite`) for the durable local suppression store: ```rust,no_run use rullst_mail::{ AttachmentInspectionGuard, BoundedMailObserver, LocalAttachmentInspector, MailDriver, MemoryDriver, Message, MutableSuppressionStore, ObservedMailDriver, SqliteSuppressionStore, SuppressionEvent, SuppressionGuard, SuppressionReason, }; use std::time::{SystemTime, UNIX_EPOCH}; # async fn deliver() -> Result<(), Box<dyn std::error::Error>> { let store = SqliteSuppressionStore::connect( "sqlite://storage/mail-suppressions.sqlite", 100_000, 500_000, ).await?; // Only record events after the provider-specific signature was authenticated. let observed_at = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); store.record(SuppressionEvent::try_new( "resend", "verified-event-id", "blocked@example.com", SuppressionReason::HardBounce, observed_at, )?).await?; let (transport, _) = MemoryDriver::isolated(); let inspected = AttachmentInspectionGuard::new( transport, LocalAttachmentInspector::strict(), ); let suppressed = SuppressionGuard::new(inspected, store); let observer = BoundedMailObserver::new(10_000)?; let driver = ObservedMailDriver::try_new("memory", suppressed, observer)?; driver.send(&Message::new() .to("recipient@example.com") .subject("Bounded delivery") .text("Hello")) .await?; # Ok(()) # } ``` `SuppressionEvent` does not verify a webhook signature: a provider-specific adapter must authenticate the exact event first. SQLite is shared durable local state, not multi-host replication or encrypted storage. Keep replay IDs at least as long as every provider's redelivery window. The local attachment inspector is a bounded heuristic, not antivirus, sandbox execution, recursive archive inspection or content disarm. The default observer is bounded and process-local; the host owns any external metrics/tracing sink, retention and alerts. --- ### 8. Authenticated open/click tracking primitives Generate versioned, purpose-bound HMAC-SHA256 tracking tokens with a mandatory 32-byte secret and bounded validity. HMAC authenticates but does not encrypt: recipient and target URL remain base64-readable in the current token. `Message` tracking builders sign the bare address the pipeline delivers to (`alice@example.com` for `Alice <alice@example.com>`), so set `to` first. The application owns consent, minimization, retention, redirects and applicable privacy-law decisions. ```rust use rullst_mail::{TrackingEngine, TrackingVerifier, PIXEL_1X1_GIF, Message}; use std::time::Duration; fn tracking_example() -> Result<(), Box<dyn std::error::Error>> { let secret = b"replace-with-32-or-more-random-key-bytes"; let now_unix_seconds = 1_800_000_000; // Fluent open & click tracking injection let tracked_msg = Message::new() .to("user@example.com") .subject("Monthly Newsletter") .html("<p>Check out our <a href=\"https://rullst.dev/pricing\">pricing</a>.</p>") .try_with_open_tracking("https://app.com", secret, "campaign_2026")? .try_with_click_tracking("https://app.com", secret)?; let token = TrackingEngine::try_generate_open_token( secret, "user@example.com", "campaign_2026", now_unix_seconds, )?; // Default verification enforces a 30-day TTL. let event = TrackingEngine::verify_open_token(secret, &token)?; println!("Email opened by {} for campaign {}", event.email, event.campaign_id); // Endpoints needing single-consumption semantics can reject replay explicitly. let verifier = TrackingVerifier::new(Duration::from_secs(24 * 60 * 60), 100_000)?; let event = verifier.verify_open_once(secret, &token, now_unix_seconds)?; let _ = (tracked_msg, event, PIXEL_1X1_GIF); Ok(()) } ``` --- ### 9. Transactional Test Fixtures with `MailFactory` Quickly generate standard transactional emails for local preview and testing: ```rust use rullst_mail::MailFactory; let welcome_msg = MailFactory::fake_welcome("alice@example.com", "Alice", "My SaaS App"); let reset_msg = MailFactory::fake_password_reset("bob@example.com", "https://app.com/reset?token=xyz", 15); let otp_msg = MailFactory::fake_otp("carol@example.com", "492015", 5); let invoice_msg = MailFactory::fake_invoice("david@example.com", "INV-2026-001", 9900, "USD"); let alert_msg = MailFactory::fake_security_alert("eve@example.com", "Unrecognized Login", "198.51.100.1", "Chrome / macOS"); ``` --- ### 10. Native AWS SES v2 with SigV4 Enable the opt-in official SDK transport: ```toml [dependencies] rullst-mail = { version = "=13.0.0-alpha.4", features = ["aws-ses"] } aws-config = "1.11" ``` `MAIL_DRIVER=ses` selects native mode when both `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` exist. `AWS_SESSION_TOKEN` is accepted for temporary credentials. Without those variables, the existing empty/`mock_*` token rule selects the offline fixture; a real `AWS_SES_BEARER_TOKEN` is usable only with an explicit trusted proxy URL. The proxy receives the SES v2 `SendEmail` JSON shape, including every attachment and inline CID asset as `Content.Simple.Attachments` (Base64 `RawContent`, `FileName`, `ContentType`, `ContentDisposition` and `ContentId`). It is checked against the same SES field limits and 40 MiB encoded estimate as native mode before any request, and failures return `MailError::ValidationError`. The proxy must forward attachments or reject the request; Rullst never drops them. SES does not support SMTPUTF8, so native and proxy requests carry 7-bit ASCII addresses: an internationalized domain becomes its IDNA A-label (`maria@bücher.de` is sent as `maria@xn--bcher-kva.de`) and a non-ASCII sender display name becomes RFC 2047 encoded-words (`=?UTF-8?B?...?=`), while an all-ASCII sender is sent unchanged. A non-ASCII local part cannot be expressed and fails before any request with `MailError::ValidationError`. (v13) Long-running services should inject a refreshing credential provider or a caller-built SDK config instead of freezing credentials: This snippet uses the application's direct `aws-config` dependency shown above, which Rullst does not re-export, so the book doctests do not compile it (`rust,ignore`): ```rust,ignore use rullst_mail::{AwsSesDriver, MailDriver, Message, aws_ses_sdk}; # async fn deliver() -> Result<(), Box<dyn std::error::Error>> { let shared = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; let config = aws_ses_sdk::Config::new(&shared); let driver = AwsSesDriver::from_native_config(config)?; driver.send(&Message::new() .to("recipient@example.com") .from("verified@example.com") .subject("Signed by AWS SigV4") .text("Hello from Rullst")) .await?; # Ok(()) # } ``` The application still owns AWS identity/domain verification, sandbox exit, IAM least privilege, quotas, reputation, bounce/complaint handling and monitoring. A successful `MessageId` is provider acceptance, not proof of inbox delivery. The native adapter rejects SES field limits and an encoded message estimate over 40 MiB before network I/O; provider 429 responses preserve a bounded delta-seconds `Retry-After` for failover/retry policy. --- ## ⚙️ Configuration (`Rullst.toml` or Environment Variables) ```toml [mail] driver = "resend" # "log" | "memory" | "smtp" | "resend" | "ses" | "sendpulse" from = "Acme <no-reply@acme.example>" # default sender for messages without `from` ``` The `Mail` facade reads each variable below from the process environment first and then from the project's `./.env`, which never overrides the environment, exactly as `Server` reads `DATABASE_URL`; `driver` and `from` in `Rullst.toml` come last. `LogDriver` reads `MAIL_LOG_PATH` the same way. The staging and production check uses the `Server` environment precedence (`RULLST_ENV`, then `APP_ENV`, from the environment and then `.env`, then `[app] env`). A malformed `.env` fails with `MailError::ConfigError` without echoing its content. The settings are read on every facade call, but the driver built from them is reused while they stay the same, so its connection pools and native SES client survive across messages and queue jobs. A changed setting builds a new driver for the next message, `Mail::reset_driver()` drops the reused one, and `MAIL_DRIVER=memory` still builds a fresh store per message. (v13) Environment variables: - `MAIL_DRIVER`: Select active driver (`log`, `memory`, `smtp`, `resend`, `ses` or its alias `aws_ses`, `sendpulse`). The values of the transports v13 removed (`sendgrid`, `postmark`, `mailjet`, `mailjet-sandbox`, `mailtrap`, `mailtrap-sandbox`, `azure-acs`) fail with `MailError::ConfigError`, which names the driver and the migration row, and never fall back to another driver. When neither it nor `[mail] driver` is set, development and test fall back to `log`, while staging and production (`RULLST_ENV`, `APP_ENV` or `[app] env`, as above) return `MailError::ConfigError` instead of logging mail that is never delivered. Select `log` explicitly to keep metadata-only logging there. - `MAIL_FROM`: Default sender (v13) for `Mail` facade messages that set no `from`, such as generated mailables. It takes precedence over `[mail] from`; an explicit `from` on the message always wins. Use one address or `Name <address>` that your provider account has verified. An invalid value fails every facade send with `MailError::ConfigError`; call `Mail::default_sender()` at startup to fail fast. Drivers used directly do not read it; the other settings below are read by the facade when it builds a driver, and `MAIL_LOG_PATH` by `LogDriver` itself. - `RESEND_API_KEY`: API key for Resend. - `SENDPULSE_API_KEY`: Static API key for SendPulse. - `AWS_REGION`: Region used by native SigV4 signing or SES proxy/mock metadata. - `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`: Select native SES when the `aws-ses` feature is enabled; both must be present. - `AWS_SESSION_TOKEN`: Optional temporary-credential session token. - `AWS_SES_BEARER_TOKEN`: Bearer token for an explicit trusted proxy; it is never sent to AWS as a substitute for SigV4. - `AWS_SES_ENDPOINT`: Native SDK base endpoint or complete proxy send URL; HTTPS is required except for loopback integration tests. - `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`: SMTP credentials. `MAIL_PORT` defaults to 25 only when unset or empty; any other value must be an integer from 1 to 65535, or the facade returns `MailError::ConfigError`. - `MAIL_LOG_PATH`: Path for log file (default: `storage/logs/mail.log`). For Resend, SendPulse and the SES fixture/proxy, an empty credential or one beginning with `mock_` selects the deterministic offline fallback. Use `driver.delivery_mode()` and `OfflineMailMock::deliveries()` to assert this explicitly in tests. The process-wide capture keeps only the newest 1,000 deliveries and at most 64 MiB of their subject, body and attachment bytes, and the first capture in a process logs a `mail.offline_mock.active` warning, because an empty production secret also selects this fallback. Every real transport needs a sender that the provider account has verified: the message's `from`, or, for `Mail` facade sends, the `MAIL_FROM` / `[mail] from` default. Without either, delivery fails with `MailError::ConfigError` before any request, and the error names both settings. Transports never invent a sender. SMTP selects the offline fallback only explicitly: an empty or `mock_*` `MAIL_HOST`, or a `mock_*` username or password. A real host without credentials is an unauthenticated relay and receives real delivery, so `MAIL_DRIVER=smtp` without `MAIL_HOST` sends to `127.0.0.1:25`. A username without a password, or the reverse (blank values count as missing), returns `MailError::ConfigError` instead of falling back to the mock. Port 465 uses implicit TLS (SMTPS); every other port, including the facade's default 25 and the usual submission port 587, requires STARTTLS. Plaintext is never used: a server that does not offer STARTTLS, or presents a certificate that is not valid for `MAIL_HOST`, fails with a typed transport error, so a local relay needs a certificate valid for the configured host name. --- ## Scope and product boundaries `rullst-mail` is a transactional-delivery library, not a marketing CRM or a claim that third-party delivery infrastructure is unnecessary. Implemented building blocks include: - typed message construction and escaped generated templates; - explicit SMTP, Resend, SendPulse, log and memory drivers, plus the opt-in official-SDK SES v2 transport and bounded SES proxy/mock adapter; - deterministic offline mode for empty or `mock_*` provider credentials; - an in-memory `MailTrap` and `MailFactory` fixtures; - bounded retry/failover helpers, tenant-driver resolution, attachments, provider-specific scheduling fields, and durable SQLite/Redis due times; - opt-in attachment inspection, process-local, shared-local SQLite or (v13) shared PostgreSQL suppression, and content-minimized local delivery observations; - HMAC-authenticated (not encrypted) tracking tokens with expiry/replay helpers, URL checks, and bounded secret-redaction heuristics. These components do not provide deliverability, sender-domain reputation, legal consent, unsubscribe policy, durable campaign orchestration, a visual marketing editor, or a production inbox. Provider acceptance is not proof of delivery. Tracking pixels/links have privacy and consent implications that the application must evaluate for each jurisdiction and use case. Choose a delivery provider and operational policy based on measured volume, region, data processing terms, bounce/complaint handling, retention, cost, and failover tests. Rullst publishes no universal latency, price, or feature comparison against commercial platforms. Recipients are parsed once by the pre-flight pipeline. It accepts one bare address, `<address>` or `Name <address>` (the name may be quoted), and hands the bare address to suppression, the disposable-domain check and every transport, so a display name is not delivered. Lists, groups, comments, quoted local parts, domain literals and malformed brackets are rejected with `MailError::ValidationError`. Suppression events and lookups use the same parser; anything it rejects fails closed. Suppression keys compare the domain case-insensitively and key an internationalized domain by its IDNA A-label (`bücher.de` and `xn--bcher-kva.de` share one entry); a non-ASCII local part cannot be keyed, so `SuppressionGuard` rejects it with `ValidationError` rather than reporting `SuppressionUnavailable`. The pre-flight pipeline rejects a subject over 2 KiB, or an HTML or plain-text body over 2 MiB each, with `MailError::ValidationError` before any content scan. Oversized content is rejected, never truncated. The link, homograph and secret-redaction scans are single forward passes, so their cost grows linearly with the bounded body. They still run on the calling task; bound user-supplied text at the request edge as well. The local inspector chooses its checks from the declared MIME type (compared case-insensitively), the filename extension and the content signature together. Both policies reject executable magic, executable or script-host extensions (`.exe`, `.bat`, `.cmd`, `.ps1`, `.vbs`, `.js`, `.hta`, `.lnk` and similar), SVG by type, extension or content, active PDF content wherever a `%PDF-` header appears in the first KiB, and a declared type that disagrees with a known extension or signature. `strict()` also rejects HTML extensions, HTML/script markup or `javascript:`/`vbscript:` URIs, unknown extensions, any declared type it does not inspect other than `application/octet-stream` (so a `text/html` attachment named `invoice.txt` is rejected), and opaque formats; `allowing_opaque()` still accepts HTML and other opaque content. PDF names written with `#xx` escapes or inside compressed streams are not decoded. Markup is classified the way a browser parses it, not by its first bytes: a document opening with `<` is SVG or active (X)HTML by its root element after any XML declaration, processing instruction, comment or DOCTYPE, matched after an optional `prefix:` (`<s:svg>`, `<h:html>`), and by the SVG or XHTML namespace URI anywhere in it, also when written with character references. Markup that declares DTD entities, which can assemble a namespace URI from pieces, is treated as SVG unless its root is `html`, and a UTF-16 document is read by its byte-order mark. (v13) Attachment limits are 32 items, 20 MiB per item and 25 MiB of raw bytes in aggregate before transport encoding. Provider/account limits can be lower. The base pipeline validates metadata but treats bytes as opaque. The opt-in local inspector recognizes only its documented bounded formats and heuristics; use a production scanner adapter when malware, archive, sandbox or CDR policy is required. --- # 12.1 account mail Source: https://rullst.github.io/Rullst/book/account-mail-v12-1.html # Account mail (since 12.1) > **Version note:** this contract was introduced in 12.1 and continues in v13. > The v13 line removed the Azure Communication Services transport (its section > below is kept for 12.x readers), together with the SendGrid, Postmark, > Mailjet and Mailtrap transports; they remain in the 12.x line, which reached > end of life on 16 October 2026. Account mail in v13 is delivered through > Resend, AWS SES, SendPulse, SMTP or an application-owned `MailDriver`; see the > [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#changes-from-the-published-1210-source) > row "Mail providers removed" and > [Writing your own mail transport](https://rullst.github.io/Rullst/book/mail-custom-transport.html). This is an opt-in contract. Updating a dependency does not migrate an application's account table, replace its session middleware, configure a sender or deploy a worker. The [12.x release record](https://rullst.github.io/Rullst/book/v12.html) lists its first publication. ## Boundaries `rullst-auth::recovery` owns the authoritative account registry, password reset, revocable opaque sessions and encrypted transactional outbox. `rullst-mail` owns message construction, mandatory preparation and transport. The small `rullst::account_mail` bridge composes them without coupling Auth to a provider or introducing another publishable crate. Enable `account-mail-postgres` or `account-mail-sqlite` on `rullst`, or the corresponding `recovery-*` feature on Auth when supplying a different worker. These features do not enable the general ORM. Features remain additive. In v13 `rullst` has no default features; on 12.x, disable the umbrella defaults when a backend-exclusive dependency graph is required. ## Adoption sequence 1. Obtain two independent persistent random 32-byte application secrets from a secret manager. `RecoverySecrets::new` separates keyed token/email digests from AES-256-GCM outbox/address encryption. Never use the fixture keys from tests. There is no automatic key rotation: plan a reviewed migration and backup restoration with the same secrets. 2. Connect `SqlRecoveryStore`, then explicitly run `migrate()` during deployment. PostgreSQL and SQLite share the fixed authoritative schema. Namespace stores by application/tenant with separate databases and secrets; a public request cannot select a tenant or supply a database URL. SQLite filesystem paths must be URL-encoded; use `sqlite:PATH?mode=rwc` without an authority for Windows drive letters. A remote PostgreSQL host always uses `sslmode=verify-full` (certificate and hostname verification), whatever the URL requests; supply `sslrootcert` for a private CA. Loopback hosts and Unix sockets keep the configured mode. Query options other than `sslmode`, `sslrootcert`, `sslcert`, `sslkey`, `host`, `hostaddr`, `port`, `dbname`, `user` and `password` fail with `Configuration` before connecting. During shutdown, stop the request/worker tasks and await `close()` before moving or deleting a database; it closes the connection pool shared by every store clone. 3. Register new accounts with `register_account_with_locale`. Account creation and the encrypted welcome notice commit together. The supported recorded locales are English, Brazilian Portuguese and Spanish; the worker supplies a deterministic fallback. An email or subject that is already registered returns `InvalidAction` (without naming which), not the `Storage` error of a database failure; present it without confirming that the email exists. Existing accounts require an application migration; there is no automatic import of arbitrary password tables. 4. Use `authenticate`, `create_session` and `verify_session` for this registry. Passwords contain at least 12 characters and at most 72 bytes, the Argon2 input limit. Registration, reset and `authenticate` reject longer input with `InvalidInput` before any account lookup, so the outcome does not reveal whether an email is registered; unknown accounts still perform Argon2 work. Verify the opaque session on every authenticated request. Set it in a Secure, HttpOnly, appropriately SameSite cookie. Use `revoke_session` for logout. Legacy encrypted-only session cookies do **not** gain revocation implicitly. 5. A protected, independently rate-limited request endpoint calls `request_password_reset(email, trusted_now)`. Return the same generic acknowledgement for unknown, throttled and accepted requests. No mail provider is contacted inline. Database failures require a generic service response. 6. A separate bounded worker calls `deliver_next_account_mail` with an `AccountMailConfig` containing the server-owned HTTPS origin, reset endpoint, sender, application name and fallback locale. The endpoint cannot contain a query or fragment. The provider must have a verified sending domain. 7. Submit the new password and opaque code to a CSRF-protected consume endpoint using `complete_password_reset`. Never include the password in a URL. On success, direct the member to login; no automatic login is performed. The reset page must set `Cache-Control: no-store` and `Referrer-Policy: no-referrer`, remove the action query from browser history, avoid third-party page resources, and ensure proxy/access logs omit the query string. The SDK cannot configure a deployed proxy. Apply separate ingress limits to login, request and consume routes, and test response timing in the deployed system. The store enforces a 250 ms minimum request duration, but this alone cannot hide arbitrary database delays or establish indistinguishability under outages. ## Reset and delivery guarantees - Reset codes use 256 bits from the operating system CSPRNG and expire after 20 minutes. The credential table retains only a purpose-separated keyed digest. A replacement request invalidates previous unused codes. - Password replacement, consumption, sibling invalidation, session revocation and creation of a password-changed notice share one database transaction. Failure to persist the outbox rolls the password change back. - Recovery requests have a three-per-account/15-minute limit and a global 120-per-minute ceiling. Consumption looks the code up before any Argon2 work, so unknown, expired or consumed codes cost one indexed read. Each code has its own budget of 10 attempts per minute, counting attempts in flight; only a failed lookup keeps its unit. An exhausted code receives `RecoveryError::Throttled` (map it to a generic "try again later"), while other members' codes are never blocked. The budget is process-local and bounded to 10,000 recent codes. These are bounded defaults, not a substitute for per-client/distributed ingress controls. - The outbox encrypts recipient and reset code, with per-record authenticated encryption. The recovery credential remains digest-only; the delivery copy is decryptable using the application encryption key until expiry/terminal cleanup. Retention is bounded at 10,000 records, with six attempts, 60-second leases and exponential retry delay. When the outbox is full, the oldest delivered or failed (ciphertext-free) records are evicted first, so only pending and leased notices can block registration or silently drop a reset request. Old workers cannot acknowledge a newer lease. - Reset emails render an absolute UTC expiry, keeping the body identical across retries. Stable delivery IDs reach Resend through observation, inspection, suppression, resolver and failover wrappers. Other transports remain at-least-once. Switching providers or crashing after acceptance can duplicate mail. Resend's [idempotency contract](https://resend.com/blog/engineering-idempotency-keys) has its own retention window; this is not an exactly-once inbox guarantee. ## Message content and feedback `ActionLink` validates the application's exact configured origin. `AccountMail` provides deterministic localized welcome, reset, password-changed, verification, new-device, security-alert, address-change, closure and export notices. These additional templates do not implement the corresponding identity transactions. `MailPurpose` identifies their purpose; a template is not a marketing consent record or a legal-compliance certificate. MAIL-001/RULLST-005 is fixed in the mandatory pipeline: bounded opaque `token=` query values inside safe body URLs survive preparation, including HTML-escaped query separators and plain-text links wrapped in `<…>`, `(…)` or Markdown `[label](…)` or followed by sentence punctuation. Other credentials and non-URL token text remain redacted. `Message`, action links and recovery notices omit sensitive content from Debug; `LogDriver` emits delivery metadata only. Do not log values explicitly exposed for delivery by `expose_url`, `expose` or `into_message`. `ResendFeedbackVerifier` authenticates the exact raw body with Svix v1 HMAC and five-minute freshness. Reject duplicate signature headers at the HTTP boundary. It classifies `email.delivered`, `email.delivery_delayed`, `email.bounced`, `email.complained` and `email.failed`; subscribe the webhook to those. Another authentic event type (such as `email.sent` or `email.opened`) returns the v13 `MailFeedbackError::UnsupportedEvent`, which an adapter can acknowledge without acting on it, while malformed or unclassifiable bounce payloads stay `InvalidPayload`. Use its `suppression_event()` with `MutableSuppressionStore::record`, then place `SuppressionGuard` around the worker transport. Permanent bounces and complaints suppress delivery; transient failures do not. A durable suppression store is required across process restarts. The SDK provides a SQLite suppression adapter; other production deployments supply their own `SuppressionStore`. Verified feedback does not automatically mutate an unrelated account registry. Security templates add no tracking or marketing unsubscribe dependency. Disable tracking at the provider/domain/account level as well; for example, the 12.x Postmark transport's [server settings can override the message flag](https://postmarkapp.com/developer/user-guide/tracking-opens/tracking-opens-per-email). Marketing consent, legal basis, preference withdrawal and retention are application responsibilities; enabling Mail cannot establish GDPR/LGPD compliance. ## Azure Communication Services (12.x only) This transport was removed in v13. On 12.x, `AzureCommunicationDriver` supports the ACS Email 2023-03-31 REST contract. Set `MAIL_DRIVER=azure-acs` and `AZURE_COMMUNICATION_EMAIL_ENDPOINT=https://RESOURCE.communication.azure.com`. In Azure Container Apps, `AzureManagedIdentity` uses the platform-injected loopback `IDENTITY_ENDPOINT`/`IDENTITY_HEADER` and optional user-assigned `AZURE_CLIENT_ID` for the Communication Services resource. The endpoint must be plain HTTP on a literal loopback IP or the exact name `localhost`, which the identity client pins to `127.0.0.1`/`::1` instead of resolving. The host must grant the identity email-sending permission and verify its domain/sender. A credential reuses its token until five minutes before `expires_on`; identity endpoint throttling (HTTP 429) and 5xx outages are `RateLimited`/`Transient`, while other refusals remain a permanent `ConfigError`. The `Mail` facade builds a new credential per send, so reuse applies to a long-lived driver instance. The driver disables engagement tracking, bounds payload/response sizes, validates the operation-polling origin, and accepts only a terminal successful operation as success. A pending operation after the bounded wait needs reconciliation; ACS retries have at-least-once semantics. An empty or `mock_*` endpoint/credential is deterministic offline behavior, not Azure acceptance. Live Managed Identity/ACS sending still requires deployment acceptance. See the official [REST contract](https://learn.microsoft.com/en-us/rest/api/communication/email/email/send?view=rest-communication-email-2023-03-31) and [Container Apps identity contract](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity?tabs=portal,http). ## Remaining proposal work The examples proposal is broader than welcome/password recovery. A Redis identity registry, durable email-verification/address-change transactions, marketing consent policy, and an authorized/audited reset-resend administration workflow are separate work. Existing Mail observers and `outbox_snapshot()` provide minimized application diagnostics; this change does not expose a global mailbox browser, reset codes or administrative resend endpoint. Local contracts cover mandatory preparation, localized mock delivery, SQLite restart/encryption/fencing, concurrent single use, rollback, session revocation, signed feedback and PostgreSQL recovery. CI runs the PostgreSQL contract against a pinned disposable image. Provider fixtures do not establish actual delivery, reputation, merchant readiness or regulatory compliance. --- # rullst-auth Source: https://rullst.github.io/Rullst/book/crates/auth.html # Rullst Auth `rullst-auth` provides Argon2id password hashing, versioned AES-GCM cookie sessions, role-based authorization middleware, WebAuthn/passkey ceremony verification, and an opt-in application JWT policy. Application JWTs are enabled with `jwt`. The `sqlite` feature also enables JWT and adds durable shared JWT revocation plus passkey device state. OAuth2/OIDC providers remain a separate trust boundary enabled with `oauth`, which re-exports `rullst-connect`. ## Passwords Use the asynchronous functions inside HTTP handlers so Argon2 work runs on Tokio's blocking pool: ```rust,no_run use rullst_auth::{AuthError, hash_password_async, verify_password_async}; async fn verify_login(password: String) -> Result<bool, AuthError> { let hash = hash_password_async(password.clone()).await?; Ok(verify_password_async(password, hash).await) } ``` Passwords longer than 72 bytes are rejected. `needs_rehash` compares the algorithm, version, memory, iteration, and parallelism parameters. The optional `SqlRecoveryStore` applies the same 72-byte bound to registration, password reset and `authenticate`; overlong input returns `RecoveryError::InvalidInput` before any account lookup, for registered and unknown emails alike. ## Encrypted sessions `make_login_cookie`, `make_login_cookie_with_epoch` and their decoders `decrypt_session` and `decrypt_session_claims` use a versioned AES-256-GCM envelope with authenticated metadata and an operating-system nonce. `APP_KEY` must contain at least 32 bytes, must not be a documented placeholder, and must satisfy the entropy check. The cookie helpers select `Secure` from `RULLST_ENV`, then `APP_ENV`, then `.env`, then `Rullst.toml`. When either process variable is set, `.env` is not read for that decision. A malformed `.env` produces a fixed `AuthError::General` message naming only the failing entry number; file content, including values after an unclosed quote, never appears in the error. A malformed `Rullst.toml` likewise reports only its line and column. Since 13.0.0-alpha.2 the Core security baseline that `Server` mounts also adds `Secure` to these `rullst_session` cookies, in any environment, when the request arrived over HTTPS: an `https` request URI, or `X-Forwarded-Proto: https` from a proxy that `[security] trusted_proxies` lists with `trust_forwarded_proto = true` (the same evidence Nexus Basic Auth accepts). It never removes `Secure`. ```rust,no_run use rullst_auth::{AuthError, decrypt_session, get_app_key, make_login_cookie}; fn round_trip(user_id: i32) -> Result<i32, AuthError> { let cookie = make_login_cookie(user_id)?; let token = cookie .split(';') .next() .and_then(|part| part.split_once('=')) .map(|(_, value)| value) .ok_or_else(|| AuthError::General("session cookie is malformed".to_string()))?; decrypt_session(token, &get_app_key()?) } ``` ### What the stateless cookie can and cannot do The server keeps no list of sessions. A `rullst_session` token holds only the user id, its expiry and, for the epoch cookies below, a session epoch, sealed with `APP_KEY`: - It proves the server issued the cookie, expires on its own, and stops working once the application changes the user's stored session epoch. - It cannot end one device's session while keeping the others, list active sessions, or be revoked without that stored epoch. `make_logout_cookie` only asks the browser to delete its copy: a copied cookie stays valid until it expires (30 days for `make_login_cookie`), the epoch changes, or `APP_KEY` is rotated, which signs out every user. For server-side sessions that can be listed and revoked one at a time, use `recovery::SqlRecoveryStore` (`recovery-sqlite` or `recovery-postgres`; see [active-session management](https://rullst.github.io/Rullst/book/session-management.html)). ### Revocable cookies: the session epoch Store a session epoch per user, an integer starting at 0, put it in the cookie at sign-in and compare it on every request: ```rust,no_run use rullst_auth::{ AuthError, DEFAULT_SESSION_MAX_AGE, decrypt_session_claims, get_app_key, make_login_cookie_with_epoch, }; /// At sign-in: the cookie carries the user's current epoch. fn sign_in(user_id: i32, stored_epoch: u64) -> Result<String, AuthError> { make_login_cookie_with_epoch(user_id, stored_epoch, DEFAULT_SESSION_MAX_AGE) } /// On every request: `token` is the value `extract_session_cookie` returns and /// `stored_epoch` loads the user's current epoch. fn signed_in_user( token: &str, stored_epoch: impl Fn(i32) -> Option<u64>, ) -> Result<i32, AuthError> { let claims = decrypt_session_claims(token, &get_app_key()?)?; let current = stored_epoch(claims.user_id).ok_or(AuthError::SessionRevoked)?; claims.verify_epoch(current) // `AuthError::SessionRevoked` on a mismatch } ``` To sign out every device, increment the stored epoch in one statement (`UPDATE users SET session_epoch = session_epoch + 1 WHERE id = ?`) when the user chooses "sign out everywhere", after a password change or reset, and when the account is deactivated or deleted. Issue a new cookie with the new epoch if the current browser should stay signed in. - `max_age` is counted in whole seconds, from 1 second to 400 days (`MAX_SESSION_MAX_AGE`), and sets both the token expiry and the cookie `Max-Age`. A shorter lifetime limits how long a copied cookie works if nobody revokes it. - `decrypt_session_claims` also accepts cookies made by `make_login_cookie` and `encrypt_session`, as epoch 0: start stored epochs at 0 and existing sessions keep working, until the first bump revokes them. - `decrypt_session` rejects epoch cookies, so a route that still calls it fails closed instead of skipping the epoch check. - `rullst-auth` has no session middleware: the application loads the stored epoch, which costs one lookup per request. `cargo rullst auth` and the SaaS starter generate this wiring (the LMS example has it too): a `users.session_epoch` column, epoch cookies at sign-in and registration, the check in `auth_middleware`, `User::revoke_sessions`, a `POST /logout/everywhere` route, and a password change and a password reset that increment the epoch (below). ### Password reset tokens For a users table the application owns, `PasswordResetToken` is the emailed secret of a reset link (new in v13): 32 bytes from the operating system's CSPRNG, written as 64 lowercase hex characters. Email `expose()` and store only `digest()`, a domain-separated SHA-256: ```rust use rullst_auth::{ AuthError, PasswordResetToken, password_reset_digests_match, password_reset_token_digest, }; // On a reset request: email `token.expose()` in a link such as // https://app.example.com/password/reset?token=…, and store the digest with // the account id and an expiry. let token = PasswordResetToken::generate()?; let stored = token.digest(); // When the link comes back: a malformed value has no digest, so it never // reaches the database; look the digest up and compare in constant time. let submitted = password_reset_token_digest(token.expose()).ok_or(AuthError::InvalidCredentials)?; assert!(password_reset_digests_match(&stored, &submitted)); # Ok::<(), AuthError>(()) ``` Expiry, single use and one live token per account belong to the application's store. The generated `password_reset_tokens` table keeps one token per account (its primary key), expires it after `RESET_TOKEN_LIFETIME_MINUTES` (30 as generated; 10 minutes to 24 hours, see [Tutorial 11](https://rullst.github.io/Rullst/book/tutorials/11-authentication-system.html#what-the-scaffold-currently-enforces)) and deletes it in the transaction that stores the new password hash and increments the session epoch. A password change keeps the browser that made it signed in with a cookie at the new epoch; a reset signs out every device and redirects to the login page. See [Tutorial 11](https://rullst.github.io/Rullst/book/tutorials/11-authentication-system.html#what-the-scaffold-currently-enforces) for the generated routes and their answers. ## Optional v13 account features With `recovery-sqlite` or `recovery-postgres`, `recovery::SqlRecoveryStore` provides an authoritative account registry, password recovery and revocable opaque sessions, including [active-session management](https://rullst.github.io/Rullst/book/session-management.html). `email-login-sqlite`/`email-login-postgres` add [single-use email login](https://rullst.github.io/Rullst/book/email-login.html), and `api-tokens-sqlite`/`api-tokens-postgres` add [scoped API tokens](https://rullst.github.io/Rullst/book/api-tokens.html). The facade features are `auth-sessions-*`, `auth-email-login-*` and `auth-api-tokens-*`. These v13 additions are opt-in and do not enable JWT or OAuth. ## WebAuthn/passkeys `PasskeyAuth` validates exact RP origin and ID binding, one-time expiring challenges (`challenge_ttl_seconds` from 1 to 86,400; `PasskeyAuth::new` rejects other values), client-data ceremony type, user-presence/user-verification flags, ES256 COSE keys, P-256 points, credential IDs, signatures, and monotonic counters. Only `none` attestation is advertised and accepted. With `sqlite`, `SqlitePasskeyStore` supplies bounded file-backed registration, listing, renaming, revocation and optimistic counter CAS shared by processes on the same SQLite file. `finish_authenticate` verifies the ES256 ceremony and atomically advances the stored counter; a stale concurrent update fails. Revoked records remain in inventory and continue to consume quota. Challenge state remains process-local inside `PasskeyAuth`, so a multi-instance deployment using that API needs sticky ceremony routing or a custom shared challenge layer. The optional `passkey-postgres` feature (added in v13) adds a separately bound shared manager with real PostgreSQL/browser evidence; see [shared passkey ceremonies](https://rullst.github.io/Rullst/book/shared-passkey-ceremonies.html). The adapter does not establish normative WebAuthn conformance, encrypt or replicate the database, or replace application device-ownership policy. ## Application JWTs The `jwt` feature provides `ApplicationJwtPolicy`, versioned HS256 claims, strong key validation, required issuer/audience/subject/time/JTI claims, bounded TTL and scope policy, and `kid`-based key rotation. Lifetimes are whole seconds; a TTL or `max_ttl` under one second is rejected. Every verification receives a `JwtRevocationStore`. Production policies reject the bundled bounded in-memory store because it is process-local. With `sqlite`, `SqliteJwtRevocationStore` persists token IDs and monotonic subject session versions behind a stored quota. Its `BEGIN IMMEDIATE` mutations are visible to local processes, expired token rows are pruned before capacity checks, and `ApplicationJwtPolicy::verify_async` checks that shared state. This store and `SqlitePasskeyStore` accept only an ordinary database file: `:memory:` in any form, `file:` URI filenames and the `vfs`, `immutable` and `mode=memory` URL parameters fail with `InvalidConfiguration`. Both bundled stores cap token-ID rows at three quarters of the quota and at 64 active rows per subject. Past either cap, `revoke_token` records a subject cutoff that rejects every token of that subject issued no later than the revoked one, so one principal cannot exhaust capacity for others and subject revocations keep a reserve. Subject entries are never pruned. Opening an existing SQLite file adds two backward-compatible columns; earlier releases ignore subject cutoffs, so upgrade every process sharing the file. The SQLite boundary is durable across restarts but not replicated across hosts. The deployment owns trusted paths, file permissions/encryption, backup, availability and disaster recovery. This API does not verify third-party OAuth/OIDC tokens or provide refresh tokens. ## RBAC Implement `HasRole` for the authenticated user type and install `RequireRoleLayer::<User>::new("Admin")`. Authentication middleware must insert that user into Axum request extensions before the role layer executes. ## OAuth2/OIDC Enable `oauth` for the `rullst_auth::connect` re-export. Provider configuration, discovery, JWKS rotation, and deterministic offline fixtures are implemented by `rullst-connect`. Security-sensitive functions return typed errors or a false verification result. The repository's zero-panic CI checks production library paths; this policy is not an absolute guarantee about all dependencies or host failures. --- # rullst-core Source: https://rullst.github.io/Rullst/book/crates/core.html # Rullst Core ⚙️ `rullst-core` contains Rullst's runtime primitives, Axum-compatible routing, state management, health probes, process telemetry, queues, and configuration helpers. Core is runtime-only by default. Enable `orm` for ORM bootstrap/artisan and database-backed feature flags, and `queue-sqlite` for the SQLite queue driver. `orm` adds no SQLx driver: pair it with `drivers-all` or one `strict-*` backend. The umbrella `rullst` crate enables none of them by default (up to 13.0.0-alpha.2 its defaults were `orm`, `drivers-all` and `queue-sqlite`), and domain crates opt in only when they actually use them. Queue monitoring capabilities are driver-specific. The trait defaults for listing all jobs, retrying failures and purging failures return `QueueError::Unsupported`; they never fabricate an empty snapshot or successful mutation. `purge_failed_jobs` is the canonical facade method. The deprecated `purge_completed_jobs` name is retained only as a source-compatibility alias for the historical operation, which actually removed failed jobs. The Redis driver implements all three: `list_all_jobs` returns at most 1,000 rows (failed jobs and dead letters newest first, then processing, pending and scheduled jobs, read without one atomic snapshot; `created_at` is empty because Redis does not record it), `retry_failed_job` moves a failed job to the tail of the pending list while keeping its attempt counter, and `purge_failed_jobs` deletes every failed job and dead letter. The v13 `list_job_previews(limit, max_field_bytes)` (on `QueueDriver` and the `Queue` facade) lists the same records as `QueuedJobPreview`s whose payload and error hold at most `max_field_bytes` bytes, cut on a UTF-8 boundary, with `payload_truncated`/`error_truncated` flags. SQLite cuts the values in SQL and Redis in one atomic Lua script that decodes at most `limit` envelopes, so complete payloads never leave the store. Custom drivers inherit a default that projects their `list_all_jobs` (and therefore still loads complete records). Cache diagnostics are driver-specific too. `Cache::inspect(limit)` accepts 1–200 and returns sorted logical-key, UTF-8 value-length and remaining-TTL metadata for Memory and Redis without the value. Exact keys are still application data and `CacheEntryMetadata::logical_key()` belongs only inside an authorized diagnostic boundary; its `Debug` output redacts the key. Custom drivers return `CacheError::InspectionUnsupported` unless they implement the bounded method. The live Redis CI/release contract checks metadata, TTL and non-disclosure; it does not prove cluster/failover or operator authorization. The Redis cache and queue drivers each keep one lazily opened multiplexed connection for all operations and replace it after a connection-level failure; each queue command waits up to 10 seconds for its reply. The memory cache stores a TTL too large for the monotonic clock (such as `u64::MAX`) as non-expiring instead of panicking. A read that finds an expired entry removes the key only while it still holds that expired value, so a concurrent refill is kept. SQLite deletes successful jobs by default. Applications that need a real Studio/operations history can opt in with `Queue::sqlite_with_completed_history(database_url, retained_jobs)`. The validated limit is 1–100,000 records; status transition and pruning commit in one transaction, and `purge_completed_history` removes the retained successes. Rows still contain the original payload, so access control and retention policy belong to the host. Redis/custom drivers do not inherit this policy implicitly. The Redis driver bounds its own failure state instead: failed jobs (with payloads) and dead letters are each retained up to 10,000 entries by default, the oldest evicted atomically, and `RedisDriver::try_with_failure_retention(failed_jobs, dead_letters)` accepts 1–100,000 for each. Failures recorded before that bound existed are not indexed, so they are not listed and never evicted automatically. `Queue::dispatch` and `dispatch_at` reject an empty job name or one longer than 256 bytes with `QueueError::InvalidConfiguration`. `Queue::dispatch_at` persists a due timestamp for at most 366 days through the built-in SQLite and Redis drivers. SQLite filters claims by local wall-clock milliseconds and claims in due-time order, so a due scheduled or handed-back job does not wait behind later immediate jobs; Redis atomically promotes bounded batches using Redis server time. Neither backend claims a scheduled job early. Execution starts on the first worker poll after it becomes due and retains the queue's at-least-once semantics. `Worker` drives each `pop` to completion instead of racing it against completions or shutdown, because both built-in claims commit before the future resolves. A job claimed after graceful shutdown was requested is requeued. When a handler finishes while its timeout or a graceful shutdown is being processed, the worker records the handler's own result: only a handler that was actually cancelled is failed as timed out or requeued, so a success is never reported as a timeout or run again. `Worker::run` rejects a zero `max_concurrency`, `poll_interval` or `job_timeout` and a `stalled_after` that does not exceed `job_timeout` with `QueueError::InvalidConfiguration`. Stalled-lease recovery runs when a worker starts and then every `min(stalled_after, 60 s)`, and it is queue-wide: it returns every processing lease in the shared SQLite table or Redis namespace that is older than the recovering worker's `stalled_after`, including leases of other workers. Since v13, workers claim through `QueueDriver::pop_with_lease` with their own `stalled_after`; SQLite and Redis store that lease with the claim and recovery honours it whatever age the recovering worker uses, so a pool with a short `stalled_after` no longer requeues a slower pool's running job. Claims without a lease (older workers, custom drivers or direct `pop` calls) still stall after the recovering worker's age: while any exist, every worker that shares a queue must use a `stalled_after` longer than the longest `job_timeout` of any of them. A job that crashes, aborts or hangs its worker would otherwise be recovered and claimed forever, so the SQLite and Redis drivers count stalled leases per job and fail the job, instead of requeuing it, when its fifth lease stalls. The failure is listed and retryable like any other failed job, and `retry_failed_job` restarts the count. Since v13, `SqliteDriver::try_with_max_stalled_leases` and `RedisDriver::try_with_max_stalled_leases` change the ceiling (1–1,000, default `DEFAULT_MAX_STALLED_LEASES` = 5). Worker transitions are fenced by the claim's attempt number. The SQLite and Redis drivers complete, fail or requeue a job only while it is still processing under the attempt that `pop` returned, so a worker whose lease was recovered and claimed again receives a `StateTransition` error instead of finishing, failing or deleting the newer claim. The new `QueueDriver::mark_complete_attempt`, `mark_failed_attempt` and `requeue_attempt` methods default to the unfenced methods, so custom drivers keep their behaviour until they override them. The stale handler may still have run its side effects (delivery stays at-least-once). SQLite and Redis `retry_failed_job` keep the attempt counter, so the fence also holds across a manual retry. A worker that claims a job whose name it has no handler for hands the claim back instead of failing it. SQLite and Redis make the job claimable again after five seconds, behind jobs that are already due, so a worker that registered the name (for example a newer version during a rolling deploy) can run it; the delay keeps the claiming worker out of a hot loop, and it still reports `HandlerNotFound` each time. From the 720th claim attempt (at least an hour of hand-backs) the job is failed with `HandlerNotFound` instead, so a job that no running worker can handle becomes visible and purgeable; since a retry keeps the attempt counter, deploy a handler before retrying it. Custom drivers that do not implement `QueueDriver::requeue_attempt_after` keep the previous behaviour and fail the job. `ValidatedForm`/`ValidatedJson` failures keep REST status codes (`400`, `413`, `415` or `422` JSON) for other clients, but an HTMX request receives its escaped HTML fragment with `200 OK` and an `X-Rullst-Validation-Status` header carrying that status, because htmx swaps only successful responses by default. `Scheduler::task` takes a POSIX five-field expression (`minute hour day-of-month month day-of-week`) evaluated in UTC. Day-of-week accepts 0-7 (0 and 7 are Sunday, 1 is Monday) and names, so `0 9 * * 1-5` runs Monday to Friday. When both day fields are restricted, a day matching either one runs the task (`0 0 1 * 1` is the 1st plus every Monday); a field starting with `*` keeps the intersection. Earlier releases passed the fields to the `cron` crate unchanged, where 1 was Sunday and 0 was rejected. Messaging's durable recurring publications keep their documented `cron`-crate projection. See [time zones for scheduled tasks](https://rullst.github.io/Rullst/book/crates/core.html#time-zones-for-scheduled-tasks) to match the fields against a local clock instead of UTC. `WorkerHandle` and `SchedulerHandle` buffer at most 256 undrained errors. Once the buffer is full, newer errors are dropped, counted by `dropped_errors()` and emitted as `tracing` warnings, so a handle that is kept alive but never drained does not grow memory. Drain `next_error` (for example from a supervising task) to observe every failure. A scheduler attached with `Server::schedule` is drained by the server: each task failure is logged as a `tracing` error on the `rullst::scheduler` target when reported, and a past task failure no longer turns a clean shutdown into `Err(ServerError::Scheduler)`; only a failed scheduler loop does. Custom drivers return `QueueError::Unsupported` for future timestamps unless they explicitly implement durable scheduling. `ApplicationLifecycle` supplies an opt-in process-local startup/readiness/drain contract. Up to 32 immutable validated component labels can gate readiness and application admission; `/ready` publishes only aggregate counts, not labels or dependency errors. `Server::with_lifecycle` marks the phase ready after binding, begins draining before Axum's graceful wait, and marks it stopped on completion or startup failure. `run_with_shutdown` accepts a caller-owned trigger for embedded supervisors and deterministic tests. The lifecycle does not run dependency probes, coordinate replicas, authorize users, or guarantee load balancer propagation. Since v13, the drain retains admission through the response body, including streaming/trailers, error and cancellation. It does not track upgraded connections or prove client receipt. The [two-replica deployment contract](https://rullst.github.io/Rullst/book/deployment-acceptance.html) exercises this boundary through a real proxy; its hosted workspace and installed-archive checks passed in PR #224, and the final release campaign remains separate. ## ✨ Core Features & Subsystems - **Axum-compatible routing:** `rullst::Router` wraps and converts to/from `axum::Router`; application latency depends on handlers, middleware, build profile, and deployment. - **Typed server functions:** concrete async `#[server_function]` items share owned Serde arguments/results through the versioned `rullst.client` v1 envelope. The generated native router and Wasm caller enforce same-origin paths, bounded bodies, correlation and redacted errors; hosts still own identity, tenant, authorization, idempotency and rate-limit policy. - **Rullst Radar (`rullst::radar`):** Collects process RSS/CPU where an OS probe is supported, Tokio task/yield observations when a runtime is available, and process uptime. Unsupported probes return `None`: RSS and CPU have probes on Linux and Windows only, so macOS reports neither. On Linux, RSS comes from `VmRSS` in `/proc/self/status` (correct on 16/64 KiB page kernels), and CPU percent is process CPU time over wall time: the host-wide `/proc/stat` delta is scaled by its host CPU count, not by the cgroup-limited `available_parallelism`, so a container saturating a 2-CPU quota reports about 200%. - **Prometheus `/metrics` Exporter:** Text-format metrics served at `GET /metrics`; formatting and collection have bounded runtime cost. - **Kubernetes probe routes (`rullst::health`):** the simple `health_router` reports process availability and uptime. `Server` records the health and Radar uptime origin when it starts, unless `init_health_boot_time` or `init_radar` was called earlier; without a `Server`, call them yourself. The opt-in `health_router_with_lifecycle` returns readiness from the same bounded state that gates Server request admission; the application still performs and times out its own dependency checks. The `version` field of both payloads is the Rullst framework version. To report your application's version too, use `health_router_with_version(env!("CARGO_PKG_VERSION"))` or `health_router_with_lifecycle_and_version(lifecycle, env!("CARGO_PKG_VERSION"))` (v13), which add it as `app_version`; any string works, such as a commit hash read at startup. - **Interactive Scalar API Docs (`rullst::scalar`):** OpenAPI documentation UI mounted at `/docs`, with a pinned CDN asset and a status-only fallback. A missing or malformed `openapi.json` returns `503`. - **Typed framework errors:** startup, queues, validation, scheduling, storage, and other subsystems expose their own typed errors. Applications may compose those into an application-owned `AppError`; Core does not define one global application error type. - **Redacted configuration `Debug`:** `DatabaseConfig` (and therefore `RullstConfig`) prints only the database URL scheme, such as `postgres://<redacted>`; `db::ReplicationConfig` redacts `auth_token` and prints only the `sync_url` scheme. Fields stay public and unchanged. - **Durable scheduled queues:** SQLite and Redis persist bounded due timestamps; the live Redis CI contract proves that an immediate job remains claimable while a future job stays unavailable. - **Opt-in completed-job monitoring:** SQLite can retain and atomically prune a configured number of successful jobs; the privacy-safe default remains immediate deletion. - **Bounded token-bucket rate limiter:** `RateLimiter` keys IPv4 peers per address and IPv6 peers per /64 by default. It tracks at most 100,000 keys, drops fully refilled buckets and evicts the least recently used ones beyond that cap; a key longer than 128 bytes (for example from a custom extractor returning a token header) is stored as its SHA-256 digest, so the map stays bounded in bytes too. State is process-local, not a distributed limit. When attached to `Server`, the limiter and the Traffic Shield let exact `GET`/`HEAD /health` and `/ready` probes through, so load shedding or an exhausted bucket cannot fail a liveness probe. In a debug Development server with `cargo rullst dev` reloading, its `/_rullst/dev-generation` poll, `/_rullst/dev-reload.js` and the `/_rullst/dev-telemetry` poll of `cargo rullst dash` also bypass both and are not access-logged. In staging and production, successful `/health` and `/ready` probes are not access-logged either (failing ones are); `RULLST_LOG_HEALTH_PROBES=true` prints them again. Without `RUST_LOG`, the default tracing filter is `info,sqlx=warn`; see the [telemetry guide](https://rullst.github.io/Rullst/book/telemetry-guide.html#health-probes-in-the-access-log). - **Development dashboard telemetry (v13):** under the same conditions `Server` serves `GET /_rullst/dev-telemetry` to loopback clients only, over HTTP/1.1 or newer and without proxy forwarding headers: bounded request counters and recent requests (method, path without query string, status, duration), ORM operation counts and slow operations from `rullst.orm.query` spans, and the pending count of a queue passed to `Server::with_dev_queue`. Bodies, headers, cookies, query strings, SQL and bindings are never recorded. See the [telemetry guide](https://rullst.github.io/Rullst/book/telemetry-guide.html#development-dashboard-endpoint). - **Production monitoring endpoint (v13, 13.0.0-alpha.3):** `Server::with_monitor(rullst_core::monitor::Monitor::new())` serves `GET /_rullst/monitor/v1` once `RULLST_MONITOR_TOKENS` (or `Monitor::with_token`, or an application `MonitorAuthorizer`) provides a credential; without one nothing is mounted. It reports request rate, latency and status classes per route pattern, registered queues, N+1 fingerprints and the radar, never request data or configuration, and requires TLS evidence in staging and production. See the [production monitoring guide](https://rullst.github.io/Rullst/book/production-monitoring.html). - **Trusted-proxy client resolution (v13):** `Server::trusted_proxies` mounts `security::TrustedProxyLayer` outside every other framework layer. Only a socket peer inside the listed networks may report the client through `X-Forwarded-For` or RFC 7239 `Forwarded`; the resolved address replaces `ConnectInfo`, so existing rate limiters and lockouts use it unchanged. See [Running behind a reverse proxy](https://rullst.github.io/Rullst/book/crates/core.html#running-behind-a-reverse-proxy). - **Bounded database flag cache:** `DbFeatureDriver` caches a found flag, a flag without a row and a failed or timed-out lookup (missing table, unavailable database) for its TTL, so an undefined flag does not query the database on every evaluation. A failed refresh keeps serving the last value read; one lookup waits at most two seconds and each driver caches at most 4,096 flag names. - **Feature flag buckets:** percentage rollouts and A/B variants in the Env, TOML, Memory and DB drivers use `calculate_hash_bucket`, a versioned SHA-256 hash over a domain tag, the length-prefixed flag and the identifier. It gives every toolchain, platform and replica the same assignment. Earlier releases used `std`'s unspecified `DefaultHasher`, so upgrading reassigns users to buckets once; percentages and variant weights are kept. `TomlFeatureDriver::reload` parses into a new map and swaps it in at once, so concurrent evaluations never see a flag as unset mid-reload. It reads `[features]` with a TOML parser, so quoted flag names (`"checkout.v2"`), `# ` inside strings and any valid header spelling work, and dotted keys or `[features.<group>]` tables become dotted flag names; a file that is not valid TOML falls back to the earlier line reader. When an A/B split's weights sum to less than 100, an identifier outside them gets the variant `"disabled"` from the driver that defines the flag; it no longer falls through to a lower-priority driver's split. Every driver evaluates an A/B flag for an identifier to its variant name, so `enabled_for` is true only for identifiers assigned a variant named `"enabled"`, and `enabled` (no identifier) is false for an A/B flag in every driver, including a `DbFeatureDriver` row without a rollout percentage; gate code on `variant`. The v13 `FeatureManager::overrides()` returns the first-priority `MemoryFeatureDriver` of `FeatureManager::default()` (and of the global `feature::manager()` when it uses the default pipeline), so programmatic overrides reach it. - **Project settings (v13):** `config::project_setting(name)` (also `rullst::config::project_setting`) reads an application setting from the process environment first and then `./.env`, which never overrides the environment and is never loaded into it, by `Server` or by Artisan commands (`db:*`, `studio`); read `.env` values in migrations and seeders with it rather than `std::env::var`. It returns `Ok(None)` when neither defines the name and fails with `ConfigError::Read`/`Parse` for an unreadable or malformed `.env`; errors never contain `.env` content. Nexus `basic_from_env` and generated billing code (`BILLING_*`) use it. The `#[doc(hidden)]` `server::ProjectSettings` and async `server::read_project_setting` apply the same precedence for first-party crates such as `rullst-mail`, and `ProjectSettings::environment` applies the `Server` precedence for `RULLST_ENV`/`APP_ENV`/`[app].env`; they are not a stable extension point. - **Bounded cache metadata:** Memory and Redis expose value length and TTL for at most 200 sorted entries, never cached values. Rullst Studio renders keyed opaque identifiers and one-entry invalidation rather than exact keys or bulk flush. --- ## 🚀 Usage Most applications can use the re-exports provided by the umbrella `rullst` crate instead of depending on `rullst-core` directly. ### Mounting lifecycle-aware Health Probes & Prometheus Metrics These optional surfaces are application-owned and must be mounted explicitly. Protect or isolate `/metrics` when its operational data should not be public. ```rust,no_run use rullst_core::{ ApplicationLifecycle, Router, Server, health::health_router_with_lifecycle, radar::radar_metrics_router, scalar::scalar_docs_router, }; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let lifecycle = ApplicationLifecycle::new(); let app = Router::new() .merge_axum(health_router_with_lifecycle(lifecycle.clone())) .merge_axum(radar_metrics_router()) // GET /metrics (Prometheus) .merge_axum(scalar_docs_router("/openapi.json")); // GET /docs Server::new(app) .with_lifecycle(lifecycle) .run(3000) .await?; Ok(()) } ``` ### Running behind a reverse proxy Behind a load balancer or TLS terminator every connection comes from the proxy, so limits and lockouts would treat all clients as one. List the networks your own proxies connect from, and nothing broader: ```rust,no_run use rullst_core::{Router, Server, security::TrustedProxyConfig}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let proxies = TrustedProxyConfig::new(["10.0.0.0/8", "fd00::/8"])? .trust_forwarded_proto(true); Server::new(Router::new()) .trusted_proxies(proxies) .run(3000) .await?; Ok(()) } ``` Or configure the same policy in `Rullst.toml`; a builder policy replaces it: ```toml [security] trusted_proxies = ["10.0.0.0/8", "fd00::/8"] trusted_proxy_header = "x-forwarded-for" # or "forwarded" (RFC 7239) trust_forwarded_proto = true ``` Forwarding headers from any other peer are ignored. The chain is read right to left, skipping trusted hops, and the first untrusted address becomes the client: `ConnectInfo<SocketAddr>` is replaced by that IP with port 0, and a `ClientAddr` extension records the client, the original socket peer and whether a proxy supplied it. A missing or malformed header from a trusted proxy keeps the proxy address and logs one `tracing` event without header contents. Enable `trust_forwarded_proto` only when your proxies overwrite `X-Forwarded-Proto` (or set `proto=`); `ClientAddr::forwarded_proto` then reports the scheme, and Nexus accepts an HTTPS report as its TLS evidence. Any host inside a listed network can choose the client address, so never list client-reachable ranges. ### Deferrable jobs and time windows Opt-in v13 API. Work that need not run immediately (reports, exports, re-indexing) can be dispatched with a `Deferral`: a deadline (`run_by`) and up to 24 daily `TimeWindow`s, each with an optional fixed UTC offset (daylight-saving changes are not applied, except for the windows of `Scheduler::deferrable_task_in_tz`; an end earlier than the start wraps midnight). `Queue::dispatch_deferred` makes the job claimable at the start of the next allowed window (immediately inside an open window), never after the deadline; when no window opens before the deadline, it becomes claimable at the deadline. Without windows, any time before the deadline is allowed, so the job is due immediately. `Queue::dispatch_deferred_with` takes a `CarbonAwarePlanner` around an application-provided `CarbonIntensitySource` (an async trait returning forecast slots with their unit; the source has a name). The job is placed at the start of the lowest-value slot that overlaps its windows before the deadline (ties go to the earliest slot). When the source returns an error or exceeds the planner's timeout (2 s by default), the time-window rule is used and one warning is logged per outage. Core ships no network source; the deterministic `FixedIntensitySource` exists for tests and documentation. The planner only chooses a time from the values the source reports; Rullst measures nothing and makes no claim about emissions. Placement is expressed through `dispatch_at`, so SQLite and Redis store a deferred job as an ordinary scheduled job: no new column or key, and rows written by 12.x keep working unchanged. Custom drivers without durable scheduling reject a future placement with `QueueError::Unsupported`. The returned `DeferredJob` carries the `DeferralPlan` (`run_at`, `reason` of `window`, `intensity` or `deadline`, the source name, and the source's value and unit for an intensity placement), and a `rullst.queue.deferral` tracing span records the same fields with the job id. Deadlines are limited to 366 days, like `dispatch_at`, and execution still starts on the first worker poll after the job becomes due. ```rust,no_run use rullst_core::queue::{ CarbonAwarePlanner, Deferral, FixedIntensitySource, Queue, QueueError, TimeWindow, }; use std::time::{Duration, SystemTime}; async fn defer_reports(queue: &Queue) -> Result<(), QueueError> { // Claimable between 00:00 and 06:00 at UTC-03:00, at the latest in 24 hours. let night = TimeWindow::daily(0, 0, 6, 0)?.with_utc_offset_minutes(-180)?; let deferral = Deferral::within(Duration::from_secs(24 * 3_600))?.window(night)?; let job = queue .dispatch_deferred("monthly_report", serde_json::json!({"month": "2026-09"}), &deferral) .await?; println!("{} due at {:?} ({})", job.id, job.plan.run_at, job.plan.reason.as_str()); // Fixed example values; applications implement CarbonIntensitySource. let now = SystemTime::now(); let hour = Duration::from_secs(3_600); let source = FixedIntensitySource::new("example-fixed", "gCO2eq/kWh") .slot(now, now + hour, 300.0) .slot(now + hour, now + 2 * hour, 120.0); let planner = CarbonAwarePlanner::new(source).with_source_timeout(Duration::from_secs(2)); let reindex = Deferral::within(4 * hour)?; let job = queue .dispatch_deferred_with(&planner, "reindex", serde_json::json!({}), &reindex) .await?; println!("{:?} from {:?}", job.plan.reason, job.plan.source); Ok(()) } ``` `Scheduler::deferrable_task(cron, ScheduleDeferral::within(max_delay)?.window(...)?, handler)` applies the same window and deadline rules to each tick of a recurring task, with the tick as the start and `tick + max_delay` as the deadline. Ticks that pass while a deferred run waits are skipped, as for a slow run. Scheduled tasks do not consult an intensity source; a task can enqueue a job with `dispatch_deferred_with` instead. ### Time zones for scheduled tasks `Scheduler::task` matches its five fields against UTC. With the optional `scheduler-tz` feature (v13; `rullst` and `rullst-core` both have it), `Scheduler::task_in_tz` matches them against the wall clock of a named IANA time zone, so "07:00 in Brasília" is written as it is said: ```rust,ignore use rullst::Scheduler; let scheduler = Scheduler::new() // 07:00 São Paulo time, which is 10:00 UTC. .task_in_tz("0 7 * * *", "America/Sao_Paulo", || async { // send the morning digest })? // Every ten minutes; the zone does not matter for this one. .task("*/10 * * * *", || async {})?; ``` An unknown zone name, such as `Brasilia`, is rejected when the task is registered with `SchedulerError::InvalidTimeZone { zone, message }`; a bad expression still returns `SchedulerError::InvalidCron`. The day-of-month and day-of-week fields also follow the local date: `30 22 * * 5` in São Paulo runs on Friday evening local time, which is Saturday 01:30 UTC. In zones with daylight saving, one rule applies: - A wall time that a gap skips (clocks move forward) runs at the first instant after the gap. In `America/New_York`, `30 2 * * *` runs at 03:00 EDT on the day clocks spring forward, and `*/15 * * * *` runs once at 03:00 for the skipped 02:00 to 02:45 and then continues at 03:15. - A wall time that a fold repeats (clocks move back) runs once, at its first occurrence. `30 1 * * *` runs at 01:30 EDT and not again at 01:30 EST, and an hourly task runs at 01:00 EDT and next at 02:00 EST. The zone rules come from the system database (`/usr/share/zoneinfo`) when it has the zone, so operating-system updates refresh them; otherwise the copy bundled with the `jiff` dependency is used, which keeps minimal container images without `tzdata` working. A task's label in scheduler errors is the expression followed by the zone, such as `0 7 * * * [America/Sao_Paulo]`. `Scheduler::deferrable_task_in_tz(expression, zone, deferral, handler)` (added in 13.0.0-alpha.3) is the zoned form of `deferrable_task`: its ticks follow `task_in_tz`, and its `TimeWindow`s are wall times of the same zone, so `TimeWindow::daily(22, 0, 6, 0)` stays 22:00–06:00 local time across a daylight-saving change. Window boundaries follow the same rule as ticks: one that a gap skips moves to the first instant after the gap, and one that a fold repeats is its first occurrence; a window that a gap skips entirely does not open that day. `max_delay` is elapsed time after the tick. A window of such a task must not set `with_utc_offset_minutes`, which registration rejects with `SchedulerError::InvalidConfiguration`. `deferrable_task` keeps UTC ticks and fixed-offset windows. ```rust,ignore use rullst::Scheduler; use rullst::queue::{ScheduleDeferral, TimeWindow}; use std::time::Duration; // The 18:00 report may wait up to 12 hours for 22:00–06:00 in São Paulo. let night = ScheduleDeferral::within(Duration::from_secs(12 * 3_600))? .window(TimeWindow::daily(22, 0, 6, 0)?)?; let scheduler = Scheduler::new().deferrable_task_in_tz( "0 18 * * *", "America/Sao_Paulo", night, || async { /* build the report */ }, )?; ``` ### Axum First-Class Escape Hatches & Tower Interoperability `rullst::Router` provides bidirectional conversion with `axum::Router` and accepts compatible `tower::Layer` values: ```rust use rullst::Router; use axum::routing::get; use tower_http::cors::CorsLayer; async fn handler() -> &'static str { "ok" } let mut router = Router::new() .route("/hello", get(handler)) .fallback(|| async { (axum::http::StatusCode::NOT_FOUND, "not found") }) .layer(CorsLayer::permissive()); // Direct conversion to raw axum::Router let axum_app: axum::Router = router.into(); // Or wrap an existing Axum router let rullst_app: Router = axum_app.into(); ``` ## 🛡️ Security baseline In staging and production, `Server` mounts the runtime security baseline from `rullst_core::security`: secure headers with a per-request CSP nonce, an exact CORS allowlist, a bounded WAF, double-submit CSRF and, when `enable_pii_masking` is set, response PII masking. `rullst-security` adds opt-in layers that overlap some of these. See [which security layer to use, and when](https://rullst.github.io/Rullst/book/security-layers.html) before you combine them. The secure headers include `Strict-Transport-Security: max-age=31536000; includeSubDomains`. `[security]` `hsts_max_age_seconds`, `hsts_include_subdomains` and `hsts_preload` change it; `preload` is opt-in and requires `includeSubDomains` and a max-age of at least one year, or the server refuses to start (see [Strict-Transport-Security](https://rullst.github.io/Rullst/book/security-layers.html#strict-transport-security)). The WAF refuses requests only in staging and production; in development it logs a `warn` event with the rule and path of each request it would refuse. `[security]` `waf_body_exempt_paths` lists exact paths whose bodies it does not inspect, for routes that accept text such as code samples (see [seeing and accepting WAF false positives](https://rullst.github.io/Rullst/book/security-layers.html#seeing-and-accepting-waf-false-positives)). ## 🔐 Security Audit & Reliability Repository workflows exercise Core with unit and integration tests on every change, and with fuzz and Miri campaigns started on demand, within their declared scopes. Consult the exact workflow run and commit for evidence; these tools do not prove the absence of every panic, leak, or vulnerability. --- # rullst-capital Source: https://rullst.github.io/Rullst/book/crates/capital.html # Rullst Capital 💰 ### *"Provider-Neutral SaaS Billing with Stripe and InfinitePay Adapters"* `rullst-capital` provides a provider-neutral billing base for SaaS and digital commerce in Rust: billing/metering traits, checkout and subscription contracts, entitlements, quotas, revenue analytics, paid-invoice rendering, canonical webhook verification with replay protection, Axum/Actix adapters and offline mocks. It ships two adapters: **Stripe** (supported) and **InfinitePay** (experimental until validated against a live account). Live readiness must be established per operation and environment. Integrate any other gateway in application code by implementing the provider traits: see **[Writing your own payment provider](https://rullst.github.io/Rullst/book/capital-custom-provider.html)**. --- ## ⚡ Capability & Lifecycle Matrix | Subsystem | Lifecycle Status | Description | | :--- | :---: | :--- | | **Stripe Adapter** | 🟢 `[Supported / Bounded]` | Owner-bound customer, subscription checkout, bound portal, reconciliation reads, direct charge, meter events, coupons and trials with deterministic mocks. Live account acceptance remains per deployment. | | **InfinitePay Adapter** | 🟡 `[Experimental]` | Deterministic offline checkout/portal/webhook fixtures. Live plan-only checkout and live callbacks fail closed until a reviewed contract is validated against a live account. | | **Custom Providers** | 🟢 `[Extension Point]` | Applications implement `BillingProvider` (and optionally `MeteredBillingProvider`) for another gateway and reuse the canonical webhook verifier. See [the guide](https://rullst.github.io/Rullst/book/capital-custom-provider.html). | | **Outbound Failure Boundary** | 🟢 `[Implemented / Bounded]` | Reviewed live methods share finite timeouts, disabled redirects/ambient proxies, one-MiB JSON parsing, HTTPS checkout-location validation, and redacted permanent/transient/rate-limited failures. Rullst performs no automatic mutation retry. | | **Subscription Lifecycle** | 🟠 `[Partial]` | Checkout, portal, cancellation, pause, usage, coupon, trial, status, and webhook APIs exist; Stripe implements the reviewed live paths, while InfinitePay returns `UnsupportedOperation` for operations it does not support. | | **Webhook Processing** | 🟢 `[Implemented / Bounded]` | Axum and opt-in Actix middleware call one canonical bounded verifier; adapters implement signature verification, with timestamp freshness checks for Stripe. The opt-in `webhook-sql` ledger shares bounded payload or semantic-event claims across SQLite, PostgreSQL, MySQL, and MariaDB processes. Relational handlers can claim a stable provider event ID with one domain mutation in a caller transaction. Cross-system exactly-once and reconciliation remain application work. | | **Metered Billing** | 🟢 `[Implemented / Bounded]` | Current Stripe Meter Events shape with provider-specific identity, bounded response binding and deterministic non-live mocks. Custom adapters can return receipts that require application-outbox deduplication. Provider-account evidence remains explicit. | | **v13 Plan Entitlements** | 🔵 `[Candidate]` | Typed per-action tenant/owner, exact feature/plan, mode, status, expiry and reconciliation-age checks. The generated SaaS report performs revision-fenced Stripe refreshes. Local and hosted acceptance remain tracked in the [delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html); snapshots assert trusted adapter state and are not payment evidence. | | **Paid Invoice Rendering** | 🟢 `[Implemented / Feature-gated]` | Exact validated minor units, escaped HTML, bounded paginated A4 PDF and a final-success e-mail/amount/currency binding. The downstream Mail bridge sends the attachment but durable outbox claiming and exactly-once delivery remain application work. | | **SaaS MRR/ARR Analytics** | 🟢 `[Implemented / Bounded]` | In-memory revenue metrics and churn calculations for supplied records; this is not an accounting ledger or provider reconciliation engine. | --- ## 📦 Built-in providers | Provider | Status | Scope | | :--- | :--- | :--- | | 💳 **Stripe** | Supported | Card checkout, Customer Portal, recurring subscriptions, Payment Intents charges and Meter Events within the reviewed operation boundaries. | | ⚡ **InfinitePay** | Experimental | Brazilian Pix/card provider; offline fixtures only until live checkout and callback authentication are validated. | Neither entry means every provider product, fee, payment method, tax promise or live API path has been homologated by Rullst. The [crate README](https://github.com/Rullst/Rullst/blob/main/rullst-capital/README.md) lists the current operation boundary of each adapter. ### Removed in v13 v13 removed the Paddle, Lemon Squeezy, Polar, Razorpay, Mercado Pago, Alipay, Coinbase Commerce and PicPay adapters, the Wise payout adapter with the payout contracts (`PayoutProvider`, `PayoutStatus`, `PayoutEvent` and the payout registry functions) and the NFS-e fiscal preparation module (`fiscal`, the `nfse`/`capital-nfse` features and `Invoice::to_dps`). NFS-e was never validated with a real municipality; it may return as a separate product outside the framework. The code remains in git history. Applications that used a removed provider implement the traits themselves ([guide](https://rullst.github.io/Rullst/book/capital-custom-provider.html)); see the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html) for the row "Capital providers and NFS-e removed". --- ## Provider conformance ladder Validate every provider operation independently. A successful checkout does not validate a portal, refund, usage report, cancellation or webhook contract, and evidence from one provider cannot be transferred to another. | Level | Required evidence | | :--- | :--- | | **1. Deterministic offline** | Bounds, redaction, failure classification, idempotency material and explicit mock behavior without network access. | | **2. Protocol fixtures** | Exact signed payloads, negative signature/freshness/replay cases and bounded provider response parsing. | | **3. Official test environment** | Provider sandbox/test-mode checkout, webhook, lifecycle and reconciliation exercises. | | **4. Controlled live acceptance** | The smallest provider-permitted real transaction only after account, legal, secret, refund, observability and reconciliation controls are ready; retain redacted evidence. | The generated SaaS blueprint and `make:billing` provide a durable Stripe integration only. It is not a conformance app for InfinitePay or for custom adapters. Record the exact provider, operation, environment and observed result; never summarize partial evidence as “all payments work.” Refer to the official [Stripe testing](https://docs.stripe.com/testing) and [Stripe sandbox](https://docs.stripe.com/sandboxes) guides when constructing acceptance cases. --- ## 🚀 Usage Examples ### Shared outbound failure contract `CapitalError::Provider` carries a redacted `ProviderFailure` for request construction, transport, non-success HTTP status, oversized/malformed JSON, or semantic response mismatch. Its provider and operation labels are static and safe for low-cardinality telemetry; the value deliberately omits URLs, credentials, bodies, and raw transport errors. ```rust,no_run use rullst_capital::{CapitalError, ProviderFailureClass}; fn record_disposition(error: &CapitalError) -> &'static str { match error { CapitalError::Provider(failure) => match failure.class() { ProviderFailureClass::Permanent => "permanent", ProviderFailureClass::Transient => "transient", ProviderFailureClass::RateLimited => "rate_limited", _ => "unknown", }, _ => "not_provider_transport", } } ``` HTTP 429 is rate-limited; transport failures and HTTP 408, 425, and 5xx are transient; request-build, response-shape, and other HTTP failures are permanent. Only numeric `Retry-After` delta seconds are retained and they are capped at 24 hours. These are scheduling hints, not a generic retry engine: non-idempotent operations must not be repeated without a durable, provider-forwarded idempotency key and reconciliation. ### 1. Initializing a Provider and Creating a Checkout Session ```rust,no_run use rullst_capital::providers::stripe::StripeProvider; use rullst_capital::BillingProvider; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let stripe = StripeProvider::new( "sk_live_your_stripe_api_key", "whsec_your_webhook_signing_secret", ); let session = stripe .create_checkout_session( "customer@example.com", "price_pro_monthly", "https://example.com/billing/complete", ) .await?; println!("Checkout URL: {session}"); Ok(()) } ``` This legacy email-based trait method remains for source compatibility. New Stripe integrations use the customer-bound `create_customer` and `create_subscription_checkout` flow; see the [owner-bound Stripe checkout](https://rullst.github.io/Rullst/book/payment-gateways-guide.html#2-create-an-owner-bound-stripe-checkout). ### 2. Provider-Specific Metered Usage `MeteredBillingProvider` uses an associated request type so each adapter keeps its own provider identity instead of guessing from one subscription ID. `StripeMeterEvent` implements the current form-encoded Meter Events contract and forwards its identifier as both event identity and idempotency header. The Stripe path validates positive bounded quantities, binds accepted responses, caps response JSON to one MiB and returns visibly non-live deterministic mocks. A Stripe identifier has only rolling provider deduplication. A custom adapter whose provider exposes no deduplication key returns `UsageDeduplication::ApplicationOutboxRequired`, so the application claims the event key in a durable outbox before sending. Live-account acceptance, retry/reconciliation and entitlements remain application/release evidence. ### 3. Payment-Bound Invoice PDF and Mail Enable `rullst/capital-mail` or the separate `rullst-capital/invoice-pdf` and `rullst-mail/capital-invoice` features. A `PaidInvoice` can be constructed only from final `Succeeded` evidence matching the invoice recipient, exact minor-unit total and currency. `PaidInvoiceDelivery::prepare` generates escaped HTML and a bounded PDF attachment and runs Mail's mandatory pre-flight. The stable delivery key is an application outbox identity, not a distributed lock. The application must reconcile webhooks, claim that key atomically and own at-least-once retries/provider attachment policy. ### 4. Verified Webhook Signature Handling The low-level provider contract below illustrates exact-byte verification. HTTP applications should normally mount `verify_webhook` on Axum or `verify_webhook_actix_with_state` on Actix so body limits, normalized event insertion, and replay rejection are applied before the handler. The default store is process-local; the opt-in `webhook-sql` feature accepts an `Arc<SqlWebhookReplayStore>` in `WebhookMiddlewareState` for cross-process admission. Active claims are never evicted to admit new work. Webhooks use constant-time cryptographic verification where applicable: ```rust use axum::{body::Bytes, http::HeaderMap, response::IntoResponse}; use rullst_capital::providers::stripe::StripeProvider; use rullst_capital::BillingProvider; use std::collections::HashMap; pub async fn handle_stripe_webhook( headers: HeaderMap, body: Bytes, ) -> Result<impl IntoResponse, axum::http::StatusCode> { let stripe = StripeProvider::new( "sk_live_api_key", "whsec_your_webhook_signing_secret", ); let signature = headers .get("Stripe-Signature") .and_then(|v| v.to_str().ok()) .ok_or(axum::http::StatusCode::BAD_REQUEST)?; let provider_headers = HashMap::from([( "stripe-signature".to_string(), signature.to_string(), )]); // Verifies the provider signature and timestamp before parsing the event. let event = stripe .handle_webhook(&body, &provider_headers) .map_err(|_| axum::http::StatusCode::UNAUTHORIZED)?; println!( "Verified subscription {} with status {:?}", event.subscription_id, event.status, ); Ok(axum::http::StatusCode::OK) } ``` SQL-backed middleware claims the payload before dispatch. Treat it as a fail-closed replay firewall, not an exactly-once delivery guarantee. For an atomic relational state change, verify the exact payload through the selected provider, obtain its stable event identifier, then call `check_and_record_event_key_with_transaction` inside the same transaction as the domain mutation. Provider API calls, e-mail, queues, and other systems still need an outbox, idempotent consumers, and reconciliation. --- ## 🔒 Security Invariants 1. **Constant-Time Verification:** HMAC webhook signatures and explicit `mock_*` fixture secrets are compared with `subtle::ConstantTimeEq` or ring's constant-time `hmac::verify`. 2. **Fail-Closed Live Modes:** Operations without a reviewed live contract, including InfinitePay live checkout and callbacks, return `UnsupportedOperation` before network I/O instead of a fabricated success. 3. **Bounded Egress:** Reviewed live provider methods use a pooled client with finite connect/request timeouts, disabled redirects and ambient proxy discovery, bounded JSON, and redacted typed failure evidence. Returned checkout URLs must be absolute credential-free HTTPS without fragments. --- # rullst-studio Source: https://rullst.github.io/Rullst/book/crates/studio.html # Rullst Studio 📊 > [!IMPORTANT] > This page describes the v13 prerelease, published as `13.0.0-alpha.4`. Cargo > never selects a prerelease on its own, so its dependency example pins that > version exactly. The v12 line (`rullst-studio@12`) reached end of life on > 16 October 2026; see the [release record](https://rullst.github.io/Rullst/book/v12.html). Use a path dependency only for checkout-local > review. `rullst-studio` is the built-in, local-first administration and monitoring dashboard for Rullst. It exposes bounded database, queue, cache and telemetry views from the sources explicitly supplied by the application. ## ✨ Features - **Database inspector:** Read and filter configured SQLx tables, edit bounded primitive non-key values, delete one complete-primary-key-selected row with exact confirmation, and inspect a live ER diagram. Tables whose key includes a column outside the ASCII identifier boundary stay read-only, and each write commits only when exactly one row changed. Views show 25 rows, cut cell text to 256 characters in the database and accept search terms up to 256 bytes. SQLite, PostgreSQL, MySQL and MariaDB run executable mutation contracts; PostgreSQL runs under both the default `sqlx::Any` build and `strict-postgres`. - **API reference:** Render the operations of an `OpenApi` document explicitly supplied by the application at `/studio/api` (server-side, no script or remote asset) and serve the document at `/studio/api/openapi.json`; Studio does not infer arbitrary Axum routes or send requests for you. - **Worker queue monitoring:** Inspect up to 50 records exposed by a supplied Rullst queue and request retries. Records come from the bounded v13 `list_job_previews` projection: at most 2 KiB of each payload and error leaves the SQLite or Redis store, and previews cut there are marked `…`. SQLite removes successful jobs, so the view is not durable completion history. - **Safe configuration view:** Environment values are deny-by-default redacted; typed runtime configuration is projected without URLs, paths, or secrets. - **Feature flags manager:** Toggle database-backed flags and immediately invalidate already-warm `DbFeatureDriver` caches in the same process. Studio never creates the `rullst_feature_flags` table; a missing table is reported with the schema to add in a migration. - **Distributed diagnostics:** Visualize in-process sources plus bounded, attribute-free v1 spans from a separately mounted HMAC-authenticated push endpoint. Slow-query and repeated-label findings are heuristics; no SQL text, bindings, attributes, headers, bodies or error details are accepted. - **Cache inspector:** An explicitly supplied memory or Redis `Cache` exposes bounded metadata and individual invalidation through opaque process-bound tokens. Values, exact keys and bulk flush remain unavailable in the UI. - **Local-first security:** The supported launcher binds to loopback, verifies the direct peer and local `Host` authority on every request, and requires a same-origin `Origin` header for mutations. - **Self-contained interface:** Compiled CSS and the bounded request-stream client are served from the Studio router itself. The core operator pages do not require Google Fonts, HTMX, Mermaid, a browser-side CSS compiler, or a third-party CDN. ## 🚀 Quickstart Install the v13 prerelease with an exact pin: `cargo add rullst-studio@=13.0.0-alpha.4`. ### Launching the Studio The supported mode is a standalone debug server. `run_studio` and `Studio::into_router(LocalStudioAccess::loopback_only())` reject release builds and requests whose direct peer is not verified as loopback. Servers composing the router manually must preserve Axum `ConnectInfo<SocketAddr>`. The access capability also rejects DNS-rebinding-style non-local `Host` values, cross-origin requests, and unsafe requests without an `Origin` header. Responses use `Referrer-Policy: same-origin`, so browsers send the real origin on Studio's own form posts. `Origin: null` passes only together with `Sec-Fetch-Site: same-origin`, as browsers send it when a host layer imposes `no-referrer`; a bare `null` origin or a same-site document on another local port is rejected. Database, queue, cache and feature-flag writes also require a crate-private marker that only this capability installs, so raw subrouters mounted elsewhere return `403` for them. ### Remote projects (added in 13.0.0-alpha.3) The `rullst-studio` binary (`cargo install rullst-studio --version =13.0.0-alpha.4 --features remote --locked`, then `rullst-studio [--port PORT]`, or `cargo rullst studio --remote`; library users enable the non-default `remote` feature) serves Studio on `127.0.0.1:5556` without an application process and reads the read-only `/_rullst/monitor/v1` document of the projects you add: overview, routes, queues and N+1 pages with a project switcher. It keeps the loopback `Host`, same-origin and marker checks above, adds a per-process form token, and runs in release builds because it serves no local data: the data browser, `.env` viewer, AI and API playgrounds and migration pages are not mounted. Projects are stored owner-only in the user's configuration directory, with each token AES-256-GCM encrypted under a key in the data directory and bound to its name and base URL. Requests require `https://` (or `http://` to loopback), follow no redirects, ignore proxy variables and read at most 1 MiB in 5 seconds. See the [production monitoring guide](https://rullst.github.io/Rullst/book/production-monitoring.html#watch-your-projects-from-studio). History and alerts: projects marked as watched are polled every 60 s while the binary runs; their aggregates go to a bounded owner-only SQLite file (seven days) charted on `/studio/history`, and per-project rules (unreachable, 5xx rate, p95, pending jobs, queue failures) fire with a cool-down on `/studio/alerts`, optionally posted once to an encrypted `https://` webhook. Studio uses the ORM pool that the application initialized. When none exists, its first database view initializes one from the resolver shared by `Server` and Artisan (process `DATABASE_URL`, then `./.env`, then `[database].url` in `Rullst.toml`). Without a configured database it reports the database tools as unavailable and creates nothing; there is no SQLite fallback. The earlier `StudioLayer` embedded-production idea was never implemented. Keeping an authenticated shared Studio is worthwhile, but it needs its own explicit identity/RBAC/TLS policy before it can become a supported mode. Since 12.1.0, the raw `data_browser::router()` also serves the assets and Cache routes its layout uses (`12.0.0` omits them; the full local builder is not affected). When upgrading from 12.0 with application-level workaround routes, see the [12.1.0 composition fix and migration checklist](https://rullst.github.io/Rullst/book/3-rullst-studio.html#v1210-browser-composition-fix). **CLI Launch:** Inside a project, the CLI runs the application with the `studio` argument (`cargo run -- studio`): ```bash cargo rullst studio ``` Core's Artisan handler then serves a smaller local compatibility UI (database, AI, telemetry, security, capital and span pages) on `127.0.0.1:5555`, not the `rullst-studio` router described on this page. ### Authenticated trace producers ```rust,no_run use rullst_studio::distributed_traces::{ DistributedTraceStore, TraceIngestionKey, TraceIngestor, }; use rullst_studio::{LocalStudioAccess, Studio}; # fn build() -> Result<(), Box<dyn std::error::Error>> { let store = DistributedTraceStore::new(2_048)?; let key = TraceIngestionKey::new(std::env::var("RULLST_TRACE_INGESTION_KEY")?)?; let ingestion = TraceIngestor::new(store.clone(), "api-1", key)?; let _push_only_application_router = ingestion.router(); let _local_viewer = Studio::new() .with_distributed_traces(store) .into_router(LocalStudioAccess::loopback_only())?; # Ok(()) # } ``` Each ingestor binds one exact producer name to one key; mount separate producer endpoints over the same store when needed. `TraceBatchSigner::new` binds the same pair and produces the byte-identical JSON body plus source, timestamp, nonce and signature headers. Mount the push-only router under an application path; it exposes no Studio reads or administrative mutations. The application/deployment still owns TLS, network policy, key distribution and rotation, clock synchronization, availability and label redaction. The store is bounded process memory, not OTLP or durable trace storage. ## 🔐 Security Audit `rullst-studio` currently supports verified-loopback development access. It does not provide a built-in shared-intranet or production authentication mode. Do not expose raw subrouters publicly; a future shared mode must fail closed behind application-owned authentication, administrator authorization, TLS, and network policy. The built-in migration page intentionally links to `cargo rullst db:*` commands. The compatibility HTTP mutation handlers return `501 Not Implemented` because the standalone Studio has no configured migration or seeder registry. Queue and revenue panels likewise show only data supplied by the selected driver or application; unsupported operations return errors instead of simulated success. The SSE request view records method, URI, status and latency only. It does not capture bodies or headers by default because those can contain authentication, session, payment, and personal data. A successful Studio toggle invalidates all already-warm `DbFeatureDriver` entries in the same process. Other processes and direct database writers remain visible through the configured TTL unless the application supplies distributed invalidation. `Studio::with_cache` opts one `Cache` into metadata-only inspection. The page shows a keyed opaque identifier, UTF-8 value byte length and remaining TTL for at most 100 entries. It never returns the value or logical key, offers no bulk flush, and requires the verified local mutation marker to invalidate one entry. Memory and Redis implement this contract; custom drivers return an explicit unsupported state unless they implement bounded inspection. ## 📚 Documentation For supported usage and security boundaries, see this book and the [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html#security-authentication-studio-and-nexus). --- # rullst-nexus Source: https://rullst.github.io/Rullst/book/crates/nexus.html # Rullst Nexus `rullst-nexus` is a server-rendered administrative panel for models that implement `NexusModel`, either manually or through `#[derive(Nexus)]`. The derive infers primitive widgets and accepts explicit semantic metadata such as `kind = "textarea"` and `kind = "enum", options = "draft, published"`. Nexus provides registered-model CRUD, search, pagination, semantically validated form widgets, bounded selected-record delete/deactivate actions, telemetry, a security view and an optional AI query page. The current interface uses server-side HTML and HTMX; Wasm islands, drag-and-drop media management and automatic relationship discovery described by older documentation were not implemented. They remain worthwhile separate features, but must not be presented as current behavior. ## Build a protected panel Nexus fails closed: `try_build()` requires an explicit validated access policy. The generated-app helper permits credential-free access only in debug builds and only for a loopback peer proven by Axum `ConnectInfo`. Release builds require `NEXUS_ADMIN_USERNAME` and a unique `NEXUS_ADMIN_PASSWORD` of at least 16 characters, each read from the process environment or else the working directory's `.env` (never loaded into the process environment). A malformed or unreadable `.env` fails with `NexusBuildError::InvalidDotenv`. ```rust use rullst_nexus::{Nexus, NexusAuthPolicy}; # fn build() -> Result<axum::Router, Box<dyn std::error::Error>> { let access = NexusAuthPolicy::local_development_or_basic_from_env()?; let nexus = Nexus::new() .with_auth_policy(access) .with_brand("Application Admin") // .register::<User>() .try_build()?; let app = axum::Router::new().nest("/nexus", nexus); # Ok(app) # } ``` That is the plain-Axum form. On a `rullst::Router`, as in the generated starters, mount it with `.nest_axum("/nexus", nexus)`: `rullst::Router::nest` accepts only another `rullst::Router`, and there is no `with_state` (pass shared state with `.layer(Extension(state))`). A release build without the credentials makes `local_development_or_basic_from_env()` return `NexusBuildError::MissingCredential`; the [Nexus guide](https://rullst.github.io/Rullst/book/4-rullst-nexus.html#serving-the-application-without-nexus) shows how to start the application without Nexus in that case, as the Portfolio and ERP examples do. An application with its own login can instead select `NexusAuthPolicy::application(authorizer)`: its `NexusAuthorizer` sees each request and answers `Allow` (with the audit actor ID), `SignIn` (redirect to the login page, or `401` with `HX-Redirect` for fragments and mutations), `Deny` (`403`) or an error (`503`). The SaaS starter and the LMS example use it, through `local_development_or_application`, for signed-in users whose `users.is_admin` is true; see [Using the application's sign-in](https://rullst.github.io/Rullst/book/4-rullst-nexus.html#using-the-applications-sign-in). The serving boundary must preserve the socket address, for example with Axum's `into_make_service_with_connect_info::<SocketAddr>()`. Basic Auth additionally requires the application-owned `NexusVerifiedTls` capability inserted only after validating a trusted TLS terminator, or an HTTPS report accepted by Core's `TrustedProxyLayer` from a listed proxy network with `trust_forwarded_proto` enabled. The same layer (`Server::trusted_proxies`) gives each client behind the proxy its own lockout bucket. Never derive either from an untrusted forwarded header. `NexusAuthPolicy::protect_router` can apply the same administrator boundary to application-owned operational routes, as the ERP example does for its whole back office: the dashboard and the inventory mutations. ## Capability boundary - Implemented: explicit model registration and a compile-tested derive; server-rendered tables/forms; bounded/validated registry metadata; parameterized and allowlisted SQL identifiers; bound record values; server-side pair/byte limits and semantic validation for Boolean, enum, JSON, number, date, e-mail and HTTP(S) URL fields; CRUD, search, pagination, sort and batch operations, also for composite primary keys; typed date/time writes that native date/time columns accept; CSRF middleware; fail-closed loopback/Basic access and per-request access through the application's own sign-in; bounded Basic Auth failure throttling; opt-in exact text-column tenant scope across every built-in read and mutation, derived only from a trusted Core `TenantContext`; and opt-in transaction-coupled minimized mutation audit with a fixed cross-database schema and bounded export API. - Batch boundary: at most 1,000 explicitly selected IDs; deactivation is available only for a writable Boolean `is_active` or `active` field. - Application responsibility: identity, tenant membership and domain policy; model/field authorization for global models and custom routes; database privileges; trusted proxy and TLS configuration; secret rotation; immutable external audit delivery, retention and backup; schema/type compatibility (on PostgreSQL Nexus casts number, relation, Boolean, date and time writes, but editable JSON and enums need text columns); and ownership rules beyond the explicit tenant column. The built-in audit table is transaction-coupled in the same database, not append-only or tamper-evident, and records committed mutations rather than rejected attempts. Multiline intent and variants from an unrelated Rust enum require explicit `#[nexus]` metadata; the struct derive does not invent them. - Not implemented: a generic `NexusLayer`, automatic ORM schema reflection, automatic `HasMany`/`BelongsTo` widgets, full rich-media management and a shared-production authentication service. These ideas may be implemented when they have typed contracts and proportional tests. For a complete model example and the local/release access flow, see [Rullst Nexus: Explicit Admin CMS](https://rullst.github.io/Rullst/book/4-rullst-nexus.html). --- # rullst-ai Source: https://rullst.github.io/Rullst/book/crates/ai.html # Rullst AI > **Vision preserved:** capability-typed schema support, local-model boundaries, > and autonomous-agent ideas remain itemized with an implementation opinion in the > [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html#ai-and-mail). `rullst-ai` is a provider-agnostic LLM client with mandatory outbound prompt-injection checks, PII masking, deterministic offline fixtures, JSON mode, explicit JSON Schema output, and a bounded tenant-aware RAG pipeline. ## Provider capabilities | Provider | Chat | Vision | Embeddings | JSON mode | Native JSON Schema | |---|---:|---:|---:|---:|---:| | OpenAI | yes | yes | yes | yes | yes | | Gemini | yes | yes | yes | yes | yes | | Anthropic | yes | yes | no | prompt-constrained | no | | DeepSeek | yes | no | no | yes | `deepseek-v4-flash` | | Ollama | yes | model-dependent | yes | yes | local API only | | OpenAI-compatible local/cloud | yes | declared | declared | declared | declared | Unsupported capabilities return `AiError::UnsupportedCapability`; the client does not silently switch to an unrelated endpoint or represent a fixture as a live-provider result. Anthropic requests allow up to 16,000 output tokens unless `AnthropicProvider::with_max_tokens` sets another limit (the Messages API requires one, and adaptive thinking counts toward it; a longer reply may also need a longer `with_request_timeout`). A reply the API reports as cut short (`stop_reason` `max_tokens` or `model_context_window_exceeded`) or declined (`refusal`) returns `AiError::ApiError` instead of the partial text. `OpenAiProvider` sends no output limit on text or vision requests, so the model's own limit applies, and a reply whose `finish_reason` is `length` or `content_filter` likewise returns `AiError::ApiError`. `OpenAiCompatibleProvider` covers servers implementing the named OpenAI `/chat/completions` and optional `/embeddings` shapes. It defaults to chat-only; vision, embeddings, JSON mode, and JSON Schema must be declared for the exact endpoint/model pair. `try_local` permits unauthenticated HTTP only on a literal loopback IP, `try_local_with_bearer` adds explicit local authentication, and `try_cloud` requires HTTPS plus a Bearer credential. All three disable redirects and environment proxies and bound response bodies. Different protocols use a custom public `AiProvider`, not an arbitrary-HTTP mode. ## Guarded client ```rust,no_run use rullst_ai::{AiClient, AiError, providers::openai::OpenAiProvider}; async fn answer(api_key: String, user_text: &str) -> Result<String, AiError> { let client = AiClient::new(OpenAiProvider::new(api_key)); client .chat() .system("Answer concisely.") .user(user_text) .send() .await } ``` `AiClient::prompt`, chat, vision, embedding, JSON, and structured-output calls run the same guardrail stage before provider dispatch. Built-in providers repeat the check on direct trait calls. Call custom `AiProvider` implementations through `AiClient` when the application needs the same mandatory boundary. The current guardrail blocks deterministic injection patterns, provider delimiter tokens, external Markdown image beacons (inline or reference images with a remote or unclassifiable destination, read with CommonMark escapes and character references and outside code spans and code blocks; an unmatched ASCII reference such as `vec![x]` is literal text; raw HTML `<img>` is not inspected), and invisible Unicode controls, including zero-width, bidirectional embedding/isolate, tag and other default-ignorable characters with no ordinary use in text. Soft hyphens, bidirectional marks and emoji variation selectors are removed before phrase matching instead of being blocked. Check-digit-valid CPF/CNPJ numbers (canonical formatted or unformatted), card-like digit runs and email usernames are masked before outbound transmission; alphanumeric CNPJs and other identifiers are not recognized. This is a bounded heuristic control, not proof that arbitrary input or model output is safe; authorization, tool permissions, output encoding, and domain validation remain application responsibilities. ## Adaptive evaluation runner `AdaptiveAiEvaluator<P>` runs application-defined multi-turn strategies over static provider dispatch and the mandatory prompt guardrail. One scenario is limited to 32 turns, 16 KiB per generated prompt and 2 MiB per response, with an independent deadline and explicit cancellation. Each observation exposes a bounded response only while the synchronous strategy chooses pass, fail, inconclusive or its next prompt. The versioned JSON report keeps the exact caller-supplied suite/subject labels, provider name, status, terminal code and per-turn byte counts/outcomes. It does not retain prompts, responses or provider error bodies and can itself be sent through `AuditDeliveryClient`. The subject label is not automatic model discovery, and the deterministic offline runner test is not live-model evidence. Operators must version their scenario code/corpus and execute it against each exact model/configuration they intend to approve; no passing suite proves universal safety, groundedness or jailbreak resistance. ## Authenticated audit delivery `AuditDeliveryClient` can export an application-minimized RAG, tool or provider event to one exact endpoint. Cloud configuration requires HTTPS; local development allows HTTP(S) only on a literal loopback IP. Each JSON envelope is limited to 16 KiB and HMAC-SHA256 authenticates the exact bytes together with the key ID and Unix-millisecond timestamp. A caller-generated event ID remains stable across at most five attempts, and success requires a closed JSON acknowledgement that repeats that ID. Cancellation covers request, response and retry waits; empty or `mock_*` keys select a deterministic offline fixture. Only transport/deadline failures, HTTP 429 and HTTP 5xx are retryable. Because a timeout can happen after remote acceptance, the receiver must enforce idempotency by event ID as well as signature/freshness validation. The client does not minimize arbitrary serialized data, retain a durable outbox, rotate keys, authorize operators or provide a SIEM receiver. Those remain explicit application/deployment responsibilities. ## Token usage (v13) `AiProvider::chat_with_usage`, `ChatBuilder::send_with_usage` and `StreamSummary::usage()` expose the token counts a provider reports (input, output, total and cached input) from its documented response fields. Absent counts stay `None`; nothing is estimated or priced. ## Bounded streaming and cancellation `StreamingAiClient<P>` is a static-dispatch extension for genuinely incremental providers. The OpenAI-compatible adapter implements the strict SSE path only when the exact endpoint/model configuration opts into `with_streaming()`. It checks the prompt before I/O, requires `text/event-stream` and `[DONE]`, bounds the raw response, chunk count, each chunk and aggregate output, and rejects malformed or truncated events and a `finish_reason` of `length` or `content_filter` with `AiError::ApiError`. Anthropic and Gemini stream through their native SSE protocols (v13) with the same bounds and cancellation; a truncated or withheld reply fails in both the streaming and non-streaming paths. `AiCancellation` is cloneable and aborts a supported request while it is waiting for headers or another body chunk. That drops the local request future; it cannot prove that an upstream server stopped work or billing. The other built-in transports and ordinary non-streaming calls retain deadline/drop semantics until their different wire protocols have equivalent tests. ## Tenant-aware chat memory `StatefulChat<M>` is a static-dispatch orchestration boundary over `ChatMemory`. It binds every conversation to trusted `TenantContext`, loads a bounded even history, calls the guarded client, and atomically appends the user and assistant halves after successful generation. A response that the guardrail would block when the history is replayed is rejected as `StatefulChatError::Generation(AiError::BlockedByFirewall(_))` and is not stored. `InMemoryChatMemory` is a bounded deterministic offline store. With the opt-in umbrella `ai-sql-memory` feature, `SqlChatMemory` supplies a dedicated SQLx Any pool and fixed schema for SQLite, PostgreSQL, MySQL, and MariaDB. Its revision compare-and-swap rejects stale cross-process writers. It does not retry the provider call, because doing so could duplicate cost or side effects. Tenant and conversation IDs compare case-sensitively on every backend: new MySQL/MariaDB tables use `ascii_bin` key columns, and every tenant-scoped MySQL/MariaDB statement also compares the key byte-exactly, so an older case-insensitive table fails closed until the migration in the [crate README](https://github.com/Rullst/Rullst/tree/main/rullst-ai#upgrading-mysqlmariadb-chat-memory-tables) is applied. The [AI integration tutorial](https://rullst.github.io/Rullst/book/6-ai-integration-tutorial.html#8-durable-chat-memory) shows the complete setup and application-owned security/retention boundary. ## Tenant-aware RAG pipeline `RagPipeline::answer` performs guarded embedding, calls a static-dispatch `RagRetriever`, applies per-document and total Unicode-safe budgets, guards and masks every selected passage, generates a grounded response, returns source metadata, and records one terminal audit event. It requires a trusted `TenantContext`, rejects differently tagged documents, and fails with `RagError::NoContext` instead of asking the model to answer without retrieved evidence. The bundled `InMemoryRagRetriever` is a bounded tenant-partitioned cosine index for offline tests, development, and small ephemeral datasets. It is neither durable nor distributed. A production application can implement `RagRetriever` over ORM pgvector or Qdrant, but that adapter must bind the trusted tenant and ownership predicates in the authoritative datastore. The pipeline's tag check is defense in depth, not a replacement for datastore authorization. The mandatory audit event stores the tenant, a SHA-256 correlation digest, counts, character budget, and outcome. It deliberately omits raw questions, documents, embeddings, provider bodies, and model answers. The digest is not encryption and can be guessed for low-entropy questions. `DurableRagAuditTrail` and `DurableToolAuditTrail` provide bounded synchronous local files with distinct version headers, SHA-256 frame integrity and restart/quota/corruption validation. They are single-process writers and do not supply authenticity, rotation, retention, backup or external delivery; multi-instance deployments can implement the same audit traits over their destination. Follow the [tenant-bound RAG tutorial](https://rullst.github.io/Rullst/book/tutorials/41-tenant-bound-rag.html) for the complete offline flow and production integration boundary. ## Versioned offline evals The packaged `evals/guardrails-v1.json` corpus freezes deterministic injection, jailbreak, and PII regressions. The repository gate validates unique IDs and required categories, then runs every case across all six built-in transports in offline mode. It is deliberately not presented as a safety benchmark: adaptive attacks, tool selection, hallucination, and live provider/model versions require separate eval suites. ## Strict egress policy `EgressPolicy::strict()` starts with no permitted destination. After an exact host allowlist is configured, `EgressFetcher` permits HTTPS and explicit ports, blocks credentials/local/private/metadata/reserved addresses, validates every DNS answer, pins those answers in a proxy-free reqwest client, verifies the connected peer, follows redirects only after repeating policy, and bounds time and streamed bytes. The fetcher is opt-in: it cannot protect arbitrary application/provider HTTP clients, and tenant authorization, content schema and data minimization remain caller contracts. ## Offline mode OpenAI, Gemini, Anthropic, DeepSeek, and compatible cloud endpoints use deterministic offline mode when their API key is empty or begins with `mock_`. Ollama uses an empty or `mock_*` host; the compatible adapter also exposes an explicit `mock` constructor. Plain `try_local` is deliberately live because no credential is its valid loopback configuration. Offline branches return before HTTP dispatch and cover each capability the provider declares. Unsupported capabilities remain typed errors in offline mode. `AiClient::auto()` checks `OPENAI_API_KEY` (with `OPENAI_BASE_URL` and `OPENAI_MODEL` for a compatible endpoint), `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `DEEPSEEK_API_KEY`, `GROQ_API_KEY` (with `GROQ_MODEL`) and `OLLAMA_HOST` (with `OLLAMA_MODEL`, default `llama3`). If none is configured, it selects an offline OpenAI fixture; it does not probe localhost implicitly. `OLLAMA_HOST` (and the host given to `OllamaProvider::new`) is read the way Ollama reads it: a scheme-less `127.0.0.1:11434` or `localhost` means `http` and port 11434 unless a port is named. ## JSON mode and structured output JSON mode requests a parseable JSON value and deserializes it in Rust: ```rust,no_run # use rullst_ai::{AiClient, AiError, providers::openai::OpenAiProvider}; # async fn example() -> Result<(), AiError> { let client = AiClient::new(OpenAiProvider::new("mock_local")); let value: serde_json::Value = client.json_prompt("Summarize this record").await?; # let _ = value; # Ok(()) # } ``` Native structured output requires an explicit schema and fails when the provider cannot enforce it: ```rust,no_run # use rullst_ai::{AiClient, AiError, StructuredOutputSchema, providers::openai::OpenAiProvider}; # async fn example() -> Result<(), AiError> { let client = AiClient::new(OpenAiProvider::new("mock_local")); let schema = StructuredOutputSchema::new("answer", serde_json::json!({ "type": "object", "properties": {"ok": {"type": "boolean"}}, "required": ["ok"], "additionalProperties": false }))?; let value: serde_json::Value = client .structured_prompt_with_schema("Evaluate the input", &schema) .await?; # let _ = value; # Ok(()) # } ``` Provider-side schema enforcement and Rust deserialization do not replace application-specific semantic validation. In offline mock mode the deterministic fixture follows at most 16 nesting levels, 32 items per array and 4,096 generated values; larger schemas fail with `AiError::InvalidSchema`. ## Current boundaries Streaming for the OpenAI, DeepSeek and Ollama transports (outside the compatible adapter), provider-native tool execution loops, first-party external vector-store `RagRetriever` adapters, maintained domain-specific evaluation corpora, and compile-time schema derivation remain roadmap work. The SQL memory does not supply raw-text encryption, ownership within a tenant, retention or provider auditing; the in-memory vector utilities and tool registry do not create an authorization boundary by themselves. Authenticated audit delivery does not replace a durable outbox or certify a receiver's retention, availability or security operations. --- # rullst-security Source: https://rullst.github.io/Rullst/book/crates/security.html # Rullst Security 🛡️ ### *"Defense-in-Depth RASP, Cryptographic Vault & Runtime Protection for Rust"* `rullst-security` provides authenticated field encryption, bounded defensive middleware, local abuse controls, and security telemetry. Its RASP/DLP rules are defense-in-depth heuristics: they reduce specific risks but do not establish complete OWASP coverage, replace parameterized SQL/authorization, or certify the application that mounts them. [Which security layer to use, and when](https://rullst.github.io/Rullst/book/security-layers.html) compares these layers with the Core baseline and gives one recommended production stack. --- ## ⚡ Capability & Lifecycle Matrix | Subsystem | Lifecycle Status | Description | | :--- | :---: | :--- | | **Rullst Vault** | 🟢 `[Implemented]` | Authenticated AES-256-GCM encryption with 96-bit nonces, AAD, versioned envelopes, and keyring rotation. Key custody remains external. | | **Bounded RASP** | 🟢 `[Implemented: defense in depth]` | ASCII signature matching plus one decoding pass for URI, headers, and bounded textual/JSON/XML/form bodies. Body media types are classified case-insensitively, with `+json`/`+xml` suffixes and prefixed form types, so every body axum's `Json` or `Form` extractor accepts is inspected. A stock PowerShell `User-Agent` product token (`PowerShell/7.4.1`, `WindowsPowerShell/5.1…`) is not treated as the `powershell` command keyword; execution syntax still blocks. Decoding and body inspection may allocate. | | **Login Guard Tarpit** | 🟢 `[Implemented: local]` | Progressive delay decisions and bounded, expiring in-memory jails keyed by a hashed identity. The caller performs the returned delay. | | **Fixed-Window Rate Limiter** | 🟢 `[Implemented: local]` | In-memory limiter keyed from the verified socket peer: per IPv4 address and per IPv6 /64, with IPv4-mapped IPv6 keyed as IPv4. It tracks at most 16,384 keys and does not coordinate multiple processes. | | **Redis Rate Limiter** | 🟢 `[Implemented: feature-gated foundation]` | `redis-rate-limit` uses an atomic fixed-window Lua script, namespace validation, hashed client keys and TTL-derived retry metadata. A limiter and its clones share one lazily opened multiplexed connection and reconnect after a connection-level failure instead of connecting per check. Empty/`mock_*` URLs select an explicit process-local test mode that tracks at most 16,384 client keys and denies new keys while all tracked windows are active; call `require_distributed()` at production startup. Windows must be at least 1 ms. A live contract proves independent clients share one Redis budget; cluster/failover remains application evidence. | | **DLP & Secret Masking** | 🟢 `[Implemented: bounded]` | Masks complete PEM private-key blocks (PKCS#8 plain or encrypted, RSA, EC, DSA, OpenSSH and OpenPGP), AWS access-key patterns, and credentials in supported textual database URLs. Textual responses are `text/*`, JSON, XML (including `+xml`), YAML and `application/javascript`. A `206` range that would need masking is withheld with `502` instead of losing its `Content-Range`; a `multipart/byteranges` response is checked part by part and withheld the same way when a textual part would need masking or the body cannot be split exactly (at most 256 parts, 8 KiB of headers per part). Each range is inspected on its own. Binary, compressed, streaming, unknown-size, and oversized bodies are not rewritten. | | **Local SIEM Journals** | 🟢 `[Implemented: bounded local]` | `DurableSiemSpool` preserves synchronized unsigned SHA-256 frames. The opt-in `AuthenticatedSiemSpool` adds HMAC-SHA256 sequence/predecessor integrity, one active plus seven historical zeroized keys, byte/record quotas and fail-closed restart/forgery/ordering/external-change behavior. Whole-tail checkpoints, multi-writer coordination, retention, delivery, retry, acknowledgement and external adapters remain operator work. | | **TOTP Multi-Factor Auth** | 🟢 `[Implemented: foundation]` | Six-digit SHA-1 TOTP generation/verification with a ±1 time-step window, percent-encoded `otpauth` URI builder, and subject-bound single-use recovery-code verifiers. Enrollment, transactional persistence, rate limits, and account policy remain application concerns. | | **CSWSH Guard** | 🟢 `[Implemented]` | Exact normalized scheme/host/port validation for WebSocket origins, with a fail-closed default for missing origins. Without an allowlist, the Origin must match the `Host` (or HTTP/2 `:authority`) and, when known from a trusted proxy or HTTP/2 `:scheme`, the request scheme. | | **HTML Sanitizer** | 🟢 `[Implemented: allowlist]` | `ammonia`'s default allowlist for user-supplied HTML; `sanitize_text` escapes for HTML text and quoted attributes only. Allowed links and images can still reference external sites. | | **JSON Schema Guard** | 🟢 `[Implemented: bounded]` | Rejects malformed, duplicate-key, over-2 MiB and over-32-level JSON; a route-scoped JSON Schema 2020-12 policy checks shape, not business rules. The global guard passes non-JSON media types through. | | **Honeypot and Decoy Routes** | 🟢 `[Implemented: local]` | Exact trap paths are refused; `HoneypotLayer` bans the direct peer's exact IP for 15 minutes by default, `deception_trap_middleware` only records. Bans are per process and avoided by address rotation. | | **LLM Prompt Filter** | 🟡 `[Implemented: heuristic]` | Fixed English phrase, chat-token, Markdown-beacon and invisible-character checks on `prompt`/`content`/`message` JSON fields. Rephrased, translated or encoded instructions and indirect injection pass; legitimate text can be refused. | | **Session Fingerprint (`zero_trust`)** | 🟡 `[Implemented: helper]` | HMAC of `User-Agent`, `Accept-Language` and the client /24 or /64. All inputs are client-controlled or shared; expect false mismatches after browser or network changes. Not a zero-trust architecture. | | **Timing Guard** | 🟡 `[Implemented: coarse]` | Pads responses to a minimum duration with jitter. Slower paths are not padded and the synthetic CPU work does not match a real password hash. | | **Canonical Server security stack** | 🟡 `[Partial]` | CSP nonce identity is shared across Core and extended layers, but Core still owns the default Server CSRF/WAF/header/PII stack. Explicit composition is required. | | **Distributed Rate Limiting Evidence** | 🟡 `[Partial]` | The Redis adapter is implemented, and the two-process [deployment acceptance](https://rullst.github.io/Rullst/book/deployment-acceptance.html) behind a Caddy proxy shares one Redis budget; eviction, failover and trusted-proxy deployment evidence remain required. The legacy no-argument `RateLimiter::try_with_distributed` still returns `DistributedBackendUnsupported` rather than guessing configuration. | --- ## 🔐 1. Rullst Vault (Authenticated AES-256-GCM) `FieldEncryptor` provides authenticated encryption at rest (AEAD) with a versioned envelope and keyring-assisted rotation. Applications can keep prior keys readable while writing with a new key; deployment coordination, key custody, data re-encryption, and retirement remain operator responsibilities. ### Usage Example ```rust use rullst_security::vault::FieldEncryptor; fn main() -> Result<(), Box<dyn std::error::Error>> { let master_key = [0x42u8; 32]; // 256-bit cryptographic key let sensitive_data = "user_ssn_123-45-6789"; // Encrypt with key ID and Additional Authenticated Data (AAD) let encrypted = FieldEncryptor::encrypt_with_key_id( sensitive_data, master_key, "key-2026-v1", b"tenant-organization-id-42", )?; println!("Ciphertext Envelope: {}", encrypted); // Decrypt and verify authentication tag and AAD let decrypted = FieldEncryptor::decrypt_with_aad( &encrypted, master_key, b"tenant-organization-id-42", )?; assert_eq!(decrypted, sensitive_data); Ok(()) } ``` --- ## 🛡️ 2. Runtime Application Self-Protection (RASP) The RASP request inspector scrutinizes incoming requests before they reach your controllers: ```rust use rullst_security::RaspInspector; let is_sqli = RaspInspector::inspect_text("admin' OR '1'='1"); assert!(is_sqli); let is_traversal = RaspInspector::inspect_uri("/files/../../../etc/passwd"); assert!(is_traversal); ``` Mount `RaspSecurityLayer` explicitly when the extended inspector is desired. The default `rullst-core::Server` currently mounts the smaller Core WAF rather than this layer; consolidation remains roadmap work. The inspector matches case-insensitive signatures, raw and after one percent-decoding pass, in the request target, every header except `Cookie` and `Authorization`, and identity-encoded textual bodies up to 1 MiB. It does not inspect multipart or binary bodies, does not undo double encoding, HTML entities or SQL comments, and refuses ordinary text that contains a signature (`../`, `sleep(`, `/bin/sh`). It reduces exposure to known payloads; it is not an injection defense on its own. --- ## 🚫 3. Anti-Bruteforce Login Guard & Tarpit ```rust,no_run use rullst_security::LoginGuard; async fn apply_login_delay() { let guard = LoginGuard::new(); // defaults: 5 failures, 15-minute local jail // Record failure and apply the returned progressive delay in the async handler. let delay = guard.record_login_failure("account:alice"); tokio::time::sleep(delay).await; if guard.is_jailed("account:alice") { println!("Identity is in the local Login Jail"); } } ``` --- ## 📱 4. Multi-Factor Authentication (TOTP MFA) ```rust use rullst_security::{ build_otpauth_uri, generate_mfa_secret, generate_totp_code, verify_totp_code, }; let secret = generate_mfa_secret(); let otp_uri = build_otpauth_uri("Rullst SaaS", "alice@example.com", &secret); let current_code = generate_totp_code(&secret); // Validate 6-digit code submitted by user let is_valid = verify_totp_code(&secret, "123456"); ``` The secret must be encrypted at rest. Recovery helpers return plaintext codes only at enrollment and salted HMAC verifiers for storage; consume/delete must be one durable transaction. The application still owns replay/attempt limiting, enrollment confirmation, recovery UX, audit and clock-monitoring policy. --- # rullst-iot Source: https://rullst.github.io/Rullst/book/crates/iot.html # Rullst IoT 📡 ### *"Embedded Sensor Protocols, Ed25519 OTA Gate & Edge Computing for Rust"* > [!IMPORTANT] > This page describes the v13 prerelease, published as `13.0.0-alpha.4`. Cargo > never selects a prerelease on its own, so its dependency examples pin that > version exactly. The v12 line (`rullst-iot@12`) reached end of life on > 16 October 2026; see the [release record](https://rullst.github.io/Rullst/book/v12.html). Use a path dependency only for checkout-local > review. Commit `Cargo.lock` for reproducible builds. `rullst-iot` provides high-assurance telemetry models, bare-metal `#![no_std]` data structures and packet encoders, and a cryptographically verified Over-The-Air (OTA) firmware update state machine. > **Maturity: Experimental.** The API may change between 13.x releases, and > nothing in this crate has been validated on physical hardware or against a > live broker. Experimental does not mean broken: the [module status](https://rullst.github.io/Rullst/book/crates/iot.html#module-status) > below separates working implementations from data models and contracts. See > the [maturity tiers](https://rullst.github.io/Rullst/book/maturity.html). --- ## ⚡ Capability & Lifecycle Matrix | Subsystem | Lifecycle Status | Description | | :--- | :---: | :--- | | **Ed25519 OTA Manifest Gate** | 🟢 `[Implemented / Bounded]` | Verifies a domain-separated signed manifest, target, firmware length/hash, and monotonic counter. A `no_std` store trait adds durable compare-and-set coordination; its concrete persistence, flashing, bootloader handoff, and hardware validation remain external. | | **`no_std` Telemetry Models** | 🟢 `[Implemented / Bounded]` | Allocation-conscious telemetry, digital-twin, and sensor models are available without `std`; board- and toolchain-specific builds must still be validated in the release matrix. | | **Protocol Frame Helpers** | 🟢 `[Implemented / Bounded]` | MQTT 5 PUBLISH, RFC 7252 CoAP base requests, Modbus CRC, I2C frame packing, BLE GATT data models, and power-policy abstractions; these are bounded packet/state helpers, not network, bus, or radio drivers. | | **Experimental Fixtures** | 🟡 `[Simulated fixtures]` | The opt-in feature exposes explicitly named deterministic MQTT formatting, HSM-byte, and PQC-byte fixtures. GPIO/I2C/BLE types are always-available state/frame helpers, not hardware simulators. | | **Native MQTT/CoAP Transport** | 🔵 `[Roadmap]` | Connections, TLS/DTLS, broker negotiation, acknowledgement/retransmission state, subscriptions, block-wise transfer, and interoperability. | | **Hardware Security Module (HSM)** | 🔵 `[Roadmap]` | Native secure-element driver interfaces (ATECC608A, TPM 2.0, STSAFE). | --- ## Module status Each module falls into one of four kinds: - **Working**: complete for its stated, bounded scope and covered by tests. - **Contract**: a trait your platform code implements; the crate supplies no implementation. - **Model only**: data structures that hold state in memory. They drive no hardware, radio or network and act as placeholders for a platform adapter. - **Fixture**: deterministic test data behind the `experimental-simulators` feature. Never use it in production. | Module | Kind | What it does | What it does not do | | :--- | :--- | :--- | :--- | | `ota` | Working + Contract | `OtaManager` verifies an Ed25519 signature over a domain-separated manifest that binds target, version, rollback counter, firmware length and SHA-256, then names the inactive A/B partition. `RollbackCounterStore` is the contract for durable compare-and-set of the counter. | No download, flash writes, bootloader handoff or concrete counter storage. The deprecated `new`, `verify_signature` and `commit_update` always fail. | | `mqtt` | Working (encoder) | `MqttPublish` encodes one MQTT 5 PUBLISH packet: QoS 0/1/2, retain, DUP, topic validation and a 1 MiB ceiling. | No CONNECT, SUBSCRIBE, acknowledgement state, socket, TLS or retry. | | `coap` | Working (encoder) | `CoapRequest` encodes an RFC 7252 GET/POST/PUT/DELETE request with Uri-Path and Content-Format options under a 1152-byte ceiling. | No UDP/DTLS, retransmission, block-wise transfer or response decoding. | | `modbus` | Working (narrow) | Modbus CRC-16 and a Read Holding Registers (`0x03`) RTU request frame. | No response parsing and no write-request builder (`WriteSingleRegister` is only an enum value). It does not check the protocol's register-count limit and has no serial or TCP transport. | | `i2c` | Working (narrow) | Builds the byte layout of a register-read transaction, rejecting reserved or non-7-bit addresses and reads above 8,192 bytes. | No bus access. | | `anomaly` | Working | Classifies a reading against a fixed mean and tolerance (normal, warning beyond 1×, critical beyond 2×); non-finite values fail closed as critical. | No learned baseline or time-series analysis. | | `twin` | Working (in memory) | `DigitalTwin` stores readings, returns the latest per metric and serializes a JSON snapshot, refusing non-finite values. | No synchronization or transport. It keeps every ingested reading, so trim `readings` on long-running hosts. | | `ui` | Working | Renders an HTML-escaped sensor snapshot card. | No live updates or connectivity status. | | `power` | Working (policy) | Recommends a power mode and harvester state from hard-coded millivolt thresholds. | Does not control sleep, wake, charging or solar hardware. | | `mesh` | Model only | In-memory node list; `best_relay` picks the online node with the strongest recorded RSSI. | No discovery, routing, failure detection or mesh transport. | | `ble` | Model only | GATT service and characteristic structs. | No radio and no GATT server. | | `gpio` | Model only | In-memory pin mode and state. | No register or pin access. | | `hsm` | Fixture | `SimulatedHsmDevice` derives deterministic SHA-256 bytes. | Not an HSM: no key protection, signatures or MACs. | | `pqc` | Fixture | `SimulatedPqcFixture` derives deterministic hash bytes. | No ML-KEM/Kyber, no encapsulation and no quantum resistance. | `SensorTelemetry` (crate root) is the shared telemetry data model. The `ota`, `modbus` and `anomaly` modules also have fuzz targets, and CI builds the crate for Cortex-M and RISC-V targets. Those builds are compile evidence, not hardware tests. ### Transports and hardware: use maintained ecosystem crates For anything that touches a network, bus or radio, use a mature ecosystem crate and pass Rullst's models or encoded bytes to it. Some starting points: - **MQTT on a host or gateway with `std`:** [`rumqttc`](https://crates.io/crates/rumqttc). A full client encodes packets itself, so give it the topic and a serialized `SensorTelemetry` payload. Use `MqttPublish` only when you own the socket and need the raw PUBLISH bytes. - **Async embedded firmware:** [Embassy](https://embassy.dev/) for executors, timers and networking (`embassy-net`). - **GPIO, I2C and SPI drivers:** the [`embedded-hal`](https://crates.io/crates/embedded-hal) traits with your chip's HAL. A HAL's `write_read` takes the address and register directly, so `I2cHelper` frames are mostly useful for logging, tests and adapters that expect a raw byte layout. - **Modbus RTU/TCP on a host:** [`tokio-modbus`](https://crates.io/crates/tokio-modbus). - **BLE from a desktop or gateway:** [`btleplug`](https://crates.io/crates/btleplug). Rullst ships no adapter for these crates and does not test them together with `rullst-iot`. They are suggestions; check each one against your target and security requirements. Embassy integration (roadmap milestone M25) has not started. --- ## 🛡️ Over-The-Air (OTA) Firmware Verification The `OtaManager` enforces a **fail-closed eligibility gate**: its state machine does not produce an `OtaCommit` receipt before strict Ed25519 verification of the signed manifest. The `RollbackCounterStore` path also requires an exact, strictly increasing compare-and-set. The receipt selects the intended inactive partition; platform code must still flash, verify, implement durable storage, configure the bootloader, and recover safely from power loss. ### The Cryptographic Invariant ```text [Signed Firmware Manifest] ├── Target Hardware ID: "stm32-sensor-node-v1" ├── Version String: "2.4.0" ├── Rollback Counter: 12 (Must be strictly > current committed counter) ├── Firmware Length: 131072 bytes └── Firmware SHA-256: [32 bytes hash] │ ▼ [Ed25519 Strict Signature Check] │ ┌─────────┴─────────┐ Passed Failed │ │ [Ready to Commit] [Revert & Reject] ``` ### Usage Example ```rust use rullst_iot::{ BootPartition, OtaCommit, OtaError, OtaManager, OtaManifest, RollbackCounterStore, }; fn process_incoming_ota<S: RollbackCounterStore>( firmware_bytes: &[u8], signature_bytes: &[u8], provisioned_public_key: [u8; 32], counter_store: &mut S, running_partition: BootPartition, ) -> Result<OtaCommit, OtaError> { // 1. Construct the expected manifest from the payload let manifest = OtaManifest::from_firmware( "esp32-sensor-node", "2.0.0", 15, // Proposed monotonic counter firmware_bytes )?; // 2. Load the last committed counter from the platform adapter // and the bank the platform bootloader started let mut manager = OtaManager::new_with_running_partition( "esp32-sensor-node", "1.9.0", running_partition, provisioned_public_key, counter_store, )?; // 3. Cryptographically verify signature, target, and anti-rollback state manager.verify_update(&manifest, firmware_bytes, signature_bytes)?; // 4. Flash and read back this bank using platform code before commit. let target_partition = manager.verified_target_partition()?; // 5. Durable CAS succeeds before local state changes. Coordinate the // receipt with the platform bootloader after this call. let receipt = manager.commit_verified_update_with_store(counter_store)?; debug_assert_eq!(receipt.target_partition(), target_partition); Ok(receipt) } ``` `verified_target_partition` and the receipt always name the bank opposite `current_partition`. The v13 `new_with_running_partition` constructor takes the bank the bootloader started; `new_with_counter_store` and `new_with_trusted_key` assume `PartitionA`, so with them platform code must set `current_partition` before verifying an update. Committing does not change `current_partition`; until the platform reboots, a further update verified in the same process targets the same inactive bank. The store contract requires power-loss-safe persistence before returning success. If a store reports a failure after committing, for example because an acknowledgement was lost, a retry of that same manifest completes the commit once the store reports, and a fresh `load` confirms, exactly its counter. A stale manager, or a different image with the same counter, still receives the conflict. The framework tests restart/replay, transient retry, corruption and stale-writer conflict at the adapter boundary, but those tests do not certify a particular flash, secure element or board. A failure after the durable counter advances can require platform recovery and a newer signed update. --- ## 🔌 Embedded Bare-Metal Telemetry (`#![no_std]`) `rullst-iot` exposes a `no_std` model layer intended for constrained microcontrollers. Compatibility is feature-, target-, allocator-, and toolchain-dependent and must be confirmed for the actual board: ```rust use rullst_iot::SensorTelemetry; fn sample_reading() -> SensorTelemetry { SensorTelemetry::new( "node-1", "temperature_celsius", 24.5, 1_724_500_000, ) } ``` ### MQTT and CoAP packet boundaries ```rust use rullst_iot::{ CoapMessageType, CoapMethod, CoapRequest, MqttPublish, MqttQos, }; let publish = MqttPublish::reliable( "nodes/node-1/temperature", b"24.5".to_vec(), MqttQos::AtLeastOnce, 7, )? .encode()?; let request = CoapRequest::new( CoapMessageType::Confirmable, CoapMethod::Post, 42, [0x01, 0x02], )? .path_segment("telemetry")? .content_format(50) .payload(br#"{"temperature":24.5}"#.to_vec())? .encode()?; # Ok::<(), Box<dyn std::error::Error>>(()) ``` These helpers follow the local [OASIS MQTT 5 PUBLISH](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html) and [RFC 7252 CoAP](https://www.rfc-editor.org/rfc/rfc7252.html) packet shapes. They deliberately stop before any socket or session state; the transport owns security, identity, timing, correlation, retry and peer interoperability. --- ## 🔬 Experimental Simulators (`experimental-simulators`) For local integration tests without physical hardware attached, enable the simulator feature: ```toml [dependencies] rullst-iot = { version = "=13.0.0-alpha.4", features = ["experimental-simulators"] } ``` This exposes `SimulatedMqttPayloadFormatter`, `SimulatedHsmDevice`, and `SimulatedPqcFixture` for deterministic sandbox execution. --- # Rullst Official Roadmap Source: https://rullst.github.io/Rullst/book/roadmap.html # Rullst Master Roadmap 🗺️ ### *"The Path to the Ultimate Full-Stack Rust Framework" — an aspiration, not a guarantee* Rullst's ambition is an asset. This roadmap preserves that ambition while separating what exists today from what is only a prototype, a research program, or a vision. An idea is never deleted merely because it is unfinished. Our philosophy: **"Security, Developer Experience and Performance, Architected for Humans and AI."** > **Single roadmap source:** `ROADMAP.md` is canonical. > `docs/src/roadmap.md` embeds it directly in mdBook instead of maintaining a > divergent copy. The deeper evidence and decision record is > `docs/src/capability-ledger.md`. The completed v12 identity and evidence map > live in [`docs/src/v12.md`](https://rullst.github.io/Rullst/book/v12.html); this roadmap owns the v13 > programme until a dedicated release checklist is approved. ## Next twelve months (October 2026 – October 2027) Intentions, not promises; dates follow the evidence gates described below. - **v13:** `13.0.0-alpha.1` (published as a prerelease on October 11, 2026), `13.0.0-alpha.2` (published as a prerelease on October 17, 2026, with the alpha.1 dogfooding fixes), `13.0.0-alpha.3` (published on October 24, 2026: PWA, Studio production monitor, OAuth providers), `13.0.0-alpha.4` (the candidate prepared on October 29, 2026 and the 13.0 feature freeze: a lean CLI with `green` built in and the terminal assistant removed, deployment guides, three starters), then `13.0.0-rc.1` with fixes and documentation only, driven by user feedback, then `13.0.0` stable with the [maturity tiers](https://rullst.github.io/Rullst/book/maturity.html) applying from that release. - **v12:** end of life on 16 October 2026; 12.3.0 is the last release and the `v12` branch is frozen. Its unpublished fixes (PR #500 and the RustSec updates) stay on the branch; new 12.x projects no longer resolve their dependencies after crates.io yanked the `zip` releases its Studio needs. - **Assurance:** OpenSSF Best Practices Silver, mutation testing extended from the critical v13 code to the whole workspace, and an independent review prepared with the [external audit kit](https://rullst.github.io/Rullst/book/external-audit-kit.html). - **Selected programmes:** the [green software roadmap](https://rullst.github.io/Rullst/book/green-software-roadmap.html) and the [legal compliance roadmap](https://rullst.github.io/Rullst/book/legal-compliance-roadmap.html). ## Status language - `[x] Implemented`: a bounded, testable implementation exists. This never means that every imaginable provider or production environment is covered. - `[~] Partial`: useful foundations exist, and the parenthetical says whether the remaining work is worthwhile and why. - `[ ] Not implemented`: the idea is preserved, and the parenthetical says whether it is worth pursuing and under which conditions. - `[!] Do not promise`: the absolute wording cannot be an honest framework guarantee; a narrower measurable goal is retained when useful. Target windows are planning intentions, not release guarantees. Promotion to `[x]` requires code, focused tests, truthful documentation, and the release gates at the end of this document. **September 22 direction:** stable v12.1 maintenance takes priority over publishing v13 to meet a date. September 26 is a review/handoff point, and development may continue in later sessions. The [investment proposal](https://rullst.github.io/Rullst/book/v13-priorities.html) groups M1–M41 into 25 themes to invest in or maintain and 16 to defer until justified, including M31's separate programme. It preserves the historical ideas; it does not require every theme for 13.0.0 or remove maintenance from already supported capabilities. The same proposal flags twelve existing themes with a separate high-maintenance marker and offers eight bounded follow-up subtasks. Those overlap the original themes and are not added again to the milestone count. The owner also adopted the [v13 maintenance scope and extension strategy](https://rullst.github.io/Rullst/book/v13-maintenance-scope.html): retain common security and productivity foundations, limit Capital's expansion, and assess separate lifecycles for specialized extensions. This decision refines investment and compatibility planning; it neither removes current capabilities nor changes package admission or the v12.1.1 candidate. **September 23 evaluation direction:** the [framework comparison plan](https://rullst.github.io/Rullst/book/v13-framework-comparison.html) defines reference projects, equivalent application tasks and measurements of effort, failures, performance and maintenance. Begin a small baseline after active v12.1.1 maintenance rather than waiting for all 25 A themes. The plan refines existing priorities; it adds no milestone, release gate or superiority claim. ## Audit of the detailed crate roadmaps The per-crate roadmaps are intentionally preserved as detailed design backlogs. Some predate this status policy, so an old `[x]` can record the original author's milestone claim rather than today's verified end-to-end contract. This table is the current interpretation; the [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html) contains the evidence boundary and recommendation for the highest-risk claims. | Detailed roadmap | What is verifiably implemented now | Partial, experimental, or not implemented | | :--- | :--- | :--- | | [`rullst-ai`](https://github.com/Rullst/Rullst/blob/main/rullst-ai/ROADMAP.md) | Guarded provider clients, deterministic mocks/eval corpus, bounded RAG, opt-in OpenAI-compatible SSE/cancellation, SQL conversational memory, guarded tools, authenticated audit export and adaptive evaluation orchestration. | Provider-native tool loops, first-party external retrievers, non-compatible streaming adapters and hosted-model conformance remain partial or application work. Local eval orchestration is not live-model validation. | | [`rullst-auth`](https://github.com/Rullst/Rullst/blob/main/rullst-auth/ROADMAP.md) | Argon2, encrypted sessions, RBAC/policies, OAuth/OIDC re-exports, bounded application JWTs, opt-in SQLite revocation/passkey-device state, a custom ES256 passkey foundation and the 12.1 PostgreSQL/SQLite account-recovery and transactional-notice contracts. PR #223 admitted an optional v13 PostgreSQL ceremony manager with bound single-use challenges and database/process/browser evidence. | Bounded opaque-session inventory and sibling logout passed v13 source/package admission in PR #236; final release admission remains separate. Broader refresh/device-session workflows and normative WebAuthn conformance remain incomplete; applications own identity, roles, credential-owner/revocation/counter checks and deployed session policy. | | [`rullst-capital`](https://github.com/Rullst/Rullst/blob/main/rullst-capital/ROADMAP.md) | Provider-neutral billing traits, Stripe and an experimental InfinitePay adapter, explicit offline mocks, canonical fail-closed webhook verification with Axum/Actix adapters, shared bounded webhook replay claims and team/workspace quotas over four relational protocols, Stripe coupon/trial contracts, billing scaffolding and analytics. | InfinitePay live validation, cross-system exactly-once/reconciliation and full tax/proration contracts are not implemented. v13 removed the other adapters, payouts and NFS-e; other gateways are application-owned adapters. | | [`rullst-connect`](https://github.com/Rullst/Rullst/blob/main/rullst-connect/ROADMAP.md) | OAuth2/OIDC/social adapters, state/PKCE/nonce lifecycle, guarded refresh/revocation contracts, encrypted account-bound tokens and opt-in shared-local SQLite token persistence with generation CAS. | Live provider conformance, remote refresh leases/reconciliation, multi-host replication, SAML/SCIM/DPoP/JWE/mTLS and risk ML remain application or roadmap work. Broker adapters belong to Messaging. | | [`rullst-iot`](https://github.com/Rullst/Rullst/blob/main/rullst-iot/ROADMAP.md) | `no_std` frames/telemetry, bounded MQTT 5 PUBLISH and CoAP request encoders, the Ed25519 OTA manifest gate, and a typed durable-counter CAS boundary with restart/retry/conflict proof. | Download, a concrete hardware-backed counter, flash/boot/rollback, MQTT/CoAP/LoRaWAN transports and session state, real hardware, HSM and PQC are not implemented; deterministic `Simulated*` types are experimental fixtures only. Keep the vision, but require target hardware and interoperability programs. | | [`rullst-mail`](https://github.com/Rullst/Rullst/blob/main/rullst-mail/ROADMAP.md) | Core REST/SMTP/log/memory/mock drivers, failover, bounded attachment/CID serialization, scheduling foundations, mandatory security/deliverability pipeline, deterministic mocks, tenant resolution, tracking tokens, factories, background worker integration, opt-in bounded attachment inspection, shared-local SQLite suppression and minimized delivery observations. Version 12.1 adds Resend/Svix feedback verification and ACS Managed Identity, SendPulse, Mailjet and Mailtrap transports with explicit protocol boundaries. Version 13 keeps Resend, AWS SES, SendPulse and SMTP and removes the SendGrid, Postmark, Mailjet, Mailtrap and ACS transports (kept in 12.x); other providers are application-owned `MailDriver` transports. | A checked item does not prove provider acceptance or inbox delivery; provider limits may be tighter, the local inspector is not antivirus/CDR, and other provider feedback adapters plus multi-host suppression remain open. Compile-time mailables/CSS inlining, inbound MIME, AI dunning, DMARC/DKIM/S-MIME and Studio Mail Radar are not implemented; additional providers require a shared contract suite. | | [`rullst-messaging`](https://github.com/Rullst/Rullst/blob/main/rullst-messaging/ROADMAP.md) | Bounded envelopes and wire/trace codec, idempotent publication, competing consumers, leases/retry/DLQ, deterministic local broker, encrypted-content SQLite state and opt-in ORM outbox relay. | The standalone Redis Streams candidate has TLS/restart/fault/outbox evidence and passed hosted source/package admission in PR #236; see [its contract](https://rullst.github.io/Rullst/book/redis-messaging.html). Other remote brokers and replication/failover remain roadmap work. Local durability does not provide cross-system exactly-once delivery. | | [`rullst-nexus`](https://github.com/Rullst/Rullst/blob/main/rullst-nexus/ROADMAP.md) | Fail-closed admin construction, generated metadata/forms, CRUD/search/pagination/batch actions, opt-in trusted-context tenant scope and transaction-coupled mutation audit. | Host authentication/tenant resolution, global-model/custom-route policy, immutable external audit, custom dashboards and a visual SQL builder remain application or roadmap work. | | [`rullst-orm`](https://github.com/Rullst/Rullst/blob/main/rullst-orm/ROADMAP.md) | SQLx pools/dialects, Active Record/repository/query/schema foundations, fail-closed tenant scopes, strict DB modes, transactions, relations/soft deletes, audit/privacy, typed Turso primary, bounded MongoDB/DuckDB/SurrealDB adapters, Qdrant vectors and Redis native structures. | The [transactional partial-update candidate](https://rullst.github.io/Rullst/book/transactional-partial-updates.html) passed native/archive-consumer and full hosted source admission in PRs #236 and #239. Several historical `[x]` entries remain partial or absent: transparent edge replication, universal external-search durability, autonomous schema/index changes, automatic graph traversal, Wasm drivers and PQC. The 45 historical claims are preserved in the [immutable v12 audit](https://github.com/Rullst/Rullst/blob/v12.0.0/docs/src/v12.md); the current capability ledger owns their boundaries. | | [`rullst-security`](https://github.com/Rullst/Rullst/blob/main/rullst-security/ROADMAP.md) | Bounded honeypot, sanitizer/CSP, RBAC, HMAC audit chain, RASP/DLP, AES-GCM vault, headers, applied Login Jail tarpit, TOTP with SVG QR, CSWSH origin policy, strict JSON/log guards, file-backed SRI, CEF formatting, compatible unsigned and opt-in HMAC-chained bounded local SIEM journals, timing/prompt filters and fail-closed CLI evidence/SBOM/doctor tools. | “Autonomous”, live reputation/external SIEM delivery, A+ guarantees, zero-leak/zero-latency, certification and total OWASP/memory-safety claims are not established. Trusted whole-tail checkpoints, spool compaction/remote acknowledgement, CSRF WebSocket tickets/frame crypto, distributed rate limits/audit sinks, KMS/rotation, adaptive WAF, SQL firewall and all PQC/kernel/Wasm containment items remain partial or absent. | | [`rullst-studio`](https://github.com/Rullst/Rullst/blob/main/rullst-studio/ROADMAP.md) | Verified-loopback developer UI, SQLx browser, supplied-OpenAPI playground, bounded queue history, ER diagrams, flags/config, authenticated trace ingestion, query heuristics and metadata-only Memory/Redis cache inspection. | Durable/OTLP trace storage, shared operator authorization, cross-process flag invalidation, Redis queue inspection and general database writes remain open. Query heuristics do not prove every N+1 or performance defect. | This audit does not downgrade ambitious ideas merely because they are difficult. Capabilities that require continuous operations, homologation, hardware, or a separate release lifecycle can follow the [Maybe SaaS incubation strategy](https://rullst.github.io/Rullst/book/maybesaas.html) instead of being forced into the framework core. It prevents a checkbox from becoming a production promise before the necessary code, tests, provider/hardware environment, and operational semantics exist. ## Executive milestone tracker | ID | Pillar and capability | Honest status and recommendation | Target window | | :---: | :--- | :--- | :---: | | **M1** | DX: CLI empowerment and `make:*` generators | `[~] Partial` *(worth finishing — the commands exist, but every generator/blueprint combination still needs a compiling temp-project matrix)* | v12 hardening | | **M2** | DX: fast linkers, build tuning, and responsible hot reload | `[~] Partial` *(v12 uses supervised process restart with build coalescing, retained service on compile failure and a development-only browser-refresh probe. The retained experimental DLL loader is not the public v12 workflow or a stable Rust ABI. Further build-latency improvements and optional state handoff need reproducible cross-platform evidence; universal sub-100ms reloads must not be promised)* | Continuous / v13 research | | **M3** | DX: Axum/SQLx escape hatches, granular features, proc-macro diagnostics, and ejection | `[~] Partial` *(worth improving as migration tooling — bare Core is now runtime-only, ORM/SQLite queues are explicit features, and the umbrella maps them; universal “zero lock-in” is still not worth promising because optional subsystems carry migration cost)* | Next SemVer cycle | | **M4** | DX: `make:resource` and Ignition-style error console | `[x] Implemented (scoped)` — resource scaffolding and a local developer error console exist; autonomous mutation is evaluated separately in M37 | v12 hardening | | **M5** | DX: documentation hub (mdBook), OpenAPI, and AST TypeScript generation | `[~] Partial` *(worth finishing — generators exist, but generated-project and serialization contract tests are still needed; AST inference is not a complete API contract)* | v13 | | **M6** | ORM: Active Record, repository pattern, seeders, and Turso/libSQL vision | `[~] Partial` *(SQLx foundations and the bounded Turso-primary Hrana transport/matrix exist; relation/hook/auto-diff parity and transparent synchronization do not)* | v13 | | **M7** | Edge/data: portable Wasm request/response runtime, distributed data, and autonomous upgrades | `[~] Partial` *(worth the portable edge runtime; distributed replication should use vendor-specific semantics, and autonomous upgrades are not worth enabling without signed artifacts, rollback, and operator approval)* | v13 research | | **M8** | ORM/AI: intent-based modeling and self-optimizing production indexes | `[ ] Not implemented` *(worth an advisory, explain-and-approve implementation — automatic production DDL without review is not worth the operational risk)* | v13 research | | **M9** | Auth: local auth, OAuth/OIDC, TOTP, passkeys, and WebAuthn | `[~] Partial` *(bounded application JWT policy, shared local revocation and opt-in PostgreSQL passkey ceremonies exist with hosted/database/browser evidence; complete refresh/recovery/session flows and normative WebAuthn conformance remain incomplete)* | v13 | | **M10** | Security utilities: mail, DTO validation, rate limiting, and Shield | `[~] Partial` *(worth completing — local controls and mail transports exist, while distributed rate limiting and some provider invariants require real backends and conformance tests)* | v13 | | **M11** | SaaS: hardened Nexus, Omni vision, billing, and entitlements | `[~] Partial` *(worth building in bounded modules — Nexus and billing foundations exist, but Omni, uniform live gateway coverage, and declarative entitlements are not complete)* | v13+ | | **M12** | Defense in depth: RASP/WAF, Vault, honeypots, HMAC audit, secure headers, Login Jail, DLP, TOTP, fingerprinting, CLI inspection, and Threat Radar | `[~] Partial` *(worth continuous hardening — concrete controls exist, but they do not prove universal OWASP coverage, zero leakage, external intelligence, or certification)* | Continuous | | **M13** | Post-quantum web architecture, `rullst-quantum`, NIST PQC, and sandboxed Wasm plugins | `[ ] Not implemented` *(worth later only for a concrete protocol and threat model, using audited primitives; home-grown “quantum-safe” crypto is not worth implementing)* | v13 research | | **M14** | Frontend: HTMX-first SSR and Leptos/Dioxus interoperability | `[~] Partial` *(worth improving — HTMX/HTML support is real, while the current Leptos/Dioxus types are compatibility wrappers rather than full framework integrations; “zero bundle” is a selectable architecture, not a universal guarantee)* | v13 | | **M15** | Runtime: queues, cache, scheduler, multi-stage Docker, and brokered messaging | `[~] Partial` *(bounded Core Memory/SQLite/Redis foundations plus `rullst-messaging` envelopes, idempotency, groups, leases, retry/DLQ, deterministic broker, contract suite and durable local SQLite state exist; the Redis Streams profile passed source admission in PR #236 and ships in the 13.0.0-alpha.1 prerelease; replication and RabbitMQ, Kafka, NATS, SQS/SNS, GCP Pub/Sub and Pulsar adapters remain future work)* | Foundation v12; remote adapters v13+ | | **M16** | Wasm islands and `#[client_component]` | `[~] Partial` *(the bounded `#[server_function]` transport is now implemented over `rullst.client` v1 with a generated Axum route, Wasm caller, compile diagnostics and native/Wasm/scaffold evidence; island hydration, packaging, real-browser interoperability and a stable component ABI remain open)* | v13 | | **M17** | Real-time, object storage, media, and `cargo rullst pkg` | `[~] Partial` *(WebSocket/SSE and local storage foundations exist. The Bunny Stream lifecycle, first published in the 13.0.0-alpha.1 prerelease, passed hosted/browser/archive source acceptance in PR #227; real provider/CDN interoperability remains unvalidated. Optional S3/R2 private operations passed source/package admission in PR #236; final release and provider interoperability remain unvalidated; image processing and a production package-registry contract remain open)* | v13+ | | **M18** | LiveView-style server-driven UI and `make:live` | `[~] Partial` *(an opt-in typed recovery candidate adds mandatory authorization, full snapshots, revision conflicts, bounded connections/actions and actual Chromium restart/disconnect evidence; source/package admission passed in PR #236; final release admission remains separate. Legacy components/scaffolds stay separate; DOM diffing, broadcast and automatic migration remain open. See [the supported contract](https://rullst.github.io/Rullst/book/live-recovery.html))* | v13 | | **M19** | AI/telemetry: Radar, agent tool schemas, spans, and Prometheus `/metrics` | `[x] Implemented (bounded)` — local telemetry and export surfaces exist; unavailable sources must remain unavailable rather than becoming invented values | v12 hardening | | **M20** | Persistence: zero-copy event streaming and immutable ledger engine | `[ ] Not implemented` *(interesting but lower priority — worth implementing only after defining persistence, consistency, recovery, and verification semantics; the HMAC audit chain is not a distributed ledger)* | v13 research | | **M21** | Omni-frontend protocol and mobile hypermedia bridge | `[~] Partial` *(the shared `rullst.client` v1 envelope exists; 13.0.0-alpha.3 retires the Tauri shells and the native offline-state foundation in favour of installable PWAs: `make:pwa`, a Google Play Trusted Web Activity with Digital Asset Links, and Microsoft Store packaging with PWABuilder. Native capabilities, physical-device evidence and store publication remain open)* | v13 research | | **M22** | Agentic DevOps and autonomous infrastructure provisioning | `[~] Partial` *(worth keeping as human-reviewed recommendations — telemetry advice exists; unattended infrastructure mutation is not worth enabling by default without preview, scoped credentials, audit, rollback, and policy)* | v13 | | **M23** | Polymorphic core and auto-healing runtime/database | `[~] Partial` *(worth keeping as diagnostics — a schema-error suggestion helper exists; automatic code/schema mutation is not worth enabling by default without validated plans, approval, and rollback)* | v13 | | **M24** | Embedded IoT: `no_std` frames and an Ed25519 OTA manifest gate | `[~] Partial` *(the frame/MQTT-PUBLISH/CoAP-request encoders, verification foundation and durable-counter CAS adapter contract exist; download, a hardware-backed store, flashing, boot slots, HSM/PQC, and transport interoperability do not)* | v12 foundation / v13 integrations | | **M25** | Async embedded IoT with Embassy | `[ ] Not implemented` *(worth implementing after transport and hardware traits stabilize, because executor integration before those boundaries would create churn)* | v13+ | | **M26** | Guided PaaS/VPS deploy for Fly, Railway, Render, and Caddy | `[~] Partial` *(worth hardening — scaffolding and helpers exist, but “one click” and zero downtime are not framework guarantees because credentials, DNS, migrations, health, and rollback remain operator concerns)* | v13 | | **M27** | Kubernetes manifest scaffolding and `/health`/`/ready` probes | `[x] Implemented (scaffolding scope)` — generated manifests remain deployment inputs that operators must review | v12 hardening | | **M28** | Compile-time DI and `Inject<T>` | `[x] Implemented (foundation)` — the typed container exists; “zero cost” remains a benchmarkable goal rather than a guarantee | v12 hardening | | **M29** | Scalar playground at `/docs` and OpenAPI generation | `[~] Partial` *(worth finishing — the UI/router/generator exist, but full OpenAPI fidelity requires typed schemas and validation rather than syntax inference)* | v13 | | **M30** | Tonic/gRPC and Protobuf scaffolding | `[ ] Not implemented — removed` *(13.0.0-alpha.4 removed the `make:grpc` starting-service generator to keep the CLI lean; gRPC stays possible with an application's own `tonic` setup, and a supported `rullst-grpc` crate is not planned)* | v13 | | **M31** | Aerospace, autonomous vehicles, robotics, and defense (`rullst-orbit` / `rullst-auto`) | `[ ] Not implemented` *(extraordinary, but not worth placing inside the web-framework Core; consider a separate safety-critical project only after hardware, standards, certification, and governance exist)* | Separate future program | | **M32** | Architecture: first-class Axum/Tower escape hatches and precise proc-macro diagnostics | `[x] Implemented (bounded)` — router conversion/interoperability and `syn::Error` diagnostics exist; continue compatibility tests | v12 hardening | | **M33** | SaaS: `#[rullst::gate]` and `GateGuard` declarative entitlements | `[~] Bounded foundation` *(typed current-state plan authorization and its authenticated SaaS consumer passed PR #221; the proposed attribute/guard syntax and broader declarative entitlement model remain open)* | v13 | | **M34** | Multi-target SDK generator for TypeScript, React, Dart, and Swift | `[~] Bounded TypeScript profile` *(one explicit OpenAPI 3.1 profile generates Rust codecs and a strict TypeScript HTTP client, with hosted consumer/archive acceptance in PR #221; React, Dart, Swift and broader schemas remain open)* | v13+ | | **M35** | Distributed OpenTelemetry trace-waterfall visualizer in Studio | `[~] Partial` *(Core/facade now has a minimized OTLP propagation/export candidate with local standard-collector TLS and independent-process ancestry evidence; source/package admission passed in PR #236; final release admission remains separate. Studio keeps its separate authenticated ingestion/local viewer; a unified OTLP-backed waterfall, durable storage and cross-host clock/skew presentation remain open. See [the tracing profile](https://rullst.github.io/Rullst/book/distributed-tracing.html))* | v13+ | | **M36** | Natural-language-to-SQL Studio data copilot | `[ ] Not implemented` *(worth a read-only, explainable assistant with schema allowlists, parameterization, preview, limits, and approval; autonomous production writes are not worth the risk)* | v13 research | | **M37** | One-click AI error-console autofix | `[~] Partial` *(the development error page no longer edits files and makes no network request. The server keeps a bounded, expiring, header- and body-free panic context that only a direct loopback request can read. The terminal assistant that fixed it (`cargo rullst ai fix`) was removed in 13.0.0-alpha.4; the page's "Ask your AI assistant" panel now gives a copyable prompt and the recorded-context path to the developer's own assistant, and the dead `/_rullst/autofix` endpoint (`410 Gone`) points there. Still open: a one-click flow, an edit audit trail beyond the checkpoint ref, evidence of fix quality with live providers, and the hot-reload server, which does not record errors)* | v13 | | **M38** | In-memory/local-NVMe SQLite read replicas with background synchronization | `[ ] Not implemented` *(worth vendor-specific adapters when demanded; generic “transparent replication” is not worth claiming because consistency and failover semantics belong to the selected database)* | v13 research | | **M39** | Optional self-hosted Rullst Gateway and load balancer | `[ ] Not implemented — abandoned` *(decided on 8 October 2026: not worth building. A Rullst-owned `rullst-gateway` crate/binary will not be developed. Self-hosted deployments use an established reverse proxy configured by generated files instead: `deploy --platform vps` already writes a Caddyfile, `foundry:deploy` installs a validated Caddy configuration and the [two-replica deployment contract](https://rullst.github.io/Rullst/book/deployment-acceptance.html) exercises Caddy; generated nginx configuration is planned. Readiness/drain signals stay in `rullst-core` for any proxy to consume. Parity with a managed global cloud load balancer remains external infrastructure. The idea stays recorded here and in the `[ ]` count.)* | Abandoned (replaced by generated proxy configuration) | | **M40** | Isolated programming labs and learning-game execution | `[~] Experimental implementation — bring your own runner` *(experimental `rullst-labs` contracts, in the 13.0 release inventory, provide durable submissions, exact grading, cancellation, recovery and retention, plus a documented [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html) and a non-executing example controller. The separately deployed Linux Rust/Wasmi `rullst-labs-runner` candidate passed 26 isolated checks and source admission in PRs #228 and #239, but was removed from the 13.0 workspace on 30 September 2026; its source remains in git history. Applications deploy and review their own runner; final release acceptance of `rullst-labs` remains pending. The web process never executes learner code or receives a container control socket. Broader language packs and offensive CTF infrastructure remain separate; see the dedicated roadmap.)* | v13 research/foundation | | **M41** | Privacy defaults and proportional age assurance | `[~] Initial foundation` *(opt-in `rullst-privacy` (first published in the 13.0.0-alpha.1 prerelease) age policies, signed evidence, asynchronous replay claims, trusted-clock rechecks, shared-local SQLite and optional PostgreSQL storage across application hosts with focused real-database tests; native declarations, versioned optional consent and generated SaaS preference/profile-export journeys have focused evidence (the LMS consumers were retired with the complete Academy scaffold). The package candidate adds optional facade features and passed hosted/archive source acceptance in PR #221; final release admission, live age providers, guardian verification, broader rights/retention and reviewed regional profiles remain open. See the [privacy roadmap](https://rullst.github.io/Rullst/book/privacy-age-assurance-roadmap.html). No automatic worldwide compliance claim.)* | v13 P0 | ## Quantified planning horizon through v13 This inventory counts the status labels of the **canonical long-term milestone programme**. It includes v13 research and v13+ ambitions, not just features selected for 13.0.0 by 26 September. It measures neither release readiness nor the percentage of implementation effort already completed. The label counts below were checked on 21 September 2026 against M1–M41. They include v12 hardening, continuous, next-SemVer, v13 and v13-research rows. M31 is excluded because the tracker explicitly assigns aerospace/autonomous/defence work to a separately governed future programme rather than the general v12/v13 framework suite. Detailed crate-roadmap checkboxes are not added again: they overlap with and decompose these canonical milestones, so a raw sum would double-count work. | State | Milestones | Share of the 40-milestone horizon | | :--- | ---: | ---: | | `[x]` bounded completion | **5** | **12.5%** | | `[~]` useful but incomplete foundation | **28** | **70.0%** | | `[ ]` not implemented | **7** | **17.5%** | | **Total in the framework programme** | **40** | **100%** | M39 stays in the `[ ]` row: it was abandoned on 8 October 2026 rather than implemented, and the roadmap keeps abandoned ideas recorded. There are **35 milestones without strict closure (87.5% of the labels)**. A partial milestone can contain substantial published functionality and years of remaining ambitions; these rows do not have equal size. The former calculation that assigned half credit to every partial row was an arbitrary index, not a measurement of completed code or remaining work. Do not use it to claim that a release implements a particular percentage of the entire framework vision. Progress toward 13.0.0 is tracked through concrete deliverables and their acceptance evidence in the [dated delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html). The September 22 publication candidate includes standalone Supervision and Media alongside Privacy; Labs joined on October 8, 2026, for twenty packages. All twenty were first published as the 13.0.0-alpha.1 prerelease on October 11, 2026; final release acceptance is still required; Labs remains experimental and ships contracts only, because its runner candidate was removed from 13.0. Compatible roadmap expansions can continue in 13.1 and later minors; they are not automatically deferred to a new major. Checking these labels does not constitute a fresh implementation audit of every underlying capability. Provider accounts, physical hardware, store acceptance, fiscal homologation, independent audits and research-grade cryptography cannot be completed by repository code alone. Do not add these counts to the historical-claim campaign or the v12 release checklist because those inventories substantially overlap. ## AI-native vision, without absolutes The original goal of becoming an **AI-native Rust framework suite** is preserved as a design ambition, not a historically provable “first” claim. The dedicated [AI maintainability and project-building roadmap](https://rullst.github.io/Rullst/book/ai-maintainability-roadmap.html) defines the v13 acceptance work for generated instructions, bounded context, golden tasks and reproducible model evaluation. 1. **“Zero Runtime Magic, Pure Compilation”:** derives, typed routes, and compiler diagnostics can make AI-assisted changes easier to inspect. *(Partial and worth pursuing as an architectural preference; literal zero magic, “zero hallucinations,” and instant correction are not promises any framework can make.)* 2. **Context-rich scaffolding:** generated projects should receive a maintained `AGENTS.md`/AI ruleset describing the actual selected blueprint. *(Partial and worth implementing; `cargo rullst new` and `generate:ai-context` write common `AGENTS.md` instructions when absent plus a bounded project map, not a blueprint-specific profile. Do not document `.ai-rules` or `.cursorrules` as generated until the generator and snapshots prove it.)* 3. **Structured system discovery:** a versioned schema should expose active routes, controllers, models, policies, and source locations. *(Partial and worth completing; `inspect schema` prints a statically derived ORM model schema and `inspect routes --json` emits recognized route declarations as versioned `rullst.cli-routes.v1` JSON, but controllers, policies, source locations and freshness must become an end-to-end contract.)* ## Preserved extraordinary capability decisions These items were previously easy to mistake for shipped functionality. They are kept deliberately, with the opinion requested for each gap. The capability ledger contains the more detailed evidence and acceptance boundaries. ### Architecture and product-contract ambitions - **Runtime-only Core with optional ORM** *(implemented in current hardening — bare Core no longer selects SQLx/ORM, `orm` and `queue-sqlite` are independent, Studio/Nexus opt in explicitly, and the application umbrella retains ergonomic database defaults).* - **One canonical security stack** *(partial — worth treating as high priority; keep policy/middleware in `rullst-security` and only minimal bootstrap contracts in Core so WAF, headers, and telemetry cannot drift).* - **Static dispatch everywhere** *(partial — not worth forcing absolutely; generic fast paths are valuable, but runtime-selected providers legitimately need a documented dynamic-dispatch boundary).* - **Every production source file below 500 lines** *(partial — worth continuous responsibility-based refactoring, but it is a design target rather than a release claim and large test fixtures may need a looser limit).* - **Uniform `#[non_exhaustive]`, fallible builders, and `impl Into<String>`** *(partial — worth completing incrementally under SemVer review; a mechanical mass rewrite is not worth breaking consumers).* - **Zero lock-in, zero panic/crash, zero latency/allocation, 100% memory safety, and 100% Pure-Rustls** *(`[!] Do not promise as absolutes` — migration tools, scoped zero-panic linting, benchmarks, a tiny documented unsafe allowlist, and a feature-specific transport inventory are all worth maintaining).* - **Framework-wide “production-ready” badge** *(`[!] Do not promise as one boolean` — worth publishing stability per crate/capability because routing can be stable while live payment and hardware integrations remain unavailable. The v13 [maturity tiers](https://rullst.github.io/Rullst/book/maturity.html) now label every crate Core, Extension or Experimental and define each promise).* - **A first-party load balancer embedded in every application** *(`[!] Do not make the default` — application serving and edge proxying need independent failure, upgrade and privilege boundaries. The opt-in `rullst-gateway` research (M39) was abandoned on 8 October 2026 in favour of generated Caddy/nginx configuration for established proxies. Matching a managed cloud load balancer's global infrastructure or SLA is not a repository-code claim).* - **Static competitor matrix claiming other frameworks lack capabilities** *(`[!] Do not maintain without dated sources` — comparative research and a reproducible benchmark repository are worthwhile; timeless absence claims are not).* ### Security, identity, and compliance - **Compliance-ready defaults for the main jurisdictions (EU, US, Brazil, Asia)** *(planned — building blocks, generated defaults and a per-regulation guide reviewed by counsel; never an automatic legal certification. See the [legal & regulatory compliance roadmap](https://rullst.github.io/Rullst/book/legal-compliance-roadmap.html)).* - **Full WebAuthn/FIDO2 conformance** *(partial — absolutely worth completing before a stable passkey claim, preferably with an audited library or normative conformance suite).* - **Zero-downtime key rotation and Cloud KMS** *(not implemented end to end — worth implementing through provider-neutral envelope/key-version contracts and named KMS adapters, not by embedding custody in the framework).* - **Adaptive WAF and eBPF kernel threat containment** *(not implemented — worth research only as opt-in, platform-specific defense in depth; not worth making a portability or complete-protection promise).* - **Anti-timing user-enumeration guard and Prompt Shield v2** *(implemented foundations — worth keeping and testing, but timing equalization and heuristic prompt filtering cannot guarantee elimination of every side channel or injection technique).* - **External reputation feeds, verified audit feeds, and SIEM delivery for Threat Radar** *(partial — worth pluggable connectors; never render a source as healthy or verified unless it is connected and current).* - **Studio automatically stripped from every release at zero cost** *(`[!] Do not promise` — explicit feature selection and route mounting are worth documenting; a universal debug/release assumption is not).* - **Distributed rate limiting and durable tamper-evident audit storage** *(partial/not implemented — worth pluggable Redis and append-only sink backends with atomicity, tenant namespacing, retention, and verification tests).* - **Automated SBOM, SPDX/CycloneDX, `cargo-vet`, signed provenance, and advisory governance** *(partial — worth making release gates; not equivalent to SLSA Level 3 or organizational certification without independent evaluation).* - **Loom/Shuttle, Kani/Miri, mutation, fuzz, and unsafe governance** *(partial — worth scoped blocking suites plus a reviewed `cargo-geiger` inventory; a full mathematical proof of the whole framework is not worth claiming).* - **An IDOR scanner that proves authorization** *(`[!] Do not promise proof` — the AST scanner is worth keeping as a heuristic warning tool, paired with route-level ownership and cross-tenant negative tests).* - **DevSecOps git-hook installer** *(partial — `hook:install` writes pre-commit and Conventional Commit hooks; worth adding backup/idempotency/permission tests, while CI remains authoritative because local hooks are bypassable).* - **Automatic SOC 2/ISO/FedRAMP PASS reports** *(`[!] Do not implement as an unconditional verdict` — evidence export is worthwhile; certification covers an organization and deployment, not a crate).* ### Fiscal, payments, messaging, storage, and mail - **Live NFS-e Nacional** *(removed from the framework in v13 — the local preparation module was never validated with a real municipality; fiscal work may return only as a separate, dedicated product with official homologation and independent crypto validation).* - **Uniform live support across many gateways** *(`[!] Do not promise` — v13 keeps Stripe and an experimental InfinitePay adapter; other gateways are application-owned adapters on the provider traits, and adapter names must not imply every payment, subscription, portal, tax, and webhook method exists).* - **Static fee/settlement/tax tables and “zero-cost invoicing”** *(`[!] Do not promise` — transparent links to current provider terms are worthwhile, but framework docs cannot erase certificate, accounting, infrastructure, support, compliance, or changing commercial costs).* - **Durable cross-instance webhook replay/idempotency** *(partial — worth a pluggable database/Redis uniqueness contract before multi-instance production billing).* - **RabbitMQ, Kafka, Redis Streams, NATS JetStream, SQS/SNS, and GCP Pub/Sub** *(Redis Streams candidate passed source admission in PR #236 — see [the supported profile](https://rullst.github.io/Rullst/book/redis-messaging.html). Other listed adapters remain unimplemented. The separate `rullst-messaging` crate supplies the common contract; add providers only after their delivery semantics pass provider-specific restart and fault evidence).* - **S3 and Cloudflare R2** *(v13 candidate in the 13.0.0-alpha.1 prerelease — the optional Core/facade `storage-s3` feature implements bounded private object operations with official signing, explicit credentials and deterministic mocks. Hosted source/package admission passed in PR #236; final release and provider interoperability remain outstanding; see the [supported contract](https://rullst.github.io/Rullst/book/private-object-storage.html). Multipart streaming and image resizing remain future increments).* - **Mailgun, Brevo, MailerSend, Plunk, and Scaleway transports** *(not implemented — worth demand-driven adapters only when each has a maintainer and passes the shared offline/live mail contract suite).* ### Efficiency and sustainability - **Measured green-software reports and efficient defaults** *(planned — the Experimental `cargo rullst green` command (measured figures only, carbon as a labelled SCI estimate per ISO/IEC 21031:2024), a reproducible cross-framework benchmark, tuned release and HTTP caching defaults, carbon-aware deferrable jobs. Reports state their method and limits; no unsubstantiated "greener than" claims. See the [green software roadmap](https://rullst.github.io/Rullst/book/green-software-roadmap.html)).* ### IoT, edge, AI, and critical systems - **MQTT 5, CoAP, Sparkplug B, CAN/J1939, LoRaWAN, GPIO/I2C, real firmware download/flashing/rollback, and hardware-in-the-loop CI** *(not implemented — worth separate transport and target-hardware packages after named boards and interoperability environments are selected).* - **Hardware HSM/secure-element and NIST ML-KEM/PQC backends** *(not implemented; simulators are experimental — worth audited adapters for named hardware and protocols, never home-grown crypto presented as secure hardware).* - **Autonomous AI admin, NL-SQL writes, self-healing code/schema, and DevOps mutation** *(not implemented as a safe production contract — read-only advice, dry runs, and human-approved changes are worthwhile; default autonomous production mutation is not).* - **Native JSON Schema enforcement on every LLM** *(partial — capability-typed support is worth completing; parseable JSON must remain distinct and providers that cannot enforce a schema should return `UnsupportedCapability`).* - **Any local model over any arbitrary HTTP API** *(`[!] Do not promise` — named Ollama and a capability-declared OpenAI-compatible local/cloud adapter are implemented, while arbitrary APIs differ in authentication, streaming, tools, schema, and error semantics and use the public provider trait).* - **Automatically air-gapped/zero-leak AI** *(`[!] Do not promise` — local endpoints can be useful, but the host network, logs, model runtime, and telemetry determine the real data boundary).* - **Aerospace/autonomous/defense framework** *(not implemented — the research is inspiring, but it is not worth conflating safety certification with web framework quality; incubate it independently if expertise, hardware, and governance become available).* ## Audited execution plan ### Phase 0 — containment and truthful boundaries - Keep unfinished IoT integrations, unconfigured S3/R2, live InfinitePay and other absent provider paths fail-closed with typed `Unsupported` results. - Keep Nexus fail-closed, generated credentials absent, production configuration validated, webhook secrets mandatory, local storage confined, and the release workflow blocked until its dependency order and evidence agree. - Label every capability implemented, partial, experimental, not implemented, or intentionally unsupported; never delete the vision to obtain truthful docs. ### Phase 1 — kernel security and reliability - Complete environment precedence, atomic/fallible DB initialization, APP_KEY policy, WebAuthn conformance, content-aware DLP/PII, signed-webhook composition, trusted proxies, tenant isolation, CSWSH, bounded workers, scheduler shutdown, and the production-path zero-panic policy. - Extend M10/M27 with cloud/VPS application-protection evidence using the existing Security/Core/CLI boundaries: one trusted-proxy and shared-rate-limit deployment profile, bounded resource policies and read-only diagnostics that redact secrets. Keep host/network administration external. This is a conditional v13 increment after supervision and shared passkey state, ahead of a new gateway; see the [delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html). PRs #224 and #225 admitted its first increments, the Caddy/Redis deployment contract and the offline `deploy:doctor`; the rest is not implemented merely by appearing here, and it adds no separate milestone or crate. ### Phase 2 — product integrity and scaffolding - Compile all generated projects in temp directories; enforce server-side Nexus policy; use real or explicitly unavailable Studio telemetry; and keep offline mocks deterministic without allowing live endpoints to fail open. ### Phase 3 — architecture and contract - Keep the new Core/ORM feature boundary regression-tested, consolidate the canonical security stack, standardize public API evolution, and split OAuth identity from future messaging adapters. The umbrella feature map is now complete and must remain covered by the isolated per-feature rows of `.github/check-feature-boundaries.sh`, which fails when a public umbrella feature has no row. - Implement ambitious providers only where a maintainer, conformance suite, and real interoperability environment exist. ### Phase 4 — release engineering - Require formatting, strict Clippy, full workspace tests, exclusive DB-feature checks, generated-project checks, fuzz tiers, unsafe review, package preflight, SBOM/advisory evidence, provenance, and topological publishing for the exact tag. ## Release strategy ### v13 execution priorities The [Verus verification pilot](https://rullst.github.io/Rullst/book/verus-roadmap.html) is planned supporting work for the privacy, authentication and payment contracts in M41, M9 and M11. Begin with a small production age-policy scope, then evaluate Auth and Capital; keep proofs beside their modules and start with a pinned manual workflow. Reproducibility, reviewed assumptions, negative controls, consumer compatibility and measured CI cost precede mandatory checks on affected v13 changes. This does not add a public crate or a v12.1 release requirement. **Execution order: verification efficiency, then the compatible 12.1.0 update experience, then concentrated v13 product development.** The published v12 packages remain immutable; important maintenance fixes remain independently reviewable. Carry the compatible updater and applicable fixes into v13 without merging unrelated breaking work into the v12 line. The phased efficiency plan is in [WORKFLOWS.md](https://github.com/Rullst/Rullst/blob/main/WORKFLOWS.md#verification-efficiency--v12-maintenance-and-v13). Application API changes still belong to the appropriate release line. The compatible **12.1.0** update experience is published. The [publication record](https://rullst.github.io/Rullst/book/v12.html#1210-published-maintenance-release) links its immutable source and successful release evidence. New capability work now belongs to v13; compatible security fixes remain eligible for v12 maintenance. [PR #218](https://github.com/Rullst/Rullst/pull/218) admitted the published 12.1.0 baseline and the SQLite/PostgreSQL replay foundation to protected `v13` at `7f48d882`. Subsequent source admissions are recorded individually in the [delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html). Keep later applicable stable fixes synchronized through reviewed changes; a development branch name alone does not prove that it includes them. The published **12.1.0** source at `b62390b4` and its documentation closeout were carried into that v13 integration: account mail, one-time Stripe contracts, machine endpoints, strict driver isolation, native CLI installation and project update/recovery join the earlier SaaS and Android corrections. The age-assurance package (first published in 13.0.0-alpha.1) and v13 Labs/privacy/Verus plans remain separate. Each later increment requires its own CI evidence; stable-release results do not certify the combined v13 source. The [delivery plan and evidence](https://rullst.github.io/Rullst/book/v13-delivery-plan.html) retains the bounded acceptance criteria and original calendar. The September 22 [priority revision](https://rullst.github.io/Rullst/book/v13-priorities.html) removes a mandatory publication date. Neither document turns planned capabilities into shipped claims. On September 21, the owner selected shared PostgreSQL consent and single-use email login links as the next two implementation priorities through September 23. The [additional-priorities section](https://rullst.github.io/Rullst/book/v13-delivery-plan.html#additional-priorities-approved-on-21-september) records their acceptance boundaries and five subsequent additions approved for the implementation queue. These entries do not claim implementation or expand the validation and publication dates. | Order | Outcome | Acceptance boundary | | :--- | :--- | :--- | | **P0 — verification efficiency** | Shorter local and hosted feedback, with measured cold/warm build and queue times | Compare test inventories; select affected crates and their consumers; preserve broad scheduled/release checks and a full-run fallback for unknown changes. Bind reusable evidence to source, dependencies, tools and policy. Prove that security, workflow, manifest and generator changes cannot silently skip required checks. | | **P0 — safe update experience** | Discover, prepare, verify and approve CLI/project updates through one guided entry point | The compatible opt-in 12.1.0 flow is published with declared native installation and isolated project acceptance. Carry it into v13 and add explicit major-version migration rules and consumer acceptance; publication of 12.1.0 does not establish 12→13 compatibility. The v13 CLI now reports v12 → v13 source findings (`rullst-upgrade-rules-v4`), each linked to its migration row. | | **P0 — SaaS maintenance** | Contain affected live operations and repair confirmed examples feedback | Follow the [15-finding triage plus Nexus configuration fix](https://rullst.github.io/Rullst/book/saas-v12-1-v13-triage.html). Compatible v12.1 fixes remain independently deliverable; new payment contracts need durable ownership/idempotency and provider acceptance evidence. | | **P0 — privacy and age assurance** | Reusable privacy defaults and age checks proportionate to risk across SaaS, LMS and examples | Complete the [M41 delivery plan](https://rullst.github.io/Rullst/book/privacy-age-assurance-roadmap.html) before additional learning/monitoring features. Reject production mocks and unverifiable results; minimize data, offer alternatives and review jurisdiction profiles. No automatic legal certification. | | **P1 — navigable API documentation** | Developers can find a capability, understand its contract and run a realistic example | Connect versioned Rust API references, task-based guides and tested REST examples. Document errors, feature flags, security boundaries and migration paths alongside each prioritized API; see the [documentation plan](https://rullst.github.io/Rullst/book/roadmap.html#api-documentation-quality). | | **P1 — Installable application delivery** | Predictable PWA installation, store packaging and guidance | Diagnose manifest, icon, HTTPS, service-worker and Digital Asset Links mistakes, distinguish a prepared package from an accepted store listing, and test install, update, navigation and interrupted networks. Device and store acceptance need their own evidence. | | **P1 — coherent application contracts** | One clear path for sessions, ownership, tenant context and typed client APIs | Consolidate existing Auth/Core/Security boundaries, complete selected session/passkey flows and validate API/SDK serialization. Preserve explicit configuration and negative authorization tests. | | **P2 — interactive learning products** | Server-authoritative progress, gamification and isolated programming exercises | Build on the LMS example (`examples/lms`, the former LMS starter), which replaced the complete Academy scaffold; version grading rules, persist idempotent results and prove tenant isolation. Use the `rullst-labs` contracts with an application-owned runner (the `rullst-labs-runner` candidate was removed from 13.0); untrusted execution stays outside the web process. | | **First conditional extension — transparent supervision** | Optional `rullst-supervision`, first published in the 13.0.0-alpha.1 prerelease, now implements bounded exam/learner and parental contracts and transparent observations; PRs #222/#226 and the repaired archive gate provide source evidence. Its generated LMS journey (`make:supervision`) was removed with the complete Academy scaffold | Deliver a generated LMS journey with independently authorized guardians/reviewers, visible sessions, explicit permissions, revocation, bounded events/retention and application-side restrictions. Automated client observations do not prove misconduct or justify automatic penalties. Device-wide controls and camera inference require separate platform contracts; see the [delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html) and [SST boundary](https://rullst.github.io/Rullst/book/spec.html#conditional-v13-supervision-crate). | | **P3 — selected integrations** | One complete real-provider or broker journey at a time | Add an adapter only with a concrete product need, protocol/failure tests, documented limits and an available acceptance environment. The self-hosted gateway (M39) is abandoned; proxy support means generated configuration for established proxies. | This is an execution order, not a promise that the entire historical backlog fits one month. Prefer completed user journeys and measured acceptance criteria over increasing the number of crates. The [workflow roadmap](https://github.com/Rullst/Rullst/blob/main/WORKFLOWS.md#preserved-next-generation-roadmap) retains the assurance experiments; move a proposal into the implementation column only after its code and evidence exist. ### Safe update experience **Status: published in 12.1.0 for the documented compatible-update scope.** The goal is the easiest practical update journey without hiding risk: one guided entry point, a clear plan, minimal repeated input, useful progress, verification and recoverable application of the approved changes. Ease and speed are acceptance criteria, not reasons to skip compatibility or security checks. The existing `cargo rullst upgrade` already provides workspace-aware plans, versioned migration rules, controlled file snapshots, compiler fixes and Cargo checks. It does **not** install the CLI or run the application's full acceptance suite. Extend this boundary in `cargo-rullst`; do not introduce another crate or count this proposal as completed work in the capability ledger. Implementation and acceptance order: **13.0.0-alpha.4 removes the CLI self-updater** (`update check`, `update verify`, `update stage`, `update install` and the CLI half of `update guided`): the CLI is installed and updated with `cargo install cargo-rullst --version =X --locked`, and a once-a-day notice prints that command. The project preparation commands below stay. The paragraphs that follow record the 12.1.0 to 13.0.0-alpha.3 history. Published 12.1.0 discovery uses interactive-only, offline/CI-aware notices and bounded HTTPS metadata. It rejects redirects, yanked versions, prereleases and unsolicited major jumps. The legacy shared temporary cache is removed; the current result is deliberately process-local. Explicit `cargo rullst update check` now provides exact-target, MSRV/platform and versioned JSON discovery with separate major/prerelease opt-ins; it grants no installation or execution authority. Explicit discovery now reuses bounded, owner/permission-checked Unix metadata for six hours and supports offline reads, forced refresh and cache opt-out. Windows persistence has a private owner/DACL implementation with native acceptance recorded in the [maintenance checkpoint](https://rullst.github.io/Rullst/book/v12.html#1210-delivery-checkpoint-unreleased). The [12.1.0 publication record](https://rullst.github.io/Rullst/book/v12.html#1210-published-maintenance-release) records final acceptance. Earlier v12.0.0 provenance exercises remain historical evidence for their own artifacts. Published preparation builds native CLI candidates on four explicit targets and binds their version, source, platform, sizes and digests in a bounded inventory. The admitted tag workflow separately attests the files and adds release assets; ordinary CI inventories have no release tag. The admitted 12.1.0 release completed this native and publication evidence. Explicit `update stage` rechecks a fresh registry selection, authenticates the manifest before executable downloads and bounds HTTPS redirects, sizes, time and hashes in private storage. It executes and installs nothing. Managed installation now revalidates eligibility, original manifest provenance, private root ownership and exact bytes under a destination-local lock. Explicit digest-approved application stages bounded version probes and backups before replacing entries; explicit recovery restores only the authenticated predecessor and rejects unrelated edits. Native installation/lock/interruption/ executing-image tests passed on Linux, Windows and macOS at `ef0a8577` in [run 35296176849](https://github.com/Rullst/Rullst/actions/runs/35296176849). The explicit local `update verify` command now authenticates a private manifest snapshot with the caller-installed GitHub CLI and checks both native binary digests. It grants no installation authority or registry eligibility and does not download, execute or install candidates. Its declared platform/release acceptance is recorded with the published 12.1.0 flow below. Working-source `update project prepare` now retains Git working contents and the root lockfile in private before/candidate copies, reuses exact dependency editing and the versioned source catalog, and rejects ambiguous inputs and downgrades. Local process fixtures cover dirty/untracked/deleted source, virtual workspaces, ignored or absent lockfiles, bounded findings and unsafe files without compiling application code. Native preparation tests passed on Windows and macOS at `88ddabad`. Preparation grants no execution or application authority. Working-source verification now revalidates records/source under an operation lock, previews commands, and requires explicit trusted-code consent before resolving/checking/testing another private copy. Local process tests cover real acceptance, stale inputs, missing consent, contention, timeout, failed tests and unexpected source writes. Verification passed Linux/macOS and the corrected Windows path contract at the recorded maintenance checkpoints; final release acceptance followed with the published 12.1.0 release. Explicit `update project review` now revalidates command logs and both source inventories and emits the bounded full dependency diff plus a review digest. It performs no builds or original-file edits; the digest does not grant application authority. Explicit digest-approved `project apply`/`recover` now revalidate evidence/access policies, stage reviewed manifest/lockfile replacements and persist an intent. They refuse divergent edits and preserve unrelated files. Source locks coordinate only the same configured cache. Native project acceptance passed on all three systems at `8b58cd12`; corrected Windows SDDL/Git path handling is included. Local child-kill, OS file-size-limit and injected replacement/recovery faults add bounded interruption proof, not power-loss or a complete disk-full matrix. Working-source `update guided --to VERSION` composes CLI/project/both flows through the same validators. It retains exact paths and separate review digests, shows complete reviews, defaults each approval to no and reports elapsed time per stage. Project code/network and original-file writes need separate consent. Noninteractive approval and unsupported project major jumps reject. A real terminal regression covers declined preparation/execution, failed application tests and approved application followed by exact recovery. The published 12.1.0 release record holds the final release evidence for this composition. 1. **Discover and explain.** Make update notices useful without blocking normal CLI startup. Respect offline/CI settings and explicit notification opt-out; use bounded responses, timeouts and a private, path-safe cache. Default to supported stable releases within the selected major, with exact target pins; prereleases and major migrations require explicit selection. Show release notes, MSRV/platform requirements and unsupported migration paths before proposing changes. Metadata or a notification never grants installation authority. 2. **Update the CLI safely.** Offer verified prebuilt binaries for supported OS/architecture pairs, with a pinned source-install fallback when appropriate. Bind artifact identity/version/platform/digest to a trusted publisher identity through signatures or verified provenance; a checksum from the same untrusted download is insufficient. Respect package-manager ownership and permissions; stage replacements with concurrency locks, interrupted-download recovery and Windows executable-lock handling. Test rejected tampering, unexpected redirects/archives, stale metadata and unauthorized downgrades. Keep an explicit, verified known-good recovery path, not a silent downgrade. 3. **Prepare and verify the project.** Reuse the versioned migration catalog in an isolated working copy, preserving uncommitted user work. Show dependency, lockfile and source diffs and the validation commands before execution; Cargo build scripts, procedural macros and tests execute code, so preparation is not a sandbox or permission to run an untrusted project. Resolve the chosen target reproducibly and validate the candidate lockfile, supported feature sets and application-owned tests before accepting changes. Unknown or breaking migrations stop with actionable instructions instead of guessed rewrites. 4. **Apply with consent and recover.** Make the interactive happy path concise; expose structured reports and explicit non-interactive policy for automation. Verify that reviewed inputs have not changed before applying. Preserve bounded backups and prove cancellation, concurrent edits, disk-full recovery and restoration on Linux, Windows and macOS. State exactly which files are restored: file rollback does not undo arbitrary test side effects, database changes or external services. Opening an application or running a build must never silently replace its framework, migrate its database or deploy it. 5. **Prove usability and speed.** Exercise published-package/generated-project fixtures, pinned versions, offline operation, unsupported targets and failed migrations. Measure cold/warm discovery, download, compilation and validation separately. Reuse only correctly keyed caches and applicable test evidence. A prebuilt CLI can avoid CLI compilation; updating a Rust application can still require rebuilding, testing and a separate deployment. Do not promise instant upgrades, zero downtime or automatic production readiness. The 12.1.0 delivery must preserve v12's public APIs, CLI/configuration behavior and opt-in boundaries. It prepares discovery and installation of a compatible migration CLI, not guesses about a future major's source changes. Actual v12-to-v13 automation requires v13's published migration catalog and tested application fixtures; the same-major restriction of the v12 `upgrade` command must not be silently removed. Reserve incompatible changes for v13. On `main`, the v13 CLI accepts Rullst 12 and 13 projects: `cargo rullst upgrade` reports v12 → v13 source findings with their [migration-guide](https://rullst.github.io/Rullst/book/migration-v13.html) rows. These support a review; they do not migrate an application automatically. Continue new capability work on v13 while actively maintaining the supported v12 line with reviewed compatible fixes and dependency updates. The website redesign is a separate documentation delivery, not a reason to bump framework versions or postpone verification work. This plan neither bumps package versions nor authorizes publication, and it does not claim that any major application migration is already automatic. ### API documentation quality **Status: planned for v13; improve documentation alongside each implemented contract, not only at release time.** Distinguish the Rust framework API (types, traits, functions and features) from guides for building HTTP/REST APIs. The existing REST quickstart intentionally covers one JSON endpoint; routing, authentication and Scalar/OpenAPI guidance live in separate chapters. More pages alone will not make those paths easier to discover or complete. The [v12 navigation index](https://github.com/Rullst/Rullst/blob/main/docs/src/api-reference.md) now connects existing guides to exact-version crate references and identifies the remaining REST walkthrough gaps. This first navigation improvement is not completion of the reference and behavioral-example programme below. Use [Qt's reference navigation](https://doc.qt.io/qt-6/reference-overview.html) and [a concrete class reference](https://doc.qt.io/qt-6/qnetworkaccessmanager.html) as organizational inspiration, not as a reason to adopt Qt or copy its text. Adapt the pattern to Rust with a searchable crate/module/task index linking the book and version-pinned rustdoc pages. For prioritized public interfaces, document purpose, imports/Cargo features, arguments, results and typed errors, security/ownership/concurrency constraints, runnable examples, related APIs, version availability and migration notes. Start with the umbrella facade, Core/routing, Auth/Security and ORM; extend coverage with each v13 increment. Provide a coherent REST learning path covering typed input validation, error responses, CRUD and pagination, authentication, owner/tenant authorization, OpenAPI, tests and deployment boundaries. Validate examples against the documented release and feature set, including rejected input and access denial. Keep the existing book-doctest integration and add behavioral fixtures where compilation alone cannot prove the documented result. Check rendered book links as well as repository-local links. Record which API surfaces were reviewed; do not infer complete reference coverage from a green book build or label unimplemented v13 contracts as available in v12. ### Ideas recorded for after 13.0 Recorded on 8 October 2026. These are ideas, not commitments or shipped features, and they add no milestone to the counts above. - **Offline IoT/health example application.** Sensor readings flow to a Rullst server, are stored in SQLite and appear on a dashboard, with no internet connection required. It would build on `SensorTelemetry`, `AnomalyDetector` and the snapshot card from the experimental `rullst-iot`, with a maintained transport crate (for example `rumqttc`) rather than a new Rullst transport. A health-oriented variant must treat readings as sensitive personal data and must not present itself as a medical device. - **Multiplayer quiz example.** Core realtime and presence (`BroadcastManager`, `PresenceTracker`) for live rounds, with `rullst-labs` grading contracts for server-authoritative, idempotent scoring. Answers are graded on the server, never trusted from the client. - **Reverse-proxy configuration instead of a gateway.** The self-hosted `rullst-gateway` (M39) is abandoned. Deployment work goes into generated, reviewable configuration for established proxies: extend the existing Caddy output and add nginx, with the readiness/drain and trusted-proxy contracts that `rullst-core` already exposes. ### Delivered in 13.0.0-alpha.3 Planned on 15 and 16 October 2026 with the maintainer and prepared on 23 October 2026 as the 13.0.0-alpha.3 release candidate; the coordinator records publication in the table below. Each item states what the release contains and what stays open. They add no milestone to the counts above. - `[x]` **First-class PWA, and retiring Omni** (done in the 13.0.0-alpha.3 source; see the [PWA guide](https://rullst.github.io/Rullst/book/tutorials/43-progressive-web-apps.html)). Decided on 16 October 2026 with the maintainer. The `make:omni` Tauri shells (desktop, Android, iOS) only open the hosted Rullst application in a native webview and deliberately expose no native API to it, so they give the same result as packaging a Progressive Web App, while costing three mandatory release workflows (Android NDK, iOS simulator, desktop on three systems), Tauri upgrades and per-platform signing. alpha.3 replaces them: 1. `cargo rullst make:pwa`: web app manifest, icons, a service worker with an offline page and an update prompt, compatible with Core's CSP nonces and `Cache-Control` defaults; browser-side offline data (IndexedDB) where an application needs it. 2. Google Play through a Trusted Web Activity: a Bubblewrap configuration (`twa-manifest.json`) and a `/.well-known/assetlinks.json` route fed by the application's signing-key fingerprint. Bubblewrap is the scriptable tool PWABuilder itself uses for Android. 3. Microsoft Store: a guide to packaging the same PWA as MSIX with PWABuilder, which Microsoft supports for its store. The Apple App Store usually rejects apps that only wrap a website (guideline 4.2), so iOS users install the PWA from Safari. 4. Remove `make:omni`, `cargo rullst omni`, the three Omni release workflows and the native-only `offline-sync` feature, whose only consumer is the Omni shell, before 13.0.0 (the last release where removing them does not wait for v14). All four are implemented: `make:pwa` (with `--android` for the Bubblewrap configuration), `rullst::pwa::AssetLinks`, the Microsoft Store and iOS sections of the guide, and the removal, with `cargo rullst upgrade` findings for an Omni shell and the `offline-sync` feature. They ship with 13.0.0-alpha.3; store acceptance and physical devices stay application work. - `[x]` **Studio as a production monitor for several projects** (phases 1 to 3 done in the 13.0.0-alpha.3 source; see the [production monitoring guide](https://rullst.github.io/Rullst/book/production-monitoring.html)). Before alpha.3 Studio is a development tool: it runs inside the application process in debug builds, listens only on `127.0.0.1:5555` and reads that process's own telemetry, queues, cache and local database. The only production-facing piece is the signed, push-only trace ingestion of M35. The plan keeps every development page and adds a remote mode in three phases: 1. A read-only production monitoring endpoint in the application, behind the application's own login policy (the policy the Nexus package for alpha.2 adds) or a dedicated token. It reports request rate, latency, errors, queue depth and failures, and N+1 findings, never database rows, `.env` values or configuration secrets. 2. A standalone Studio "remote projects" mode on the developer's machine: a project list with a switcher, per-project credentials encrypted locally, and the monitoring pages (radar, traces, jobs, security radar) reading the selected project's endpoint. Studio stays bound to loopback; it only connects outwards. Reaching private networks is left to established tools (WireGuard, Tailscale, an SSH tunnel), which the guide documents instead of Studio implementing VPN access. 3. History and alerts. All three phases are implemented for 13.0.0-alpha.3: the `/_rullst/monitor/v1` endpoint (`Server::with_monitor`), the standalone `rullst-studio` binary with overview, routes, queues and N+1 pages, and its bounded local history with per-project alerts and an optional webhook. Traces and the security radar are not part of the phase 1 document, so remote mode does not show them yet; desktop notifications are not implemented (alerts show in Studio and can go to a webhook). The data browser, `.env` viewer, AI and API playgrounds and migration pages stay local-only, because in production they would expose secrets and personal data or run arbitrary operations. The work needs a threat-model update before phase 1 ships. Estimated at three to four focused packages plus mutation tests. - `[x]` **The rest of the mutation campaign** (all but `cargo-rullst` done in the 13.0.0-alpha.3 source; `cargo-rullst` moves to the next section). As the [verification order](https://rullst.github.io/Rullst/book/v13-delivery-plan.html) requires: - `rullst-orm` and `rullst-orm-macros`: re-measured in full for alpha.3 on hosted runs, with all-feature targeted runs and the `orm-sqlite`, `orm-polyglot`, `orm-sql-servers`, `orm-strict-postgres`, `orm-strict-mysql` and `orm-polyglot-servers` contracts of `mutants.yml`, which run the SQLite, optional-backend and database-server suites that the all-feature build compiles out. `rullst-orm` has 2,414 mutants: 1,967 caught, 421 unviable, 17 timeouts and 9 missed, of which 8 are accepted equivalents. The ninth is the MySQL `FOR UPDATE SKIP LOCKED` claim query of `Outbox::claim_next_at_inner`, which only concurrent claims tell apart from the portable query. `rullst-orm-macros` has 542 mutants: 514 caught, 26 unviable and 2 accepted equivalents. A full all-feature campaign still reports the mutants that only those contracts reach. - `rullst-core`: the files the first pass left ([PR #465](https://github.com/Rullst/Rullst/pull/465)) were finished in [PR #532](https://github.com/Rullst/Rullst/pull/532): 78 files and 2,435 mutants, 72 missed at the end (71 documented equivalents; the remaining one needs an open PostgreSQL pool). - `rullst-nexus`: measured for the first time in PR #532 (26 files, 876 mutants); 170 of 171 first misses were killed by new tests, one is a documented equivalent, and one real defect was fixed. - `cargo-rullst` is not measured yet; a targeted campaign was stopped before completion and is not part of alpha.3. - The 29 `rullst-capital` mutants that only a live Stripe response reaches stay with provider sandbox testing. - `[x]` **Feedback from the platforms built on alpha.2.** The reports from the platforms built on 13.0.0-alpha.2 chose the fixes in this release (see the changelog), as the alpha.1 report did for alpha.2. ### Delivered in 13.0.0-alpha.4 Planned with the maintainer on 29 October 2026 and prepared the same day as the 13.0.0-alpha.4 release candidate; the coordinator records publication in the table below. Goal: a lean CLI for people and a predictable framework for AI assistants. The framework crates' APIs do not change. They add no milestone to the counts above. - `[x]` **Lean main CLI** ([PR #555](https://github.com/Rullst/Rullst/pull/555)). Foundry, `nixify`/`new --nix`, `generate:buildah`/`new --buildah`, `make:grpc`, the Fly.io, Render and Railway deploy targets and the self-updater's download and binary replacement were removed; a once-a-day notice prints the `cargo install` command for a newer CLI. Each removal has a [migration row](https://rullst.github.io/Rullst/book/migration-v13.html) and, where an application can be detected using it, an upgrade review rule. - `[x]` **`green` in the main CLI, the terminal assistant removed**: `cargo rullst green` (Experimental; measured size, memory, dependency and CPU figures, carbon only as a labelled SCI estimate) replaces `footprint` in `cargo-rullst`. `cargo rullst ai` was removed from 13.0 (its code stays in git history) and now prints what replaces it; the `rullst-ai` crate is unchanged. A separate `cargo-rullst-extras` package was prepared in PR #555 and dropped before publication, so the release still has twenty packages. - `[x]` **Deployment guides** (PR #555): one `deploy` path (Dockerfile, `compose.yaml` and `Caddyfile` for Docker or Podman), the [Linux VM](https://rullst.github.io/Rullst/book/vm-deployment.html), Hetzner Cloud, DigitalOcean, Oracle Cloud and Google Cloud guides and [Advanced deployment](https://rullst.github.io/Rullst/book/advanced-deployment.html) (Nix, Buildah, Podman Quadlet, Kubernetes; described, not generated). The guides were written from the providers' documentation and not followed on real accounts, and no automated test starts the generated stack. - `[x]` **Three starters, the others as examples** ([PR #558](https://github.com/Rullst/Rullst/pull/558)): `cargo rullst new` offers Blank, API and SaaS; LMS, ERP and Portfolio are workspace applications under [`examples/`](https://rullst.github.io/Rullst/book/examples.html) with their own tests. - `[x]` **For AI assistants** (PR #555): one `--json` contract with a `schema` id (new for `inspect models`, `inspect schema` and `pkg list`), and `llms.txt` with an optional section, checked by `validate-site.py`. - `[x]` **Shorter development builds** ([PR #559](https://github.com/Rullst/Rullst/pull/559)): generated projects get a `[profile.dev]` (line tables for the application, no debug information for dependencies), and the SaaS starter no longer declares `rullst-connect`. On the measuring machine cold builds took 15 to 19 % less time, rebuilds after an edit about half and `target/` about 41 % less disk ([Build times](https://rullst.github.io/Rullst/book/build-times.html)). The framework-level items are planned below. - `[x]` **Targeted `cargo-rullst` mutation campaign** ([PR #557](https://github.com/Rullst/Rullst/pull/557)): hosted targeted runs with the CLI's library tests measured 83 CLI files and 3,189 mutants of the self-update, audit and IDOR scanner, `add` and consumer-file editors, `doctor` and deployment configuration and `upgrade` code. New tests killed 1,041 of the 1,129 first-run survivors and 51 are documented equivalents; the 76 files left without a survivor were listed in `.github/mutants-pr-cli.txt`, so `mutants-pr.yml` mutates their changed lines on every pull request. Left: size and count bounds that only very large projects reach (hundreds of mebibytes, or 10,000 to 250,000 files): upgrade snapshot 6, upgrade source walk 3, project snapshot 13, replacement and verification totals 2 and 2, the 64 MiB preparation report 4. The Studio remote mode (20 files) was measured in the same campaign and has no survivor left. The self-updater's binary replacement was removed in 13.0.0-alpha.4, with its 7 remaining installation mutants; its 13 measured files left the gate list with it, and `generators/deploy.rs` and `update/guided.rs`, rewritten in that release, left it until they are measured again. ### Planned after 13.0.0-alpha.4 Work items for later prereleases, not shipped features. They add no milestone to the counts above. - **13.0.0-rc.1: fixes and documentation only.** 13.0.0-alpha.4 is the 13.0 feature freeze. rc.1 freezes the public APIs that 13.0 supports and states which packages and features its stability promise covers, starting from the [maturity tiers](https://rullst.github.io/Rullst/book/maturity.html). After rc.1, changes are limited to defects found in the release candidate. - **Framework build time.** Measured on the generated starters ([Build times](https://rullst.github.io/Rullst/book/build-times.html)). Rullst keeps `aws-lc-rs` as the TLS crypto provider (rustls' post-quantum hybrid key exchange), so the `aws-lc-sys` build script (about 52 s, reached through Studio → `rullst-capital` → `reqwest`/`rustls`) stays in a project's first build; later builds reuse it, and the generated Dockerfile's dependency layer (the cargo-chef approach) caches it in images. Moving to `ring` is not planned. Planned for 13.0.0-rc.1 if they need no breaking change, otherwise for 13.1: SQLx's compile-time macros compiling some crates, SQLite among them, a second time, and `rullst-nexus` pulling `rullst-ai` into the SaaS starter. - **Re-measure `generators/deploy.rs` and `update/guided.rs`** with targeted `mutants.yml` runs and return them to `.github/mutants-pr-cli.txt` once only accepted equivalents are left. - **The first hour and the documentation:** follow the path from installation to a deployed starter as a new user, and fix the documentation and CLI messages that get in the way. - **Turso/libSQL beyond the blank and API starters**, if users ask for it. Turso can be the primary database only in those starters and does not work with `#[derive(Orm)]`, `Schema`, `artisan!`, `Server::with_db`, Nexus or Studio. A partial bridge (`Schema` emitting Turso migrations, `#[derive(Orm)]` also generating the Turso model) is a candidate for a 13.x release. Full parity, which would let the ORM core run on a backend other than SQLx, is deferred to a later major release because it would put the SQLx drivers at risk. - **Feedback from the platforms upgraded to 13.0.0-alpha.3 and alpha.4.** Their reports choose the fixes and priorities of the next prerelease, as the alpha.1 and alpha.2 reports did before. - **Then 13.0.0**, once rc.1 leaves no open defect in the stability scope. ### Published and planned release lines | Version | Status | Honest scope | | :--- | :---: | :--- | | **v12.0.0** | `[x] Published stable` | Tag `v12.0.0` at `eb11f892` completed the protected release workflow and published all sixteen packages on September 15, 2026. | | **v12.x** | `[x] End of life` | Ended on 16 October 2026 with 12.3.0 as the last release; the `v12` branch is frozen. New 12.x projects no longer resolve dependencies after crates.io yanked the `zip` releases its Studio needs; use v13. | | **v12.1.0** | `[x] Published compatible minor` | All sixteen packages were published from `b62390b4` on 20 September 2026 UTC. Guided CLI/project updates, SaaS/Nexus fixes, account mail and the other bounded contracts are recorded with immutable source and registry evidence in the [publication record](https://rullst.github.io/Rullst/book/v12.html#1210-published-maintenance-release). | | **v12.1.1** | `[x] Published patch` | All sixteen packages were published from `d27db26c` on 24 September 2026 UTC; see the [publication record](https://rullst.github.io/Rullst/book/v12.html#1211-published-maintenance-release). | | **v12.1.2** | `[x] Published patch` | Tagged `v12.1.2` on 28 September 2026 UTC from the `v12` maintenance line; its publication evidence is kept in the [maintained v12 release record](https://github.com/Rullst/Rullst/blob/v12/docs/src/v12.md). | | **v12.2.0** | `[x] Published compatible minor` | Tagged `v12.2.0` at `565e222a` on 3 October 2026 from the `v12` maintenance line; see the [maintained v12 release record](https://github.com/Rullst/Rullst/blob/v12/docs/src/v12.md). | | **v12.3.0** | `[x] Latest published stable` | Tagged `v12.3.0` at `67b2d0e5` and published on 9 October 2026: hickory 0.26.3, fewer WAF/RASP false positives and deprecations of the Capital and Mail APIs that 13.0 removes. It is the last 12.x release: the `v12` line reached end of life on 16 October 2026 and no further patch will be published. | | **v13.0.0-alpha.1** | `[x] Published prerelease` | Tag `v13.0.0-alpha.1` at `fc44bf76`; all twenty packages were published on 11 October 2026 for evaluation. See the [delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html). | | **v13.0.0-alpha.2** | `[x] Published prerelease` | All twenty packages were published on 17 October 2026 from tag `v13.0.0-alpha.2` at `0daf4941` ([release run 39041726912](https://github.com/Rullst/Rullst/actions/runs/39041726912)): the fixes from the alpha.1 dogfooding report and the breaking `html!` → `Markup`, `Schema` date/time and Nexus changes. A first tag at `669befe6` failed release verification before publishing anything and was replaced. | | **v13.0.0-alpha.3** | `[x] Published prerelease` | All twenty packages were published on 24 October 2026 from tag `v13.0.0-alpha.3` at `be336833` ([release run 40129821475](https://github.com/Rullst/Rullst/actions/runs/40129821475)) after all 25 required workflows passed on that commit. See [Delivered in 13.0.0-alpha.3](https://rullst.github.io/Rullst/book/roadmap.html#delivered-in-1300-alpha3). | | **v13.0.0-alpha.4** | `[ ] Release candidate` | Prepared on 29 October 2026 and not yet published; the coordinator records the tag, release run and publication here. Twenty packages. See [Delivered in 13.0.0-alpha.4](https://rullst.github.io/Rullst/book/roadmap.html#delivered-in-1300-alpha4). | | **v13.x** | `[ ] Next feature line` | Compatible and breaking improvements move together into the next deliberate cycle: generated-project coverage, auth/session consolidation, typed SDKs, selected adapters, security-stack consolidation and research-heavy architecture all require fresh acceptance boundaries. | The framework may call a milestone implemented only when the same commit passes the repository's formatting, strict lint, full-test, feature-matrix, security, and packaging gates. Performance numbers must cite a reproducible benchmark; security and compliance claims must state their threat model and evidence scope. --- <div align="center"> <p><i>"All glory and honor to God יהוה in the name of Yeshua the Messiah (Jesus Christ)."</i></p> </div> --- # Blueprints Roadmap Source: https://rullst.github.io/Rullst/book/blueprints_roadmap.html # Rullst Blueprints Roadmap 🗺️ ### *"A practical collection of reviewed Rullst application starters"* This document maps the expansion plan for the Rullst **Starter Blueprints** ecosystem. The goal is to provide reviewable application starting points with explicit production checklists and capability boundaries. --- ## 🚀 Blueprints Design Philosophy Every blueprint added to the CLI must meet three fundamental principles: 1. **Clear first experience:** Responsive, accessible interfaces whose browser assets and CSP are explicit. 2. **Native Rust/Rullst Features:** Demonstrate measured resource use, typed concurrency and explicit server/realtime boundaries. 3. **Production-minded defaults:** Generate `.env.example`, database configuration, and a conservative `.gitignore`; deployment readiness remains an application-level review. The v13 [privacy and age-assurance programme](https://rullst.github.io/Rullst/book/privacy-age-assurance-roadmap.html) applies across blueprint shapes: minimize collection, disable optional tracking, generate explicit policies and rights workflows, and require age evidence only when the selected risk policy calls for it. Camera checks are not a universal default. The [SaaS maintenance triage](https://rullst.github.io/Rullst/book/saas-v12-1-v13-triage.html) separately covers the v12.1/v13 payment and generator defects reported by `Rullst/examples`. --- ## 🗺️ Proposed blueprints (ordered from easiest to hardest) These rows describe design targets and are not selectable CLI starters; ERP exists as the `examples/erp` application. A proposal becomes implemented only when its generated project and negative boundaries pass the release gates. Since 13.0.0-alpha.4 `cargo rullst new --blueprint` offers `blank`, `api` and `saas`; the former LMS, Blog, Portfolio and ERP starters are complete applications under the repository's `examples/` (see [examples](https://rullst.github.io/Rullst/book/examples.html)), and a new domain is more likely to arrive as an example than as a starter. | ID | Blueprint Name | Technical Focus in Rullst | Commercial Differentiator | |:---|:---|:---|:---| | **4** | 💼 ERP Pocket (Inventory) | Embedded SQLite + `rullst::nexus` (Auto-CMS) + Single Binary | The `examples/erp` application (a starter until 13.0.0-alpha.4); crash recovery and backup remain application work. | | **5** | 📋 Member/Club Management | Validation + Nexus + reviewed receipt adapter | Proposed member and billing domain starter. | | **7** | 🤖 AI Agent & RAG Boilerplate | `rullst-ai` + opt-in document parsing/embedding adapters | Proposed RAG starter; uploaded content remains untrusted. | | **8** | 🪙 AI Credit-Based SaaS | SSE + transactional usage ledger + payment adapter | Proposed AI SaaS starter with server-owned credit reservation. | | **9** | 🏥 Scheduling & Clinics | HTMX calendar + scheduler + database conflict policy | Proposed scheduling starter with database-specific contention tests. | | **10**| 🚪 Biometric Access Control | WebSocket foundation + real-time concierge panel | Planned access-control starter; device trust and latency need deployment-specific validation. | | **11**| 📈 Affiliate Checkout | SSR + commission-split domain model + landing page | Planned sales starter; performance and Lighthouse scores must be measured per application. | | **12**| 🏢 B2B Multi-Tenant Platform | Tenant context + RBAC + `rullst-mail` | Planned B2B starter; isolation must be proven across every storage and messaging boundary. | | **13**| 💬 Discord-Like Realtime Chat | Server-driven UI + authenticated WebSockets | Proposed chat starter; distributed presence and load evidence are required. | | **14**| 🛵 Delivery / Food App | Background queue + explicit order state machine | Proposed delivery starter with idempotent jobs and notification adapters. | --- ## 🔍 Highlighted Architectural Details ### 🪙 8. AI Credit-Based SaaS (The Token-Burner) * **Architecture goal:** a server-owned chat flow with bounded SSE streaming, cancellation and provider error handling. * **Data Security goal:** use a database transaction and provider-specific lock semantics to reserve credit before an LLM request. This workflow is not yet a generated, cross-database guarantee. * **Monetization goal:** integrate a reviewed usage ledger with a supported payment adapter; billing portals remain provider/application work. ### 🏢 12. B2B Multi-Tenant Platform (The Corporate Boilerplate) * **Isolation goal:** derive a validated `TenantContext` at the HTTP boundary and carry it explicitly through database, cache, queue and realtime operations. Rullst does not inject a tenant predicate into every arbitrary SQL statement. * **Permissions goal:** typed roles (`Admin`, `Member`, `Billing`) enforced in middleware and again at sensitive service boundaries. * **Invitations goal:** hashed, single-use, expiring invitation tokens delivered through a configured mail adapter. ### 💬 13. Discord-Like Realtime Chat * **Client goal:** use server-rendered messages and an explicit WebSocket client without making bundle size a proxy for correctness. * **Scale gate:** publish connection count, message mix, backpressure behavior, CPU/RSS, hardware and distributed topology before attaching capacity numbers. ### 🏥 9. Scheduling & Clinics (The Scheduler) * **Conflict-prevention goal:** enforce a database constraint plus a transaction strategy tested on every declared backend; isolation level alone is not a universal double-booking proof. * **Reminder goal:** use bounded, idempotent scheduler jobs and a configured mail adapter. Multi-instance leadership and durable retry must be explicit. ### 🤖 7. AI Agent & RAG Boilerplate (AI-Native) * **Structure goal:** quarantine and scan bounded uploads, parse supported formats in an isolated adapter, generate embeddings and store an authorized index. * **Provider goal:** expose an explicit provider selection and capability check. Changing an environment variable does not make data policy, schema or model behavior interchangeable. --- # Dependency Helper & Package Roadmap Source: https://rullst.github.io/Rullst/book/packages-spec.html # Community dependency helper and package roadmap Rullst (v12 and the v13 prerelease line) does **not** define a runtime `RullstPackage` plugin ABI, execute third-party generators, discover a private package registry, or register routes automatically. Community integrations are ordinary Rust crates selected through Cargo and reviewed like any other dependency. ## Current `cargo rullst pkg` boundary The CLI provides two small local manifest helpers: ```bash cargo rullst pkg add rullst-auth cargo rullst pkg list cargo check ``` `pkg add`: - accepts only ASCII Cargo names of at most 64 bytes beginning with `rullst-` or `rullst_` and ending in an alphanumeric character; - parses `Cargo.toml` as TOML and inserts the dependency into the real `[dependencies]` table using the installed CLI's version; in a virtual workspace manifest (`[workspace]` without `[package]`), which cannot declare `[dependencies]`, it uses `[workspace.dependencies]` instead, and members opt in with `rullst-auth = { workspace = true }`; - leaves an existing dependency and its features/version unchanged; - does not contact a registry, execute code, edit routes, or run a scaffold. The subsequent `cargo check` performs normal Cargo resolution and compilation. A prefix is only a naming filter; it is not proof that a crate is official, safe, compatible, maintained, or endorsed by Rullst. Review the crate source, publisher, checksum, license, advisories, feature graph and release policy before adding it. `pkg list` reads dependency keys from that same table and prints those with the same Rullst prefix. It is not a vulnerability, license or provenance scan; use the repository's audit and dependency-policy tooling for those questions. ## Integrating a community crate today A community crate can expose ordinary Axum routers, Tower layers, SQLx types or constructors. The host application initializes it explicitly and mounts only the capabilities it intends to trust: ```rust use axum::Router; # fn community_router() -> Router { Router::new() } fn application_router() -> Router { Router::new().nest("/community", community_router()) } ``` Authentication, authorization, tenant policy, secrets, migrations, shutdown, telemetry and failure handling remain visible application responsibilities. ## Future package protocol A first-class extension protocol remains roadmap work; the v13 source does not include one. It should not be declared stable until the repository has all of the following: - a versioned manifest and compatibility contract; - explicit capability permissions for routes, storage, network and secrets; - deterministic lifecycle and failure semantics without runtime reflection; - package ownership, provenance and revocation policy; - compile/runtime conformance tests and safe uninstall/upgrade behavior; - a decision on whether untrusted extensions require a real Wasm sandbox with enforceable CPU, memory and I/O limits. Until that contract exists, documentation and packages must not claim that `RullstPackage.toml`, `RullstPackage`, automatic route registration, or third-party generator execution are implemented APIs. See the [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html#connect-real-time-queues-storage-and-data) and the [roadmap](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md) for the preserved vision. --- # v13 Delivery Plan & Evidence Source: https://rullst.github.io/Rullst/book/v13-delivery-plan.html # v13 delivery plan and evidence **13.0.0-alpha.4 prepared on October 29, 2026 (not yet published).** The release commit sets the twenty packages of the release order to `13.0.0-alpha.4`; the coordinator records the tag, the exact-SHA admission and the release run here after publication. It makes the main CLI leaner: `green` (formerly `footprint`) stays in it as an Experimental command and the terminal AI assistant (`cargo rullst ai`) is removed; it removes Foundry, Nix, Buildah, gRPC, the Fly.io, Render and Railway deploy targets and the self-updater's binary replacement; gives `deploy` one path for any Linux VM with guides for Hetzner Cloud, DigitalOcean, Oracle Cloud and Google Cloud; keeps three starters and moves LMS, ERP and Portfolio to the repository's examples; adds one `--json` contract and an optional section to `llms.txt`; and shortens the development builds of generated projects. The framework crates' APIs do not change. The first targeted `cargo-rullst` mutation campaign is merged, and the CLI's measured files are in the pull-request mutation gate. 13.0.0-alpha.4 is the 13.0 feature freeze: next come 13.0.0-rc.1 (fixes and documentation only) and then 13.0.0. Rullst keeps `aws-lc-rs` as the TLS crypto provider (rustls' post-quantum hybrid key exchange), so the `aws-lc-sys` build script stays in a project's first build; moving to `ring` is not planned. SQLx's macros compiling some crates twice and Nexus pulling `rullst-ai` into the SaaS starter are planned for 13.0.0-rc.1 if they need no breaking change, otherwise for 13.1 ([Build times](https://rullst.github.io/Rullst/book/build-times.html)). Work planned next is under [Planned after 13.0.0-alpha.4](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#planned-after-1300-alpha4). **13.0.0-alpha.3 published on October 24, 2026.** All twenty packages were published from tag `v13.0.0-alpha.3` at `be336833` after the 25 required workflows passed on that exact commit ([release run 40129821475](https://github.com/Rullst/Rullst/actions/runs/40129821475)). It carries the fixes from the alpha.2 dogfooding reports, first-class PWA support and the retirement of Omni, the Studio production monitor (endpoint, standalone remote mode, history and alerts), OAuth login with `make:oauth` for Google, GitHub, Microsoft and Apple, CLDR plurals and formatting, and the breaking changes listed in the changelog. The mutation campaign finished `rullst-orm`, `rullst-orm-macros`, `rullst-core` and `rullst-nexus`; `cargo-rullst` is not yet mutation-measured and its targeted campaign continues after this release. The work planned next was delivered in [13.0.0-alpha.4](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#delivered-in-1300-alpha4). **13.0.0-alpha.2 published on October 17, 2026.** Tag `v13.0.0-alpha.2` points at `0daf4941`. [Release run 39041726912](https://github.com/Rullst/Rullst/actions/runs/39041726912) followed an exact-SHA admission of all 28 required workflows; the release fuzz campaign (42 targets, no crash) ran on `01aa4b10` and was reused for the release commit, whose fuzz inputs were identical. It carries the fixes from the alpha.1 dogfooding report and the breaking `html!` → `Markup`, `Schema` date/time and Nexus changes (see the changelog). Admission found and fixed, before publication: the upstream `zip` yank (Studio's Swagger UI page replaced), Windows, sanitizer, coverage and Miri failures, an exact-pin packaging check, and the `zip` 6 advisories (a governed exception until 2026-10-31). A first tag at `669befe6` failed release verification before publishing anything (the CLI evidence audit did not receive the exception) and was replaced after the maintainer's approval. The work planned next was delivered in [13.0.0-alpha.3](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#delivered-in-1300-alpha3). **13.0.0-alpha.1 published on October 11, 2026.** Tag `v13.0.0-alpha.1` points at `fc44bf76`. [Release run 38151068563](https://github.com/Rullst/Rullst/actions/runs/38151068563) followed an exact-SHA admission of all 28 required workflows. Its fuzz evidence was reused from campaign 38056935372. All twenty crates on crates.io match the release checksums. The GitHub release is a prerelease, and 12.3.0 remains the latest stable release. A smoke test used the published Linux CLI to generate a Blank SQLite project from the registry crates. The project built, passed its tests and served `/`. First publication of the four new names is recorded in the [release recovery runbook](https://rullst.github.io/Rullst/book/release-recovery.html#v13-package-registration). **Direction revised on September 22:** maintain stable v12.1 and continue v13 according to product value, evidence and available resources. The owner no longer requires publication by September 26; it is a review/handoff point, and work may continue in later sessions. The earlier calendar below is retained as history, not a release obligation. The [priority proposal](https://rullst.github.io/Rullst/book/v13-priorities.html) classifies all 41 master milestones without requiring the entire roadmap for 13.0.0. The [SST](https://rullst.github.io/Rullst/book/spec.html) governs APIs and architecture; no deadline or alpha label waives a security or publication gate. **Maintenance direction adopted on September 22:** follow the [maintenance scope and extension strategy](https://rullst.github.io/Rullst/book/v13-maintenance-scope.html) for future v13 work. Preserve common application foundations, contain Capital's scope and assess independent lifecycles for specialized extensions. This planning decision does not change current package admission, publication inventory or v12.1.1. Earlier implementation queues below are historical where they imply broader expansion without the strategy's product, maintenance and acceptance criteria. **Evaluation direction adopted on September 23:** after the active v12.1.1 maintenance admission, begin the [bounded comparison plan](https://rullst.github.io/Rullst/book/v13-framework-comparison.html) with a usable application journey. The 25 A themes are investment priorities, not a requirement to finish every theme before measuring effort, failures, performance and maintenance. Use findings to select later work; this adds no release gate, paid resource, live-account test or claim of superiority. The owner asked about an alpha-first release on September 22. The `13.0.0-alpha.N` prerelease versions support that evaluation path: consumers opt in explicitly and APIs may change before stable v13. Alpha, beta and RC labels do not waive the applicable verification, ownership or protected-publication checks. Choosing a prerelease label does not authorize an upload; stable `13.0.0` still requires its own readiness decision. **Direction recorded on 2 October:** publish 12.2.0 from `v12` first, through its complete admission and the protected crates.io step. The v13 publication decision is deferred to the owner's next working session, after 7 October. The owner is considering a first v13 alpha that includes the current candidate packages as well as the sixteen published ones. That option is not yet approved, and the alpha label does not waive the checks above: each candidate still needs its own admission and a place in the release order before upload. `main` remains the v13 development line. When stable 13.0.0 is published, a `v13` maintenance branch can be cut from its release source, as `v12` was, and `main` moves on to the next major. **Verification order adopted on 3 October:** the 12.2.0 admission showed that mutation findings arrive too late once the release gates have started, because any correction then forces a new exact-source admission. For v13 releases, mutation testing moves ahead of the version freeze. It stays manual and informational: 1. During development, a pull request that changes security-critical code gets a manual targeted `mutants.yml` run for each changed production file before merge. Critical code covers secret masking and redaction, authentication and sessions, rate limiting, request, mail and upload inspection, webhook and token verification, and access control. Meaningful survivors get tests in the same pull request. 2. Before the version freeze, a full campaign runs on the candidate source. Each survivor is triaged as a defect to fix, a missing test in critical code to add, or an equivalent or low-value mutant accepted with its reason. Critical areas aim for at least 90% caught under the conservative count; other areas must not fall below the previous release. 3. Only then does the version change land, followed by one run of the exact-source release gates, with fuzzing dispatched first because it takes longest. Mutation testing stays out of the required pull request checks. Each mutant rebuilds and retests its crate, so a run takes from tens of minutes to hours, and it would compete with the release gates for the same hosted runners. A non-blocking run triggered by a pull request label can be reconsidered if runner capacity grows. ## Source-line transition approved on 21 September The owner approved development on `main`, stable maintenance on `v12`, and retention of the Pages branch. Stable source `184bc1f7` is preserved in the `v12` history. [PR #237](https://github.com/Rullst/Rullst/pull/237) prepared its maintenance workflows and versioned release rules; it merged at `0bb7406b` after all 43 required checks and 24 relevant workflows passed. `main` and `v12` have the same strict, admin-enforced 43-check protected profile. The six-feature source was admitted in PR #236 as recorded below. [PR #238](https://github.com/Rullst/Rullst/pull/238) combines that source with all six Dependabot updates and the `13 → main`, `12 → v12` transition. Its exact combined source passed workspace/platform, coverage, security and extracted-package admission and merged normally at `0ce3306d`. No tag or published crate changed. ## Approved delivery sequence The release owner approved this scope on 20 September 2026 UTC: a first-party, provider-independent privacy foundation, optional external integrations, and a local facial engine as follow-up work. This does not make declarations into verified attributes or permit incomplete higher-assurance checks in production. The privacy decision sets the first required product journey. The broader execution queue below was reconciled with the master roadmap on 20 September after the owner asked for more of the planned v13 capabilities. Privacy, migration and release preparation are the minimum priorities, not a ceiling on useful implementation during the active sessions. The prioritized implementation sequence is: 1. Retain the admitted v12.1 closeout and admit the v13 integration after its existing hosted checks pass; carry applicable stable corrections and keep mutation findings visible. 2. Admit the optional PostgreSQL replay candidate after its hosted checks. Its real-database tests cover concurrency, quota, expiry, failures, cancellation, clock rollback and server restart. Keep SQLite's shared-local boundary and PostgreSQL's operator-owned replication/failover obligations explicit. 3. Build an authenticated first-party declaration journey, with server-owned policy, tenant/session/action binding and rejection when stronger assurance is required. Exercise the supported SaaS/LMS consumer shapes without a paid provider, fake age determination or mandatory camera capture. 4. Implement purpose/version choices, effective withdrawal of optional processing and one scoped rights workflow that actually exports or erases application data. Test ownership, cross-tenant denial, retention and restore boundaries. 5. Extend the product queue with server-side SaaS plan entitlements, a typed API contract with its first TypeScript consumer, and CLI verification of signed Android artifacts. Their bounded acceptance requirements appear below. Resolve their SST/API decisions before implementation; none is shipped merely by appearing in this plan. Independent work can advance while hosted checks run, without launching duplicate campaigns for unchanged inputs. 6. Inventory the actual 12.1-to-13 compatibility changes as each feature lands, implement the supported migration rules and exercise generated consumers, stale inputs and recovery. 7. Improve the documentation and bounded generated project context for the delivered scope. Attempt the small production-linked Verus pilot only after the mandatory feature work is ready for its verification campaign. 8. **First additional priority, explicitly requested on 20 September:** evaluate `rullst-supervision` for transparent exam/learner supervision and parental rules within the application, ahead of other conditional extensions. Start after the required deliveries are implemented and validated, leaving time for combined acceptance and publication. The preliminary engineering estimate is one to two days for a bounded first journey and its tests, plus hosted CI; it is not a commitment to complete device-wide controls by 26 September. 9. **Subsequent September 20 direction: finish usable journeys before opening more optional implementation streams.** After the current deployment and supervision implementations, prioritize Bunny Stream private course video, then Labs with an actual isolated exercise/grading path. A signing helper or mock-only Labs contract is an intermediate checkpoint, not either product's completion. Remote brokers, a new gateway and expansion of the Verus pilot follow these journeys; mandatory fixes and release checks retain precedence. ## Additional priorities approved on 21 September After the six-feature source admission, the owner selected the following two deliveries for implementation through September 23. PR #238 passed source admission and was merged into `main` at `0ce3306d`; the v12 maintenance line is preserved. Consent and email login joined the other five increments in PR #239, which passed combined hosted and extracted-package source admission on September 22 and merged at `9f543ed5`. Final release acceptance remains separate. | Order | Selected delivery | Required acceptance | | :--- | :--- | :--- | | 1 | Shared PostgreSQL consent in `rullst-privacy` | Preserve purpose/version binding, exact-revision grants, withdrawal precedence, clock checks and durable tombstones across independent application pools. Add explicit initialization, restricted-role operation, bounded waits, concurrency/cancellation/restart tests, opt-in facade features and a documented consumer. Keep database restore/failover obligations explicit. | | 2 | Single-use email login links in `rullst-auth`, composed with `rullst-mail` | Complete issuance, delivery and deliberate redemption into the existing authenticated session lifecycle. Require purpose-separated secret tokens, durable atomic consumption, expiry, tenant/account binding, bounded abuse controls, enumeration-resistant responses and safe redirects. Email scanners must not consume credentials merely by following a GET. Test replay races, mail/storage failures, stale accounts and session invalidation with deterministic delivery fixtures. | The email-login item now has a local Auth/Mail candidate: explicit account opt-in, independent browser/email secrets, atomic opaque-session creation, durable SQLite/PostgreSQL state, fenced encrypted delivery and deterministic localized Mail templates. Local SQLite, native PostgreSQL, Chromium, fresh-process, database-restart and extracted-facade Auth/Mail contracts passed. Combined hosted source/package/coverage admission passed in PR #239. See the [email-login contract](https://rullst.github.io/Rullst/book/email-login.html). No owner-provider accounts were used. The owner subsequently approved the five additional items below as the next implementation queue after the two priorities, emphasizing security throughout. All seven are targeted through September 23; completion still requires the advertised behavior and its acceptance evidence. Report any unfinished item before the freeze instead of silently including it in the supported release. The previously discussed metadata-only Studio messaging inspector remains a separate candidate. | Priority | Additional candidate | User benefit and required boundary | | :--- | :--- | :--- | | 1 | Scoped, revocable application API tokens | Let integrations access explicit tenant/account permissions without borrowing browser sessions. Store token digests, bound scopes and lifetime, support revocation/rotation, and test cross-tenant denial and concurrent revocation. Existing provider credentials and application JWT helpers do not implement this lifecycle. | | 2 | Shared PostgreSQL mail suppression | Let independent mail workers honor the same hard-bounce, complaint and explicit suppression state. Extend the current Memory/SQLite contract with atomic event replay handling, minimized state, restart/concurrency evidence and a final pre-delivery check; actual provider inbox acceptance remains separate. | | 3 | Durable recurring schedules across application instances | Coordinate recurring occurrences through authoritative storage and leased dispatch into the existing queue/outbox. Define missed-run policy, cancellation, restart, stale-worker fencing and idempotent occurrence keys. Current per-process cron and delayed queue jobs are foundations; neither proves exactly-once external effects. | | 4 | Durable outgoing application webhooks | Deliver application events to explicitly approved destinations with signatures, bounded retries and inspectable terminal failure. Compose the existing outbox, destination/SSRF policy and idempotency contracts; incoming payment-webhook verification is a different capability. | | 5 | Resumable multipart uploads for private S3-compatible storage | Support larger attachments and interrupted uploads with bounded parts, authenticated tenant/object ownership, checksums, completion/abort and orphan cleanup. Extend the existing private-object adapter; this is separate from Bunny Stream resumable video uploads and requires native protocol evidence. | The API-token item now has a local implementation candidate with exact scopes, owner-only management, revision-based rotation, account-epoch invalidation, SQLite/PostgreSQL authoritative checks and a Core machine-route verifier. Local SQLite, native PostgreSQL, fresh-process/database-restart and HTTP contracts passed, including the extracted facade/Auth/Core archives. Full workspace, coverage, security and hosted package admission were then still required; see the [API-token contract](https://rullst.github.io/Rullst/book/api-tokens.html). The shared mail-suppression item has a local PostgreSQL candidate with keyed recipient/event identifiers, namespace-bound configuration, atomic replay/quota enforcement and the existing final-dispatch guard. Native independent-pool, restricted-role, failure/cancellation, worker/tenant and process/database-restart contracts passed, including the extracted facade/Mail/Core archive consumer. Full hosted admission was then still required; see [shared mail suppression](https://rullst.github.io/Rullst/book/shared-mail-suppression.html). The durable recurring-publication item has a local Messaging candidate with PostgreSQL coordination, encrypted frozen occurrences, bounded UTC catch-up and coalescing, cancellation, fenced retries and metadata retention. Native tests passed real SQL rollback/lock deadlines, restricted roles, competing instances, fresh processes/database restart and durable broker replay after lost ACK. Messaging all-feature regression and strict Clippy passed; extracted archive consumers passed the native journey. Full hosted admission was then still required; see [durable recurring publications](https://rullst.github.io/Rullst/book/recurring-publications.html). The outgoing-webhook item has a local Messaging candidate with an encrypted SQLite outbox, approved destinations, fresh DNS validation and address pinning, exact-body HMAC signatures, bounded retry/manual recovery and permanent cancellation. Owned HTTP/TLS and fresh-process journeys passed receiver deduplication after lost responses, untrusted certificates, redirect/private address denial, SQL failure, retention and delivery uncertainty. These journeys also passed through extracted facade/Messaging archives. A reproduced SQLite lock-contention defect was corrected: lease deadlines are sampled after acquiring the write lock. Full hosted admission was then still required; see [durable outgoing webhooks](https://rullst.github.io/Rullst/book/outgoing-webhooks.html). The private multipart item now has a local Core/facade candidate with encrypted object/tenant-bound resumable checkpoints, exact-size SHA-256-checked parts, remote receipt reconciliation, completion uncertainty and bounded abort/cleanup. Owned S3 tests passed independent clients, process/service restart and both endpoint profiles; HTTP failure tests reject tampering, foreign resources, DTDs, oversized XML, redirects and embedded errors under HTTP 200. Checkpoints require durable application retention/CAS; provider lifecycle expiration handles uploads whose initiation response was lost. Full hosted admission was then still required; see [private multipart uploads](https://rullst.github.io/Rullst/book/private-multipart-uploads.html). All seven then passed combined hosted source admission in PR #239. This does not freeze their APIs or admit a crates.io release. The next priorities are package readiness, targeted security review and corrections before September 24. The September 21 dependency refresh includes `aws-sigv4` 1.5.3 (with its Smithy runtime API patch) in the multipart S3 evidence, and updates the Labs structural parser to `wasmparser` 0.259.0. Its byte offsets now use a wider range; checked subtraction preserves the existing 65,536-byte function-body limit. A parse-only boundary regression and the ordinary runner tests/strict Clippy passed locally. The Wasmi executor was pinned to 2.0.0 with its own validator; hosted isolation and package source admission subsequently passed in PR #239. The runner, and with it these dependencies, was removed from the workspace on 30 September 2026 before any independent isolation review or release acceptance. New OpenTelemetry 0.33 proposals remain pending compatible adapter evidence. The current [`tracing-opentelemetry` 0.33 manifest](https://docs.rs/crate/tracing-opentelemetry/0.33.0/source/Cargo.toml) depends on OpenTelemetry 0.32; a registry probe found no 0.34 adapter release at this observation. Do not treat those five bot PRs as integrated or combine incompatible public types. The next default-branch Dependabot configuration groups the OpenTelemetry and tracing adapter family into one reviewed proposal, without ignoring updates or weakening required checks. Recheck upstream availability before the validation freeze. The v12 maintenance proposals remain separate, with the same family grouping to avoid incompatible individual updates. ### Repository housekeeping on September 21 `main` already contains the v13 promotion from PR #238; `v12` is the stable maintenance line and `gh-pages` serves the site. The obsolete `v13` reference (`d246ea07`) was retired after proving it is an ancestor of `main` (`0ce3306d`). Fourteen additional remote feature references were removed only after confirming their complete history remains in `main` or the consolidated PR #239. Local worktrees and unique local commits were retained. Both maintained branches now require the existing 43 checks, CodeQL Rust and JavaScript analyses, and the separate `CodeQL` findings check from GitHub's security app (46 in total). Dependabot PRs #244 and #247 are superseded by the AWS and Wasm parser changes in PR #239; those compatible updates passed source admission. Individual OpenTelemetry PRs #241–243, #245–246 and #248–250 are closed as incompatible pending the coordinated adapter upgrade described above. Registry inspection still finds `tracing-opentelemetry` 0.33.0 as the latest stable adapter, requiring OpenTelemetry 0.32. No OpenTelemetry 0.33 adoption is claimed. Compatible v12 maintenance updates are consolidated in PR #240, preserving #251's complete history. Its SES constraint check now expects the 1.6.4 pin while retaining the historical 12.0.0 SemVer baseline's separate 1.6.3 guard. The combined maintenance commit passed all 46 protected checks and complete CI/SemVer workflows. PR #240 merged normally into `v12` at `6dde0cb1` on September 22; no package was published. Scorecard's binary finding points to the trusted `checked_sum.wasm` test vector, not application code. Rebuilding its adjacent Rust source with pinned Rust 1.96 reproduced every byte; the Labs acceptance job repeated that check until the fixture, the job and the runner were removed on 30 September 2026. The scanner annotation records test data without suppressing SARIF output. The historical SAST finding persisted in Scorecard run `35675329836`, reporting 15/30 despite verified successful CodeQL checks on all three PR heads associated with the sampled 30 commits: #228 (`977e40a3`, six commits), #236 (`54821688`, 15 commits), and #238 (`87a3bdbf`, nine commits). Both REST check runs and GraphQL check suites report success. Scorecard v5.5.0 reads only the first 30 check suites; #228's CodeQL suite is 32nd, demonstrating incomplete detection. The exact remaining discrepancy is not established. The alert is classified as a false positive for this reviewed snapshot; it does not imply that every intermediate commit was scanned independently. Required CodeQL jobs now enforce both language analyses for future PR admission. Scanner output remains enabled. The first combined PR #239 campaign exposed a Redis TLS provider-selection panic and omitted public-guide doctest registrations. The corrected Redis adapter preserves an installed provider and installs ring only when none exists; the actual Redis/TLS/restart suite and strict targeted Clippy pass locally. All three newly registered guide examples compile with their real features. Windows Labs initialization also returned `Storage` after about three seconds. The unchanged store passed both its local four-test suite and the fresh Windows workspace run at `5c6a8136`; the original transient cause is not established. Redis/TLS, strict quality, coverage, isolated Labs and the actual archive/installed CLI campaign `35676290050` also passed on that snapshot. Other jobs were still pending, so this is not full-source or release admission. The same snapshot's successful CodeQL analysis workflow produced two high-severity XSS findings (#348 and #349) in the test-only email-login HTTP fixture. All dynamic HTML attribute values now use Core's HTML escaping function. The Chromium test parses both actual rendered forms with hostile quote/tag/handler input and checks that field values round-trip without injected elements or event handlers, in addition to the original SQLite/PostgreSQL sign-in, CSRF, replay and logout journeys. CodeQL confirmed both findings fixed on head `7776ccda`, with zero open PR alerts. The CI observer now checks the separate security result and PR-ref alerts; success of the analysis workflow alone never counts as a clean security scan. Execution order may respond to measured implementation and validation cost. Do not replace full journeys with mock-only placeholders to increase the feature count. Keep September 24–25 for combined validation and September 26 for final adjustments and separately authorized publication. No owner/provider account tests are authorized; disposable local services and deterministic protocol fixtures remain available. Architectural/API decisions must be recorded in the SST as each candidate becomes implementation work. ## Seven-increment source admission and package preparation on September 22 [PR #239](https://github.com/Rullst/Rullst/pull/239) merged into protected `main` at `9f543ed5326029f8e4fbf0693dd7faab9288a526`. Its admitted head `7776ccda` and GitHub test merge `3a84b107` have identical tree `abbbade9`. - All 46 protected requirements and 24 PR workflows passed. Additional S3 and isolated Labs jobs passed, both review threads were resolved and CodeQL had zero open PR alerts. - [Coverage](https://github.com/Rullst/Rullst/actions/runs/35680761156) passed both unchanged 90% floors: 108639/120114 whole-repository lines (90.4466%) and 80139/87903 governed library lines (91.1675%, 627 files). Downloaded nextest evidence records 2744 all-feature cases in 234 suites with no failures, errors or skips. Default/browser/database profiles ran separately. - [Archive acceptance](https://github.com/Rullst/Rullst/actions/runs/35680800044) audited 21 diagnostic archives, exercised archive-only consumers and compiled all six blueprints with the installed CLI. Diagnostic archives do not establish release eligibility for the four packages then excluded from publication. The next publication-configuration candidate adds standalone `rullst-supervision` and `rullst-media` to the synchronized inventory (19 packages). Both preserve empty default features and explicit application authorization. Their archive consumers and the generated LMS supervision journey now run in the ordinary release rehearsal, without the diagnostic flag. The CLI upgrade inventory includes both names. Supervision's stable CLI generator pins the matching registry version; prerelease evaluation still requires the matching local source and rejects absent or wrong source before applying edits. The reviewed initial-registration allowlist contains Privacy, Supervision and Media; the registry ownership check still rejects another owner. A read-only crates.io check on September 22 found all three names unregistered. The normal release workflow still rejects unregistered names; the allowlist does not enable a bootstrap credential path. No initial registration, tag, release approval or publication has occurred. (Update: `make:supervision` and its generated LMS journey were later removed with the complete Academy scaffold. The release rehearsal now tests the extracted Supervision archive directly and compiles only the SaaS age/privacy opt-ins.) (Update: all three names were first published in 13.0.0-alpha.1 on October 11, 2026.) `rullst-labs` and `rullst-labs-runner` remain `publish = false`, outside the release inventory and available only through explicit diagnostic packaging. Their independent isolation-review requirement is unchanged. (Update: the runner candidate was removed from the 13.0 workspace on 30 September 2026; see the M40 row below.) The publication changes require their own hosted campaign and actual archive acceptance; previous-source results above do not admit them. Bunny account/CDN acceptance remains unvalidated. (Update: on 8 October 2026 the maintainer admitted `rullst-labs` to the release inventory as `13.0.0-alpha.1`; it packages contracts only, and the removed runner is not published.) This candidate also incorporates the newly opened Dependabot PRs [#253](https://github.com/Rullst/Rullst/pull/253) and [#254](https://github.com/Rullst/Rullst/pull/254): SES 1.136.0 and compatible AWS runtime updates, tokio-rustls 0.26.5, and immutable install-action 2.87.16 pins. Their declared Rust requirements stay below the 1.96 MSRV; the combined platform, feature and archive campaign is still required. The corresponding v12 PRs #252 and #255 require their own maintained-line compatibility checks. The framework README now highlights the separately maintained Rullst Academy. Its public landing page returned HTTP 200 on September 22 and describes free Rust/Rullst and broader technology learning in Portuguese, English and Spanish. This is a product link, not validation of private accounts or application flows. ## Depth before additional optional features The owner asked to pursue a complete, comprehensive implementation of Bunny and Labs before moving to other new capabilities. Finish the supported user journey, its failure/recovery paths, integration documentation and executable acceptance before widening the provider, language or backend matrix. This changes priority and the intended outcome; it is not evidence that either implementation exists or a promise to finish both by the 26th. The feature freeze and validation dates remain unchanged. | Order | Intended usable outcome | Completion boundary | | :--- | :--- | :--- | | 1 — Bunny Stream | An instructor manages a course-owned video through creation, authorized resumable upload, processing, publication, metadata changes, withdrawal and deletion; an entitled learner obtains and renews private playback access. | Complete the selected provider lifecycle, durable ownership/state, notification reconciliation, retry/recovery paths, application/browser consumer, automated protocol tests and operations guidance. Live-account interoperability remains unvalidated under the owner's no-live-testing constraint. See the [detailed scope](https://rullst.github.io/Rullst/book/managed-video-roadmap.html#completion-target-before-another-optional-integration). | | 2 — Labs | An instructor defines a versioned exercise and grader; an authorized learner submits, executes and receives a bounded result through a separately deployed runner, with cancellation and recovery. | Complete the contract stage, then pursue one named isolated backend and a supported exercise/toolchain profile with actual execution, deterministic grading, isolation and failure tests. Contracts and a mock alone do not complete this outcome. See the [delivery target](https://rullst.github.io/Rullst/book/rullst-labs-roadmap.html#v13-usable-journey-target). | Divide implementation into small reviewable commits and PRs without declaring the journey complete early. While hosted checks run, advance related documentation, recovery tests and packaging. Move implementation effort to the next journey after the preceding supported lifecycle and focused acceptance pass; required hosted admission may continue independently. Do not open an unrelated optional feature while useful completion work remains. If a journey cannot meet its acceptance by the evening of September 23, report the precise unfinished behavior and keep it out of the stable supported scope. An independently useful contract package may remain explicitly experimental, but cannot be presented as a completed execution product. Preserve the reserved validation days and the isolation, provider-evidence and publication requirements. A provider adapter advertised as live-validated requires environment, protocol and acceptance evidence. The owner's later direction allows opt-in integrations with automated protocol evidence and explicit unvalidated-provider status; do not use personal/live libraries for testing. Provider availability does not suspend independent framework work. The final privacy package scope must accurately identify every supported method; unimplemented facial or provider methods cannot be advertised as functioning production verification. The package remains unpublished until its scoped consumer/state/API and release admission criteria pass. (It was first published in the 13.0.0-alpha.1 prerelease on October 11, 2026.) ## Blueprint scope and automated acceptance On September 20 the owner prioritized implementation and testing over extending every new feature to every blueprint. Keep existing working integrations and their regression tests. For new work, choose the smallest real consumer that exercises the capability: supervision in LMS, billing entitlements in SaaS, and a focused server/client fixture for an API contract. A public generator must still test each supported output shape; do not advertise an untested shape. Additional blueprint coverage belongs in the release only when it demonstrates a different supported contract or provides clear product value. Opt-in features must remain opt-in, and source/manifest, authentication, persistence and packaged installation checks cannot be replaced with mock-only examples. Since 13.0.0-alpha.4 `cargo rullst new` keeps three starters (Blank, API and SaaS). The LMS, ERP and Portfolio blueprints became workspace examples under `examples/` with their own tests, the Blog blueprint was dropped in favor of the existing blog showcase, and the CLI's generated-project gates cover only the three starters. Human exploratory testing is not assumed to be available during this delivery window. Use automated unit, integration, real-database, HTTP/browser and failure tests appropriate to each supported feature. Record environmental and usability limits explicitly; neither AI authorship nor a passing suite proves every deployment. Missing provider/device/isolation evidence stays missing and limits the supported scope. Defer optional scope and non-critical improvements when needed, but fix known authorization, privacy or data-integrity failures before admission. The possibility of a later patch does not turn a failing contract into an accepted release. ## Starting evidence - All sixteen v12.1.0 packages are published from `b62390b4`; the [release record](https://rullst.github.io/Rullst/book/v12.html#1210-published-maintenance-release) retains their receipt. - The post-release closeout in [PR #217](https://github.com/Rullst/Rullst/pull/217) merged into `main` at `184bc1f7` on 20 September UTC, after 84 successful checks and three skips. It records publication, repairs mutation discovery and corrects the SemVer baseline resolver; the published source and artifacts remain unchanged. - The integration candidate carries that exact published runtime source into v13, preserving the unpublished privacy crate and Labs/Verus plans. The combined source needs fresh CI; the v12.1.0 results do not certify v13. - [PR #218](https://github.com/Rullst/Rullst/pull/218) admitted the integration and SQLite/PostgreSQL replay foundation into `v13` at `7f48d882` on 20 September UTC. Its reviewed head `03601566` passed 85 hosted checks with four declared skips, including workspace tests, strict Clippy and the exact coverage floors. Three remaining review threads referred to the already individually reviewed test-only alerts 327–329 and were resolved after rechecking those fixtures and their existing dispositions. The later native declaration, challenge transport and generated consumers require their own hosted campaign; this admission does not certify those subsequent changes. - The initial privacy foundation passed 16 integration tests and one doctest. The persistence change adds asynchronous verification, trusted clock rechecks and optional shared-local SQLite claims. Its 29 local integration tests cover independent pools, a fresh process, reopen, concurrency, quota, cancellation and failure paths. Hosted candidate validation and a live age provider remain outstanding; this is foundation evidence, not end-to-end production acceptance. - The PostgreSQL candidate passed focused real-database bootstrap, restricted runtime-role, multi-pool replay/quota, expiry during lock wait, schema/durability drift, cancellation and server-restart checks. CI and coverage explicitly run that disposable-database contract; combined candidate evidence is still required. - The next local increment adds native declarations and authenticated challenge transport, plus an opt-in generated SaaS dashboard journey. Local checks passed 42 privacy integration tests, one unit test and five doctests; the disposable PostgreSQL lifecycle and restart contract also passed explicitly. A generated SaaS executes real session/user lookup, production browser middleware, explicit choice and durable replay denial; a second process verifies missing-key denial, and the PostgreSQL consumer profile compiles. Installer tests cover formatted source, changed authentication, source selection, preserved edits and rollback after a write failure. Strict local CLI Clippy and book/link checks passed. The follow-up LMS consumer also passes a generated SQLite journey: bounded school selection, real membership/role resolution, header selection carried into a browser form, cross-school/user denial, replay and revoked membership. Its declaration writes no subject-age or guardian-consent row and does not install age-state middleware around other learning routes. Hosted admission and broader purpose/rights workflows remain outstanding; these checks do not establish deployed browser/provider acceptance. - The initial CodeQL scan's 26 age-replay alerts were individually reviewed: 23 intentional integration-test identifiers, two policy-field data-flow conflations and one unmodeled OS-random buffer overwrite. The [review receipt](https://github.com/Rullst/Rullst/blob/main/docs/src/evidence/v13-codeql-age-review.json) binds those dispositions to the analyzed source. No query, workflow or source/test path was excluded. - The initial review found main-only release admission/security filters and an unprotected `v13` branch. The preparation change binds each major to its release branch and enables the missing automatic checks. On 20 September UTC, hosted v13 protection was enabled and read back with 43 required existing checks; [the observed profile](https://github.com/Rullst/Rullst/blob/main/WORKFLOWS.md#observed-v13-protection) records its scope. Combined hosted evidence and packaging remain pending. - The v12.1.0 post-release mutation campaign is informational. Its repaired controller discovers the measured source's complete inventory and limits the 80 shards to four concurrent jobs. Findings are reviewed separately from v13 readiness, with security-relevant corrections carried to each affected line. ## Release priorities and acceptance The next local increment adds two privacy fuzz targets to the v13 release inventory (42 total; v12 remains 40). Two five-minute ASan diagnostics completed without a reproducer: 1,442,644 challenge-token inputs and 929,346 attestation inputs. These are local diagnostics, not the complete hosted release campaign. The independent `consent`/`consent-sqlite` foundation now provides explicit purpose/notice choices, revision-bound grants, unconditional withdrawal, default denial and per-action checks. Its shared-local adapter retains bounded state and clock metadata, refuses implicit initialization/repair and preserves withdrawal across pools and processes. Local contracts cover stale forms, version/tenant/subject boundaries, capacity, clock/expiry, cancellation and faulty adapters. The opt-in `make:privacy` consumer now supplies authenticated preferences, an optional name-based greeting and a direct own-account profile export through a concrete parameterized SQL adapter. Local generated SaaS/full LMS HTTP fixtures passed both age/privacy installation orders, explicit choices, withdrawal and stale grants, CSRF, account/session/school changes, expiry, unknown and oversized inputs, and missing/failed consent state. The profile export stays independent of optional-consent storage and form keys and never claims to export other application records or complete queued rights requests. A normal PostgreSQL-primary SaaS profile also compiles; live primary-database isolation and deployed browser acceptance are separate from that compile check. These HTTP fixtures found a Core header composition defect: the outer layer replaced an endpoint's `no-referrer`. Core and both Security header layers now share a narrow preservation rule; 36 local composition cases cover layer order, configured defaults, weaker handler policies and duplicate values. The prior PR #219 campaign finished with 84 successful checks, four skips and one macOS unit-fixture failure caused by the system temporary-directory symlink. Its fixture now resolves the same root as the CLI, with an explicit child-symlink rejection regression. Four new CodeQL policy-threshold conflations were individually reviewed and dismissed; the [declaration review receipt](https://github.com/Rullst/Rullst/blob/main/docs/src/evidence/v13-codeql-declaration-review.json) binds those decisions to the old analyzed source. No query was disabled. PR #219's corrected head `d783f3b7` subsequently passed 85 hosted checks with four declared skips, including all-feature workspace tests, strict Clippy, coverage floors and the generated privacy consumers. It merged into `v13` at `df589770` on 20 September UTC. This admits that privacy increment; it does not certify the later adoption, Android or entitlement changes. The next adoption increment gives the existing sixteen release packages and their internal requirements the explicit development version `13.0.0-alpha.1`. Privacy remains unpublished. Migration catalog v2 admits source major 13, retains target-major selection and downgrade rejection, and invalidates old preparations. Local updater contracts exercise a distinct 12.1.0→13 resolution, review, stale-input rejection, application and recovery. Both executable names report the actual source version; prerelease tests keep the explicit opt-in. The [adoption guide](https://rullst.github.io/Rullst/book/migration-v13.html) inventories the current additive changes and distinguishes tiny updater protocol packages from actual generated framework consumers. Combined hosted acceptance and an unpublished package rehearsal remain outstanding. The development train passed 348 local CLI unit tests, 46 updater process contracts, 32 command/scaffold contracts and the generated foundation matrix (Blank/API, SaaS and full LMS, including LMS authorization negatives). Release preflight still identifies sixteen publishable packages with a consistent version and topological order; it does not admit the unpublished privacy package. Strict CLI all-feature/all-target Clippy and production panic checks passed. These are local checks, not the outstanding combined hosted/package campaign. The Android candidate now binds one fresh bounded APK to the application's expected certificate through the SDK verifier, checks its final bytes, rejects stale/ambiguous outputs and withholds captured signing-tool diagnostics. Native process fixtures exercise success, explicit selection, wrong certificates, missing/stale/oversized/changed files, verifier/build failures and output/time bounds. The hosted Android workflow now exercises this CLI against a real signed release and independently checks its certificate and receipt. That new hosted SDK job passed on PR #220 head `52657955`: it compiled and verified a signed release APK through the CLI and the independent SDK check ([run 35497371156](https://github.com/Rullst/Rullst/actions/runs/35497371156)). The rest of the combined campaign remains pending. This is SDK interoperability evidence; device/store acceptance remains separate. The PR #219 follow-up CodeQL analysis reported one additional threshold-to-nonce conflation in the isolated privacy fuzz helper. Alert 338 was individually reviewed against SARIF 1806278303 and dismissed; the [fuzz-policy review receipt](https://github.com/Rullst/Rullst/blob/main/docs/src/evidence/v13-codeql-fuzz-policy-review.json) records the exact source and reasoning. The policy literal is 18; the fuzz-only nonce is a separate deterministic fixture, and production issuance retains OS entropy. No source path, query or fuzz target was disabled. PR #220 head `52657955` passed the real SDK signed-APK job, but its coverage and macOS CLI campaign exposed two Android fixture/build-timing issues. The follow-up preserves OS process helpers in the fixture PATH and compares artifact timestamps against a temporary filesystem anchor. Local native selection and redaction scenarios passed after correction; a fresh combined campaign is required. [PR #220](https://github.com/Rullst/Rullst/pull/220) admitted the adoption, Android and conditional supervision-planning changes into `v13` at `6ccbe2a7` on 20 September at 14:20 UTC. Its exact head `ee4f46df` completed 86 successful checks and four declared skips, with no unresolved review threads. This includes the full OS/workspace matrix, strict Clippy, coverage and the real SDK signed-APK job. The initial campaign's changelog, filesystem timing and terminal prerelease fixture failures were corrected without weakening their checks. This admission does not certify the later entitlement, context, API, Verus or packaging changes. The entitlement candidate adds a typed current-state gate in Capital and an authenticated SaaS billing report. It requires an exact server plan allowlist, fresh revision-fenced Stripe reads, a matching live/sandbox mode and an active, unexpired subscription. The six focused local contracts passed tenant/owner, status/mode, revocation, expiry, stale/future clock and adapter-failure cases. The existing generated SQLite/Turso billing contract also passed. The additional generated SaaS HTTP/reconciliation fixture passed all four configuration profiles, including real session/owner checks, a delayed active read losing to revocation, the 20-second provider deadline and strict generated all-target Clippy. Its test-only provider boundary is explicit; it is not a live Stripe acceptance run. CMS projections and offline billing fixtures cannot grant the new report. Local validation also passed the public doctest, strict Capital default-feature all-target Clippy, CLI all-feature/all-target Clippy, production panic checks, 353 CLI unit tests (one parent-owned child fixture ignored by the outer harness), 20 command/scaffold contracts and book/local links. Combined hosted acceptance remains outstanding. The project-context candidate replaces raw source concatenation with a bounded, versioned file/dependency/configuration-key inventory. It creates common project instructions only when absent, preserves user `AGENTS.md`, supports non-writing freshness checks and records source scope/exclusions explicitly. Local contracts cover private-value omission, links, size/depth/count budgets, stale inputs, legacy migration, conflicting output and all six generated blueprints. Semantic route/authorization inference and task-specific context shards remain follow-up work; the [guide](https://rullst.github.io/Rullst/book/project-context.html) states the exact candidate boundary. The [schema-first API candidate](https://rullst.github.io/Rullst/book/typed-api.html) now generates Rust DTOs/operation codecs and a strict TypeScript client from one bounded OpenAPI 3.1 JSON profile. Seven generator contracts cover unsupported shapes, links, duplicate keys, collisions, regeneration and freshness. Local validation passed 371 CLI unit tests (one parent-owned child fixture ignored), strict all-target Clippy, the real generated Rust/TypeScript HTTP consumer, ten existing command contracts and the six-blueprint context journey. The HTTP fixture proves typed statuses, Unicode, optional/null semantics, exact safe integers, nested nullable arrays, unauthenticated/cross-owner denial and adversarial transport rejection. A GET-only Rust module also compiles with strict Clippy. Hosted Linux CLI acceptance requires the pinned TypeScript compiler and the actual HTTP journey; combined hosted admission remains pending. The Verus pilot has a local candidate for the unchanged production `AgePolicy::permits` predicate. Rust syntax extraction checks the actual package, module path, enum/type/signature domain and body; the verifier covers all nine risk/method combinations. Both local runs verified the predicate and rejected all three deliberately incorrect implementations. They took about 12.1 seconds including complete pinned-tool validation, with prover peak RSS below 292 MiB. The runtime age suite and MSRV 1.96 check passed. The prepared manual workflow is separate from release admission; clean hosted evidence and any promotion remain pending. See the [proof boundary](https://rullst.github.io/Rullst/book/verus-roadmap.html); this does not prove age evidence, expiry, replay state or application authorization. The next packaging candidate adds `rullst-privacy` as the seventeenth package, with its license, a bounded source archive, opt-in `rullst::privacy` features, coordinated upgrade inventory and registry-shaped age/privacy generators. The local seventeen-archive packaging/content audit and the durable facade reopen test passed. Both generated SaaS/LMS privacy journeys passed, including the registry-shaped LMS dependency and the existing local-source SaaS profile. Context freshness for both new commands, 372 CLI unit tests (one parent-owned child fixture ignored), strict CLI Clippy and the minimal facade panic gate also passed. A separate consumer compiled only the extracted framework archives and passed that durable facade test. All 42 fuzz targets' eleven locked dependency graphs resolved offline without running a new fuzz campaign. Combined hosted acceptance remains outstanding. All seventeen local archives were subsequently generated from clean commit `30e2f753`; their source identity, licenses and excluded secret/database paths were audited. The archive-only privacy consumer and privacy publication dry run passed without uploading a version. These are local rehearsals, not final release receipts. The archive-only hosted test then compiled the installed CLI's SaaS/LMS opt-ins and executed the same durable facade contract from extracted packages. It passed at `365d2252` in [run 35520146394](https://github.com/Rullst/Rullst/actions/runs/35520146394) on September 20: seventeen package archives were audited, the durable facade test passed and all six installed-CLI blueprints compiled. This diagnostic does not replace the final full/native release campaign or publication authorization. A crates.io read on 20 September returned 404 for `rullst-privacy`; initial registration and Trusted Publishing configuration remain required. The stable publisher continues to reject an unregistered package. The ownership-policy validator now checks each proposed bootstrap name against the inventory, with unknown/duplicate/malformed-name negatives; the empty bootstrap allowlist and the stable publisher's refusal remain in force. PR 220's Linux terminal fixture also needed an explicit prerelease dependency requirement and `--prerelease` when exercising the alpha CLI. All four local terminal scenarios passed after that correction: decline copying, decline execution, apply/recover, and a failing application. The corrected `ee4f46df` head then passed its own combined hosted campaign, as recorded above; previous Android/macOS passes were not substituted. On 20 September at 07:31 UTC, the first four v12 mutation shards had hit their 330-minute execution budget; their partial artifacts were retained. For example, shard 2 had executed 72 of 220 scheduled mutations (40 caught, 30 surviving, two unviable), so it is explicitly incomplete. Surviving mutations require individual review and are not automatically confirmed runtime defects. The remaining campaign continues; partial results must not be reported as a complete mutation pass, and any later continuation must preserve source/inventory identity. The first combined [PR #221](https://github.com/Rullst/Rullst/pull/221) campaign at `4773171a` passed 85 checks, including the OS/workspace and generated consumer matrix, strict Clippy, coverage, feature boundaries and MSRV. Two inventory checks failed: the README omitted the added Verus workflow, and the observational scorecard omitted the seventeenth package. The correction retains both checks, adds a conservative unpublished privacy ceiling and preserves its planning floor. The corrected head requires its own hosted admission. A new explicit package-only diagnostic runs the existing archive/installed-CLI acceptance without repeating the OS matrix; a regression proves that this subset cannot admit a release. The corrected `365d2252` head subsequently passed **87 hosted checks**, with four declared skips and no unresolved review threads. PR #221 merged normally into `v13` at `48a6ee51` on September 20 at 17:23 UTC, with all 43 required checks and administrator enforcement preserved. Its seventeen-archive/installed-CLI diagnostic passed separately as recorded above. This admits the combined application/privacy candidate; the final full/native/security release campaign and initial new-package registration remain outstanding. The first conditional supervision implementation now has a separate unpublished crate, bounded domain contracts and shared-local SQLite state. Its focused local tests cover explicit acknowledgement, scoped/revocable authority, session transitions, event bounds, parental course windows, retention, fresh-process reopen, concurrent revisions, lock-wait expiry, cancellation and corrupt rows. An explicit full-SQLite-LMS generator now installs the original learning-service gate, signed/scoped SSR forms, local operator provisioning and a bounded visibility collector. Three local CLI integration tests pass, including both privacy/age composition orders, public-profile compilation, the real HTTP lifecycle and Chromium keyboard/no-JavaScript/visibility controls. A start form is bound to the last retained session revision so an old acknowledgement cannot silently start a new session after end. The generated application passes all fourteen original LMS library tests and strict production Clippy/zero-panic checks, and all 376 CLI library tests pass. These local results do not admit the package: full workspace regression, installed archives and hosted acceptance remain outstanding. PR #222 head `20a9fca7` subsequently passed 86 hosted checks with four declared skips and no unresolved review threads. Its independent packaged-distribution run `35534311217` also passed at that exact head. The supervision source merged normally into `v13` at `081c3805` on September 20, 21:14 UTC, preserving all 43 required checks and administrator enforcement. This admits that source increment; stable package registration/publication and the final combined v13 campaign are still outstanding. The next shared-passkey candidate is under its own hosted campaign; the deployment/body and local diagnostic increments remain separate. The local deployment diagnostic passed seven executable contracts, including a generated SaaS, and two additional literal-parser/boundary contracts. All 376 previous CLI unit tests passed; strict all-target CLI Clippy, book/local links and architecture checks passed. A bounded six-mutation decision sample was fully caught. Inspection also reproduced acceptance of the public `.env.example` application-key placeholder in Auth; explicit rejection now passes the runtime regression, 65 Auth unit tests and three key-resolution process contracts, with strict all-target Auth Clippy. These are local candidate results; hosted and installed-package acceptance remain outstanding. The corresponding stable-line backport still needs preparation and its own checks; published artifacts remain immutable. The next four source increments were merged into `v13`; their evidence is recorded individually below: - [PR #223](https://github.com/Rullst/Rullst/pull/223), shared PostgreSQL passkey ceremonies, merged at `b354d67c`. The exact `15addc3d` candidate passed hosted checks and [archive acceptance](https://github.com/Rullst/Rullst/actions/runs/35542202337). - [PR #224](https://github.com/Rullst/Rullst/pull/224), HTTP response-body draining and the Caddy/Redis deployment contract, merged at `a4a3b3e6`. The exact `2e348f96` candidate passed hosted checks and [archive acceptance](https://github.com/Rullst/Rullst/actions/runs/35547984264). - [PR #225](https://github.com/Rullst/Rullst/pull/225), deployment diagnostics and rejection of the public Auth key placeholder, merged at `873bc14a`. The exact `45a573cc` candidate passed hosted checks. The archive job in [run 35554979487](https://github.com/Rullst/Rullst/actions/runs/35554979487) was skipped, so that run is **not archive acceptance**. Correctly selected [exact-commit validation](https://github.com/Rullst/Rullst/actions/runs/35564762822) subsequently passed on September 21 at 06:32 UTC, including the actual archive audit, extracted consumers and all six installed-CLI blueprints. This repairs that missing gate retrospectively. - [PR #226](https://github.com/Rullst/Rullst/pull/226), transparent exam observations and bounded analysis adapters, merged at `bb71dad8`. The exact `ae9b6d57` candidate passed 86 hosted checks. The archive job in [run 35558912935](https://github.com/Rullst/Rullst/actions/runs/35558912935) was skipped, so that run is **not archive acceptance**. The correctly selected [exact-commit validation](https://github.com/Rullst/Rullst/actions/runs/35564765646) subsequently passed on September 21 at 06:27 UTC, including the actual archive audit, extracted consumers and all six installed-CLI blueprints. This repairs that missing gate retrospectively. The September 21 audit found that these last two merges preceded their required installed-archive validation: a successful workflow was incorrectly credited despite its skipped archive job. Both gaps are now repaired by the actual exact-commit runs recorded above. These retrospective results do not erase the missed premerge gates; subsequent source admission requires checking the named archive job itself before integration. These merges do not publish packages or replace the final combined release campaign. Bunny subsequently passed its own source admission in [PR #227](https://github.com/Rullst/Rullst/pull/227): the exact `b21d52e7` candidate passed all 87 current checks, the actual [installed-archive job](https://github.com/Rullst/Rullst/actions/runs/35564717398), and review with no unresolved threads, then merged at `618655c1`. [PR #228](https://github.com/Rullst/Rullst/pull/228) subsequently admitted Labs source at `278a115b`. Candidate `977e40a3` passed all 24 native shards, 43 branch-required checks, actual isolated execution, the actual installed-archive job and both 90% whole/library coverage floors. Its non-required Codecov patch check remained at 83.45936%, below its 90% target; the [profile evidence](https://rullst.github.io/Rullst/book/labs-first-profile.html#recorded-linux-acceptance) records this limitation without exporting worker profiles or relaxing isolation. Independent isolation review and final release admission remain outstanding. Stable-line backport preparation remains separate as well. The September 20 observation extension now adds selected browser categories, capture-status contracts and bounded analysis adapters to the supervision candidate. Its five generated CLI/composition/process contracts pass with real Chromium, alongside 378 CLI unit tests, isolated feature checks, strict Clippy and an offline simulated adapter example. A targeted nine-mutation guard sample is fully caught. No camera/audio model, automatic misconduct determination or live-provider validation is claimed. Hosted workspace checks passed in PR #226; the missing archive gate subsequently passed as recorded above. The final release campaign remains required. See [the integration guide](https://rullst.github.io/Rullst/book/supervision-observations.html). | Priority | Deliverable | Acceptance before calling it complete | | :--- | :--- | :--- | | P0 | Published v12.1.0 corrections integrated without losing v13 work | Review conflicts; retain the stable runtime changes; pass the combined workspace tests, strict Clippy, format and feature/consumer checks. | | P0 | Protected v13 verification and publication path | Explicit major-version/branch policy, protection equivalent to the stable line, complete required workflow/job inventory, negative wrong-branch/tag/evidence tests and an unpublished package rehearsal. Keep v12 maintenance independently releasable. | | P0 | Usable proportional age-assurance boundary | SQLite shared-local and PostgreSQL multi-host replay protection, authenticated tenant/session/action binding, a first-party declaration journey only where policy permits it, explicit method strength and fail-closed outage/expiry/replay behavior. Production must reject mocks, process-local state and unsupported stronger methods. | | P0 | First complete privacy journey in a generated consumer | Explicit purpose/version choices, withdrawal enforced on subsequent optional processing, one authorized rights workflow with actual adapter effects, tenant isolation and documented retention/restore boundaries. Do not count a request row as completed export or erasure. | | P0 | Safe 12.1→13 adoption | Inventory actual compatibility changes, implement only supported migration rules, run generated SaaS/LMS consumer fixtures and prove review, stale-input rejection and recovery. A major-version flag alone is not a migration. | | P1 | Server-side SaaS plan entitlements | A typed authorization gate bound to authenticated tenant, subject, feature and validity, consumed by a generated SaaS route. Deny expired/revoked or cross-tenant grants. Payment redirects, mock results and unverified events must never grant access. Keep subscription reconciliation explicit. | | P1 | Typed API and first TypeScript consumer | One explicit schema source for a bounded supported set of request/response shapes, parameters and errors; compile and execute a generated consumer against its server fixture. Prove nullability, serialization, error handling and denied access; reject unsupported schema shapes rather than emitting guessed types. | | P1 | Android artifact verification through the CLI | Select the intended release output, run signature verification and bind the signer to the application-owned certificate. Reject missing, ambiguous, stale, unsigned or wrong-key artifacts; test process failures and redaction. Validate a real generated APK in hosted Android CI; device/store acceptance remains separate. | | P1 | Better application context for maintainers and assistants | Accurate generated `AGENTS.md`, a bounded deterministic project map, configuration key names without values, explicit file/secret exclusions, freshness information and executable regression fixtures. | | P1 | Focused Verus pilot | One production age-policy property with checked linkage, pinned tooling, negative controls and measured cost. No framework-wide verification claim or automatic expansion to every crate. | | First conditional extension | `rullst-supervision` candidate | One generated LMS journey with visible sessions, explicit permissions, independently established guardian/reviewer authority, effective revocation, bounded events/retention and tested application-side restrictions. Prove cross-school/subject/reviewer denial and browser behavior. Client observations cannot prove misconduct or automatically change grades. Device-wide control, camera inference and a mock-only package do not satisfy this scope. | P0 items have precedence over new integrations, cosmetic rewrites and expanding the number of crates. Each implementation should remain a small reviewable change with its own tests and migration/documentation updates. The acceptance column is a requirement, not a description of code already present. ## Coverage of the wider roadmap ### Additional increments approved on September 21 The owner approved the following order after the Bunny/Labs source work. Finish each supported journey and its acceptance before expanding its advertised scope. The six implementations passed the source admission recorded below; their combined dependency validation and final release admission remain separate. | Order | Increment | Completion boundary | | :---: | :--- | :--- | | 1 | Private S3/R2 storage in existing Core/facade APIs | Upload, download, metadata, deletion and temporary private GET grants; current tenant/owner authorization, bounded failures, an independent disposable S3 service and an extracted-package consumer. See [the candidate contract](https://rullst.github.io/Rullst/book/private-object-storage.html). | | 2 | Active-session inventory and effective revocation in Auth | Authenticated inventory, expiration, selective logout and logout of other sessions with rejection by actual request verification across processes. Preserve account/tenant isolation, recovery and fail-closed storage behavior. See [the candidate and migration contract](https://rullst.github.io/Rullst/book/session-management.html); source/package admission passed in PR #236. | | 3 | One remote Messaging adapter | Redis Streams selected; see [the candidate contract](https://rullst.github.io/Rullst/book/redis-messaging.html). Prove publication acknowledgement, restart/redelivery, competing consumers, retry/DLQ and outbox composition against a disposable broker. Source/package admission passed in PR #236. | | 4 | Reconnection and recovery for server-driven real-time interfaces | The opt-in Core/facade candidate uses full snapshots, current authorization, transactional expected revisions and bounded connections, actions and slow peers. Actual WebSocket and Chromium tests cover conflicts, uncertain writes and application restart without automatic mutation replay. See [the recovery contract](https://rullst.github.io/Rullst/book/live-recovery.html); source/package admission passed in PR #236. | | 5 | Operation tracing across processes | Core/facade's optional candidate has explicit parent trust, approved operation labels, minimized bounded OTLP export and an owned lifecycle. Separate Messaging processes reach a standard TLS collector with verified ancestry; local failure/queue/TLS contracts pass. See [the tracing profile](https://rullst.github.io/Rullst/book/distributed-tracing.html); source/package admission passed in PR #236. | | 6 | An application-driven ORM increment | Transactional partial updates selected: merge a submitted lesson/profile patch into the current row and reuse policy, hooks, encryption, atomic audit and post-commit effects. Preserve the caller model on rejected operations and add explicit transaction support. See [the candidate contract](https://rullst.github.io/Rullst/book/transactional-partial-updates.html); local database/cancellation/cache/Scout and extracted-consumer checks passed; source/package admission passed in PR #236. | ### Six-increment source admission on September 21 [PR #236](https://github.com/Rullst/Rullst/pull/236) admitted all six increments at source `548216881e062b83d6b73a824abe140936aca5b8`. It merged normally into protected `v13` at `d246ea072a0bddd1ced86471c49ffdb22d2b96cc`; its tree matches the tested merge candidate. Evidence for this source: - [Rust CI](https://github.com/Rullst/Rullst/actions/runs/35634162531): all 43 protected requirements passed, including the Linux/macOS/Windows matrix, strict Clippy, format, MSRV and feature boundaries. Private S3 and isolated Labs acceptance also passed. All 21 relevant PR workflows completed successfully. - [Coverage](https://github.com/Rullst/Rullst/actions/runs/35634162210): 103382/114571 whole-repository lines (90.2340%) and 74906/82383 governed library lines (90.9241% across 581 files). Both unchanged 90% floors passed; the downloaded summary was independently checked. The all-feature nextest artifact records 2701 cases with zero failures, errors or skips; default, browser, database, collector and isolated-controller profiles ran separately. - [CodeQL](https://github.com/Rullst/Rullst/actions/runs/35634162178): Rust and JavaScript scans bound to the exact merge source completed with zero open findings. All seven review conversations were resolved. - [Packaged distribution](https://github.com/Rullst/Rullst/actions/runs/35634201254): 21 candidate archives audited, archive-only consumers exercised, and all six blueprints compiled through the isolated installed CLI. The actual package job executed; skipped diagnostic jobs were not credited as PR checks. This admits the documented source contracts, not a crates.io release or every parent roadmap milestone. No owner/provider accounts were exercised. The Labs profile retains its separate independent isolation-review boundary. The new main-line dependency combination must pass its own exact-source campaign. After this approved round, report the delivered scope and remaining time to the owner before selecting another round. September 24–25 remain reserved for combined validation; September 26 remains the conditional final-adjustment and publication day. Publication is excluded from the current implementation goal. The [master roadmap](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#executive-milestone-tracker) owns the current status of the framework milestones and the separately governed M31 programme. Its milestones differ substantially in scope and remaining effort; counting their status labels cannot tell us whether this release adds "10% of all future work". Nor does finishing one increment close its entire parent milestone. The wider v13/v13+ programme remains available for subsequent minor releases. Items deferred beyond the September 26 target do not automatically become v14 work. Compatible additions can ship in 13.1, 13.2 and later minor releases; breaking changes belong in a future major under the [compatibility policy](https://rullst.github.io/Rullst/book/compatibility-policy.html). The current major's concrete CLI/configuration change and the limits of its SemVer evidence are recorded in the [migration inventory](https://rullst.github.io/Rullst/book/migration-v13.html#why-the-candidate-uses-a-new-major). The owner explicitly confirmed retaining the v13 release train on September 20. This queue deliberately builds on existing code instead of restarting those capabilities. Within P1, a small independent increment can precede a larger one when its dependencies and verification capacity are ready. | Roadmap area | Starting point and next bounded increment | Scheduling boundary | | :--- | :--- | :--- | | M41 — privacy and age | Hosted admission includes proportional policy, declarations, signed attestations, SQLite/PostgreSQL replay adapters, optional processing withdrawal and a scoped own-account profile export in generated consumers. | Preserve those admitted journeys in the final campaign. Initial package registration remains outstanding; live age providers and facial models remain outside the delivered scope. | | M11/M33 — SaaS entitlements | The typed current-state Capital gate and generated SaaS enforcement journey were admitted with PR #221. | Preserve current tenant/owner, plan and subscription checks during combined validation; the broader billing roadmap is not complete. | | M5/M29/M34 — API/SDK contracts | The bounded schema-first Rust/TypeScript API profile and real HTTP consumer were admitted with PR #221. | Retain schema/transport/ownership regression coverage. React, Dart and Swift targets remain follow-up work; route scanning does not supply typed response semantics. | | M21 — Omni/Android | CLI verification of the exact signed APK and configured certificate was admitted with PR #220 after hosted Android SDK acceptance. | Retired in 13.0.0-alpha.3 with Omni: `make:pwa` replaces the shells, and its Google Play path (Bubblewrap and Digital Asset Links) has no framework-built APK to verify. Physical-device and store acceptance remain unvalidated. | | M9 — Auth/session consistency | PR #223 admitted optional PostgreSQL passkey ceremonies with tenant/account/session/RP binding, bounded single use, real database/process recovery and a Chromium virtual authenticator. | Preserve credential-owner/revocation/counter CAS at the host and repeat affected combined-release checks. See [the contract](https://rullst.github.io/Rullst/book/shared-passkey-ceremonies.html). | | Transparent supervision | The baseline passed hosted and archive acceptance in PR #222. PR #226 merged the prioritized exam-platform extension after hosted checks; its missing archive gate subsequently passed in the exact-commit run recorded above. The generated LMS journey (`make:supervision`) was later removed with the complete Academy scaffold; the crate's contracts remain. | Preserve transparent permissions, typed uncertain observations, manual review and no raw media retention in the final campaign. See the [integration boundary](https://rullst.github.io/Rullst/book/supervision-observations.html). | | M27 — deployment acceptance with an existing proxy | PR #224 admitted the response-body lifetime correction and the real two-process Caddy/Redis contract for readiness, draining, shared budgets/outage, forwarding, CSRF/body limits and WebSocket behavior. | Preserve combined-release coverage. This loopback fixture does not establish generated multi-replica Foundry, cross-host failover or zero downtime. See [deployment acceptance](https://rullst.github.io/Rullst/book/deployment-acceptance.html). | | M10/M27 — cloud and VPS application protection | PR #225 merged the offline `deploy:doctor` with explicit environment sources, bounded inputs, redacted reports and rejection of the public Auth key placeholder after hosted checks; its missing archive gate subsequently passed in the exact-commit run recorded above. | Preserve the installed-archive and diagnostic coverage in the final campaign and prepare the separate stable Auth backport. No automatic host/cloud changes or volumetric DDoS guarantee. See the [diagnostic](https://rullst.github.io/Rullst/book/deployment-diagnostic.html) and [deployment boundary](https://rullst.github.io/Rullst/book/security-architecture.html#cloud-and-vps-deployments). | | LMS/Academy — managed private video | PR #227 admitted the unpublished `rullst-media` candidate after hosted workspace/platform, browser and installed-archive acceptance. It implements Bunny lifecycle management, resumable upload, authoritative processing, private playback and deletion with SQLite recovery. | Preserve the supported journey in the final combined release campaign. Live-account interoperability stays unvalidated. The package has since joined the release inventory (`.github/release-order.json`) and was first published in the 13.0.0-alpha.1 prerelease; final release admission remains separate. See the [managed-video candidate](https://rullst.github.io/Rullst/book/managed-video-roadmap.html). | | M40 — Labs | The `rullst-labs` candidate, first published in the 13.0.0-alpha.1 prerelease, provides encrypted durable exercises/jobs, exact grading and cancellation/recovery/retention. The former `rullst-labs-runner` candidate, a separate Linux Rust/Wasmi executor, passed 26 ordinary and 26 instrumented hosted journey checks at `977e40a3`. On 30 September 2026 the maintainer removed it from the 13.0 workspace; its source remains in git history. `rullst-labs` is now bring-your-own-runner, with a documented [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html) and a non-executing example controller. | PR #228 passed workspace/platform and actual extracted-package source admission, plus both 90% coverage floors; that historical evidence covered the removed runner. Final release admission of `rullst-labs` remains outstanding, and each application-owned runner needs its own isolation review. See the [recorded profile evidence](https://rullst.github.io/Rullst/book/labs-first-profile.html#recorded-linux-acceptance). | | M15 — remote messaging | PR #236 admitted the optional standalone Redis Streams profile with TLS, restart/redelivery, fenced leases, exact replay, retry/DLQ and outbox evidence. | Preserve the documented Redis and operator boundaries in combined validation. Other broker adapters, native Redis group interoperability and replication/failover remain separate roadmap work. | | M39 — optional Rullst Gateway | No `rullst-gateway` crate or executable exists. On 8 October 2026 the maintainer abandoned it in favour of generated Caddy/nginx configuration for established proxies; see the [master roadmap](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#ideas-recorded-for-after-130). | No delivery. Proxy work continues as generated configuration (Caddy today, nginx planned) consuming the existing readiness/drain signals. | | M1/M3/M7/M12 — adoption and assurance | The compatible updater is carried forward. `cargo rullst upgrade` reports v12 → v13 source findings from `rullst-upgrade-rules-v4`, each tied to a [migration-guide](https://rullst.github.io/Rullst/book/migration-v13.html) row. Next: improve generated guidance and connect new code to the relevant verification inventory. | Required adoption/security work plus bounded maintainer tooling; Verus begins with one production-linked pilot. | Gateway/load-balancer implementation, physical IoT/Embassy, PQC protocols, local facial inference and broad database replication retain their dedicated roadmap scope. They are not counted as completed by a proposal, an empty crate or a mock. Reconsider them through their own dependency and acceptance evidence instead of promising the whole long-term programme by the 26th. At each implementation checkpoint, record the delivered behavior, source, focused checks, pending hosted checks and next useful increment. The decision to start the next item uses remaining integration/test capacity as well as coding time. Preserve working increments and report unfinished acceptance explicitly; do not inflate a feature count by splitting contracts into crates. ## Age and privacy design decisions to close first Keep age assurance and reusable privacy contracts in the existing optional `rullst-privacy` package. Do not add a second age crate or make cameras, a vision runtime, a database or a provider a default Core dependency. The v13 `ReplayStore::claim` interface now returns a statically dispatched `Send` future. Verification samples a trusted clock before validation and after storage, denying expiry or rollback during the wait. The initial SQLite adapter uses serialized durable transactions, persisted quota/clock metadata and hashed nonces. Local tests cover independent verifier pools, reopen, simultaneous claims, capacity, expiry, cancellation, uncertain acknowledgement and unavailable storage. Native hosted evidence remains pending. Its shared-local boundary is distinct from the PostgreSQL adapter's multi-host operation; restoring an old backup requires quiescing verification and invalidating all outstanding challenges through a newly enforced policy or retired signing keys. Use the current server-owned policy and signed threshold result rather than storing photos or full birth dates in the framework. Provider authenticity, subject/session binding and the quality of an age determination are separate contracts. A facial estimate cannot silently become a verified age attribute. Every denied, inconclusive or unavailable result needs an explicit product outcome; an alternative path must meet the same policy instead of bypassing it. A live adapter requires an available authorized sandbox, a reviewed native protocol, failure tests and appropriate provider/capture evidence. The release owner confirmed that no provider environment is available and directed work on the foundation and local tests first. Prioritize that independently testable scope while the external dependency is unresolved. If it remains unavailable, do not label a simulated flow as live verification or mark the provider milestone complete. The approved independent foundation has its own consumer/state/API acceptance; any live adapter additionally needs its provider acceptance. Review this precise publishable package boundary at feature freeze, preserving the security guarantees of every advertised method. For broader privacy, implement reusable processing controls and one real consumer journey before claiming coverage across every blueprint. Keep policy applicability, notices, processor obligations and external legal review explicit. The [privacy roadmap](https://rullst.github.io/Rullst/book/privacy-age-assurance-roadmap.html) remains authoritative for the wider jurisdiction and provider programme; this plan does not certify GDPR, LGPD or worldwide compliance. ## Improvements that reduce maintenance friction 1. **A current source baseline.** Synchronize stable fixes before feature work; avoid repeatedly rediscovering or reintroducing already repaired SaaS, mail and updater problems. 2. **One executable test map.** Preserve complete test inventories and choose focused development checks from actual crate/dependent changes. Unknown, security, generator, dependency and workflow changes retain conservative coverage. Measure job execution separately from runner queue time. 3. **Accurate generated guidance.** Derive agent instructions and context from the chosen blueprint/features. Link to version-matched public APIs and commands; do not include credentials or imply that optional adapters are active. 4. **Small complete examples.** Use reproducible SaaS/LMS reference journeys with negative ownership, replay, withdrawal and recovery assertions. A compiling tutorial alone does not establish those behaviors. 5. **A durable progress record.** Record the source, checks, limitations and next action for each delivered item. Avoid duplicated readiness statements that drift apart across the README, roadmap, spec and release guide. These implement the narrow first phase of the [AI-maintainability roadmap](https://rullst.github.io/Rullst/book/ai-maintainability-roadmap.html). Broad model benchmarks and quality guarantees about arbitrary assistants are outside this release window. Keep changes modular; avoid unrelated mass refactoring merely to meet a line-count target. ## Calendar and scope control **Superseded deadline:** the owner revised this calendar on September 22. September 26 is no longer a required publication date. Use the checkpoints below as the original sequencing record; prioritize stable maintenance and completed, tested journeys under the [revised plan](https://rullst.github.io/Rullst/book/v13-priorities.html). Dates below use Brasília time. Work may move earlier when its dependencies and checks are ready; slow external acceptance reduces scope, never test quality. The owner reconfirmed this calendar on September 20: prioritize remaining implementation through September 23 by user benefit, maintenance cost and acceptance capacity; reserve September 24–25 for validation/corrections and September 26 for final adjustments and protected publication. Local deployment configuration diagnostics and the supervision observation extension are merged with hosted checks passed. Their archive gaps have now been repaired as recorded above. The owner's subsequent direction selects the complete supported Bunny journey next, followed by usable Labs execution and grading, before remote messaging or other optional expansion. Bunny has passed source admission; Labs has now passed source admission with its remaining quality, independent-review and release requirements tracked above. Increasing the crate count is not a priority. The owner subsequently requested no manual or real-provider account testing in this window, including the confirmed existing Bunny library. Continue automated local/hosted tests, disposable databases and protocol fixtures. Preserve required regression/security gates and report unvalidated provider interoperability explicitly; do not describe simulated acceptance as a live integration pass. New optional provider scope can be delivered with that stated limitation, rather than waiting for manual tests or using the owner's live resources. | Date | Checkpoint | | :--- | :--- | | 20 September | Close v12.1 documentation, admit the integration baseline, settle privacy/storage API decisions and prepare the v13 release/branch policy. | | 21–22 September | Admit and repair the existing candidates; prioritize completing the Bunny lifecycle, then the Labs execution/grading journey as dependencies and verification capacity permit. Maintain migration fixtures and automated acceptance with each API change. Assess provider scope by the end of the 22nd using automated protocol evidence, without live-account testing. | | 23 September | Close supported product journeys, recovery paths, generated guidance and documentation. Resolve the Bunny/Labs acceptance boundaries before considering another optional implementation. Freeze feature scope by the evening. | | 24 September | Run the complete candidate verification campaign, including required native matrices, fuzzing, Miri, Kani and sanitizers. Preserve explicit evidence boundaries and start long jobs early. | | 25 September | Repair findings, invalidate and repeat affected evidence, test packaged consumers and rehearse the complete publication transaction. | | 26 September | Buffer for final failures, review and protected publication. Publish stable only with all mandatory criteria satisfied; otherwise report the exact blockers and an honestly scoped candidate. | At the feature freeze, move unfinished optional work to a named follow-up rather than leaving partially enabled behavior in a stable release. A blocking age, authorization, data-loss or migration defect is not an optional follow-up. Keep approved feature scope in the release notes and tests; do not mark the historical roadmap complete merely because the version is published. ## Verification and final publication Run `cargo test --workspace --all-features`, strict workspace/all-feature Clippy and formatting, plus the existing feature, threat, provider-protocol, generated-application, native-platform and release gates. New privacy parsers, state transitions and durable adapters need their own meaningful negative, concurrency and fuzz/property coverage. Local focused passes never substitute for the final workspace campaign. Use hosted CI for builds whose peak cannot fit the workstation's reserve; retain at least 12 GiB available and intervene at 15 GiB. Reuse verification only through the existing explicit input-equivalence/admission policy. Changes to a candidate invalidate the affected evidence; neither a green older commit nor cached compilation is a release result. Before tagging, check every package/internal requirement, lockfile, release order, architecture edge, feature forwarding, CLI version string, documentation link and supported-platform claim. Include the new `rullst-privacy`, `rullst-supervision` and `rullst-media` packages in publication only after their advertised package scopes meet their gates and their ownership/trusted publisher configuration is ready. Confirm 12.1→13 consumer behavior, authenticated native assets, attestations and the protected deployment approval. Published 12.1.0 tags and archives remain immutable. Execution proceeds within the active authorized session, while GitHub jobs can continue independently. Session availability and a running CI job are different things; this plan does not promise an unattended uninterrupted week. At each handoff, retain the next action and distinguish work still possible from work actually waiting on an external result. --- # Sustainable v12 Maintenance & v13 Priorities Source: https://rullst.github.io/Rullst/book/v13-priorities.html # Sustainable v12 maintenance and v13 priorities ## Direction revised on 22 September 2026 The owner prioritizes a maintained, secure stable v12.1 line and useful v13 deliveries over publishing a major version merely to meet September 26. Work may continue in later funded sessions. September 26 is a handoff/review point, not a mandatory release date. An alpha is an optional evaluation milestone, not a substitute for safety, packaging or publication checks. Keep `main` for v13 development and `v12` for stable maintenance. Compatible bug/security/dependency fixes can become 12.1.x after validation; new compatible public functionality may require a minor release, and breaking changes remain in the major development line. A dependency's SemVer number alone does not prove compatibility. Preserve the supported compiler, features, serialized contracts and generated-project behavior. Do not backport v13 wholesale. This document proposes investment priorities, not approval to implement every row, publish, expand supported platforms or claim a permanent LTS programme. Actual release status and support remain governed by [SECURITY.md](https://github.com/Rullst/Rullst/blob/main/SECURITY.md) and the [compatibility policy](https://rullst.github.io/Rullst/book/compatibility-policy.html). The owner subsequently adopted the [crate maintenance and extension strategy](https://rullst.github.io/Rullst/book/v13-maintenance-scope.html) on September 22. It turns the cost assessment into a direction for scoped investment: preserve common security/productivity contracts, contain Capital's provider and fiscal scope, and give specialized extensions explicit ownership and migration boundaries. It does not remove supported capabilities or change the v12.1.1 candidate. Specific restructuring remains follow-up work. ## Counting method The [canonical roadmap](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md#executive-milestone-tracker) contains **41 top-level milestones, M1–M41**. Their detailed per-crate roadmaps overlap and decompose these themes; adding every checkbox would double-count work. This is a prioritization of those 41 themes, not a fresh audit of every source file or a claim that all of them were committed for 13.0.0. | Proposed category | Themes | Existing roadmap labels | | :--- | ---: | :--- | | A — invest and maintain | 25 | 5 bounded implemented; 20 partial | | B — defer expansion until justified | 16 | 7 partial; 9 not implemented | | Total | 41 | 5 bounded implemented; 27 partial; 9 not implemented | Use a separate **C — high maintenance cost** marker across either category. The initial review flags 12 existing implementation themes below (11 in A and 1 in B). C overlaps A/B; it is not another twelve items and does not mean the feature should be removed. Cost assessments are architectural estimates, not measured monthly spending or guarantees about future contributor availability. M31 is a separately governed aerospace/autonomous/defence programme. Excluding it gives **40 framework themes: 25 A and 15 B**, matching the master roadmap's 40-theme horizon. A partial theme may include substantial working software and years of optional extensions. Counts do not measure effort or completion percentage. Supervision is cross-referenced under the education/privacy themes; Media belongs to M17. They are not added again as duplicate milestones. The assignment concerns the *next investment*. Existing supported functionality still receives defect/security maintenance even when its expansion is in B. For broad A themes, only the bounded scope below has priority; selecting a row does not select every research idea in its original wording. ## A — invest and maintain | ID | Priority scope | Why it is worth the maintenance cost | | :--- | :--- | :--- | | M1 | CLI and supported generator/blueprint combinations | A broken generated application prevents adoption; preserve real compilation and behavior checks. | | M2 | Measured build/reload improvements | Reduce repeated developer/CI cost; keep supervised restart and defer a stable dynamic Rust ABI. | | M3 | Feature boundaries, escape hatches and migrations | Keep upgrades reversible and optional capabilities out of small applications. | | M4 | Resource generator and local error console | Maintain the bounded existing implementation; autonomous modification belongs to deferred M37. | | M5 | Navigable, executable documentation and typed API contracts | Users need accurate examples and errors more than additional undocumented surfaces. | | M6 | Reliable relational ORM, transactions and supported adapters | Prioritize data integrity and recovery; do not bundle transparent replication into this scope. | | M9 | Sessions, account recovery, passkey assurance and revocation | Authentication failures directly affect users; close named security/conformance gaps before enlarging identity scope. | | M10 | Validation, bounded requests, mail and shared rate limits | Improve common SaaS paths with reusable contracts and explicit backend failures. | | M11 | SaaS administration and supported billing journeys | Stabilize actual Academy/SaaS needs; do not equate this with universal gateways or the whole Omni vision. | | M12 | Security, tenant isolation, secrets and supply-chain assurance | Continuous prevention and repair have broad value; additional tools need a concrete uncovered risk. | | M14 | Accessible HTMX/SSR and clear frontend interoperability | Improve the primary supported experience before full Leptos/Dioxus integration. | | M15 | Durable jobs, scheduling and one supported remote broker | Finish recovery, retries, quotas and operations before adding a broker catalogue. | | M17 | Private files, resumable upload and course video | Directly serves SaaS/Academy; retain access, expiry and recovery checks. A package marketplace is not part of this priority. | | M18 | Recoverable server-driven UI | Maintain the admitted authorization/reconnect journey before adding a new rendering ecosystem. | | M19 | Honest runtime telemetry and standard exports | Preserve implemented diagnostics; unavailable sources must not become fabricated values. | | M26 | Safe deployment guidance and read-only diagnostics | Help operators find configuration mistakes and rehearse rollback without automatic infrastructure mutation. | | M27 | Readiness, draining and deployment scaffolds | Maintain the bounded implementation and actual proxy/application contracts. | | M28 | Existing typed dependency injection | Keep its API small and tested; no new DI framework is necessary for release. | | M29 | Accurate OpenAPI and its playground | Prefer schema/transport fidelity over adding UI controls around incomplete contracts. | | M32 | Axum/Tower interoperability and precise macro errors | Preserve existing escape hatches and diagnosability. | | M33 | Current-state SaaS entitlements | Access must reflect current account, tenant, plan and payment state; syntactic convenience comes later. | | M34 | One dependable TypeScript SDK profile | Maintain serialization and migration evidence; additional React/Dart/Swift targets require users and tests. | | M35 | Standard trace propagation/export and useful diagnosis | Build on existing OpenTelemetry collectors; a custom durable observability service is not a prerequisite. | | M40 | One bounded Academy Labs profile | Product value is concrete, but untrusted execution needs independent isolation review. The runner candidate was removed from 13.0; applications bring their own runner through the documented contract. Labs need not block the rest of v13. | | M41 | Practical privacy and proportional age policies | Preserve minimal data, explicit decisions, consent and rights workflows. Supervision remains transparent and scoped; declarations/signals do not prove age, guardianship or cheating. | M4, M19, M27, M28 and M32 are the five already-implemented labels. The other twenty A themes need scoped decisions, maintenance or further work; none is a blank cheque to finish all of its historical ambitions. Within A, use this order: security/data-loss and stable regressions first; authentication, tenant boundaries and billing correctness next; migrations, recovery and usable documentation next; then product expansion supported by Academy/SaaS demand. Run one bounded feature effort at a time alongside the stable maintenance lane. Reassess after each completed journey. The [Nexus application integration plan](https://rullst.github.io/Rullst/book/nexus-integration-plan.html) refines M11 with host-session authorization, read-only capabilities, strict-CSP assets, localization and explicit data ownership. It also identifies a separate M32 boolean-attribute ergonomics proposal. These are scoped follow-ups within the existing counts, not delivered APIs or additional top-level milestones. ## B — defer expansion until justified | ID | Deferred expansion | What would justify revisiting it | | :--- | :--- | :--- | | M7 | Portable edge runtime and distributed-data expansion | A named deployment target and application need, with vendor-specific consistency and rollback evidence. | | M8 | Intent-based modelling and index recommendations | A measured database problem; begin with explainable, reviewed advice, never unattended production DDL. | | M13 | Post-quantum web architecture and plugin containment | A concrete protocol/threat model, maintained audited primitives and an interoperability programme. | | M16 | Wasm islands and broad hydration/component packaging | A user journey that SSR/HTMX cannot reasonably meet and a supported browser/toolchain matrix. | | M20 | New immutable ledger/event-streaming engine | A consistency/recovery requirement unmet by established storage, plus a funded maintenance owner. | | M21 | Broader Omni/mobile/offline platform | Omni was retired in 13.0.0-alpha.3 in favour of installable PWAs (`make:pwa`, Google Play TWA, Microsoft Store via PWABuilder). Native capabilities need a real mobile product with physical-device, secure-key, networking and store acceptance resources. | | M22 | Agentic infrastructure provisioning | A named operator need and reviewed plans, scoped credentials, audit and rollback; advisory diagnostics already fit A. | | M23 | Auto-healing production code or databases | A safely bounded, approved repair contract; no general autonomous mutation promise. | | M24 | Additional IoT transport/firmware/hardware integrations | Selected boards, actual devices and transport/rollback interoperability tests. Preserve existing no_std and manifest contracts. | | M25 | Embassy integration | Stable hardware/transport boundaries and a maintained target application. | | M30 | Dedicated gRPC crate and broader scaffolding | A concrete service consumer and protobuf compatibility matrix. The `make:grpc` generator was removed in 13.0.0-alpha.4. | | M31 | Aerospace, autonomous vehicles and defence | A separate project with domain expertise, governance, hardware and applicable certification. | | M36 | Natural-language database copilot | Concrete operator demand and a read-only, bounded, explainable contract; production writes remain separate. | | M37 | AI autofix workflow | A confined local patch/review/test/rollback journey with demonstrated value over existing development tools. | | M38 | Generic local read replicas and background replication | A named database's supported semantics and recovery guarantees; no generic transparent-replication claim. | | M39 | Rullst's own gateway/load balancer | Abandoned on 8 October 2026: generated Caddy/nginx configuration for established proxies replaces it. Revisiting it would need a new maintainer decision. | “Deferred” means lower expected return under the current product, budget and validation constraints, not that the idea is intrinsically bad. An explicit user requirement can change this ranking. Unsupported guarantees such as automatic worldwide legal compliance, zero leakage or general autonomous safe mutation are not future deliverables to promise at any budget. ## C — high maintenance cost, including existing implementations The adopted [crate-level decisions](https://rullst.github.io/Rullst/book/v13-maintenance-scope.html#crate-level-investment-decisions) refine this theme-level assessment without changing its counts. Their [transition sequence](https://rullst.github.io/Rullst/book/v13-maintenance-scope.html#transition-sequence-and-acceptance) governs future scope reductions and extractions. The source already contains at least a bounded implementation in each theme below. “Existing” includes v13 prerelease code and does not imply a complete milestone or a stable crates.io release. The primary repeated costs are provider drift, supported combinations, security review and recovery evidence, not merely writing the first implementation. | Theme | Existing surface requiring care | Recurring cost and how to contain it | | :--- | :--- | :--- | | M1 — CLI/scaffolds | Multiple blueprints, database selections and installed generators | Changes multiply into generated applications. Maintain a declared supported matrix and shared templates; add combinations only for a user need. | | M2 — build/reload | Cross-platform process supervision, filesystem behavior and compiler/toolchain integration | OS-specific regressions and repeated native compilation. Preserve reproducible targeted tests and measure build versus queue time. | | M6 — persistence | Relational and heterogeneous database/native adapters | Different transactions, types, consistency and native libraries. Keep adapter boundaries explicit and avoid universal parity promises. | | M9 — authentication | Passwords, sessions, recovery, tokens and passkey state | Security-sensitive lifecycle, standards and negative/concurrent cases. Prefer maintained primitives and one coherent supported identity path. | | M10 — mail/protection | Multiple external transports, provider feedback and shared limits | Provider schemas, auth, quotas and delivery behavior change. Use common contracts while retaining provider-specific assertions and capability limits. | | M11 — payments/SaaS | Multiple payment adapters, signed webhooks, subscriptions and entitlements | Remote semantics, API changes, retries and money/state reconciliation. Prioritize used methods and never claim untested uniform gateway coverage. | | M12 — security/assurance | WAF/DLP, tenant policy, secrets, audits and verification tooling | Threat/advisory changes and tooling/fixture upkeep. Keep a risk-to-test map and prevent duplicated or unbounded campaigns. | | M15 — async delivery | Local/durable queues, retry/leases/outbox and Redis profile | Failure ordering, replay, shutdown and backend recovery. Finish one durable journey before broadening brokers. | | M17 — files/video | Private storage, multipart and Bunny browser/provider lifecycle | Upload interruption, signing/expiry, notifications, SDK and browser changes. Bound supported protocols and separate fixture evidence from live interoperability. | | M21 — native delivery | Desktop/mobile scaffolding and Android signing/artifact verification | SDK, OS, signing and device changes. Maintain current guarantees but defer broader device/offline promises without test resources. | | M40 — Labs | Trusted orchestration and the bring-your-own-runner contract | Contract stability, leases and grading. Execution, OS/toolchain changes and isolation review belong to the application-owned runner; keep one narrow profile and never trade containment for convenience. | | M41 — privacy/supervision | Age/consent/replay and transparent observation contracts | Sensitive state, retention, authorization and evolving product/legal requirements. Minimize data and keep application/jurisdiction obligations explicit; no automatic legal certification. | PQC, embedded hardware, autonomous production changes, generic replication and a new gateway also have potentially high *future* maintenance costs. Their B status means those costs are not accepted merely by keeping the ideas on a list. Other themes still need maintenance; omission from C does not label them free or low-risk. Before enlarging a C surface, record a named product need, supported boundaries, repeatable acceptance, a person/team responsible for reviewing incidents and updates, and a fallback if that support is unavailable. Prefer freezing new scope or deprecating with migration notice over silently abandoning a supported capability. A feature's past implementation cost is not a reason to keep adding expensive options without users. ## Eight concrete follow-up suggestions These are proposed **subtasks**, several already implicit in the detailed roadmaps. They do not create eight additional top-level milestones and are not automatically authorized implementation work. | Proposal | Category and parent | Bounded useful result | | :--- | :--- | :--- | | S1 — downstream regression fixtures | A; M1/M3/M11 | Compile and exercise sanitized SaaS/Academy fixtures against a candidate release, including upgrades and denied access, without production data or provider accounts. | | S2 — application recovery rehearsal | A; M6/M11/M15 | Restore a disposable application snapshot and verify accounts, tenant isolation, durable jobs and billing idempotency. Document quiescence, keys and restore order; this does not become a managed backup service. | | S3 — reusable authorization test scenarios | A; M9/M12 | Offer a small application-test recipe or helper for anonymous/user/admin and two-tenant negative cases, using actual routes. Do not treat a static scanner as proof. | | S4 — resumable maintenance handoff | A; M1/M3/M12 | Keep a concise source-bound receipt of supported versions, unresolved findings, exact CI evidence, next commands and session/build cost. Reuse existing reports so later, less frequent sessions need not repeat the investigation. | | S5 — first-party facial age engine | B; M41 | Consider only with a suitable dataset, accuracy/bias and attack evaluation, capture/privacy controls, alternative paths and a long-term maintainer. Continue policy/attestation support independently. | | S6 — additional Labs languages | B; M40 | Add one demanded protocol profile only after a reviewed application-owned runner proves the first profile operationally, with separate toolchain/resource/threat tests. | | S7 — accessible, localized primary journeys | A; M5/M14 | Exercise keyboard focus, form errors, captions and translation/locale boundaries on the chosen auth, checkout and learning journeys. Reuse the existing LMS groundwork; automated checks do not establish universal accessibility or translation quality. | | S8 — per-tenant resource budgets | A; M10/M11/M15 | Define explicit quotas/backpressure for stored bytes, concurrent jobs, mail and AI usage, with atomic claims and visible rejection. Extend existing bounded contracts only where an application gap is demonstrated; no automatic provider-bill guarantee. | S4 and stable defect/compatibility maintenance are the immediate cost-control priorities. S1–S3 should be selected by an observed gap, not implemented as duplicate test frameworks. S5 and S6 illustrate valuable possibilities with substantial continuing costs; neither belongs on the release critical path. S7/S8 can add practical user value without another standalone subsystem, but must have a bounded accepted contract before implementation. These proposals comprise **six A subtasks and two B subtasks**. S5/S6 would also carry C; S1/S2/S8 can become expensive if allowed to grow into an unbounded platform matrix. Do not report “49 independent milestones”: the eight proposals refine the existing 41. Promote a truly distinct need to a new canonical milestone only after checking overlap and documenting who will use and maintain it. A richer framework delivers reliable useful journeys; raw crate or checkbox counts are not the success criterion. ## Comparative evaluation adopted on 23 September Use the [framework reference and evaluation plan](https://rullst.github.io/Rullst/book/v13-framework-comparison.html) to study Loco/Laravel productivity, Django security/documentation and Axum composition, with other frameworks as targeted references. Measure development effort, failures, performance and maintenance on equivalent application journeys. This refines existing A themes and S1-S3; it does not add milestones or change the 25/16 counts. The 25 A themes are not prerequisites for the first comparison. After the active v12.1.1 maintenance work, start with a small usable SaaS journey, establish a baseline and use the findings to choose subsequent improvements. The first Rullst-side acceptance contract is the opt-in `cli-saas-journey` diagnostic described in that plan. Broader evaluation can accompany later v13 checkpoints. Keep the same acceptance contracts, record unfavorable and inconclusive results, and distinguish AI-assisted measurements from independent user evidence. Only free resources and disposable offline fixtures are authorized; no superiority is established by this planning decision or the internal quality scorecard. ## What makes v13 ready Do not wait for all 41 themes, all 25 A themes or a percentage of this list. Choose a bounded release contract, document intentional breaking changes and prove its supported user journeys, migration/recovery, security, packaging and protected publication. Existing admitted work should be completed and hardened before unrelated expansion. Any deferred capability stays disabled or clearly scoped; a known security or data-loss defect is not an optional future feature. An optional `13.0.0-alpha.N` can collect explicit evaluation feedback while stable users remain on supported v12. It does not resolve registration, independent-review or safety blockers. Further compatible capabilities can ship in 13.1 and later minors; unfinished roadmap ideas do not all require v14. Before an extended pause, leave stable maintenance and candidate code with their actual evidence, pending jobs and next actions recorded. Prefer a small recoverable checkpoint over another half-finished subsystem. This approach supports intermittent work without promising continuous model availability or unfunded indefinite maintenance. --- # v13 Maintenance Scope & Extension Strategy Source: https://rullst.github.io/Rullst/book/v13-maintenance-scope.html # v13 maintenance scope and extension strategy **Decision adopted on 22 September 2026:** concentrate Rullst's investment on secure, productive application development and keep specialized capabilities within explicit maintenance boundaries. Reduce scope before removing useful software. Code generation by an AI assistant does not remove the need for maintenance, integration evidence or security review. This is an approved planning direction, not an implemented package split or a deprecation notice. It leaves the published v12.1.1 release, supported APIs, defaults, MSRV and current publication inventory unchanged. Specific extractions, provider selections and retirements need their own reviewed changes. The [SST](https://rullst.github.io/Rullst/book/spec.html) describes the current architecture; the [compatibility policy](https://rullst.github.io/Rullst/book/compatibility-policy.html) governs transitions. **Direction reaffirmed on 24 September 2026:** prioritize measured maintenance cost and coherent application journeys before expanding the adapter catalogue. The next planning deliverable is a decision record per integration, beginning with Capital, specialized database adapters and the Labs runner. Retain useful maintained implementations; an interface without a usable implementation can transfer both development work and security mistakes to every application. This direction can improve the conditions for broad adoption by concentrating effort on reliability, predictable upgrades and developer experience. It is not evidence of a global framework ranking. Use the [comparative evaluation plan](https://rullst.github.io/Rullst/book/v13-framework-comparison.html), independent feedback and application adoption to assess progress. Feature count and mutation scores alone do not establish superiority. ## How to assess maintenance cost The assessments below are qualitative, not measured engineering hours or monthly costs. They include v13 prerelease candidates. Review each capability using its actual user journey and these recurring obligations: - external API, protocol, operating-system and toolchain changes; - supported database, platform, blueprint and feature combinations; - consequences of security, data-integrity and recovery failures; - dependency updates, diagnostics, migrations and documentation; - repeatable acceptance and any outstanding provider or independent review; - a named person or team able to review incidents and maintain the contract. Source size and crate count alone are poor cost measures. A small untrusted-code runner can require more operational assurance than a larger template library. An optional dependency reduces the consumer's selected build surface; it does not eliminate the project's support obligations. Moving code to another repository saves work only when scope, ownership or release coupling changes. For each integration, record its actual consumer, maintained operations, dependency/API versions, compatibility matrix, validation gaps and accountable maintainer. Measure update/review effort, regressions and CI execution separately from queue time; leave unknown costs explicitly unknown. Recommend one of: - retained official support with a bounded expansion scope; - specialized optional extension with an explicit maintenance/release owner; - an application/community integration with a tested contract and transition; - deprecation with a replacement and compatibility plan, when justified. Provider release frequency alone does not decide the category: a compatible upstream release need not require an adapter rewrite, while a rarely changing executor can still carry substantial security obligations. Evaluate independent adapter releases where stable interfaces and dependency ranges allow them; a provider correction should not unnecessarily force unrelated framework releases. This requires an actual packaging, compatibility and release-workflow migration, not just a new repository name. Security support for shipped capabilities remains in force throughout that transition. ## Measured cost snapshot (13.0.0-alpha.3) Measured on 29 October 2026 at the `main` that follows the 13.0.0-alpha.3 publication. These are proxies for recurring work, not hours: each line of production code, each mutant and each fuzz target is something every release must keep compiling, tested and admitted. Hosted CI minutes per crate and the time spent on reviews and incidents were not measured; they stay unknown. | Package | Tier | Production lines | Test lines | Tests | Mutants | Fuzz targets | Dependencies | Commits (60 days) | | :--- | :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | `cargo-rullst` | Core | 66,544 | 36,138 | 1,212 | 9,454 | 0 | 307 | 520 | | `rullst-core` | Core | 41,987 | 34,002 | 1,193 | 5,494 | 0 | 287 | 398 | | `rullst-orm` | Core | 21,871 | 26,402 | 487 | 2,414 | 5 | 304 | 258 | | `rullst-security` | Core | 12,178 | 7,426 | 404 | 1,842 | 7 | 248 | 119 | | `rullst-orm-macros` | Core | 11,504 | 2,376 | 138 | 542 | 0 | 4 | 133 | | `rullst-macros` | Core | 3,084 | 2,165 | 77 | 0¹ | 0 | 11 | 36 | | `rullst` (facade) | Core | 560 | 4,733 | 133 | 10 | 12 | 525 | 174 | | `rullst-connect` | Extension | 13,796 | 12,484 | 391 | 1,200 | 3 | 242 | 88 | | `rullst-ai` | Extension | 13,529 | 6,068 | 232 | 2,131 | 3 | 231 | 72 | | `rullst-studio` | Extension | 11,893 | 5,637 | 157 | 17¹ | 1 | 302 | 103 | | `rullst-mail` | Extension | 11,750 | 8,347 | 272 | 1,735 | 4 | 306 | 131 | | `rullst-capital` | Extension | 11,157 | 6,342 | 214 | 1,782 | 1 | 311 | 140 | | `rullst-auth` | Extension | 10,236 | 12,251 | 244 | 1,818 | 0 | 277 | 72 | | `rullst-messaging` | Extension | 8,830 | 9,156 | 176 | 1,576 | 0 | 205 | 57 | | `rullst-nexus` | Extension | 7,422 | 9,455 | 239 | 920 | 1 | 326 | 135 | | `rullst-media` | Experimental | 4,397 | 3,882 | 60 | 874 | 0 | 160 | 28 | | `rullst-labs` | Experimental | 3,785 | 3,274 | 78 | 558 | 0 | 110 | 43 | | `rullst-privacy` | Experimental | 3,482 | 4,392 | 102 | 562 | 2 | 134 | 35 | | `rullst-supervision` | Experimental | 3,113 | 3,769 | 66 | 598 | 0 | 95 | 24 | | `rullst-iot` | Experimental | 2,593 | 1,395 | 83 | 307 | 3 | 34 | 37 | ¹ Most of the crate is excluded from mutation with `mutants::skip`, so the listed count understates its test obligation. **By tier**, the share of the workspace totals (263,711 production lines, 5,958 tests, 33,834 mutants, 42 fuzz targets): | Tier | Production lines | Tests | Mutants | Fuzz targets | Commits (60 days) | | :--- | ---: | ---: | ---: | ---: | ---: | | Core | 60% | 61% | 58% | 57% | 63% | | Extension | 34% | 32% | 33% | 31% | 31% | | Experimental | 7% | 7% | 9% | 12% | 6% | What the numbers show: - **The CLI is the largest single cost.** `cargo-rullst` alone holds a quarter of the production code and 28% of the mutants, and changes most often, because every generator multiplies database, blueprint and platform combinations. Reducing generator combinations saves more than retiring any optional crate. - **The experimental packages are a small share.** Together they are about 7% of the code and 9% of the mutants. Narrowing their promise lowers support obligations more than CI work; retiring them would not change the release cost much. - **External services are concentrated in four Extensions** (`rullst-connect`, `rullst-mail`, `rullst-capital`, `rullst-ai`): about 19% of the code, and the part whose correctness depends on providers changing their APIs. Each new provider adds to this group. - **Two production applications (October 2026) enable** the facade with `orm`, `strict-postgres`, `nexus` and `auth`, plus `rullst-connect`. The packages neither application enables directly (messaging, AI, Capital, mail, Studio and the five experimental packages) are about 28% of the code. That is evidence of current use, not a reason to retire a package by itself. How the columns were measured: Rust lines under each package, excluding `target/` and `fuzz/` (test lines are `tests/`, `benches/`, `*_tests.rs` and `tests.rs`); tests are `#[test]`, `#[tokio::test]` and `#[rullst_orm::test]` attributes; mutants are `cargo mutants --list -p <package> --all-features`; dependencies are the unique packages in `cargo tree -p <package> --all-features -e normal`, without the package itself; fuzz targets are the `[[bin]]` entries of `<package>/fuzz/Cargo.toml`; commits are `git log --since=60.days -- <package>`. ## Crate-level investment decisions | Crate or area | Maintenance assessment | Adopted direction | | :--- | :--- | :--- | | `rullst-core`, `rullst`, `rullst-macros` | Essential shared surface with substantial compatibility and security impact. | Maintain explicit HTTP/lifecycle contracts, safe defaults, useful diagnostics and Axum/Tower interoperability. Keep specialized domains optional and avoid expanding the kernel into a general infrastructure platform. | | `cargo-rullst` | High: generated applications multiply database, blueprint, platform and installer combinations. | Retain as a central productivity investment. Share templates, declare the supported matrix and validate complete generated journeys. Defer additional combinations until there is demand and acceptance capacity. | | `rullst-orm`, `rullst-orm-macros` | High: transactions, schema changes, database differences and native dependencies. | Prioritize dependable relational persistence and migrations. Treat specialized stores as explicit adapters; do not promise universal parity or generic replication. | | `rullst-auth`, `rullst-security`, `rullst-connect` | High and necessary: failures affect application identity, secrets and tenant boundaries. | Maintain narrow, coherent contracts using established primitives. Reduce duplicated responsibilities and close documented gaps before expanding providers or security-product ambitions. | | `rullst-messaging` | High: leases, retries, duplicates, outbox consistency and backend recovery. | Maintain bounded durable delivery and supported backends. Additional brokers need a demonstrated application requirement and their own recovery evidence. | | `rullst-capital` | High: provider-specific money/state semantics, reconciliation, subscriptions and fiscal expansion. | Preserve reusable SaaS billing: the provider-neutral base plus Stripe and an experimental InfinitePay adapter. The other adapters, payouts and NFS-e preparation were removed from 13.0 as described below. | | `rullst-mail`, `rullst-ai`, `rullst-media` | Medium to high: external services, feedback/events, transport changes and provider-specific behavior. | Maintain common contracts and adapters justified by actual use. Prefer interoperable protocols where appropriate, while retaining provider-specific assertions and explicit unsupported operations. Mail keeps Resend, AWS SES, SendPulse and SMTP in 13.0, as described below. | | `rullst-privacy` | Bounded current foundations; broader legal or biometric promises would create high continuing cost. | Keep reusable consent, minimal-data and proportional age-policy mechanisms. Provider attestations remain separate from native declarations; global legal certification and a first-party facial model are not part of this direction. | | `rullst-supervision` | Specialized, sensitive application state; models, capture and operational expansion raise the cost. | Keep transparent observation contracts as an optional education/parental extension. Product workflows, verified relationships, media models and human review belong to the application or separately governed integration. | | `rullst-labs` | Bounded trusted orchestration with sensitive authorization, leases and grading contracts. | Keep separate from execution: bring your own runner through the documented [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html). Retain one useful Academy profile without making broader language or exercise support a framework release prerequisite. | | Labs execution (former `rullst-labs-runner`) | Very high security and operational burden despite its small source size. | **Removed from the 13.0 workspace** (30 September 2026); its source remains in git history. Applications deploy and review their own runner against the controller contract. Never move execution into the web process or weaken containment to simplify maintenance. | | `rullst-studio`, `rullst-nexus` | Medium, increasing with operational dashboards and autonomous administration. | Preserve useful local diagnostics and authorized administration. Avoid growth into a complete security-operations or autonomous infrastructure product. | | `rullst-iot` | The current helper surface is small; broad hardware and transport support would be expensive. | Preserve existing contracts and defer hardware expansion until there are named users, target devices and interoperability evidence. Treat future expansion as a specialized extension. | These decisions refine the existing [A/B priorities and overlapping C cost marker](https://rullst.github.io/Rullst/book/v13-priorities.html). They do not add milestones, change the 41-theme count or label an expensive capability dispensable merely because it is expensive. ## Capital: retain billing, contain the responsibility **Owner-selected v13 scope, 8 October 2026** (superseding the 24 September selection of Stripe and Paddle): `rullst-capital` keeps a maintainable provider-neutral base and two adapters. The base is the billing traits, checkout/subscription/entitlement/quota/usage contracts, billing analytics, invoices, canonical webhook verification with replay protection, the Axum and Actix adapters and the offline mocks. Crypto integrations are excluded from this release. | v13 provider | Reason and current boundary | | :--- | :--- | | Stripe | Supported. Existing SaaS use and the generated durable billing integration. Preserve the reviewed operation boundaries rather than promising every Stripe product. | | InfinitePay | Experimental until validated against a live account. Offline fixtures only; live checkout and callbacks fail closed until a reviewed authoritative pricing and callback-authentication contract exists. | Removed from the 13.0 workspace (8 October 2026; the source remains in git history and in the 12.x line): the Paddle, Lemon Squeezy, Polar, Razorpay, Mercado Pago, Alipay, Coinbase Commerce and PicPay adapters, the Wise payout adapter with the payout contracts, the generated Paddle billing candidate and the NFS-e preparation module (`nfse` feature). NFS-e was never validated with a real municipality; it may return as a separate product outside Rullst. Applications integrate other gateways by implementing the provider traits, as shown in [Writing your own payment provider](https://rullst.github.io/Rullst/book/capital-custom-provider.html); the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#changes-from-the-published-1210-source) lists the removed names. Do not silently redirect existing customers or subscriptions to another provider. Keeping these two adapters still incurs upstream API and security maintenance. The framework owns bounded protocol validation and reference lifecycle tests; applications own credentials, catalog setup, billing-owner permissions, business rules and deployment-specific operation. An adapter is not a promise to implement every provider product. Generated reference flows must reuse storage/security foundations and retain explicit limits, without growing into a full billing platform. Capital remains an optional feature. An application-specific need does not automatically justify another official adapter. Crypto research is deferred beyond v13. Preserve applicable billing extension contracts, but do not add a crypto adapter, universal blockchain API, custody service or new crypto-specific release gate. The former Coinbase Commerce adapter was removed with the other excluded providers. Two findings remain relevant if a future application requests this: - Coinbase's [Commerce transition notice](https://help.coinbase.com/en/transitioning-from-coinbase-commerce-to-coinbase-business) sets 31 March 2026 as the Commerce shutdown deadline. Its [Business availability page](https://help.coinbase.com/en/coinbase/other-topics/business/business-overview) currently lists eligible receiving businesses in the United States and Singapore. Existing Commerce code is not a Business API implementation. - [BTCPay Server](https://docs.btcpayserver.org/FAQ/General/) is an optional evaluation candidate, not a selected Rullst provider. Its [altcoin FAQ](https://docs.btcpayserver.org/FAQ/Altcoin/) separates the core Bitcoin scope from community integrations. Its [August 2026 advisory](https://blog.btcpayserver.org/security-advisory-btcpay-server-2-4-2/) records an exploited LND credential vulnerability fixed in 2.4.2. This dated reference does not identify the latest release or certify another deployment. Future evaluation must include current advisories, exact plugin/engine versions and operator maintenance, not merely API access. Plan three explicit responsibilities, using the existing implementation as the starting point rather than rewriting it: 1. **Reusable billing contracts:** amounts/currencies, supported checkout and subscription state, current entitlements, idempotency and safe event processing. Keep authorization and money/state integrity reusable instead of asking every generated application to reconstruct them. 2. **Provider adapters:** document capabilities per operation and provider, version assumptions, signature verification, duplicate/out-of-order events, ambiguous outcomes and reconciliation. Select the actively supported expansion scope from real product demand and available validation. A common trait must not imply that every provider supports the same operations. 3. **Fiscal and specialized finance:** outside the framework. The bounded NFS-e preparation was removed from 13.0 without live validation; fiscal work may return as a separately maintained product with its own lifecycle. These responsibilities need not produce one crate per provider. Decide whether modules, features, packages or repositories provide a measurable benefit after checking dependencies, downstream consumers and release costs. Independent versioning is a future migration decision, not a property already implemented. The 12.x line keeps its published support obligations for the removed adapters. In v13 each removed name has a migration row and an assisted-upgrade finding, and the replacement is an application-owned adapter on the retained contracts. The [deprecation policy](https://rullst.github.io/Rullst/book/compatibility-policy.html#deprecation-and-removal) also expects `#[deprecated]` markers in a released 12.x minor before a stable API is removed. By owner decision on 8 October 2026, the 12.3.0 minor release marks the Capital and Mail APIs removed here `#[deprecated]`, pointing to their migration rows, before 13.0.0 ships. The assisted-upgrade findings and the fail-closed configuration errors apply in addition. Tests with deterministic mocks establish local contracts, not actual provider interoperability. The prohibition on real-account tests remains in force; outstanding external evidence stays explicitly pending. The recurring obligations are concrete: Stripe documents duplicate events, delivery ordering and retries in its [webhook guide](https://docs.stripe.com/webhooks), and version transitions in its [API upgrade guide](https://docs.stripe.com/upgrades). Other providers require their own evidence rather than inferred equivalence. ## Mail: four delivery transports **Owner-selected v13 scope, 8 October 2026:** `rullst-mail` keeps the Resend, AWS SES, SendPulse and SMTP transports, the `log`, `memory` and offline `mock_*` development drivers, and the shared infrastructure they use: failover, the REST helpers, the pre-flight pipeline, attachment inspection, suppression, observations, verified Resend feedback and queued delivery. Removed from the 13.0 workspace (the source remains in Git history and in the 12.x line): the SendGrid, Postmark, Mailjet, Mailtrap and Azure Communication Services transports with their `MAIL_DRIVER` values and settings. Upgrades must not lose mail or data silently. A configuration that still names a removed driver fails facade resolution with a typed `MailError::ConfigError` that names the driver and the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#changes-from-the-published-1210-source) row "Mail providers removed"; it never falls back to a development driver. Jobs queued by 12.x carry no provider name and are delivered by the upgraded worker's configured driver, or fail visibly and stay retryable. Suppression rows and delivery observations keep their provider labels and stay readable. Applications integrate other providers by implementing `MailDriver`, as shown in [Writing your own mail transport](https://rullst.github.io/Rullst/book/mail-custom-transport.html). Like the Capital removal, these transports are marked `#[deprecated]` in 12.3.0 before 13.0.0 ships. Tests with deterministic mocks establish local contracts, not provider interoperability. ## Turso: assess a complete application journey before universal parity **Assessment on 24 September 2026; implementation is not scheduled here.** SQLite, PostgreSQL and MySQL/MariaDB are the principal SQLx ORM backends. MariaDB shares the MySQL driver but has its own executable container contract. This does not imply every optional crate or generated application supports all four databases: some domain stores deliberately have a SQLite-only or PostgreSQL-specific implementation. The current Turso/libSQL path already supplies typed CRUD, equality filters, ordering, bounded pagination/counts, explicit SQL transactions and reversible checksummed migrations. Blank/API generation and selected generators have bounded acceptance; the remote matrix runs against an official libSQL server. SQLx-specific relations/hooks, schema auto-diff, seed generation, full blueprint parity and transparent replica synchronization remain outside that profile. See the [current persistence contracts](https://rullst.github.io/Rullst/book/polyglot-persistence.html). The remote adapter owns an HTTP/Hrana protocol implementation; local offline execution uses SQLite through SQLx. Reusing SQL syntax does not make those transports, row codecs, transactions or generated consumers interchangeable. The current [official Rust guide](https://docs.turso.tech/sdk/rust/quickstart) also distinguishes libSQL, the newer Turso Database engine, remote clients and local-first sync. The existing libSQL evidence does not validate every newer engine or sync configuration. Select an engine/protocol/version profile before expanding support, and preserve Rust 1.96.0 compatibility. | Potential scope | Qualitative maintenance assessment | | :--- | :--- | | Maintain the current bounded libSQL profile | Moderate: existing transport, macro, migration and local/remote acceptance obligations remain. | | Complete one named SaaS journey on one selected profile | Medium to high initial effort; recurring work can be bounded by an explicit operation and platform matrix. Inventory actual Auth, billing, tenant, migration and recovery dependencies first. | | Match every ORM feature, domain store and blueprint across remote/local/replicated modes and both engine families | High to very high: multiplies implementation and recovery/consistency validation, not just connection strings. | These are engineering judgments from current boundaries, not measured hours or update-frequency forecasts. Retain useful Turso support. If an actual SaaS or Academy requirement selects expansion, implement the smallest complete journey and review its recurring cost before adding another profile. Validate atomic updates/rollback, tenant isolation, concurrency, uncertain network outcomes and schema changes. Keep replica freshness and authoritative authorization/payment decisions explicit. Local/container evidence remains distinct from managed Turso Cloud acceptance; real-account tests are still prohibited in this session. ## Education, supervision and Labs remain supported use cases Rullst remains a foundation for complete education, parental-supervision and exercise platforms. Preserve the useful domain capabilities already implemented in the optional packages, alongside authentication, tenant boundaries, storage and durable work. Their current candidate status and evidence limits still apply; this decision does not promote prerelease code to a stable guarantee. | Layer | Responsibility | | :--- | :--- | | Framework foundations | Reusable identity, authorization helpers, persistence, queues, request protections and explicit extension contracts. | | Optional domain packages | Existing Supervision observation/session/collection contracts and Labs exercise/job/lease/grading contracts, maintained within their declared boundaries. | | Application | School or family workflows, authenticated membership and verified relationships, user interfaces, review decisions, exercise content, grading policies and model/provider selection. A developer or assistant may implement these through the documented contracts. | | Separately deployed execution | An application-owned, reviewed runner implementing the Labs [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html), with operator-owned isolation/resources for untrusted submissions. The application must not improvise execution in the web process. | Keep small tested examples and supported integration paths where they serve actual users. Developers should be able to compose existing capabilities rather than recreate sessions, leases or observation protocols. They still own domain authorization and deployment obligations. Observation signals do not establish cheating, and an AI-written integration does not prove model accuracy or safe code execution. Each deployed Labs runner still needs its own isolation review. ### Labs and the runner have different maintenance responsibilities `rullst-labs` keeps the trusted exercise, authorization, submission/job, cancellation, receipt-verification and grading contracts. It does not execute the learner's program. A runner is the separately deployed program that takes authorized work, compiles/executes the submitted code under enforced limits, returns a bounded authenticated result and tears down the execution environment. Trusted grading and expected answers remain outside the untrusted worker. Two kinds of separation must not be confused: - **Execution isolation is mandatory today.** Student-controlled code can loop, exhaust resources or attempt to read data. Run it in the separately restricted worker without application secrets and enforce CPU, memory, time and output limits. Merely putting code in another crate or process is not adequate containment; the runner's full observed isolation controls remain required. - **The runner's maintenance is no longer a framework obligation.** The Linux/compiler/sandbox matrix has different deployment and review obligations from an ordinary web library. This plan originally considered a separately maintained official runner in the same repository. **Decision of 30 September 2026:** the experimental `rullst-labs-runner` candidate is removed from the 13.0 workspace and is not published; its source remains in git history. `rullst-labs` becomes a bring-your-own-runner library: it keeps the trusted exercise, authorization, encrypted job, fenced lease, cancellation, retention and pinned-receipt contracts, and documents the [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html) an application-owned, separately deployed runner must implement, with a minimal non-executing example. The application operator now owns the runner's isolation, review, deployment and updates. Isolation evidence must match the deployed toolchain and image. `rullst-labs` still declares `13.0.0-alpha.1` with publication disabled and is not part of any published 12.x release. (Labs later joined the release inventory and was first published in 13.0.0-alpha.1 on 11 October 2026.) There is one wire protocol and no adapter layer; interchangeable external executors are not implemented. The removed candidate's [first-profile record](https://rullst.github.io/Rullst/book/labs-first-profile.html) remains a reference design, not a supported runner. The current profile is a restricted Rust pure-function exercise named for Wasm on Linux. It does not run arbitrary Rullst projects or support every language. Do not imply production support for any runner, and do not ask applications to recreate isolation with an unrestricted process launcher. ## What belongs in the framework in an AI-assisted workflow Prioritize explicit APIs, typed errors, safe defaults, version-matched executable documentation, inspectable generators and predictable upgrades. Keep reusable authentication, tenant authorization, payment verification and data-integrity contracts centrally tested. AI-generated application code must still satisfy those contracts; an assistant is not a replacement for their review. Application-specific forms, reports, pages and business workflows are good candidates for generated or manually written application code. Provide extension points and small tested examples rather than maintaining every possible product inside the framework. Application owners still maintain the resulting code. See the [AI maintainability roadmap](https://rullst.github.io/Rullst/book/ai-maintainability-roadmap.html) for proposed task fixtures and evaluation boundaries; this decision claims no model benchmark. ## Transition sequence and acceptance 1. **Preserve stable maintenance.** Keep the published v12.1.1 release immutable and finish the active test-hardening follow-up without structural changes. Keep `v12` for stable maintenance and `main` for v13. Continue assessing applicable fixes in both directions. 2. **Make support scope observable.** For each proposed expansion or extraction, record the product user, maintained operations/platforms, dependencies, evidence gaps, responsible reviewer and estimated recurring work. Start with current SaaS and Academy journeys. The v13 billing-provider set is Stripe (supported) and InfinitePay (experimental), with crypto excluded; the other adapters and NFS-e were removed with migration guidance. The v13 mail transports are Resend, AWS SES, SendPulse and SMTP; the other transports were removed the same way. 3. **Prioritize separation and expansion freezes.** The Labs runner's assessment concluded with its removal from 13.0 in favor of a bring-your-own-runner contract, and Capital's fiscal domain was removed from 13.0. Next assess IoT hardware expansion and specialized educational monitoring. Keep their existing bounded implementations and security duties visible. Reuse existing package boundaries where they already solve the problem. 4. **Review one bounded migration at a time.** Demonstrate the benefit before extracting modules or changing defaults. Trace facade features, generated applications, direct package consumers, serialization and data migration. Do not create a collection of nominally separate but unsupported packages. 5. **Validate and integrate normally.** Preserve workspace tests, strict Clippy, formatting, MSRV, feature-boundary, package-consumer and generated-application checks. A smaller future support matrix requires an explicit compatibility decision; it is not permission to skip today's required evidence. Update the SST, manifests, release inventory, support documents and migration guide together when an actual boundary changes. The current synchronized release train remains authoritative. Labs is already excluded from normal publication and its runner candidate was removed; other crates in the inventory are not removed by calling them extensions. Their possible future independent release lifecycles must be explicitly implemented and validated. Unfinished optional roadmap work need not block a bounded v13 release, but unresolved safety or admission requirements for what it ships still do. Retirement is appropriate only after a concrete review finds insufficient benefit, no sustainable maintainer or a better supported replacement. Follow the existing deprecation policy: retain a deprecated stable API for at least one released minor version before removal in the next major, subject to the documented security exception. Preserve migration guidance and history. At each review, record actual maintenance effort where available: incident and dependency-update work, CI execution versus queue time, repeated failures, supported combinations and downstream acceptance. Keep unknown measurements unknown. Prefer freezing low-demand expansion over weakening required tests or transferring critical security logic into unreviewed application code. --- # v13 Framework References & Comparative Evaluation Source: https://rullst.github.io/Rullst/book/v13-framework-comparison.html # v13 framework references and comparative evaluation **Planning direction adopted on 23 September 2026.** Study useful designs from other frameworks and measure comparable application journeys to guide Rullst investment. This document specifies future evaluation, not measured results or a claim that Rullst is superior. Finish the active v12.1.1 maintenance admission before beginning this work. The [SST](https://rullst.github.io/Rullst/book/spec.html) still governs architecture. ## Relationship to the priorities The [25 A priorities](https://rullst.github.io/Rullst/book/v13-priorities.html) are broad themes to invest in or maintain, not 25 unfinished features that must all ship before evaluation. Some already have bounded implementations; others contain substantial optional follow-up. Start a small baseline once the selected application journeys are usable, then repeat relevant measurements after meaningful changes. Findings should help select the next investment instead of arriving only after the entire roadmap. This work refines existing themes such as M1/M2/M3/M5/M9/M11/M12/M15 and the downstream fixtures, recovery and authorization proposals S1-S3. It adds no top-level milestone and does not change the 41-theme or 25/16 counts. It is not a new release gate, a promise to complete all themes for v13, or authorization to publish. Preserve the [maintenance scope](https://rullst.github.io/Rullst/book/v13-maintenance-scope.html): common security contracts remain maintained, while specialized extensions need owners. ## Reference frameworks The following official documentation was consulted on 23 September 2026. These are design-study recommendations, not a ranked list or independent verification of the projects' claims. Pin actual versions when executing a comparison; mutable documentation links alone do not identify tested source. | Reference | Study focus | Rullst application | | :--- | :--- | :--- | | [Loco](https://loco.rs/docs/) | Integrated Rust models, controllers, authentication, mail, workers and CLI | Closest initial application-productivity comparison; evaluate complete generated journeys. | | [Laravel](https://laravel.com/starter-kits) | Authenticated starter applications and a coherent initial experience | Reduce manual setup and improve the supported SaaS path. | | [Django security](https://docs.djangoproject.com/en/stable/topics/security/) and [admin](https://docs.djangoproject.com/en/stable/ref/contrib/admin/) | Integrated protections, documented limitations and administration | Review safe defaults, authorization recipes, documentation and Nexus boundaries. | | [Axum](https://docs.rs/axum/latest/axum/) | Explicit HTTP APIs and Tower composition | Technical baseline for overhead and interoperability; Rullst already depends on Axum. | | [Actix Web](https://actix.rs/docs/server/) | HTTP workers, connections and graceful shutdown | Targeted server lifecycle and load-behavior comparison. | | [Ruby on Rails](https://guides.rubyonrails.org/getting_started.html) | Coherent generators, models, migrations and deployment journey | Learn from conventions while preserving explicit, inspectable Rust contracts. | | [ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-10.0) | Application-host and infrastructure integration testing | Make realistic HTTP, identity and persistence acceptance easier to run. | | [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view/welcome.html) | Interactive server-rendered applications | Study the supported realtime/UI journey when product demand selects it. | | [Leptos](https://book.leptos.dev/) | Reactive and full-stack Rust interfaces | Evaluate a specific frontend requirement before expanding beyond SSR/HTMX. | Begin with Loco for comparable Rust application work, Laravel for onboarding, Django for security/documentation study, and Axum for a small HTTP baseline. The other references are targeted follow-ups. Do not build and permanently maintain nine parallel application suites merely because nine references are listed. Adoption of an idea requires a demonstrated gap and a bounded change; the list does not authorize copying each framework's architecture or features. ## First application journeys Use synthetic data, disposable databases, local provider fixtures and supported public APIs. Reuse existing SaaS/Academy regression fixtures where appropriate. Write equivalent acceptance contracts before timing implementations. | Journey | Required observations | | :--- | :--- | | Create a small SaaS application | Install, generate, migrate, start and implement an authenticated resource; reject anonymous and cross-tenant access and prove allowed access. | | Recover an account | Capture mail locally; exercise expiry, purpose binding, single use, concurrent reuse and the documented session policy. | | Process a billing event | Use the same signed offline event contract; reject tampering, replay and cross-tenant mutation, and handle duplicate/out-of-order delivery without duplicate domain effects. This is not live gateway certification. | | Recover background work | Interrupt a disposable worker, restart it and check leases, retries, idempotency and documented recovery behavior. Record delivery guarantees explicitly. | | Update an existing application | Upgrade a pinned previous supported version, resolve required changes and verify preserved data, access and jobs. Exercise a declared rollback or restore procedure; do not assume migrations are reversible. | The first executable pilot should cover the small SaaS journey and one authorization-negative case. Expand to account recovery, events, durable work and upgrades only as the acceptance fixtures and available resources permit. An unsupported journey is a recorded scope gap, not an invented successful run. ### Bounded SaaS pilot The opt-in `cli-saas-journey` diagnostic in the existing **Rust CI** workflow implements the first Rullst acceptance contract. Dispatch it with `platform=ubuntu-latest`; it is outside the default platform/shard matrix and does not replace any required CI or release gate. Run records, not this source description, establish whether a particular SHA passed. Its canonical resource, migration and runner live in [`examples/saas`](https://github.com/Rullst/Rullst/tree/main/examples/saas). `python3 examples/saas/run.py` runs the diagnostic locally; `--serve` keeps the same disposable application open for exploration without claiming a test pass. The example README documents registration, local membership provisioning, notes requests, disk requirements and cleanup. ```sh gh workflow run ci.yml --ref <candidate-branch> \ -f platform=ubuntu-latest -f shard=cli-saas-journey ``` The script installs the reviewed source CLI in an isolated prefix, verifies its version, generates the SQLite SaaS, checks its source dependency paths and runs the installed `db:migrate` command twice. It adds one explicit application-owned migration/resource, then checks the generated application with strict Clippy. Real loopback HTTP exercises three disposable users: permitted registration, login, create/read/update, wrong-password and anonymous denial, same-tenant ownership denial, cross-tenant denial, forged tenant hints, CSRF, bounded input, rejected owner/tenant mass assignment, persistence across an owned process restart, fresh membership revocation and client-cookie logout. The resource uses the starter's real Auth middleware, database-owned membership, `TenantLayer`, parameterized SQL and `RbacGuard`. The fixture operator provisions membership directly in its disposable database; this is **application code**, not automatic organization management in the SaaS blueprint. Neither a tenant header nor a request body grants membership. A missing tenant hint can select the authenticated user's sole trusted membership, as documented by Core. The additional resource mounts CSRF, secure headers and WAF explicitly, including in this loopback development profile. The starter's existing middleware and Server's production baseline remain unchanged. `saas-journey-ubuntu-latest` retains sanitized JSON evidence even on failure: source and lock identities, installed CLI version/hash, toolchain, phases, acceptance outcomes, application fixture size and observed wall times. No database, generated secrets or raw application output is uploaded. Failed commands expose only a bounded, redacted diagnostic tail in the job log. This first profile uses a **source-installed debug CLI**, a local SQLite file and HTTP clients. It does not establish archive/registry installation, browser cookie enforcement, production TLS/provider behavior, stateless-cookie server revocation, load capacity, application upgrades, rollback safety or superiority over another framework. Independent archive/platform suites retain their own source identities. Setup edits and compile/startup times are observable; active implementation effort and failed CI attempts must still be retained in the trial report. A small functional run cannot predict support for 1,000–10,000 concurrent users. Local execution requires Linux, a fetched workspace lock and at least 32 GiB available on the source and temporary filesystems for a conservative cold-build allowance. The runner checks space between phases and during owned builds, stopping those builds at the 12 GiB reserve. Prefer hosted CI on workstations near the 15 GiB preventive threshold. This is an operating check, not a disk quota or protection against writes from unrelated processes. ## Measurements | Dimension | Record | | :--- | :--- | | Development effort | Setup and active implementation time, manual steps, relevant application code/configuration, documentation lookups, failed attempts and fixes. Report build/queue/wait time separately. Code size alone is not productivity. | | Correctness and failure behavior | Acceptance and negative-case outcomes, reproducible defects, timeout/retry/recovery behavior, error clarity and diagnostic effort. More test files do not establish stronger protection. | | Performance | Cold and warm builds, startup, representative authenticated/database requests, latency distributions including p50/p95/p99, throughput, errors, CPU and memory at declared loads. | | Maintenance | A repeatable change such as adding an authorized field, updating a dependency or migrating a release; record time, touched components, regressions and downstream fixes. Longer-term incident/update effort remains unknown until observed. | A fast implementation that fails the agreed security, durability or correctness contract does not qualify as a performance/productivity winner. Report the failure; do not disable protections to improve numbers. Internal Rullst [scorecard grades](https://rullst.github.io/Rullst/book/quality-scorecard.html) are not comparative measurements. ## Fairness and reproducibility - Freeze task requirements, acceptance tests, effort budget and measurement method before the trial. Use idiomatic supported approaches on every stack. Give competing implementations a comparable debugging/tuning budget. - Record framework/application commits, exact versions, dependency locks, toolchains, OS, machine/container limits, database version/schema/data, cache state and commands. Distinguish released software from unpublished candidates. - Compare matching authentication, validation, persistence, durability and response semantics. A minimal Axum HTTP baseline is not a full SaaS competitor; disclose application code and libraries needed to assemble missing layers. - Isolate the load generator, check for bottlenecks, warm up appropriately and repeat trials. Declare trial counts, load schedules, errors and variability; report inconclusive differences. Hosted CI timing is useful for regression detection but is noisy evidence for cross-framework performance rankings. - Separate local fixture acceptance from provider/device/production evidence. Use only free resources and standard public CI runners where appropriate; respect the local disk reserve. No real provider accounts, production data, paid services or automatic deployments are authorized by this plan. - For AI-assisted work, retain the actual model/version, tools, instructions, budgets and interventions. Keep the procedure comparable and disclose familiarity and tuning differences. One assistant's result measures that assisted workflow, not universal human productivity or model capability. Reuse the [AI evaluation protocol](https://rullst.github.io/Rullst/book/ai-maintainability-roadmap.html), without changing models automatically or claiming independent human validation. - Keep scripts, sanitized fixtures, raw results and a source-bound report. Retain failed trials and tradeoffs; do not publish only favorable workloads or turn a weighted internal score into a universal league table. ## Delivery sequence and acceptance 1. **After stable maintenance:** select the first bounded SaaS task, references, supported versions and free execution budget. Confirm fixture reuse and document the threat/behavior contract before adding implementation work. The [bounded SaaS pilot](https://rullst.github.io/Rullst/book/v13-framework-comparison.html#bounded-saas-pilot) implements that task's Rullst-side acceptance contract. 2. **Baseline:** exercise usable Rullst source and the selected reference with the same acceptance contract. Record current gaps and uncertainty. Do not wait for every A priority to be completed and do not claim unrun results. 3. **One improvement:** choose a measured problem with a clear user benefit, check the SST and maintenance cost, implement it, and rerun affected cases. Existing release and compatibility requirements still apply. 4. **Release review:** when the selected v13 scope is ready, summarize the relevant measurements and remaining limits. Required release checks decide admission; this optional comparison cannot certify security or waive them. 5. **Maintenance checkpoints:** repeat relevant cases after upgrades or material changes. Keep focused regressions in normal CI when useful; run expensive cross-framework campaigns explicitly at checkpoints, not on every PR. An evaluation deliverable needs reproducible commands and source identities, actual acceptance results, comparable measurements, disclosed exclusions and a short prioritized response to the findings. A defensible conclusion names the specific workload and dimension in which Rullst improved, matched or lagged. The outcome may be to fix a defect, simplify a journey, retain an existing design or defer an expensive extension. There is no predetermined winner. --- # Nexus Application Integration Plan Source: https://rullst.github.io/Rullst/book/nexus-integration-plan.html # Nexus application integration plan Status: proposed scope, reviewed on 25 September 2026. These are planned improvements, not APIs available in a published release. The current contract remains in the [specification](https://rullst.github.io/Rullst/book/spec.html#102-rullst-nexus-nexus) and [Nexus guide](https://rullst.github.io/Rullst/book/4-rullst-nexus.html). The goal is to embed Nexus into an existing application without duplicating its login, tenant policy or administration shell. Keep this work in `rullst-nexus`; a new crate, a universal admin platform and automatic domain authorization are outside this scope. Applications continue to own membership, parental relationships, consent, business transactions and deployment policy. This refines M11, with dependencies on M9/M12/M14 and a separate M32 macro ergonomics item. It does not add milestones to the 41-theme inventory in [v13 priorities](https://rullst.github.io/Rullst/book/v13-priorities.html), or override the [maintenance scope](https://rullst.github.io/Rullst/book/v13-maintenance-scope.html). ## Existing foundation and remaining gaps Preserve the existing authenticated policies, administrator role checks, parameterized CRUD, bounded inputs, server-side field restrictions, opt-in tenant scoping and transactional mutation audit. Their limits remain explicit: host membership and domain authorization are not inferred from registered models, and the mutation audit does not provide an immutable external log. `FieldMeta.hidden` already excludes a field from list and form rendering, and the input validator rejects writes to hidden fields. On `main` its field documentation now says so, and the list and edit views select only visible, non-password columns (the list also reads the primary key); v12 still documents the old form visibility. This rendering behavior is not yet the never-read projection contract of stage 3. Field visibility, database projection and authorization need distinct documented contracts. Existing mobile/navigation improvements and pagination links are useful; they do not establish an end-to-end administration journey without JavaScript. Since this review, the existing shell serves its stylesheet, script, vendored htmx 2.0.4 and logo same-origin under `/nexus/assets` without inline code or external fonts, so it runs under the default production nonce CSP. A configurable mount path and a translation catalog remain open. Since 13.0.0-alpha.2 the host-session identity part of stage 1 exists: `NexusAuthPolicy::application` asks an application `NexusAuthorizer` to validate the session and the administrator permission on every request before data access, and Nexus creates the principal only after it allows the request (see the [Nexus guide](https://rullst.github.io/Rullst/book/4-rullst-nexus.html#using-the-applications-sign-in)). It is a read-write mode with Nexus's CSRF, role and audit boundaries, not the read-only mode with domain-service writes planned below; the SaaS starter and the LMS example use it with a generated `users.is_admin` check. ## Delivery order | Stage | Bounded result | Required behavior | | :--- | :--- | :--- | | 1 — identity and operations | An opt-in host-authenticated, read-only integration mode | Validate the current session and administrative permission separately on each request, before data access or rendering. Nexus creates the principal after successful validation. Reject direct mutation requests as well as hiding unavailable controls. | | 1 — self-contained shell | Versioned local assets and a shell compatible with strict CSP | No required external scripts/fonts/CDNs, inline handlers or `unsafe-inline`. Test actual browser behavior and outgoing asset requests. Keep application security middleware active. | | 2 — paths and language | A validated mount path and explicit translation catalog | Use the configured path in every link, form, redirect and endpoint; encode path segments and query values correctly. Support explicit locale selection and deterministic fallback, beginning with en/pt-BR/es. | | 2 — usable SSR | Keyboard and no-JavaScript read journeys | Search uses GET forms, pagination uses links, and labels, focus, skip navigation and table captions remain usable at narrow widths. HTMX enhances an already working read journey. | | 3 — data ownership | An explicit per-instance pool or bounded typed collection provider | Keep independent instances isolated. Support application-owned projections without requiring registration of sensitive base tables, arbitrary SQL input or a second ORM. | | 3 — field and domain policy | Positive read/write projections and operation capabilities | Define never-read secrets, masked values and write-only updates separately. Recheck operation and tenant authorization on the server. Sensitive mutations invoke application domain services and preserve transactional audit requirements. | | Throughout — optional modules | Explicit configuration of chat, security and telemetry surfaces | In the new integration mode, unconfigured modules have neither mounted routes nor menu entries. Preserve the existing supported mode until an explicit migration is designed. | Stages describe ordering, not permission to ship an incomplete security boundary. Stage 1 may use an application-owned read renderer/provider, with a documented trusted-code boundary and mandatory escaping of interpolated data. It must not silently activate generic writes or trust client-supplied identity, roles or tenant IDs. Exact public type and method names require design review; an application's private extension is evidence of demand, not a stable API. Prefer generic/static-dispatch integration contracts and typed failures. Keep the existing Basic credential validation, transport requirements and verified development-loopback restrictions. A public unrestricted principal constructor is not the session integration contract. Revocation must take effect on the next request, including HTMX fragments and direct resource URLs. Before adding sensitive write operations, define domain-service delegation, CSRF handling, authorization, transaction boundaries and any required reauthentication. Do not let generic CRUD bypass consent, family relationships or account-deletion workflows. Additional read-access auditing and assurance levels remain separately scoped work; the first read-only mode does not claim to provide them. ## Stable v12 maintenance boundary Correct inaccurate field documentation and add a tested recipe for conditional HTML boolean attributes in the stable documentation. Preserve current APIs, MSRV and serialized contracts. Narrow defect fixes follow stable maintenance and the [security policy](https://github.com/Rullst/Rullst/blob/main/SECURITY.md); new public integration features require a versioning decision and are not automatically patch backports. For HTML boolean attributes, presence means true: `selected="false"` does not disable selection. The current macro escapes and serializes a dynamic value; the safe existing recipe emits the complete selected or unselected element, including a quoted `selected="selected"` only in the selected branch. See [MDN's boolean attribute semantics](https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML). An explicit conditional-attribute API is a separate v13 ergonomics proposal. It must preserve escaping and distinguish actual HTML boolean attributes from string-valued `aria-*` and `data-*` attributes. Do not reinterpret all false values or silently change the stable macro's serialization behavior. 13.0.0-alpha.2 provides it as `name?={value}` (a `bool` toggles presence, an `Option` omits the attribute for `None`) while `name={value}` keeps its serialization. ## Acceptance and evidence Use synthetic downstream fixtures and offline browser tests; no application production data or provider accounts are needed. Test the following observable properties alongside the unchanged workspace and release requirements: - Missing, expired and revoked sessions fail before the provider is called; ordinary authenticated users still cannot administer records. Cover direct URLs, fragments, unsupported methods and attempts to replace tenant context. - A read-only configuration cannot mutate through individual, batch or custom routes. Existing tenant predicates and required-audit rollback remain intact in any mode that enables writes. - Two instances with different pools, policies and prefixes cannot share data or identity. A never-read field is absent from the query projection, response, logs and audit payload, not merely hidden with CSS. - The mounted shell passes strict CSP and resource-request assertions in a real browser, on success and denial paths. All locales and mount paths keep working with JavaScript disabled; verify actual form submission and results. - Keyboard navigation, focus and table semantics work at 320/390/1440 px. Conditional boolean-attribute fixtures assert the browser's selected value and submitted query, rather than only searching generated HTML strings. - New APIs compile on Rust 1.96.0 and the declared supported platforms. Retain the full tests, Clippy, formatting, packaging and applicable admission gates; passing a downstream application's tests is not framework release admission. No part of this document claims that these APIs, browser journeys or release checks have already been implemented or passed. Scope implementation after stable maintenance priorities, starting with one complete read-only journey. --- # AI Maintainability & Project-Building Roadmap Source: https://rullst.github.io/Rullst/book/ai-maintainability-roadmap.html # AI Maintainability and Project-Building Roadmap Rullst aims to be understandable to humans and coding assistants without making model-independent quality claims. Explicit Rust types, conventional project structure and generated context can reduce ambiguity, but no framework can guarantee that an arbitrary model will produce correct, secure or maintainable software. This document records the measurable work required to make Rullst excellent for AI-assisted framework maintenance and application development. It is a **v13 roadmap**, not a v12 release gate and not a description of capabilities that have already shipped. ## Current foundation The repository already provides useful controls: - `AGENTS.md` defines the framework architecture, coding invariants, security boundaries, validation commands and release order. - [`spec.md`](https://rullst.github.io/Rullst/book/spec.html) is the architectural Single Source of Truth. - [`capability-status.md`](https://rullst.github.io/Rullst/book/capability-status.html) and the [`quality-scorecard.md`](https://rullst.github.io/Rullst/book/quality-scorecard.html) separate implementation claims from engineering evidence. - `cargo rullst make:*` emits conventional, inspectable Rust source instead of hiding application behavior behind runtime reflection. - The v13 `generate:ai-context` candidate emits a bounded metadata inventory, a freshness check and preserved project instructions; see [the implemented candidate scope](https://rullst.github.io/Rullst/book/project-context.html). - Generated-project tests, compile-fail tests, deterministic provider mocks and the workspace validation gates catch classes of mistakes independently of the assistant that proposed a change. These controls are a good foundation. They do not yet prove that a smaller or less capable model can maintain the framework or build a complete application at a defined quality level. ## Known limitations The published v12.1 context generator concatenates manifest/source content. The v13 candidate replaces that with a bounded metadata inventory and preserved application instructions. It still does not infer a complete semantic contract for routes, configuration behavior, model relationships, tests or authorization. Task-ranked semantic views remain follow-up work. The framework documentation is extensive. Breadth helps difficult work, but a model with weaker retrieval or reasoning can select an obsolete example, miss a more specific invariant or consume too much context unless it receives a short task-oriented map first. Maintaining framework internals is also substantially harder than building an application from stable public APIs. Cross-crate feature unification, procedural macros, database dialects, authentication, cryptography and release engineering require stronger review and broader gates than ordinary application CRUD. ## v13 workstreams ### 1. Project-specific agent instructions The first v13 candidate creates concise common application instructions and points to the fresh file/dependency inventory; existing instructions remain maintainer-owned. Future blueprint-specific semantic profiles should describe: - the chosen database, rendering mode and enabled Rullst subsystems; - the canonical locations for routes, models, controllers, policies, migrations, jobs and tests; - the exact format, lint and test commands for that application; - mandatory ownership, CSRF, headers, WAF and secret-handling boundaries; - which generated files are application-owned and which commands may refresh them; and - links to version-matched Rullst documentation. Acceptance requires snapshot tests for every supported blueprint and a generated-project compile gate. The generated document must not claim that an optional integration is active merely because its crate is available. ### 2. Structured and bounded AI context The v13 candidate now supplies deterministic bounded file/dependency/key metadata, freshness checks and project instructions. It deliberately excludes source excerpts and does not infer domain semantics. Broader future maps should add explicitly tested contracts for: - dependency and feature selections; - routes and their authentication/ownership policies; - models, relationships and migrations; - controllers, middleware, jobs and external-provider boundaries; - configuration keys by name, never secret values; - tests and the commands that exercise each subsystem; and - freshness metadata sufficient to detect stale generated context. The generator must enforce an explicit size budget, stable ordering, secret and binary exclusions, path confinement and deterministic output. Large projects should receive an index plus task-selectable context shards rather than one unbounded prompt payload. ### 3. Golden application tasks Create small, executable reference tasks that represent common real work: 1. add a validated CRUD resource with tenant ownership; 2. add session authentication and a role-protected route; 3. add a migration and repository query without SQL injection; 4. enqueue an idempotent background effect through an outbox; 5. verify a signed webhook and reject replay or invalid signatures; 6. add a provider integration with a deterministic offline mock; 7. diagnose and repair a deliberately broken generated application; and 8. perform an assisted framework upgrade and verify rollback. Each task needs a fixed starting fixture, an executable acceptance suite, security-negative cases and a reference solution. Tutorials alone are not evidence; the fixture must compile and its assertions must prove the behavior. ### 4. Reproducible AI evaluation harness Evaluate candidate assistants using the same repository revision, task fixture, tool permissions, time budget and acceptance tests. Record at least: - functional correctness and hidden-test pass rate; - formatting, strict Clippy and test results; - security-invariant violations; - unsupported or hallucinated APIs and dependencies; - unnecessary diff size and changes outside the requested scope; - ability to recover from compiler and test failures; - documentation truthfulness; and - elapsed time, model/tool configuration and cost when publishable. Results must name the exact model/version, agent harness, reasoning setting, date and commit SHA. A vendor description or one successful demonstration is not compatibility evidence. Preview and mutable model aliases must be reported as such. The first matrix should distinguish at least two profiles: - **application builder:** works through documented, stable public APIs and generated projects; and - **framework maintainer:** changes crate internals, feature combinations, macros, security boundaries or release infrastructure. A model may qualify for one profile without qualifying for the other. The [v13 framework comparison plan](https://rullst.github.io/Rullst/book/v13-framework-comparison.html) reuses these metadata and acceptance principles for application journeys across frameworks. Keep model/workflow comparisons distinct from framework comparisons, disclose confounding factors and do not infer universal superiority from one trial. ### 5. Task-oriented documentation routing Add a short machine-readable and human-readable entry map that answers “which document should I read for this task?” before exposing the full manual. It must route architecture changes to the SST, release work to the release programme, security changes to the relevant threat model and ordinary application work to the smallest applicable tutorial and API reference. Where documents overlap, one must be declared authoritative and the others must link to it rather than restating mutable facts. Documentation examples should continue to be compiled or exercised wherever practical. ### 6. Governance and review boundaries AI assistance never replaces contributor accountability or independent review. The disclosure and verification rules in [`CONTRIBUTING.md`](https://github.com/Rullst/Rullst/blob/main/CONTRIBUTING.md) apply to every model and tool. Exact prompt disclosure remains optional context, not proof of authorship, completeness or safety. Routine documentation, test and mechanical dependency work may use a faster model when the gates prove the result. Authentication, cryptography, ORM/macro contracts, release signing and cross-crate security changes require heightened human review regardless of the model used. No evaluation score authorizes unattended merge or production mutation. ## Planned additions (recorded 2026-10-01) These items were requested by the maintainer during the v13 review and are tracked here so they are not lost; they are plans, not shipped capabilities, unless marked as delivered. - **Documentation for humans and assistants:** a "start here" page, short per-crate quickstarts, a task-oriented cookbook, and an `llms.txt` index of the book. Visible maturity tiers per crate and per CLI generator were delivered on `main` on 8 October 2026; see [maturity tiers](https://rullst.github.io/Rullst/book/maturity.html). - **Code-level clarity:** every public item documented with a compiling example (enforced with `missing_docs` and doctests), consistent constructor and error patterns across crates, and actionable error messages (the v13 CLI error reports are the first step). - **Assistant grounding:** keep the `generate:ai-context` output and the published `llms.txt` in sync with the book, with a test that every command and API they name exists. (The `cargo rullst ai` primer went with the terminal assistant, removed in 13.0.0-alpha.4.) - **Assisted v12 → v13 upgrade (delivered on `main`):** `cargo rullst upgrade` performs the backed-up dependency upgrade, `cargo fix` and `cargo check`, and reports source findings from `rullst-upgrade-rules-v4`; every row of the [v13 migration guide](https://rullst.github.io/Rullst/book/migration-v13.html) is classified as must-change, review or not detectable. See the [assisted upgrade tutorial](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html#v12-to-v13-source-findings). - **Legal and regulatory readiness:** see the [legal & regulatory compliance roadmap](https://rullst.github.io/Rullst/book/legal-compliance-roadmap.html). ## Delivery sequence | Phase | Delivery | Promotion evidence | | :--- | :--- | :--- | | A | Generated application `AGENTS.md` and context format v2 | Blueprint snapshots, deterministic output, secret-exclusion and generated-project gates | | B | Golden task fixtures and evaluation runner | Fixed inputs, hidden assertions, security-negative tests and reproducible metadata | | C | Public model/harness results | Exact versions, commit SHA, limitations and repeatable commands | | D | Continuous regression programme | Scheduled or manual reruns, versioned baselines and reviewed score changes | Phases belong to the v13 development line after the stable v12 release. Compatible documentation corrections may land in v12 maintenance, but new generator formats, fixtures and public support profiles belong to the v13 feature line unless a separate release decision says otherwise. ## Completion criteria This roadmap may be called complete only when: - every supported blueprint produces accurate, tested agent instructions; - context generation is bounded, deterministic, secret-safe and task-routable; - the golden application and maintenance suites run from clean fixtures; - published results are tied to exact models, harnesses, commits and dates; - at least one lower-cost model completes the application-builder profile at the project-defined quality threshold without security-critical failures; and - failure cases and unsupported maintenance classes are documented as clearly as successful ones. Until then, the honest claim is narrower: Rullst is intentionally structured to support AI-assisted work and provides useful safeguards, but model suitability must be evaluated for the concrete task and reviewed like any other contribution. ## Non-goals - guaranteeing correct output from every current or future model; - detecting all AI-generated code or proving the complete prompt history; - replacing human ownership, security review or coordinated disclosure; - auto-merging changes based on a model name or benchmark score; or - claiming superiority over other frameworks without dated, reproducible comparative evidence. --- # Legal & Regulatory Compliance Roadmap Source: https://rullst.github.io/Rullst/book/legal-compliance-roadmap.html # Legal & Regulatory Compliance Roadmap > **Not legal advice.** This page is an engineering roadmap written by the > framework maintainers. It does not create a lawyer–client relationship and is > not a statement that any application built with Rullst complies with any law. > Every item below must be validated by qualified counsel in each jurisdiction > before it is described as satisfying a legal requirement. Status: **planned (recorded 2026-10-01)** for the v13 feature line. Nothing on this page is a shipped capability unless it links to existing documentation. ## Goal and honest boundary A framework cannot make an application lawful. Compliance depends on what the operator collects, why, where it is stored, which contracts and notices exist, who the users are and which country's law applies. What Rullst *can* do is make the default path **compliance-ready**: 1. ship building blocks that implement common obligations correctly (consent, data-subject rights, retention, minimization, security, audit); 2. generate starters whose defaults do not create avoidable legal risk (no unnecessary personal data, no non-essential cookies, no PII in logs); 3. document, per regulation, which obligations a Rullst feature helps with and which ones remain the operator's responsibility; and 4. keep the framework's own posture clear (MIT license and warranty disclaimer, SBOM, coordinated vulnerability disclosure). ## Priority jurisdictions The first guide covers the regulations most likely to affect Rullst users. Each row must later link to the official text and be reviewed by counsel. | Region | Main instruments | Areas where Rullst can help | | :--- | :--- | :--- | | European Union | GDPR; ePrivacy Directive (cookies); AI Act (transparency obligations); Cyber Resilience Act; European Accessibility Act; Digital Services Act (platforms); NIS2 | Lawful-basis and consent records, data-subject rights, records of processing, breach-response evidence, cookie defaults, AI content disclosure, SBOM and vulnerability handling, accessible starters | | United States | State privacy laws (e.g. CCPA/CPRA); COPPA; FERPA (education records); HIPAA and GLBA (sector-specific); ADA accessibility; CAN-SPAM; FTC Act; state breach-notification and biometric laws (e.g. Illinois BIPA) | Opt-out and deletion flows, children's data gates, education-record handling in the LMS starter, unsubscribe headers, accessibility, biometric-free supervision defaults | | Brazil | LGPD and ANPD regulations (incl. incident reporting); Marco Civil da Internet (application access-log retention); Código de Defesa do Consumidor and e-commerce rules; ECA Digital (children and adolescents online); Lei Brasileira de Inclusão (accessibility) | Consent and rights, incident timelines, configurable access-log retention, consumer information pages, age assurance and parental supervision, accessible starters, NFS-e fiscal preparation | | Asia | China PIPL, Data Security Law and Cybersecurity Law (localization and cross-border transfer); Japan APPI; South Korea PIPA; India DPDP Act and Rules; Singapore PDPA; others (Indonesia, Vietnam, Thailand) | Data-residency configuration, cross-border transfer inventory, consent and rights flows, retention, breach-response evidence | | Global / industry | PCI DSS (payment cards); OWASP ASVS; open-source license obligations; export controls on cryptography | Hosted-checkout-only payment flows (no card data stored), security baseline mapping, SBOM and license audit | ## What already exists (v13 working source) These are building blocks, not compliance claims; see each page for limits: - `rullst-privacy`: proportional age assurance, consent/withdrawal state and a bounded own-account profile export ([roadmap and scope](https://rullst.github.io/Rullst/book/privacy-age-assurance-roadmap.html)). - ORM field encryption and masking, `SecretString`, `#[privacy]` personal-data tagging and `ComplianceModel::compliance_schema()` / `personal_fields()`. - Security baseline: CSRF, secure headers and CSP, WAF/RASP, DLP and PII masking on responses, audit logging, rate limiting ([security architecture](https://rullst.github.io/Rullst/book/security-architecture.html)). - Mail: List-Unsubscribe (RFC 8058), recipient suppression, minimized delivery observations ([mail crate](https://rullst.github.io/Rullst/book/crates/mail.html)). - Capital: provider-hosted checkout and webhook verification. Fiscal documents such as NFS-e are outside the framework since v13. - Supervision: no media models or biometric identification are included; the host owns capture permission and reviewer workflow. - Supply chain: SBOM generation, license policy (`deny.toml`) and coordinated vulnerability disclosure. ## Planned work | # | Item | Notes | | :--- | :--- | :--- | | 1 | **Compliance guide** in the book, one section per regulation above | Obligation → Rullst feature → operator responsibility → official source; reviewed by counsel before publication as guidance | | 2 | **Records of processing / data inventory** generated from `#[privacy]` tags and `compliance_schema()` | e.g. a `cargo rullst privacy:inventory` report (fields, purposes, retention, processors, transfer regions) | | 3 | **Data-subject rights toolkit** | Access/portability export, rectification, deletion and anonymization across models, with audit trail and deadlines | | 4 | **Retention policies** | Declarative per-model retention, scheduled purge/anonymization, configurable access-log retention (e.g. Marco Civil) | | 5 | **Cookie and tracking defaults** | Starters keep strictly necessary cookies only; an optional consent component gates analytics and marketing | | 6 | **Incident-response support** | Security-event export and a breach timeline template (e.g. 72 h under GDPR, ANPD deadlines) | | 7 | **Children and education** | Age assurance and parental-consent flows (COPPA, ECA Digital, GDPR Art. 8); FERPA/LGPD notes for the LMS example | | 8 | **AI transparency** | Disclosure helpers for chatbots and AI-generated content (AI Act), provider data-use notes in `rullst-ai` | | 9 | **Accessibility** | WCAG 2.2 AA targets for starters, Studio and Nexus, with automated checks in CI | | 10 | **Data residency** | Region configuration and documentation for storage, mail and AI providers (PIPL and transfer rules) | | 11 | **Legal page scaffolds** (optional) | Privacy policy, terms and cookie notice templates with explicit placeholders and a prominent "review with counsel" warning | | 12 | **Framework posture** | CRA position for an open-source project, release security attestations, license and trademark guidance | ## Principles - **Privacy by default:** starters collect the minimum, never log secrets or personal data, and keep optional processing off until enabled. - **Evidence over claims:** every helper ships tests and documents what it does *not* cover, like the rest of Rullst. - **No hidden legal advice:** templates and guides are starting points with clear placeholders, never presented as complete or jurisdiction-proof. - **Small surface:** features land as optional building blocks so applications that do not need them pay nothing. --- # Green Software Roadmap Source: https://rullst.github.io/Rullst/book/green-software-roadmap.html # Green Software Roadmap Status: **planned (recorded 2026-10-01)** for the v13 feature line. Nothing on this page is a shipped capability unless it links to existing documentation. Item 1 and parts of items 3 and 4 exist in the v13 development source (`13.0.0-alpha.4`); see [measured release defaults](https://rullst.github.io/Rullst/book/green-software-roadmap.html#measured-release-defaults). ## Goal Rullst applications are compiled Rust services that render HTML on the server with little client-side JavaScript. Independent studies of programming-language energy efficiency rank Rust among the most efficient languages, and a small, long-running native process usually needs less CPU and memory than an interpreted stack for the same work. That is a real advantage, but it must be **measured per application**, not assumed. The goal is to make the efficient path the default, and to let each team **measure and report** the footprint of its own Rullst application with a recognised, reproducible methodology. ## Honest boundary: reports, not "greener than" certificates Rullst will not issue certificates claiming that an application is "greener than" one built with another framework. Such comparative or generic environmental claims are regulated: - In the European Union, Directive (EU) 2024/825 restricts generic environmental claims and requires claims to be substantiated. - In the United States, the FTC Green Guides govern environmental marketing. - In Brazil, consumer law and advertising self-regulation prohibit misleading environmental claims. An unsubstantiated "green" badge could expose Rullst users to the very legal risk the [legal & regulatory compliance roadmap](https://rullst.github.io/Rullst/book/legal-compliance-roadmap.html) aims to reduce. Instead, Rullst will produce **dated, reproducible measurement reports** that state the method, the environment and the limits, which teams can choose to publish. ## Planned work | # | Item | Notes | | :--- | :--- | :--- | | 1 | **`cargo rullst green` report** | **13.0.0-alpha.4 development source, Experimental command of the CLI:** [`cargo rullst green`](https://rullst.github.io/Rullst/book/green.html) (formerly `footprint`) reports only measured figures, each with its method: release binary size, a named local container image's size, the started app's idle resident memory, the `Cargo.lock` dependency count and, with `--bench`, the app's CPU time per request under a bounded loopback load. It measures no energy; a Software Carbon Intensity figure (SCI, ISO/IEC 21031:2024) appears only as a labelled estimate from the measured CPU time and the power figure and grid intensity you supply. Still planned: a fixed multi-route reference scenario, repeated runs with a reported spread, and stated uncertainty for the estimate's inputs. | | 2 | **Reproducible public benchmark** | The same reference application implemented idiomatically in Rullst and in other frameworks, with published code, scenario, hardware and dates, so comparisons are verifiable instead of claimed. | | 3 | **Efficient release defaults** | v13 source: generated projects get a tuned `[profile.release]` (thin LTO, one codegen unit, stripped symbols; `panic` stays `unwind` so one panicking handler cannot stop the server) and a distroless runtime image ([CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-new-name)). Remaining: measured image sizes and fast start-up suitable for scale-to-zero hosting. | | 4 | **Efficient HTTP defaults** | v13 source: the standard `/static` mount sends `Cache-Control: public, max-age=31536000, immutable` for content-hashed file names and `no-cache` with `ETag`/`Last-Modified` revalidation (`304`) for other files ([static assets](https://rullst.github.io/Rullst/book/tutorials/10-static-assets-and-compression.html#step-3-cache-headers)). Pre-compressed assets already exist: `cargo rullst build` writes Brotli/Zstandard siblings that the Core static handler serves. Remaining: fingerprinted names written by the asset pipeline itself and modern image formats. | | 5 | **Lean pages** | Keep zero-bundle HTMX rendering the default, report page weight in `cargo rullst dash`, and warn on oversized assets. | | 6 | **Database efficiency** | Development-time N+1 query warnings are in the v13 dash ([N+1 query warning](https://rullst.github.io/Rullst/book/cli_reference.html#n1-query-warning)); slow-query hints are already visible there. Default pagination and index suggestions remain planned. | | 7 | **Carbon-aware jobs** | Started in v13: see [carbon-aware deferrable jobs](https://rullst.github.io/Rullst/book/green-software-roadmap.html#carbon-aware-deferrable-jobs-v13). Region selection remains planned. | | 8 | **AI efficiency** | Token usage is already reported by `rullst-ai`; add response caching, prompt-size budgets and guidance for choosing smaller or local models when they are sufficient. | | 9 | **Resource view in the dash** | Show the app process's CPU and memory next to the request metrics so developers see the cost of a change while they work. | | 10 | **Green hosting guidance** | Document how to choose lower-carbon regions and providers, and show region information during `cargo rullst deploy` when it is available. | ## Measured release defaults Measured on 2026-10-08 with the `13.0.0-alpha.1` development source. The numbers describe this one starter on this one machine; they are not a prediction for other applications or hardware. - **Application:** `cargo rullst new green_probe --default --skip-initial-migration` (Blank starter, SQLite; `rullst` features `orm`, `strict-sqlite`, `studio`), dependency versions from the repository `Cargo.lock`, built `--offline`. - **Toolchain:** Rust 1.98.1 (`RUSTUP_TOOLCHAIN=1.98.1`), `x86_64-unknown-linux-gnu`, rustc's default linker (the generated `.cargo/config.toml` selected neither mold nor lld). - **Machine:** AMD Ryzen 5 7520U (8 logical CPUs), 3.4 GiB RAM, NVMe disk, Ubuntu with GCC 15.2, `CARGO_BUILD_JOBS=2`, a shared developer workstation. - **Command:** `/usr/bin/time -v cargo build --release --offline --target-dir <new empty directory>`; size from `ls -l <target-dir>/release/green_probe`. - **Samples:** two cold builds per profile, in alternating order (default then generated, then generated then default). | `[profile.release]` | Binary size (bytes) | Wall-clock build (run 1 / run 2) | User CPU time (run 1 / run 2) | Largest process RSS (run 1 / run 2) | | :--- | ---: | :--- | :--- | :--- | | Cargo's default (section absent) | 13,966,600 | 6:17 / 6:17 | 648 s / 648 s | 723 MiB / 726 MiB | | Generated: `lto = "thin"`, `codegen-units = 1`, `strip = "symbols"` | 7,776,880 (−44.3 %) | 5:20 / 5:18 | 517 s / 516 s | 923 MiB / 970 MiB | Each profile produced the same size in both runs. For comparison, `strip --strip-all` applied to the default binary alone gives 10,031,552 bytes (−28.2 %); thin LTO and one codegen unit account for the rest. Limits of this measurement: - With only two parallel jobs, a single codegen unit costs little parallelism; the lower CPU time is consistent with skipping the per-crate local ThinLTO across 16 units, but that cause was not isolated. With more parallel jobs the wall-clock result may reverse; that was not measured. - `lto = true` (fat LTO) was not measured: it needs more memory than this machine could spare alongside other work. - Runtime behaviour (requests per second, CPU time per request, memory) and energy were not measured. - Container image size was not measured: Docker is not available on this machine. Measure the generated distroless image with `docker image ls` in CI or on a machine with Docker before publishing a number. ## Carbon-aware deferrable jobs (v13) Opt-in; first published in the 13.0.0-alpha.1 prerelease. A queue job or scheduled task can be marked deferrable with a deadline and daily time windows; see [deferrable jobs and time windows](https://rullst.github.io/Rullst/book/crates/core.html#deferrable-jobs-and-time-windows). The mechanism: - **Time windows.** The job becomes claimable at the start of the next allowed window (immediately inside an open one), never after its deadline; when no window opens in time, at the deadline. - **Optional intensity source.** An application-provided `CarbonIntensitySource` returns forecast slots with their unit. A `CarbonAwarePlanner` places the job at the lowest-value slot inside its windows before the deadline. Core ships no network source; if the source fails or times out, the window rule applies and one warning is logged per outage. - **Persistence.** Placement uses the queue's existing scheduled-job timestamp, so no schema changes and 12.x rows keep working. - **Observability.** The chosen time, the reason (`window`, `intensity` or `deadline`) and the source name are returned in `DeferredJob` and recorded on a `rullst.queue.deferral` tracing span. The planner only shifts when a job runs, using the values the source reports. Whether that changes an application's emissions depends on the grid, the workload and the data; Rullst does not measure or claim it. ## Principles - **Measure, then claim:** every number comes from a stated method and environment; estimates are labelled as estimates with their uncertainty. - **Defaults over options:** efficiency should not require expert tuning. - **No hidden network calls:** carbon-intensity data sources are opt-in, with an offline default. - **Small surface:** measurement tooling lives in the CLI and development builds, never in production request paths. --- # Efficiency Figures for an App (`green`) Source: https://rullst.github.io/Rullst/book/green.html # Efficiency Figures for an App (`cargo rullst green`) **Status:** experimental command of the main CLI, `cargo-rullst` (13.0.0-alpha.4 and later; it replaces `cargo rullst footprint`). Part of the [green software roadmap](https://rullst.github.io/Rullst/book/green-software-roadmap.html). `cargo rullst green` reports efficiency figures it **measured** for your application, says how it measured each one, and says which ones it could not measure. It measures no energy. A carbon figure appears only as a Software Carbon Intensity (SCI) **estimate**, computed from a power figure and a grid intensity that you supply, and it is labelled `estimate` in the text and the JSON together with every input. The report describes one run on one machine; it is not a certification and it does not compare your application with other software. ## Install `green` is part of the CLI; nothing else to install: ```bash cargo install cargo-rullst --version =13.0.0-alpha.4 --locked ``` Its flags are listed in [the CLI reference](https://rullst.github.io/Rullst/book/cli_reference.html#cargo-rullst-green). ```bash # Build the release binary, start it on a free loopback port, measure, stop it cargo rullst green # Also measure a local container image and run a 10-second benchmark cargo rullst green --image my-app:latest --bench --duration 10s # Measure an app that is already running on loopback cargo rullst green --url http://127.0.0.1:3000 --path /health --bench # Add an SCI estimate from your own power figure and grid intensity cargo rullst green --bench --cpu-watts 15 --grid-intensity 120 --json ``` ## What is measured | Figure | Method | Notes | | :--- | :--- | :--- | | Binary size | file size of the release binary built by `cargo build --release` (or of `/proc/<pid>/exe` with `--url`) | Size on disk, not memory use. | | Image size | `docker image inspect --format {{.Size}}` (then `podman`) of the image you name with `--image` | Uncompressed size reported by the local engine. Only on a local socket (`DOCKER_HOST`/`CONTAINER_HOST` unset or `unix://`/`npipe://`); otherwise, or without `--image`, `NOT MEASURED` with the reason. | | Idle RSS | `VmRSS` from `/proc/<pid>/status` after start-up, before any benchmark request | Linux only. | | Dependencies | packages with a registry or git `source` in the nearest `Cargo.lock` | Every third-party crate Cargo resolved, for all targets and features; path and workspace packages are not counted. | | CPU time (with `--bench`) | `utime + stime` delta of the app process over the benchmark, from `/proc/<pid>/stat` and the clock tick rate | Linux only; all threads of the process, not child processes. | | CPU per request (with `--bench`) | CPU time ÷ completed requests, in milliseconds | Responses of any status count. | A value that could not be measured reads `NOT MEASURED` and keeps the reason. Nothing else is reported: there is no throughput score, energy reading or rating. ## What happens during a run 1. **Target.** Without `--url`, green runs `cargo build --release` (Cargo reuses an up-to-date build), starts the binary from the project root with `RULLST_ENV=production`, `HOST=127.0.0.1` and `PORT=<free port>`, and waits up to 60 seconds for the first HTTP response to `GET --path`. It stops the app with SIGTERM (then a kill after 5 seconds) when the run ends. Production refuses to start without production settings, such as the SaaS starter's real billing credentials; set them, or pass `--env staging`, which accepts the starters' mock credentials and is recorded in the report. With `--url`, it measures an app that is already running. The URL must be `http://` on a loopback address (`127.0.0.0/8`, `::1` or `localhost`, which is pinned to `127.0.0.1`); anything else is refused before a request is sent. green finds the process listening on the port by matching the socket in `/proc/net/tcp{,6}` with `/proc/<pid>/fd`; it only sees your own processes. 2. **Benchmark (only with `--bench`).** `--concurrency` connections (default 4) each send `GET --path` (default `/`), wait for the whole response body, then send the next request, until `--duration` (default 10 s, 1 s to 10 min) ends. Proxies are ignored and redirects are not followed, so no request leaves loopback. The load generator runs on the same machine; its own CPU use is not part of the app's CPU time. 3. **Report.** Terminal tables (plain text under `NO_COLOR`, `CI` or when stdout is not a terminal) or, with `--json`, the versioned `rullst.green.v1` document. The command makes no network calls beyond loopback and the local container engine, and never fetches carbon intensity data. The release build itself may download crates as any `cargo build` does. ## What is estimated: SCI green follows the Software Carbon Intensity specification (ISO/IEC 21031:2024): ```text SCI = ((E × I) + M) / R ``` | Term | Source | Kind | | :--- | :--- | :--- | | E | measured CPU time × `--cpu-watts`, in kWh | estimate: green measures no energy | | I | `--grid-intensity` in gCO2e/kWh | yours, never fetched | | M | `--embodied` in gCO2e, the share of hardware emissions you allocate to this run | yours; *not included* when absent | | R | requests completed in the benchmark; the functional unit is one HTTP request | measured | `--cpu-watts` and `--grid-intensity` require `--bench` (and each other in that order), so the estimate is either computed from all of its inputs or not at all. Without them the SCI row reads `NOT ESTIMATED` and lists the missing inputs. The `--cpu-watts` figure is your assumption of power per busy CPU second (from the processor's documentation or your own measurement); the estimate ignores memory, disk, network and idle power. ### Choosing a grid intensity Use a value from a source you can cite for the place and time the software runs, such as your electricity supplier's disclosure, your grid operator or energy agency's published average, or a dataset your organisation already uses. Record the value, the source, the year or hour it describes and whether it is an average or a marginal figure next to the report. green never looks it up, so the same inputs always give the same result. ## Why results vary Numbers depend on the CPU model and its frequency scaling, power profile, thermal state, operating system, build profile, database contents, the request path, concurrency, and other processes on the machine (including the load generator). Compare runs only on the same machine with the same setup, repeat runs and look at the spread, and publish the report together with its `machine` and `benchmark` sections. The report header records the time, OS, architecture, logical CPU count and CPU model. ## JSON report `--json` prints one document whose `schema` (and `schema_version`) is `"rullst.green.v1"`, with `cli_version`, `measured_at` and the `machine` section. Every key is always present (null when unknown) so scripts can rely on the shape: - `target`: `mode` (`release_build` or `existing_url`), the URL, PID and how it was found. - `measured`: `binary_size_bytes`, `image_size_bytes`, `idle_rss_bytes` and `dependency_count`, each `{status, value, unit, method, reason}` with `status` `measured` or `not_measured`. - `benchmark`: `status` (`run` or `not_run`), its inputs, requests, errors, and `cpu_time_seconds` and `cpu_ms_per_request` in the same metric shape. - `sci_estimate`: `status` (`estimate` or `not_computed`), `label` (always `estimate`), the standard, formula and energy method, every input in `inputs`, `energy_kwh`, `sci_g_per_request` and `missing_inputs`. - `notes`: the fixed caveats printed under the tables. ## Exit status `0` when the measurement ran, even with `NOT MEASURED` fields; `1` when the release build failed or the app could not be started or reached; `2` for invalid arguments, including a non-loopback `--url` or an estimate input without `--bench`. ## Changes from `footprint` `cargo rullst footprint` (13.0.0-alpha.1 to alpha.3) is now `cargo rullst green`, in the same `cargo-rullst` package. It no longer reports requests per second, latency percentiles or peak RSS, no longer reads RAPL energy counters (whole package, including other processes) and no longer guesses an image name from the package name. The benchmark runs only with `--bench`, and the JSON schema is `rullst.green.v1` instead of `rullst.cli-footprint.v1`. --- # Rullst Labs Roadmap Source: https://rullst.github.io/Rullst/book/rullst-labs-roadmap.html # Rullst Labs roadmap: bring your own runner > **Status:** `rullst-labs` is a v13 implementation candidate; final release > admission remains outstanding. The separately deployed `rullst-labs-runner` > candidate passed [source admission in PR #228](https://rullst.github.io/Rullst/book/labs-first-profile.html#recorded-linux-acceptance) > and was then **removed from the workspace for 13.0** (maintainer decision of > 30 September 2026); its source remains in git history. Applications bring > their own runner through the [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html). > This document is not evidence that Rullst can safely execute untrusted code in > production. Rullst should make interactive programming exercises, deterministic graders and bounded learning games easier to build without placing untrusted code inside an application server. The design deliberately separates the trusted control plane (`rullst-labs`) from the untrusted execution plane, which is now owned and operated by the application. ## Package and deployment boundaries `rullst-labs` is an implementation candidate in the Rullst monorepo, first published in the 13.0.0-alpha.1 prerelease. It must remain opt-in and must not become a default dependency of `rullst`. ### `rullst-labs` `rullst-labs` is a safe Rust library for the trusted application side. The first candidate implements `Exercise`, `ExecutionLimits`, `Submission`, worker/receipt contracts and an optional encrypted shared-local SQLite job plane. Broader profile-independent contracts remain roadmap work. Its responsibilities are: - versioned exercise, bounded execution, request, receipt and grading types; - tenant-, learner- and submission-bound authorization inputs; - resource, language, toolchain, network and artifact policy validation; - idempotent submission orchestration, cancellation and result reconciliation; - static-dispatch backend traits and a deterministic offline mock; and - minimized audit events that exclude submitted source and secrets by default. It must never interpret raw shell strings, spawn learner code, grant execution permissions or treat an AI-generated assessment as authoritative evidence. ### Application-owned runner The runner is a separately deployed program owned and operated by the application for the untrusted execution plane. Its trusted controller uses the `rullst-labs` job plane (`claim_next`, `lease_status`, `complete`, `abandon_attempt`, `cleanup_candidates`, `reconcile_cleanup`) and signs receipts; its isolated worker receives only bounded `WorkerInput` data. The [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html) states the exact obligations and the non-executing `byo_runner_controller` example shows the controller flow. The runner's responsibilities are: - leased, cancellable work with stale-attempt fencing and recovery; - pinned images and toolchains, disposable workspaces and capped artifacts; - actually enforced and observed isolation and resource limits; - exact, bounded worker outcomes for the trusted grader; and - signed, privacy-minimized execution and cleanup receipts. The runner must never be embedded in the HTTP application process. The application must not mount a Docker, Podman, containerd or Kubernetes control socket merely to operate it. Deployment credentials, host secrets and cloud metadata must be absent from the execution environment. ### Removed `rullst-labs-runner` candidate The workspace contained an experimental `rullst-labs-runner`: a Linux x86-64 controller that compiled the fixed Rust 1.96.0 profile to Wasm and interpreted it with Wasmi 2.0.0 under delegated cgroups v2, unprivileged namespaces, seccomp and separate Landlock domains. Its named Linux journey and workspace/archive source admission passed in PR #228. On 30 September 2026 the maintainer decided to remove it from the 13.0 workspace rather than publish it, consistent with the [maintenance-scope](https://rullst.github.io/Rullst/book/v13-maintenance-scope.html) assessment of its very high security and operational burden. Its independent isolation review never took place, and it was never published. The source remains in git history, and the [first-profile record](https://rullst.github.io/Rullst/book/labs-first-profile.html) preserves its design and evidence as a reference for application-owned runners. ## Isolation baseline Every real backend must apply defence in depth rather than treating a container as a complete security boundary: - deny network access by default; explicit destinations require a reviewed per-lab allowlist; - run rootless and unprivileged, drop capabilities, set `no_new_privs`, and use a read-only root filesystem with a fresh bounded workspace; - enforce CPU, wall-clock, memory, process, file, disk, output and artifact limits, preferably with cgroups v2 on Linux; - use namespaces plus supported seccomp, Landlock or equivalent platform restrictions; - pin immutable image digests, compiler versions, dependencies and grader revisions; - tear down each workload completely and quarantine abnormal results; and - never expose other tenants' jobs, caches, network namespaces or artifacts. “Sandboxed” may be used in product documentation only for a named backend whose threat model, platform requirements and executable adversarial evidence are published. No backend may promise that arbitrary hostile code is risk-free. ## Backend progression These phases now describe what an application-owned runner must demonstrate; Rullst does not ship a backend for any of them. | Phase | Scope | Required evidence before promotion | | :--- | :--- | :--- | | 0 | Protocol, threat model, ADR, policy validation and offline mock | Contract, abuse-case and compatibility tests; no untrusted execution claim | | 1 | Restricted Wasm exercises; the protocol's first profile names pinned Rust compilation and Wasmi in a separate restricted Linux process | Compiler and interpreter resource bounds, filesystem, environment, network, fuel/memory, output and recovery tested; WASI/component capabilities need their own later profile | | 2 | Rootless OCI backend for workloads that require native toolchains | Pinned runtime choice, kernel hardening, resource-exhaustion and escape regressions | | 3 | Optional microVM or independently operated cloud sandbox tier | External operational evidence, image provenance, isolation review and incident procedures | The [first-profile decision](https://rullst.github.io/Rullst/book/labs-first-profile.html) selects a bounded Rust pure-function journey; native Rullst server exercises remain outside it. Additional languages need a new `rullst-labs` protocol profile reusing the same policy and receipt contracts instead of adding ad-hoc shell execution paths. ## Learning and game scope The foundation may support compilation exercises, unit and property-based graders, lint challenges, bounded command-line programs and server-authoritative learning games. AI may explain a result or suggest the next exercise, but it must not weaken an execution policy, grant network access, reveal secrets or convert an unverified run into a passing grade. General-purpose realtime games still require application-specific gameplay, assets, moderation, anti-cheat, state synchronization and operational design. `rullst-labs` is not intended to become a graphics or physics engine. ## Offensive security and CTF boundary Ordinary secure-coding exercises may run under the standard restricted profile. A narrowly scoped offensive exercise may be explored only through a distinct, experimental profile with a dedicated target and deny-by-default egress. A complete offensive CTF arena is external deployment infrastructure integrated with Rullst, not an in-process framework feature. It requires separate network segments and accounts, disposable machines or hardened workloads, vulnerable targets created solely for the exercise, rate and abuse controls, monitoring, cleanup, incident response and applicable legal authorization. It must not share hosts, clusters, databases, credentials or control planes with the Rullst application or other production workloads. Rullst may provide typed provisioning contracts, challenge metadata, identity, scorekeeping and receipts for such an arena. Operators remain responsible for deploying and governing the isolated infrastructure. A local crate alone cannot make a full offensive CTF safe. ## Acceptance gates Before `rullst-labs` with any runner can be described as production-ready for untrusted code, the runner's operator must: - approve a threat model and architecture decision record; - obtain an independent security review of the deployed isolation backend; - test fork/process, memory, CPU, disk and output exhaustion; - test path traversal, symlink races, environment leakage and artifact escapes; - test network denial, cloud-metadata denial and cross-tenant replay/isolation; - test cancellation, worker loss, duplicate delivery and bounded recovery; - publish the supported Linux/runtime matrix and fail closed elsewhere; and - define retention, deletion, observability and incident-response contracts. ## Explicit non-goals - a claim that arbitrary code can be executed with zero escape risk; - embedding an executor or container-control socket in the web server; - a hosted Rullst service being implied by the open-source packages; - a malware-analysis environment or unrestricted offensive range; - letting an LLM decide security policy or authoritative grading; and - store, certification or platform guarantees that require external evidence. ## v13 usable journey target **Historical.** This target guided the removed runner candidate. Since its removal, the execution and isolation parts of steps 3 and 5 belong to the application-owned runner; the contract, grading, cancellation, recovery and retention parts remain in `rullst-labs`. On September 20 the owner prioritized completing Bunny Stream and then Labs before opening more optional implementation streams. Phase 0 remains necessary for architecture and protocol review, but contracts and a mock are an intermediate milestone. The intended Labs outcome is a usable exercise and grading journey through a separately deployed runner: 1. An authorized instructor defines a versioned exercise, exact toolchain/grader revision and execution policy; tenant and learner permissions are checked by the trusted application before submission and result access. 2. An authorized learner submits bounded input to a durable, idempotent job. Retries cannot silently create a new execution or apply a result twice. 3. One named isolated backend executes the supported exercise profile with explicit resource, filesystem, environment, network and output limits. The initial candidate is the Phase 1 Wasm backend; supported Rust exercise and compilation requirements must be demonstrated before advertising a language pack. Rust/Rullst native server exercises are not implied by Wasm support. 4. The runner returns an integrity-bound receipt with exact grader revision, outcomes and bounded diagnostics. Validate request/result binding and reject stale, cross-tenant, forged or incompatible results before applying them. 5. Cancellation, timeout, duplicate delivery, worker restart/loss, cleanup and reconciliation have executable recovery tests. Retention and deletion cover source, workspaces, outputs and artifacts rather than only the job record. 6. A focused application consumer exercises submission, status, cancellation and result access. Package/deployment guides explain installation, supported platforms, capacity limits, required configuration and failure recovery. Use real automated disposable-runner tests for execution and isolation; no manual or paid provider account is required to develop those fixtures. Local disk/build limits still apply, so use hosted acceptance for large runtime builds. Passing protocol mocks alone cannot establish isolation. The production-ready criteria above, including independent review of the selected backend, remain distinct requirements; automated tests do not substitute for that evidence. The September 23 feature freeze and September 24–26 validation/publication window applied to this target. Pursue this outcome before other optional features, but do not promise the full runner before its acceptance evidence exists. If incomplete at freeze, retain an explicit experimental boundary and carry the missing journey forward. Additional languages, OCI/microVM backends and full CTF infrastructure do not block completion of a supported first profile and must not be bundled into its claim. ## v13 delivery checklist Checked items describe implemented candidate work, not stable publication or production readiness. Broader schemas/backends retain their own acceptance requirements. - [x] Record the Phase 0 threat model and first-profile architecture decision. - [x] Implement bounded versioned exercise/request/worker/receipt types and decoding for the selected profile. - [x] Implement policy validation and an explicit simulation that cannot produce real execution evidence. - [x] Implement application-authorized shared-local SQLite transport, encrypted content, idempotency, fenced leases and cancellation. - [x] Exercise the pinned Rust-to-Wasm/Wasmi Linux profile against the named hosted execution and denial fixtures (removed runner candidate, PR #228). - [x] Exercise tenant/learner denial, minimized results, terminal source removal and bounded retention. - [x] Pass workspace/platform and extracted-package source admission (PR #228). - [x] Remove the runner candidate from the 13.0 workspace; document the bring-your-own-runner [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html) with a non-executing example controller. - [ ] Complete final release/package admission of `rullst-labs`. - [ ] Application-owned runners: independent isolation review of each deployed backend before any production hostile-code claim. - [ ] Evaluate rootless OCI or microVM profiles as separate later protocol work. - [ ] Add native Rust/Rullst application, general Cargo, test and lint language packs; the pure-function profile does not provide them. - [ ] Reconsider offensive CTF infrastructure only through its separately governed experimental deployment and review. `rullst-labs` is in the release-order manifest and has shipped since `13.0.0-alpha.1` (maintainer decision, 8 October 2026). It packages contracts only; no runner or execution engine is published with it. The versioned protocol keeps applications independent of any particular runner implementation. --- # Labs Runner Contract: Bring Your Own Runner Source: https://rullst.github.io/Rullst/book/labs-runner-contract.html # Labs runner contract: bring your own runner > **Status:** `rullst-labs` is a v13 candidate, first published in the > 13.0.0-alpha.1 prerelease. The experimental > `rullst-labs-runner` candidate (a Linux Rust-to-Wasm/Wasmi executor) was > removed from the workspace for 13.0. Its source remains in git history, for > example at `main` revision `f7909033`. Rullst no longer ships an executor. > Applications deploy their own runner against the contract below. `rullst-labs` owns trusted exercises, authorization, encrypted durable jobs, fenced leases, exact grading, cancellation, recovery and retention. It never compiles, interprets or spawns learner code. A **runner** is the application's separately deployed program that takes leased work, executes it in an isolated environment and reports an Ed25519-signed receipt. This page states what that runner must do with the existing `rullst-labs` API, what the library verifies and what the host must guarantee. Nothing here certifies a runner as safe. ## Roles | Component | Holds | Never holds | | --- | --- | --- | | Application (web process) | `SqliteLabs` handle, content key, pinned receipt **public** key; `register_exercise`, `submit`, `get_job`, `cancel`, `set_exercise_enabled`, `expire_queued`, `purge_terminal`, `remove_exercise` | Receipt signing seed, an executor, a container control socket | | Trusted controller (your runner's trusted half) | The same job database file and content key, the receipt signing seed; `claim_next`, `lease_status`, `complete`, `abandon_attempt`, `cleanup_candidates`, `reconcile_cleanup` | Application identity/session credentials | | Untrusted worker (your isolated execution) | One `WorkerInput`: source, case inputs, limits and an opaque binding | Job database, content key, signing seed, expected answers, application secrets, network, control sockets | The controller operations are not student or HTTP capabilities. Never wire them to an application route. The shared-local store means one trusted local filesystem with explicit process separation; it is not a remote broker or a network-filesystem database. ## Controller prerequisites - Depend on `rullst-labs` with the `sqlite` and `receipt-signing` features. - Open the application's store with `SqliteLabs::open(path, config, key, clock)`. `StoreConfig` (namespace, `max_jobs`, `max_exercises`, profile and any `learner_jobs` or `tenant_exercises` quota) and the `ContentKey` must be identical to the application's. The store binds them; any difference returns `Configuration`. - Use `ExecutionProfile::LinuxExperimental { tools, receipt_key }`. It is the only profile whose signed receipts `complete` and `reconcile_cleanup` accept; `Simulation` returns `Unsupported` there. Despite the name, `rullst-labs` checks neither Linux nor isolation. `ToolIdentity` holds seven SHA-256 digests (`runner`, `compiler`, `wasm_toolchain`, `runtime`, `launcher`, `syscall_policy`, `filesystem_policy`) that the operator pins for the runner's installed files. They are bound into every job, not verified. - Create the signer with `ReceiptSigner::from_seed` from a dedicated random 32-byte seed (all zeroes is refused). Its `public_key()` is the profile's `receipt_key`. Never reuse the content key or an application secret as the seed, and never give the seed to the application or the worker. Keep both secrets in owned private regular files under protected ancestors, and validate the opened no-follow descriptor and exact 32-byte length. - The profile, including tool digests and receipt key, is part of the store's configuration binding. Rotating the key or changing a pinned tool therefore requires a new store: drain and retire the old one first. - Use a clock synchronized with the store's. The store refuses time that moves backwards (`Clock`) and receipts outside the lease window (`Protocol`). ## Lifecycle ### 1. Recover before claiming On startup, and periodically, call `cleanup_candidates(limit)` (1–32). It returns leased jobs that are no longer `Running` (cancelled, abandoned with cleanup pending, or expired) and `Running` jobs whose lease or job lifetime ran out. A still-`Running` record is fenced to `Uncertain` (or `Expired`) before it is returned. Withdrawing an exercise changes no job record: the controller that owns a lease sees it as `Stop` from `lease_status` (and `complete` returns `Denied`), while a withdrawn job whose controller was lost is returned only once its lease runs out, at most the wall limit plus 15 seconds after the claim. For each `CleanupJob`, tear down everything your runner owns for `binding.nonce`, then attest it as described in step 7. Recovery never releases source and can never award a grade. ### 2. Claim `claim_next()` returns at most one `LeasedJob`, or `None`. It skips up to 32 queued jobs that are withdrawn (marked `Cancelled`), too close to expiry or out of attempts (marked `Expired`). A job is claimed only while more than its wall limit plus 5 seconds of lifetime remain, and at most twice. Each claim creates a fresh random nonce and a lease that ends at the claim time plus the wall limit plus 15 seconds, capped at the job's expiry. `LeasedJob` exposes `scope()`, `id()`, `revision()`, `expires_at()` (the lease end) and `input()`. ### 3. Hand the worker its input `job.input()` is a `WorkerInput`: | Field | Bound | | --- | --- | | `binding()` | `AttemptBinding`: request, profile and source digests plus the attempt nonce | | `source()` | `RustSource`, at most 32 KiB; `expose_source()` is for the isolated boundary only, never logs | | `inputs()` | 1–64 `[i64; 2]` case inputs, in grader order | | `limits()` | `wall_seconds` 5–60, `fuel_per_case` 1,000–1,000,000, `memory_pages` 32–256 (2–16 MiB) | It carries no tenant or learner identity and no expected answers. Its JSON form is decoded by `WorkerInput::from_bytes`, which refuses more than 131,072 bytes. Protocol version 1 (`PROTOCOL_VERSION`) defines one profile, `PROFILE` (`rust-function-wasm-v1`): a pure `solve(i64, i64) -> i64` function evaluated once per input. `TOOLCHAIN` and `INTERPRETER` name the removed candidate's Rust 1.96.0 `wasm32-unknown-unknown` and Wasmi 2.0.0 semantics; all three constants are bound into exercise and profile digests. Another language or native profile needs a new `rullst-labs` protocol, not a runner-side extension. ### 4. Monitor the lease Before releasing source, and while the worker runs, call `lease_status(&job)`. `Active` requires an enabled exercise, a `Running` job, the same revision, lease and binding, and an unexpired lease. `Stop` or an error means stop now. A status read never extends the lease. Enforce `limits().wall_seconds()` yourself. The removed candidate polled every 200 ms; choose an interval that bounds wasted work. ### 5. Validate the worker's reply Decode untrusted worker bytes with `WorkerOutput::from_bytes` (at most 20,480 bytes) and require `output.binding == *job.input().binding()`. Outcomes: - `Executed { artifact, cases }`: one `CaseOutput` per input, in order. Each is `Value(i64)` or `Trap(Fuel | Memory | Stack | Guest)`. A case count different from the exercise's is `Protocol`. `artifact` is an opaque digest. - `CompileRejected { diagnostics }`: `Diagnostic` text of at most 8,192 bytes, no control characters except newline and tab. It is untrusted and can quote source; escape it when rendering. - `Rejected(failure)`: `Compile`, `InvalidModule`, `ResourceLimit`, `WorkerProtocol`, `WorkerLost` or `Isolation`. The worker never sends expected values or a pass flag. Sign `Executed` only for code that actually ran: `complete` grades it against the hidden answers. ### 6. Sign and report Build an `ExecutionReceipt` and sign it with `ReceiptSigner::sign`: - `started_at`: sampled **after** `claim_next` returns; it must not precede the claim time the store recorded. - `finished_at`: at most 90 seconds after `started_at`, not later than the store's time when `complete` runs and strictly before `job.expires_at()`. - `observation_digest`: a digest of your actual per-job isolation and resource observations. `rullst-labs` stores it as evidence but cannot interpret it. - `teardown`: `Confirmed` only after the worker group and workspace are gone. `complete` refuses `Uncertain`. `sign` validates the receipt first: a wall clock that steps back between the two samples makes it return `Protocol`. Handle a failed clock read or signing like any other failure (step 7) instead of returning with the lease still live. Send it to `complete(job.scope(), job.id(), &signed)`. `Executed` becomes a `Completed` job with per-case `Passed`/`WrongAnswer`/`Trapped` feedback; any other outcome becomes `Failed`. Serialized receipts are decoded by `SignedReceipt::from_bytes` (at most 20,480 bytes, lowercase hex signature). An exact replay of an accepted receipt returns the same view; a different receipt for a finished job is `Conflict`. ### 7. Abandon, clean up and reconcile On any failure, stop signal, timeout or rejected `complete`: 1. `abandon_attempt(&job)` fences the attempt immediately (`Running` becomes `Uncertain`, or `Expired`) and returns a `CleanupJob`. An old nonce cannot fence a newer attempt. 2. Tear down everything owned for `cleanup.binding.nonce`. If absence cannot be confirmed, sign nothing; the job stays fenced with `cleanup_pending` and `cleanup_candidates` returns it on a later pass. 3. Sign a cleanup receipt whose output is `Rejected(ExecutionFailure::WorkerLost)` for `cleanup.binding`, with `Teardown::Confirmed`, whatever the attempt did before teardown. 4. Call `reconcile_cleanup(scope, id, &receipt, retry)`. Cancelled and expired jobs stay terminal. An `Uncertain` job becomes `Queued` again only with `retry = true`, an enabled exercise, fewer than two attempts and enough lifetime (a fresh nonce on the next claim); otherwise `Cancelled`, or `Expired` when too little time remains. Reconciliation never grades. A `WorkerLost` output sent to `complete` instead becomes a terminal `Failed` job without a retry. The removed candidate never retried automatically. ### 8. Retention `complete` and terminal reconciliation erase the encrypted source and grader snapshot from the job record. The application schedules `expire_queued` and `purge_terminal` (at least 24 hours) with `ManageJobs` authorization; neither requires the runner or claims worker teardown. The runner must delete its own workspaces, artifacts, worker output and logs, and must not persist source or diagnostics beyond the attempt. Backups, WAL pages and physical erasure remain operator responsibilities. ## Verified by `rullst-labs` versus guaranteed by the host | `rullst-labs` verifies | The host and its runner must guarantee | | --- | --- | | Ed25519 signature against the pinned `receipt_key`, never a key inside the receipt | Custody of the signing seed; only the trusted controller can sign | | Binding to the current lease: request, profile and source digests and nonce, plus job revision | That the worker actually executed that source under that profile | | Lease and job expiry, receipt timing window, exercise still enabled | Clock synchronization; enforcing the wall limit and stopping on `Stop` | | `Teardown::Confirmed` is present | That teardown really happened before signing | | Case count, bounded wire sizes and decoded invariants | Fuel, memory, process, disk, output and network limits actually enforced | | Exact grading against stored expected answers; minimized public feedback | Expected answers, keys and the database never reach the worker | | Idempotent replay, single award and fencing of stale, cancelled or withdrawn results | Real isolation evidence behind `observation_digest`; tool provenance behind `ToolIdentity` | | Encrypted, integrity-checked job content and erasure at terminal states | Deletion of runner-side workspaces, artifacts, logs and backups | ## Isolation the host owns Treat a container, a separate crate or a separate process alone as insufficient. The minimum baseline, from the [roadmap](https://rullst.github.io/Rullst/book/rullst-labs-roadmap.html#isolation-baseline): - run the worker outside the web application process, preferably on a separate host or VM, with no job database, content key, signing seed, expected answers, application credentials, inherited environment, cloud metadata access or container/orchestrator control socket; - deny network access; run unprivileged with no new privileges, dropped capabilities and a read-only root plus a fresh, bounded, disposable workspace; - enforce CPU, wall-clock, memory, process, file, disk and output limits (cgroups v2 or an equivalent) covering compilation as well as execution; - apply syscall and filesystem restrictions and **observe** them in effect before releasing source; refuse work on unsupported hosts with no weaker fallback; - pin toolchains and images by digest, tear down each attempt completely and treat compiler output and worker bytes as untrusted. The removed candidate's [first-profile threat model](https://rullst.github.io/Rullst/book/labs-first-profile.html) records one detailed Linux design (namespaces, cgroups v2, seccomp, Landlock) and its historical hosted evidence. It is a reference, not shipped software. Describe a runner as sandboxed only with a published threat model, platform requirements and adversarial evidence for that runner, plus independent review before any production claim about hostile code. ## Minimal non-executing example [`rullst-labs/examples/byo_runner_controller.rs`](https://github.com/Rullst/Rullst/blob/main/rullst-labs/examples/byo_runner_controller.rs) opens the store as a separate controller, recovers leftover leases, claims a job, polls `lease_status` under the wall limit, signs a fixed `Rejected(Isolation)` verdict and submits it to `complete`. It also fences failures through `abandon_attempt` and `reconcile_cleanup`. It never compiles or runs learner code; two `PLUG-IN POINT` functions mark where an isolated worker and its teardown belong. ```bash cargo run -p rullst-labs --example byo_runner_controller --features sqlite,receipt-signing cargo test -p rullst-labs --all-features --example byo_runner_controller ``` The [`course_app` example](https://github.com/Rullst/Rullst/blob/main/rullst-labs/examples/course_app.rs) shows the application side: registration, submission, status, cancellation, withdrawal and retention. ## Compatibility There is one wire protocol (version 1) and one profile. All wire types reject unknown fields. A runner must use these exact types; there is no adapter layer or runner version negotiation, and interchangeable external executors are not provided. New profiles, languages or a remote job transport are `rullst-labs` roadmap work and require their own threat model and acceptance evidence. --- # Labs First Profile Decision & Threat Model (Removed Runner) Source: https://rullst.github.io/Rullst/book/labs-first-profile.html # Labs first execution profile: decision and threat model > **Historical record.** The `rullst-labs-runner` implementation described here > was removed from the workspace for 13.0 (maintainer decision of 30 September > 2026); its source remains in git history, and its CI jobs and helper scripts > were removed with it. `rullst-labs` still defines the `rust-function-wasm-v1` > profile, but Rullst no longer ships an executor: applications bring their own > runner through the [controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html). Read the > requirements below as a reference baseline for such a runner, not as shipped > or currently tested software. Status: implementation decision for an **unpublished experimental candidate**. The named Linux journey has hosted evidence below; independent security review and production readiness are not claimed. The September 23 freeze and September 24–26 validation window applied when this decision was recorded. Contracts and an offline simulation are intermediate work. ## Recorded Linux acceptance On September 21, [run 35582398251](https://github.com/Rullst/Rullst/actions/runs/35582398251) passed at `977e40a31393b6403c960919b9ea72c54813ef5a` on disposable Ubuntu 24.04 with the pinned Rust 1.96 profile. Its minimized artifact records 26 checks: actual isolation, compiler descriptor denial, shared kernel group capacity, compilation/grading, ownership/idempotency, wrong answers, fuel/stack/memory boundaries, compiler errors, filesystem/environment/import/output denial, fresh guest state, compiler deadline/cleanup, cancellation, controller loss, recovery at full group capacity, competing controllers and terminal source removal. Separate real Landlock and seccomp regressions also passed. The same run repeated all 26 checks with instrumentation and generated the scoped controller coverage report. Both ordinary and instrumented artifacts preserve the named profile and outstanding independent-review boundary. No live provider account was used. The compiler-deadline case observes a real compiler before requiring the controller's own five-second deadline to cancel the attempt and remove its entire group; neither an application cancellation nor a test-owned kill can satisfy it. Excess compiler output must produce the bounded resource-limit outcome, and retained diagnostics may not exceed the protocol byte limit. [PR #228](https://github.com/Rullst/Rullst/pull/228) subsequently passed source admission on the same head and merged into `v13` at `278a115b`: - [Native CI](https://github.com/Rullst/Rullst/actions/runs/35583180166) passed all 24 test shards across Linux, macOS and Windows, strict Clippy, formatting, database contracts and the evidence scorecard. All 43 branch-required checks passed; CodeQL reported no open alerts on the PR. - The actual [installed-archive job](https://github.com/Rullst/Rullst/actions/runs/35583179745) audited 21 archives, exercised extracted Media/Labs contracts and the independent application consumer, installed CLI `13.0.0-alpha.1` and compiled all six blueprints. The application consumer has no executor dependency. - [Combined coverage](https://github.com/Rullst/Rullst/actions/runs/35583179992) passed both exact 90% floors: 101,088/112,148 whole-repository lines (90.1380%) and 72,607/79,958 framework-library lines (90.8064%). The PR checkout tree equals the candidate's tree. Codecov's separate, non-required patch check still reports **83.45936% against its 90% target**, with 525 changed lines unmeasured. Compiler, worker and probe paths account for much of that gap: their sandbox counters deliberately are not exported. Passing isolated acceptance does not establish which missing lines ran. Preserve this quality limitation for follow-up without weakening isolation, excluding production paths or claiming that every check passed. This evidence applies to that source revision and named host/profile. The final release campaign and independent isolation review remain separate requirements. It does not prove every adversarial case or every deployment environment. Coverage must also measure the trusted controller's lifecycle paths. The instrumented fixture retains identical controller/worker executable hashes and all namespace, environment, filesystem and resource checks. A CI-only LLVM hook initializes profile output only for a trusted host with an explicit output path; in the worker's cleared environment it performs no profiling initialization. Only trusted host processes write profiler data outside the sandbox; the worker does not receive profiling variables or a host output directory. These counters are merged with ordinary workspace coverage, without changing either 90% floor. The scoped manual `labs-isolation` measurement diagnoses that path and cannot replace the complete coverage or release campaign. ## Supported journey An authorized instructor creates an immutable exercise revision with a bounded exact grader. A currently authorized learner submits Rust source to a dedicated durable job. A separately deployed runner compiles and evaluates that submission under one named Linux profile. The application exposes authorized status, result and cancellation, with durable restart/lease recovery and bounded retention. The first exercise is a pure function with the fixed ABI `solve(i64, i64) -> i64`, evaluated over at most 64 instructor-defined cases. Rust 1.96.0, the `wasm32-unknown-unknown` standard-library/toolchain files and the runner artifact and minimal dynamic runtime tree must be pinned and checked. Submission compilation uses fixed arguments and no Cargo build scripts, package downloads, external dependencies, arbitrary shell commands or student-selected toolchain paths. Rust source, compiler output and resulting Wasm are all untrusted bounded data. Native Rullst applications, arbitrary-language workspaces and network exercises remain later profiles rather than implicit capabilities of this one. Wasmi 2.0.0 is selected for the first interpreter implementation: validation, extra runtime checks, explicit structural/store/stack limits and deterministic fuel are required; WAT parsing, WASI, host imports, shared memories, threads, start functions and unsupported proposals are excluded. A fresh instance per test case avoids hidden state between cases. Reference-type decoding is enabled for the pinned Rust standard library's `call_indirect` encoding; tables are restricted to bounded `funcref`, and imports remain prohibited. The interpreter is one boundary inside process isolation, not a substitute for it. Wasmtime/component/WASI support remains a separate profile decision. ## Ownership and process boundaries | Component | Authority and data | | --- | --- | | Application and `rullst-labs` | Current tenant/course/enrollment policy; exercise/grader revision; durable job submission/status/cancel/result. No execution engine, learner process or control socket. | | Dedicated shared-local job store | Versioned, integrity-checked encrypted source/grader content; bounded jobs, leases, cancellation and minimized results. Separate key and database from application identity/session state. | | Trusted runner controller | Dedicated job-store and receipt authority; pinned tool paths; leased work, compiler/executor launch, bounded output and teardown. No general application credentials. | | Per-job compiler/interpreter process | Only the selected source, fixed toolchain, case inputs and disposable output/workspace. No expected answers, job database, runner signing key, host home, environment credentials or control sockets. | Shared-local means one trusted local filesystem with explicit process separation; this profile does not supply a remote broker, network filesystem or distributed runner service. Application authorization never follows from a client-provided learner, course or provider identifier. File possession/configuration is not a student capability. Neither signed requests nor an AI evaluation can weaken the execution policy or manufacture a passing result. Content and receipt keys require private regular files owned by the controller under protected canonical ancestors. The controller validates the opened descriptor, rejects symlinks and non-regular files without blocking on a FIFO, and requires exactly 32 bytes. File permissions alone do not protect a key whose parent directory lets another local user replace it. ## Mandatory Linux execution boundary The implementation must apply and test all of the following before accepting untrusted input into the worker: - A dedicated delegated cgroups v2 subtree for each job, with hard memory, swap, process and CPU limits plus controller-owned wall-clock deadlines. Place the inert child in its bounded group before releasing input; cover compiler initialization/translation as well as guest execution. The delegated root must have a kernel-enforced `cgroup.max.descendants` between 1 and 32, including probe groups. Counting directories before creation cannot enforce capacity across concurrent controllers. Reconcile authenticated expired/cancelled attempts before preflight so a full subtree can recover; cleanup never releases source and cannot award a successful grade. - Unprivileged user, PID, mount, IPC and network namespaces; capability drop, no-new-privileges, no inherited terminal/session or additional user namespaces. The launcher is a reviewed, pinned configuration of Bubblewrap, not an arbitrary command string supplied by an application request. - A minimal read-only toolchain/runtime tree, a fresh bounded workspace and explicit descriptors. No host home, D-Bus/container sockets, job-store mount, writable toolchain/cache, application files or metadata endpoint reachability. Tool files/directories and their canonical ancestors must be owned by root or the dedicated controller UID. Shared-writable ancestors are refused except sticky parents protecting the next owned component; tool trees themselves must never be shared-writable. Host administrators/controller ownership remain trusted, and digest checks do not replace trusted installation or custody. - Before source is released, require fully enforced Landlock filesystem restrictions (ABI v3 rights) for fixed tools, runtime files and the bounded workspace. Spawn the trusted compiler helper before the interpreter applies Landlock, then restrict each in a separate domain. The helper checks its own actual OS boundary and denied access to its parent's descriptors/memory before acknowledging readiness. Only then may the interpreter receive source. Regular `/proc` and cgroup files are denied by the filesystem policy; the kernel does not mediate a process's own anonymous pipes through `/proc`, so cross-process protection must use Landlock's ptrace/domain hierarchy. Compiler stdin/stdout become null at exec; only capped stderr diagnostics return. Inherited descriptors are checked against the exact owned protocol handles. - Reviewed syscall restrictions in addition to namespaces; no extra privileges or unbounded resource allowance on unsupported kernels/platforms. The compiler selects the pinned linker by its absolute toolchain path. Rust's changed-`PATH` spawn fallback uses a socket pair, so a bare linker name would fail under the unchanged no-sockets policy. A real seccomp regression verifies absolute-path spawning while socket-pair creation remains denied. Rust's captured linker streams also require `ioctl(FIONBIO)` to toggle nonblocking reads on existing descriptors. The filter permits that exact request only; it does not grant device-control ioctls or socket creation. A real filter regression captures both child streams and verifies another ioctl request is still denied. The namespace exposes no device-control handles. - Structural Wasm validation, bounded compilation, memory/stack/table/fuel limits, no guest imports and bounded, normalized output. No deserialization of untrusted native/precompiled engine caches. - Mandatory teardown of the entire job group and workspace. Uncertain cleanup, memory/process exhaustion, malformed output and worker loss cannot be graded as successful execution. Runtime probes must check actual restrictions, including namespace separation, denied filesystem/network/control-socket access, capability state and effective cgroup limits. Property names, process exit zero or a launcher version alone are not admission evidence. The development machine accepted a user-service `PrivateNetwork` request while retaining the same network namespace; a subsequent namespace launch failed. Those probes are evidence of an unsupported environment, not permission to remove network restrictions or execute learner code there. Automated acceptance must provision a supported disposable Linux host explicitly. The Ubuntu 24.04 acceptance host needs an AppArmor namespace permission for its trusted launcher. The hosted test provisions a root-owned private copy of the distro Bubblewrap executable and a separately named, checksum-reviewed copy of Ubuntu's `bwrap-userns-restrict` profile, attached only to that launcher path. Its child profile denies capabilities. This setup is confined to the disposable host; it never disables AppArmor or changes a global namespace sysctl. The runner still requires its own namespace, capability, seccomp, Landlock and cgroup probes. Ordinary workstations are not reconfigured by the runner or local test suite. The domain split addresses the kernel's documented [special-filesystem limitation](https://docs.kernel.org/userspace-api/landlock.html#special-filesystems). A failed denial probe blocks source release; removing that probe without a replacement boundary is not an acceptable compatibility fix. ## Integrity, grading and recovery An immutable request binds protocol/profile version, tenant, course, learner, exercise/grader revision, source digest, pinned tools, resource policy and local submission ID. Reusing an ID with different content fails. Database content is authenticated before use and bound to indexed identity/configuration. Source and expected answers do not appear in Debug, routine events, metric labels or errors. Claiming a job creates a bounded durable lease and execution nonce before launch. Worker output contains bounded case values/traps, never authoritative expected answers or a caller-controlled `passed=true`. Trusted grading uses the exact stored exercise revision. Result acceptance binds the current lease/request and refuses stale, expired, cancelled, cross-tenant or incompatible receipts. A replayed receipt cannot award credit twice. Compiler diagnostics are bounded, treated as untrusted text and escaped for any HTML/terminal presentation. Cancellation records denial before process termination and fences any late result. On restart, reconcile persisted work and owned job groups; do not assume that a missing connection or process means compilation/execution succeeded. Pure functions permit deliberate bounded re-execution after confirmed teardown; there is no exactly-once external side-effect claim. Retention covers encrypted source, workspaces, artifacts, diagnostics and result/idempotency tombstones. Restored state needs clock/configuration/lease reconciliation, not silent reuse of stale authority. Operators own encrypted backups and physical storage erasure. Authorized application maintenance expires queued source independently of runner availability, without touching leased work. A withdrawn grader can be removed only after all referencing job records have passed retention and been purged. Removed immutable revision identifiers must not be reused. ## Adversarial acceptance required The executable test plan includes malformed/oversized source and Wasm, incompatible ABI/imports, infinite loops, recursion, memory/table/output/disk/process exhaustion, compiler denial of service, filesystem traversal/symlinks, environment and file descriptor leakage, network/metadata/control-socket denial, concurrency, worker restart/loss, stale-result fencing, cancellation/expiry, idempotency collisions, retention and actual application submission/status/result access. Include both working Rust examples and failing submissions; a mocked receipt cannot pass the execution/isolation gate. Memory acceptance separately verifies denied growth, out-of-bounds access and Rust allocation failure. An allocator abort is a generic guest trap because a Wasm `unreachable` instruction does not reliably identify its original cause; it must not be relabeled as a memory-specific trap merely to fit a test. The platform matrix and named profile remain experimental until the required tests and independent isolation review exist. No claim of risk-free hostile-code execution, complete Rust/Rullst language support or generic secure CTF hosting follows from the contracts or this decision. ## Primary implementation references - [Wasmi 2.0 API and feature boundaries](https://docs.rs/wasmi/2.0.0/wasmi/) - [Wasmi configuration and validation defaults](https://github.com/wasmi-labs/wasmi/blob/v2.0.0/crates/wasmi/src/engine/config.rs) - [Bubblewrap policy responsibilities and limitations](https://github.com/containers/bubblewrap/blob/main/README.md) - [Wasmtime security boundaries, including compilation denial of service](https://docs.wasmtime.dev/security-what-is-considered-a-security-vulnerability.html) - [Linux cgroups v2](https://docs.kernel.org/admin-guide/cgroup-v2.html) - [Landlock Rust bindings and enforcement status](https://docs.rs/landlock/0.4.7/landlock/) - [Linux seccomp filter](https://docs.kernel.org/userspace-api/seccomp_filter.html) - [Rust 1.96 captured process output](https://github.com/rust-lang/rust/blob/1.96.0/library/std/src/sys/process/unix/common.rs) - [Rust 1.96 Linux nonblocking descriptor operation](https://github.com/rust-lang/rust/blob/1.96.0/library/std/src/sys/fd/unix.rs) Reviewed September 20, 2026. Runtime/toolchain updates require fresh policy and compatibility review; benchmark claims from runtime projects are not Rullst performance or isolation evidence. --- # Managed Private Video Roadmap Source: https://rullst.github.io/Rullst/book/managed-video-roadmap.html # Managed private video candidate On September 20, 2026 the owner asked about Bunny.net video hosting and a native Rullst integration. The `rullst-media` candidate now implements the selected Bunny Stream lifecycle with opt-in HTTP and shared-local SQLite, an authenticated browser acceptance consumer and an executable offline example. It remains outside the framework facade. Its standalone package is now included in the v13 release inventory and was first published in the 13.0.0-alpha.1 prerelease; the final release campaign remains outstanding, and real-provider interoperability remains unvalidated. [PR #227](https://github.com/Rullst/Rullst/pull/227) passed workspace/platform checks and the actual [installed-archive job](https://github.com/Rullst/Rullst/actions/runs/35564717398) at `b21d52e7692ffceec689223a04658a95156778a0`, then merged into `v13` at `618655c1` on September 21. The final release campaign remains separate; real Bunny interoperability has not been tested. Existing LMS fields do not automatically enable this service. A Bunny Stream integration is selected for implementation after the deployment diagnostic and supervision observation base. The owner's subsequent September 20 direction prioritizes completing the supported private-video lifecycle before starting Labs or another optional integration. This is an acceptance target, not a promise that every Bunny service/API belongs in v13. Final release acceptance remains required; since September 22 the owner no longer requires publication by September 26 (see the [delivery plan](https://rullst.github.io/Rullst/book/v13-delivery-plan.html)). ## Supported candidate journey 1. An authorized instructor creates a tenant/course-bound asset record. Resolve ownership on the server; caller-supplied library/video IDs cannot authorize upload, playback, modification or deletion. 2. Grant a scoped, expiring upload capability without exposing account/library management keys to the browser. The TUS client bounds bytes/requests/time and handles resume, pause and cancellation. Provider tokens do not prove one-shot use or enforce these client limits against a modified client. 3. Track processing through authenticated notifications and authoritative provider reads. Verify exact raw-body signatures in constant time. The current Bunny webhook v1 signature does not bind a timestamp: duplicate/reordered or replayed notifications cannot independently authorize publication or regress current state. 4. Check current membership and course entitlement before issuing a short-lived playback token. Permission withdrawal stops new grants; a previously issued provider bearer token may remain usable until expiry. Do not advertise it as inherently bound to a Rullst login session or immediately revocable. 5. Exercise a real HTTP/browser consumer, unavailable/deleted assets, expiry, tenant/owner denial and replay/failure cases. Keep captions/transcripts and keyboard accessibility explicit. A mock or signed URL unit test alone cannot establish real provider playback acceptance. ## Completion target before another optional integration The first release target is a usable private course-video integration, with the complete supported lifecycle and its recovery paths: - Persist tenant/course ownership, provider asset identity and versioned local lifecycle state. Provide authorized creation, bounded listing/status reads, metadata updates, publication/withdrawal and deletion with reconciliation. - Authorize resumable uploads without browser access to library management secrets; handle expiry, retry, cancellation and ambiguous remote outcomes. Record the provider limits actually enforced instead of promising unsupported restrictions. Reconcile creation that may already have succeeded remotely before retrying it. - Process authenticated notifications with durable deduplication and explicit authoritative refresh. Cover duplicates, replay, reordering, failed processing and missed notifications without silently publishing or regressing assets. - Issue and renew short-lived playback grants only after current entitlement and asset-state checks. Document required embed and direct-file/CDN settings, expiry after withdrawal, unavailable/deleted assets and key/configuration changes; do not claim instantaneous invalidation of issued bearer tokens. - Exercise the lifecycle in a focused authenticated application/browser consumer. Include visible processing/failure states, keyboard-accessible playback controls and a documented captions/transcripts integration path. - Deliver typed errors, bounded request/response/deadline behavior, redacted diagnostics, deterministic offline mode, setup and operations documentation, and packaged-consumer tests. Production must explicitly reject mock grants. The implementation must first settle and record its SST/storage/API decisions. Automated HTTP fixtures must test the selected provider protocol and failure paths; browser tests must exercise the actual application authorization and player lifecycle with controlled media. Those tests cannot establish real Bunny transcoding or playback compatibility. The owner's no-live-account-testing direction below remains in force and must appear in the delivered status. Paid DRM, additional video providers and other Bunny products remain separate scope. They do not prevent completion of this private-course-video profile. Conversely, a token helper and mocked success response alone do not complete it. ## Architecture and provider boundary The SST selects `rullst-media` for managed-video lifecycle responsibilities, separate from Core's default runtime and object-storage helpers. Reuse Auth, tenant context and existing application entitlements through explicit composition. Start with one provider and support declared capabilities instead of pretending all video services have interchangeable APIs. Embed-view tokens protect the provider player entry point. CDN/file tokens, direct playback/original-file exposure, allowed domains, CSP/COEP and DRM are distinct controls and must be reviewed together. Domain restrictions are not student authentication. No integration can promise prevention of every copy, recording or redistribution. Enterprise DRM needs its own provider configuration, fees and device/license acceptance; it is not implied by ordinary signed URLs. The owner confirmed an existing Bunny account and Stream library on September 20, then explicitly requested no manual or real-provider testing during this implementation window. Do not use that account/library, create paid resources, upload user media or change live settings. Continue automated tests with local protocol fixtures, disposable infrastructure and simulated provider failures. Empty/mock credentials need deterministic offline behavior; production cannot use that mock as playback authorization. Document real-provider interoperability as unvalidated and keep the initial integration opt-in with a precise supported scope. Later application fixes should be contributed back to the framework. ## Primary references reviewed on September 20 - [Bunny Stream pricing](https://bunny.net/docs/stream/pricing): storage counts generated renditions and optional retained files; delivery tier and viewer region affect the bill. Basic token integration does not include paid DRM. - [Embed view authentication](https://bunny.net/docs/stream/token-authentication): server-generated video/expiry signatures and separately enabled library policy. - [Signed processing webhooks](https://bunny.net/docs/stream/webhooks): exact body, version/algorithm headers and library read-only API key; no signed timestamp. Implementation also reviewed the official Stream OpenAPI, TUS and advanced CDN HMAC documentation. See the [candidate integration and operation guide](https://rullst.github.io/Rullst/book/managed-video.html) for exact support, recovery, provider configuration and acceptance limitations. This candidate is not a claim that Bunny is universally cheapest or that the published Rullst release already supports it. --- # Verus Verification Pilot for v13 Source: https://rullst.github.io/Rullst/book/verus-roadmap.html # Verus verification pilot for v13 **Status: locally verified v13 candidate, 20 September 2026.** One production method predicate and three deliberate failing controls have passed the pilot harness. A manual workflow is prepared; hosted acceptance and any promotion remain pending. There are no new runtime dependencies. It adds no v12.1 gate and preserves the existing Kani, Miri, fuzzing, integration and provider acceptance requirements. The first property uses the existing `AgePolicy::permits` implementation. Expand to Auth and Capital only after measuring compatibility, proof maintenance and CI cost. The first privacy/age journey retains its product priority. ## Implemented candidate and measured boundary The source-linked projection verifies exactly this engineering policy table: | Risk preset | Self-declaration | Facial estimation | Verified attribute | | --- | --- | --- | --- | | Low | Allowed method | Allowed method | Allowed method | | Elevated | Denied method | Allowed method | Allowed method | | Restricted | Denied method | Denied method | Allowed method | An allowed **method** does not authorize an application action or establish a person's age. The predicate receives an `AgePolicy` and an `AgeMethod`; it does not inspect evidence, signatures, challenges, replay state, clocks or guardians. Those boundaries retain their runtime/integration tests and separate contracts. The specification lives in `rullst-privacy/verification/age-policy.spec.rs`. An ordinary CLI integration test parses actual production Rust with `syn`, checks the package/root/module/export path and the exact reviewed enum/type/signature domain, and extracts the executable body unchanged. It rejects new calls, macros, unsafe code or conditional attributes until their proof closure is reviewed. Type/body/source/specification fingerprints accompany the projection. The only additions are Verus annotations, `Structural` support for the existing fieldless derived-equality enums, a closed specification accessor and an erased reveal step. Production source and normal package dependencies remain unchanged. There is no handwritten copy of the decision algorithm and no proof precondition that excludes one of the nine risk/method combinations. The trusted boundary includes the syntax extractor, Rust's derived structural equality, Verus/its proof library, Rust compiler, Z3 and the executing host. `--no-cheating` rejects local `assume`, admitted properties and external bodies; the imported verifier standard library remains part of the trusted toolchain. The runner requires the whole projection, exactly one verified production function and its actual successful result. The three negative implementations weaken restricted/elevated policies or deny low-risk methods. Each must fail that function's postcondition; an absent proof, compiler error, zero-test pass or changed tool/source cannot substitute for a functioning negative control. The pinned Linux bundle is Verus `0.2026.09.13.671956e`, upstream commit `671956ec527d3b7164779f767bdbfe769bedce6c`, Rust `1.98.1` and Z3 `4.16.0`. The archive digest, flags and inventory are in `.github/verus-toolchain.json`. Every run checks the archive and all extracted members before executing them. Ordinary application builds still use the framework's own stable/MSRV policy and do not install Verus. Two initial local runs took 12.105 and 12.139 seconds including full bundle validation. In the first, the production proof took 0.82 seconds and each negative control about 0.53–0.54 seconds; peak prover RSS stayed below 292 MiB. The compressed tool download is about 486 MB and expands to 1.63 GB. These are measurements on one workstation, not hosted-CI or cold-download predictions. Execution uses one verifier thread, a 60-second wall limit per case, 45-second CPU limit, 2 GiB address-space limit and 2 MiB output-file limits. Local runtime age tests and a Rust 1.96 age-feature check also passed. ## Running the isolated pilot On Linux, download the exact URL from `.github/verus-toolchain.json` into a disposable directory, install the pinned Rust toolchain and use fresh absolute output paths. The installer verifies the archive before extraction: ```text python3 .github/verus-pilot.py install --archive /absolute/release.zip --output /absolute/tools RULLST_VERUS_OUTPUT=/absolute/projection cargo test -p cargo-rullst --test verus_policy_linkage --locked python3 .github/verus-pilot.py verify --archive /absolute/release.zip --bundle /absolute/tools/verus-x86-linux --projection /absolute/projection --output /absolute/evidence ``` `evidence.json` records source SHA, dirty-worktree status, source/spec/type/body linkage, exact tool identities/flags, result hashes, wall/CPU time and peak RSS. Dirty local evidence is explicitly developmental. The prepared `verus.yml` workflow uploads the projection and reports for its exact selected commit; availability through GitHub's manual workflow registry and a successful hosted run must be established before citing hosted evidence. It remains outside the required release workflow inventory. ## Subsequent scope, not proven by this pilot | Module | Proposed property | Explicit boundary | | :--- | :--- | :--- | | `rullst-privacy::age_assurance` | A restricted policy permits only a verified age attribute; expired, inconclusive or unavailable evidence cannot authorize an action requiring valid evidence. Threshold arithmetic stays within validated bounds. | Server time, authenticated subject/tenant binding, issuer trust, signature verification and atomic durable replay claims must have explicit contracts. Facial accuracy, document authenticity, guardianship and legal compliance are separate evidence. | | `rullst-auth` | The selected authorization predicate rejects expired sessions and revoked session versions, given authoritative revocation state. | State freshness, persistence, races, cryptographic implementations and application routing are outside a pure predicate proof unless separately specified and verified. | | `rullst-capital` | Selected integer money calculations respect currency scale, amount limits and the documented rounding rule; unrepresentable results return a typed error without overflow or silent truncation. | Provider prices, taxes, exchange rates, webhook delivery, distributed effects and live payment acceptance remain external or integration contracts. | Begin with the existing age-policy method and threshold decisions, then the evidence decision boundary. Identify the exact Auth and Capital functions before expanding the inventory. Review preconditions against public input validation; excluding a difficult valid input is not a fix for a failed proof. ## Code and package placement Keep specifications and proof support adjacent to the owning module. Verify the executable functions compiled into the library; any separate abstract model needs a checked connection to those functions before it can support a production claim. A passing copy of an algorithm is only model evidence. No new public framework crate is planned solely for Verus. The adoption review must choose and test how annotations, proof support and any required macros fit the existing packages. Keep the verifier toolchain isolated and validate stable Rust/MSRV, feature combinations and packaged consumer builds before accepting dependency or source changes. Ordinary application builds must not require installing the verifier. Every proof records its property, executable entry points, valid input domain, external contracts and trusted assumptions. Review `assume`, external bodies, axioms and unverified dependencies explicitly. An unfinished proof or an assumed target property cannot count as successful verification. Preserve runtime validation at boundaries reached from unverified callers. ## CI adoption and promotion The candidate dedicated `verus.yml` workflow uses `workflow_dispatch` and a small named proof inventory. Its checks fail on a proof failure, timeout, missing proof or tool error; the pilot remains outside release admission until promotion is explicitly implemented. - Pin the verifier, compatible Rust toolchain, proof libraries and solver; verify downloaded artifacts and retain their identities. - Use bounded concurrency, per-proof time/resource limits and cancellation of superseded runs. Measure cold/warm duration, memory, disk and maintenance cost before expanding the matrix. - Retain the exact source SHA, proof inventory, assumptions, dependency/tool versions, flags and results. Cached builds never substitute for checking the candidate's proofs. - Exercise representative incorrect implementations to show that the intended properties fail verification. Retain ordinary behavioral and integration tests for the surrounding application boundary. - Promote only after repeated reproducible runs, reviewed assumptions, production-code linkage, consumer compatibility and an agreed CI budget. Assign maintenance ownership and document the tool-upgrade procedure. - After promotion, require verification for changes to the selected code, specifications, relevant callers/contracts, transitive dependencies, locks or verification tooling. Unknown impact must trigger the complete proof inventory. Required-check wiring must not leave skipped checks indefinitely pending or accept a missing run as proof. ## Delivery checkpoints - [x] Select the first age-policy method predicate and review its threat model, contracts, assumptions and compatibility with a pinned Verus version. - [x] Implement source-linked local proof and negative controls, stable runtime tests and an MSRV check without changing production dependencies. - [x] Prepare the manual workflow and local resource/evidence collection. - [ ] Obtain clean exact-commit hosted evidence and the combined package-consumer rehearsal; the local dirty-worktree samples are not release admission. - [ ] Review results and decide whether to make the selected scope mandatory on relevant v13 changes. - [ ] Evaluate the Auth and Capital predicates independently before expanding the scope or any release requirement. A result supports only the named properties under their documented assumptions. It does not establish that the whole framework is formally verified, that an age estimate is accurate, or that a deployment complies with privacy law. ## References - [Verus project and supported-language boundary](https://github.com/verus-lang/verus) - [Specifications, assumptions and external bodies](https://verus-lang.github.io/verus/guide/requires_ensures.html) - [Rullst privacy and age-assurance delivery plan](https://rullst.github.io/Rullst/book/privacy-age-assurance-roadmap.html) - [Current Rullst verification contract](https://rullst.github.io/Rullst/book/workflows.html) --- # SaaS Findings: v12.1 & v13 Triage Source: https://rullst.github.io/Rullst/book/saas-v12-1-v13-triage.html # SaaS findings: v12.1 maintenance and v13 contracts > **v13 update (8 October 2026):** this triage is a historical record. The v13 > line later removed the Paddle, Lemon Squeezy, Polar, Razorpay, Mercado Pago, > Alipay, Coinbase Commerce, PicPay and Wise adapters and the NFS-e module; see > the [migration guide](https://rullst.github.io/Rullst/book/migration-v13.html#changes-from-the-published-1210-source). > Findings about those adapters apply to the 12.x line only. It also removed > the SendGrid, Postmark, Mailjet, Mailtrap and Azure Communication Services > mail transports (including the ACS Managed Identity transport mentioned > below); see the migration row "Mail providers removed". ## 2026-09-18 account-mail and follow-up candidate The latest examples account-lifecycle proposal adds MAIL-001/RULLST-005. The current change set fixes reset-link preparation and CLI plain-text output, adds localized typed lifecycle templates, an opt-in PostgreSQL/SQLite account registry with atomic reset/session revocation and encrypted outbox, a facade worker, native Resend idempotency/verified feedback and ACS Managed Identity. See [the exact adoption and remaining proposal scope](https://rullst.github.io/Rullst/book/account-mail-v12-1.html). Redis recovery, other identity-action transactions, marketing consent and an audited administrative resend interface remain separate work; they are not silently counted as implemented by the mail template fix. RULLST-004 now has exact authenticated machine-route CSRF exceptions while retaining browser CSRF, WAF and headers. SAAS-013 now checks fifteen isolated ORM/Core/Studio/Nexus/facade driver graphs. SAAS-014 has an additive one-time Stripe SDK contract with price/ownership/amount/mode binding, reconciled paid/refunded/disputed receipts and verified notification hints. Generated subscription code stays recurring; one-time application persistence and new provider sandbox acceptance are still required. Production SaaS sales are identified as real money in the README, separately from staging. These are candidate-source changes. Historical mainline CI and sandbox records below do not certify this revision. Full workspace all-feature testing, strict Clippy, formatting and release admission must pass on the candidate commit. **Status: SAAS-002, SAAS-003 and SAAS-004 have typed replacements in maintenance source. Stripe test-mode API, hosted Checkout and CLI-relayed signed events passed at `0f15d70a`; generated SQLite controller/webhook persistence passed at `370a4170`. Final candidate CI remains in progress after correcting a test-only dependency in the no-default build; SemVer, coverage and CodeQL passed. Other provider accounts, publication and candidate deployment remain unverified.** The input is the two reports from `Rullst/examples`, branch `feat/saas-staging-ai-fixes`, pinned to commit `d6094dbd67d12ee965ce73dfb539d3ada9b4195a`: - [saas-improvements-needed.md](https://github.com/Rullst/examples/blob/d6094dbd67d12ee965ce73dfb539d3ada9b4195a/saas-improvements-needed.md) - [rullst-errors.md](https://github.com/Rullst/examples/blob/d6094dbd67d12ee965ce73dfb539d3ada9b4195a/rullst-errors.md) Those reports inspect published 12.0.0. This review compares their findings with framework source at `de77430ccd86c847c98d67002f3478e57637180f` on `v13`. The table records that initial source review. The implementation notes below track subsequent corrections. The initial review was inspection, not a fresh Windows build, resolved feature-graph test, provider sandbox run, or examination of either deployed SaaS site. Application workarounds do not fix the packages. The follow-up report is pinned to branch `fix/saas-staging-healthcheck-waf`, commit `c49d6ee8f1b8df79dca8e9255a87b1a322fdc08b`: [rullst-errors.md, RULLST-003](https://github.com/Rullst/examples/blob/c49d6ee8f1b8df79dca8e9255a87b1a322fdc08b/rullst-errors.md#rullst-003--shield-blocks-legitimate-machine-clients-by-default). The report attributes an HTTPS reproduction to the examples team; this review independently confirms the source cause and exercises a local production router. The additional `fix/saas-nexus-trusted-tls` report at [`ac9a6b0625b703de8ca84d0bd10906395f500033`](https://github.com/Rullst/examples/blob/ac9a6b0625b703de8ca84d0bd10906395f500033/saas-improvements-needed.md#app-saas-002--azure-nexus-mount-omitted-the-trusted-tls-capability) adds APP-SAAS-002: the application omitted Nexus's explicit trusted-TLS capability behind Azure ingress. The existing framework correctly rejects Basic authentication without that evidence; forwarding headers alone must not enable it. The report retains SAAS-001–015 and adds no new framework defect. The subsequent report is on `examples/main`, pinned to [`1af10ed4da60a5e101c9f0dd20b2c823f38ac5ee`](https://github.com/Rullst/examples/blob/1af10ed4da60a5e101c9f0dd20b2c823f38ac5ee/saas-improvements-needed.md): - **APP-SAAS-003:** a rejected duplicate checkout consumed the application's new-session limit, and the owner could not resume an already-created session. The report describes a correction in the example's application orchestration, not the framework limiter. Generated Stripe billing now persists owner-bound attempts and retrieves an existing open session before creating another. It distinguishes complete, expired and unknown outcomes and tests retries and lost responses. Applications adding a new-session quota must count newly persisted sessions, return `Retry-After` on quota rejection, and keep any all-request abuse limit separate. - **APP-SAAS-004:** Chromium blocked the local POST's hosted Checkout redirect under `form-action 'self'`. The maintenance SaaS generator now adds only `https://checkout.stripe.com` to its own CSP, matching its explicit default provider. Core's default is unchanged. Other providers need an exact reviewed store/custom origin; `make:billing` explains this without overwriting existing policy. Server-side URL/session ownership validation remains mandatory. Browser regression covers generated policy rendered by Core with a synthetic POST/303 and intercepted destinations. It is not deployed SaaS, authentication, provider, payment or full generated-application acceptance. Both integration requirements carry into v13. The report's correction claims are attributed to the examples repository; no deployed site or live session was tested here. The CSP rule is described in the W3C [form-action contract](https://www.w3.org/TR/CSP3/#directive-form-action). ## Completed SAAS-003/004 implementation Maintenance commit `8c3c8391` and its v13 carry `d44f9520` provide: - Polar's current product checkout, external customer identity, sandbox endpoint, optional typed trusted client IP, response/return-placeholder binding, and signed subscription matching. Legacy price-only callers migrate explicitly. - Generated Stripe customer/checkout intents and session IDs, account/mode scope, signed Checkout and subscription events without email ownership, ID-bound portal, current-state reconciliation, database revision fencing and atomic event/subscription commits on SQLx and Turso. Unknown old outcomes have bounded read recovery and an explicit verified customer-recovery function for operators. - Local evidence: 171 Capital tests with Actix; 341 CLI library tests; ten structural contracts; strict Clippy; materialized SQLite/Turso compilation, migrations, lost-response/replay/rollback, cross-owner and stale-read rejection, replacement subscriptions and process restart. Full candidate CI remains distinct from this targeted evidence and from live provider-account testing. ## Final maintenance consolidation The Stripe/Polar source at `8c3c8391` passed [SemVer for all sixteen packages](https://github.com/Rullst/Rullst/actions/runs/35337758652), [coverage](https://github.com/Rullst/Rullst/actions/runs/35337761613) and [CodeQL](https://github.com/Rullst/Rullst/actions/runs/35337764638). The documentation-only follow-up `4b5aca21` passed the [full 48-job Rust matrix](https://github.com/Rullst/Rullst/actions/runs/35338109743). These runs do not cover the subsequent Paddle implementation or substitute for the required exact-main release gates. SAAS-002 now has typed Paddle customer and recurring transaction creation, approved payment-page binding, explicit sandbox selection, read-only recovery, transaction-bound signed subscription events and current-state reconciliation. Cancellation and pause use the selected API and validate the returned change. Local Capital tests and strict all-target Clippy passed; protocol fixtures are not evidence of an approved Paddle account or a completed sandbox payment. The Paddle source at `e7f63a2e` passed [SemVer for all sixteen packages](https://github.com/Rullst/Rullst/actions/runs/35349062411), [line and branch coverage workflows](https://github.com/Rullst/Rullst/actions/runs/35349065882) and [CodeQL](https://github.com/Rullst/Rullst/actions/runs/35349069091), with no open branch alerts observed. Line coverage reached 90.1240% repository-wide (86,711/96,213) and 90.6740% for framework libraries (62,838/69,301). The test-server dev-dependency repair at `0f15d70a` passed 154 no-default-feature Capital tests and strict Clippy; the [full candidate Rust matrix](https://github.com/Rullst/Rullst/actions/runs/35352552936) remains the separate cross-platform gate. Subsequent release-preparation changes to documentation and workflow validation still require exact-main admission. Reports on `examples/main` at `6c3e371cc48eae9ba0311b42155f2cf3ff8b0cb4` retain the same framework findings and add APP-SAAS-005–011, covering application ingress, seller disclosure, OIDC/resource permissions, payment-mode presentation and backup/restore operations. Their reported corrections belong to the example and infrastructure. They introduce no additional framework defect. The staging health endpoint answered successfully, and the examples repository's [deployment run](https://github.com/Rullst/examples/actions/runs/35345667696) passed its authenticated Chromium Stripe handoff. That app uses 12.0 dependencies and an application-owned one-time payment flow. It does not validate 12.1's recurring subscription contract, final payment or webhook delivery. On 18 September, a separate candidate executable built from `a4c07bfa` used the staging account's test key without deploying or changing the SaaS. Two bounded runs passed: API-only acceptance and API plus signed events delivered through the official Stripe CLI 1.51.0 relay. They exercised account/mode binding, customer and checkout idempotent retries, authorized reads, rejection of a foreign owner, expired-checkout recovery, current trial/canceled subscription reads, and signed Checkout expiration/subscription creation/deletion events. Signature verification used the relay's actual secret and original payload; test events were not locally fabricated or resigned. All synthetic customers, sessions, subscriptions and catalog objects were deleted, closed or archived. No credentials, contact data or hosted checkout URLs are retained in this book. The subsequent browser run at `0f15d70a` also completed hosted Checkout in Chrome using Stripe's official test card. Provider reads confirmed a paid test Checkout and invoice matching the expected BRL 100 minor units, customer and subscription. The candidate verified the actual signed Checkout completion and subscription creation/deletion deliveries, read active state, canceled the test subscription and confirmed canceled state. Customer deletion, terminal session state and archived catalog responses were checked. See the [redacted acceptance record](https://github.com/Rullst/Rullst/blob/main/docs/src/evidence/v12-1-stripe-sandbox.json). This proves the listed adapter operations against Stripe test mode, including simulated payment completion. It does not prove delivery to the deployed application's webhook. A subsequent isolated run at `370a4170` generated fresh SQLite billing modules through the candidate CLI and checked them against its templates without modifying the billing implementation. The generated controller persisted customer/checkout intents before payment, returned HTTP 303, and resumed the same open session in a fresh process. Hosted Checkout completed in test mode, and provider reads confirmed the expected customer, BRL 100 minor units and paid invoice. The genuine signed Checkout completion and subscription creation/deletion events passed through the generated webhook router. Fresh processes read the matching active and canceled subscription rows and durable event receipts. Replaying each delivery left persisted state unchanged; altered signed bytes returned HTTP 400 without changing state or receipts. All fifteen checks and cleanup operations passed; see the [generated SQLite acceptance record](https://github.com/Rullst/Rullst/blob/main/docs/src/evidence/v12-1-generated-stripe-sandbox.json). The harness supplied a synthetic trusted owner identity and used an isolated local database. It did not exercise deployed SaaS ingress, login/session authentication, or that deployment's database. Turso has separate materialized domain tests. Polar/Paddle account acceptance also remains separate; no credentials for those providers were available here. The 12.0 application deployment was unchanged. Use the [12.1 migration guide](https://rullst.github.io/Rullst/book/migration-v12-1.html) and the Capital provider matrix for current behavior; the chronological containment notes below are historical. ## Implementation checkpoints The maintenance candidate is [PR #206](https://github.com/Rullst/Rullst/pull/206). None of these checkpoints means the complete release or a live-provider journey has passed. Pending items in older checkpoint descriptions describe that historical point; the completed implementation above supersedes their SAAS-003/004 containment: - `20218538`: configured Lemon Squeezy store/variant binding; bounded Stripe rotated-signature verification; explicit unsupported Paddle/Polar checkout, Wise email transfer and generated live portals; generated HTTP 303 handoffs, committed application lockfiles and supported MSVC flags. - `9976da5b`: shared AI provider configuration, including explicit Groq and custom OpenAI-compatible endpoints, for client and Nexus. - `8b7761a2`: RULLST-003 removes generic clients from the default User-Agent blocklist. Local production-router tests retain configurable crawler denial, CSRF, payload limits and secure headers. Existing explicit configuration is not rewritten. Core library: 238 tests passed; strict default-feature Clippy for all Core targets passed. This is not an all-feature workspace result. - `efb9518b`: Razorpay lifecycle normalization and signed-event negatives; 127 default-feature Capital tests and strict Clippy for all targets passed. - Materialized billing tests pass for SQLite and Turso, including 303 redirects, cross-owner denial and unavailable live portals before database access. The customer/subscription pair now commits atomically; fault injection in either table proves rollback and a successful handler retry on both backends. Stable provider customer binding, checkout idempotency, provider namespaces and atomic inbox/domain state still need implementation and acceptance. - Additive `StripeCheckoutRequest`/`create_subscription_checkout` binds an existing customer, local reference, recurring price, redirects and retry key; validates the returned session and line item; and distinguishes local mocks. All 132 default-feature Capital tests and strict all-target Clippy passed. This is protocol/local evidence; the generated checkout still needs durable customer provisioning, attempt persistence and the verified event flow. - Standalone ORM consumers can disable defaults and enable exactly one strict SQLx backend. Generated CRUD and transaction methods passed strict Clippy on all three profiles; their normal/build graphs contained only the selected SQLx driver. The default `drivers-all` profile retains existing convenience. Facade/Studio compositions still need their own opt-in isolation boundary. - The follow-up full CI run at `e1d8fe4d` exposed unconditional enum codecs in backend-exclusive builds. Runtime-gated codecs fix the no-driver all-target check, and the expanded standalone consumers now compile enum bindings and decoding for PostgreSQL, MySQL and SQLite without importing other drivers. These targeted checks do not supersede the next full candidate matrix. - Stripe subscription normalization now reads Basil item-level periods and rejects missing/confused event kinds, identities, states and price items. Legacy subscription-level periods and custom plan IDs remain supported; contradictory periods fail. Three real-HMAC fixture tests cover those branches; 139 Capital tests with Actix enabled and strict all-target Clippy passed. Subscription state is still not invoice-settlement evidence. - The additive verified Stripe event envelope retains event/scope metadata, the exact provider status and separate mutation/payload digests without a pre-handler replay claim. Four signed-envelope contracts cover tampering, scope/mode confusion, mutation identity, delivery variation and explicit mock rejection. All 143 Capital tests with Actix and strict Clippy passed locally. The generated handler has not yet switched to a durable atomic inbox. - Additive Stripe customer provisioning binds opaque owner metadata and an immutable retry request, with optional contact email and validated response mode/identity. Five protocol/negative/mock contracts pass; all 148 Capital tests with Actix and strict all-target Clippy pass. This supplies the provider operation, not durable intent, account/owner binding or generated integration. - `SqlStripeEventInbox` atomically retains a verified event's scoped identity, mutation digest and outcome with caller-supplied domain SQL. SQLite tests cover restart and injected inbox-write failure; shared PostgreSQL, MySQL and MariaDB contracts cover retries, conflicting content, eight concurrent deliveries, domain rollback, cancellation, immutable capacity and scope/mock denial. These pass with both Any and the matching native ORM pools. This is database/protocol evidence; customer/attempt persistence, event ordering and replacement of the generated handler remain required. - Subscription retrieval now binds the current Stripe response to a persisted customer/owner/price/subscription/mode and preserves its exact provider state. Four protocol, negative and mock contracts pass; all 152 Capital tests with Actix and strict all-target Clippy pass. The read must still be serialized with its domain update; it is not invoice settlement or event ordering. - The SaaS blueprint now reuses the shared billing models, removing a raw PostgreSQL placeholder from subscription lookup. All ten scaffold contracts, the four materialized foundation applications and CLI all-feature/all-target strict Clippy pass locally; this is not provider sandbox acceptance. - New SaaS/`make:billing` routes now contain the unfinished live flow: real or mixed credentials return HTTP 503 before provider I/O, replay claims or SQL. Offline development fixtures remain available. Existing applications require a reviewed controller migration; new typed provider/inbox APIs do not replace their owner binding automatically. Full live integration remains required before enabling that generated operation, as permitted by the P0 allocation. All eleven v12 adapters are in scope: Stripe, Lemon Squeezy, InfinitePay, Polar, Paddle, Razorpay, Mercado Pago, Coinbase Commerce, PicPay, Alipay and Wise. Wise is a payout adapter; the others implement the billing trait. Test each operation independently and record explicit unsupported boundaries. A passing mock or a failure-before-dispatch test is not sandbox acceptance. The generated SaaS currently only selects Stripe or Lemon Squeezy; extending that selector requires provider-specific configuration and lifecycle contracts. ## Release allocation P0 below means necessary before enabling the affected live operation, not that every provider must become live before a framework maintenance release. An explicit unsupported operation is acceptable containment; a malformed request or fabricated success is not. Important compatible fixes can ship separately on the v12 maintenance line without waiting for all v13 capabilities. | Finding | Evidence in reviewed baseline | v12.1 maintenance target | v13 target / acceptance | | :--- | :--- | :--- | :--- | | **SAAS-001 · P0** Lemon Squeezy store | `providers/lemonsqueezy.rs` still sends store `1`. | Add validated store configuration without removing the existing constructor; an unconfigured real request must fail explicitly. | Bind response to store/variant, protocol fixtures and official sandbox evidence. | | **SAAS-002 · P0** Paddle checkout | `providers/paddle.rs` sends nested `customer.email` and top-level `return_url`. | Correct the supported flow if it fits the contract; otherwise disable that live operation with a typed error and accurate matrix. | Typed transaction/customer/checkout boundaries, approved payment-link prerequisites and usable sandbox checkout evidence. | | **SAAS-003 · P0** Polar checkout | `providers/polar.rs` uses `product_price_id` and `/v1/checkouts/custom/`. | Contain the obsolete path; do not silently reinterpret an existing price ID as a product ID. | Current products-based API, local-subject binding and reviewed webhook lifecycle. Any client IP forwarding requires a trusted proxy boundary and a stated purpose. | | **SAAS-004 · P0** Stripe ownership | `providers/stripe.rs` lacks a local subject in checkout; the generated handler requires webhook email. | Add an opt-in typed checkout/receipt path and safe generator flow, or keep the affected live scaffold disabled. Do not claim an email lookup solves ownership. | Persist attempt/session/customer/subscription bindings; accept the relevant signed lifecycle events; test missing email, changed email and cross-owner/tenant denial. | | **SAAS-005 · P0** POST redirect | `billing_controller.rs.template` uses `Redirect::temporary`. | Emit HTTP 303 after POST; assert status and `Location` in a materialized route test. Review the portal handoff too. | Preserve browser method semantics in every checkout flow. | | **SAAS-006 · P0** Portal capability | Scaffold mounts a portal backed by a live `UnsupportedOperation`. | Expose an honest unavailable state and capability-gate route generation; no invented customer portal. | Provider-specific sessions bound to persisted customer identity, with their own sandbox tests. | | **SAAS-007 · P0** Wise transfer | `providers/wise.rs` constructs recipient/quote/idempotency values from email/profile/amount. | Reject the unsupported live flow before dispatch; preserve deterministic offline behavior. | Separate recipient, quote, transfer and funding operations; UUID idempotency, corridor rules and reconciliation. Transfer creation is not funding confirmation. | | **SAAS-008 · P0** Checkout idempotency | Generic checkout has no durable attempt/idempotency input. | Add an optional typed API without adding required methods to downstream trait implementations; legacy ambiguity must be documented and contained. | Persist attempt before dispatch, bind key to request digest, forward it only under a reviewed provider contract and reconcile unknown outcomes. | | **SAAS-009 · P0 for multiple gateways** Provider namespace | Generated customer/subscription schemas omit provider; uniqueness is email/subscription ID only. | Keep the supported scope explicit and provide a reviewed opt-in migration; never guess the provider for existing rows. | Namespace by provider account and test/live mode as well as provider, with authenticated tenant/subject ownership; cover ID collisions and provider migration. | | **SAAS-010 · P0** Atomic billing state | Generated customer and subscription saves are separate. | Couple event processing and billing mutation in a transaction; add the event-envelope boundary needed to do so compatibly. | Durable inbox identity/digest/outcome, transactionally updated entitlements and outbox effects; crash, cancellation, duplicate, out-of-order and retry tests. | | **SAAS-011 · P1** MSVC flags | `project/env_config.rs` still emits `/DEBUG:FASTLINK`. | Remove the unconditional unsupported flag and validate generated Windows builds. The reported LNK4315 was not reproduced here. | Capability-tested linker choices without unmeasured performance claims. | | **SAAS-012 · P1** Reproducible applications | Generator ignores `/Cargo.lock`; Docker build omits `--locked`. | Retain the binary application's lockfile and require it in deployment builds, including dependency-cooking stages. Document initial lockfile generation. | Generate/build/package from one committed lockfile; test missing/stale lockfile failures. | | **SAAS-013 · P1** Strict backend graph | ORM unconditionally enables SQLx SQLite/PostgreSQL/MySQL/Any drivers; its own `default` and `strict-*` features are empty. Studio also requires `queue-sqlite`. | Audit all consumers and offer a backend-exclusive profile with migration guidance. Preserve existing default convenience deliberately rather than breaking it accidentally. | Explicit Studio queue feature and standalone PostgreSQL consumer graph excluding unrelated SQLx drivers. Confirm with `cargo tree`; a workspace `--all-features` graph is not an isolation test. | | **SAAS-014 · P1** One-time checkout | Stripe checkout always uses `mode=subscription`; `charge()` is a different off-session contract. | Describe the existing method as subscription-only; add an opt-in one-time API only with full evidence. Never use a recurring plan for an advertised one-time purchase. | Explicit payment/subscription modes, server-owned prices, success/cancel URLs, durable receipts and the correct paid/refunded/disputed event sets. | | **SAAS-015 · P0** Stripe key rotation | Verifier overwrites each earlier `v1` signature. | Bound header/candidate sizes and accept any valid candidate with constant-time HMAC verification, one valid timestamp and freshness enforcement. | Test valid first/middle/last, malformed neighbors, duplicate timestamps, all-invalid, stale and oversized headers; preserve the fix. | | **RULLST-001 · P1** Nexus AI configuration | Nexus detection and `AiClient::auto()` both omit Groq configuration. | Add an explicit application-supplied client path or one consistent resolver, with documented precedence. | Reuse configuration across authorized panels; distinguish offline, configured and unavailable. Test Groq-only configuration, failure handling, authorization and safe rendering. | | **RULLST-003 · P1** Machine-client WAF denial | Default blocklist rejects curl, Wget, Python and Go regardless of request intent. | Remove those defaults and preserve request inspection and configurable crawler policy; test the production baseline. | Treat User-Agent as a forgeable traffic preference, never authentication or authorization. | Provider files above are under `rullst-capital/src/`. The shared billing template is under `cargo-rullst/src/generators/` and feeds both SaaS and `make:billing`; fixes must cover both. The SaaS schema is assembled by `cargo-rullst/src/blueprints/saas/models.rs` from the templates under `cargo-rullst/src/blueprints/saas/src/`, with separate SQLx and Turso generator templates that also require review. The Nexus resolver mismatch is broader than Groq: detection advertises `OPENAI_BASE_URL` while `AiClient::auto()` does not consume it, and the latter consumes `DEEPSEEK_API_KEY` while the panel detector does not. One source of configuration should replace this drift. An environment variable is never connectivity evidence. ## Additional framework review: Razorpay lifecycle The legacy adapter maps `subscription.authenticated` and `payment.captured` to an active subscription and can substitute an order ID for a subscription ID. The maintenance correction requires the subscription entity's own bounded identities and matching lifecycle state. Authentication, standalone payments, missing/confused identities and mismatched states fail instead of granting access. Supported activation/charged/resumed and pending/halted/paused/cancelled fixtures are signed with real HMACs. No live account was used; event ordering, durable processing and provider/customer/tenant binding remain host work. Razorpay documents distinct [states](https://razorpay.com/docs/payments/subscriptions/states/) and [subscription events](https://razorpay.com/docs/webhooks/subscriptions/). ## Compatible maintenance boundaries Paddle review found the same last-signature-only pattern as Stripe, plus duplicate-timestamp acceptance. Its verifier now accepts any matching bounded `h1` candidate and requires exactly one timestamp. Two real-HMAC regressions failed before the correction and pass afterward, together with all 159 Capital tests under Actix and strict all-target Clippy. The existing configured freshness window remains in force. Paddle's [signature contract](https://developer.paddle.com/webhooks/about/signature-verification/) allows multiple `h1` values; this correction does not imply live checkout or subscription-lifecycle acceptance. Polar review reproduced discarded RFC3339 billing periods/current customer contacts and acceptance of unrelated event kinds or conflicting identities. A bounded subscription parser now separates lifecycle events from orders, preserves current and unambiguous legacy fields, and distinguishes scheduled cancellation from final revocation. Three signed regressions failed before the fix; all 162 Capital tests with Actix and strict all-target Clippy pass afterward. The reviewed [subscription schema](https://polar.sh/docs/api-reference/2026-04/subscription_updated) and [event sequences](https://polar.sh/docs/integrate/webhooks/events) define that boundary. The legacy event still omits account/mode, scheduling and ordering metadata; hosts must retain verified raw evidence and reconcile ownership/settlement. No sandbox or generated checkout is enabled by this fix. InfinitePay's implemented HMAC/subscription payload does not match the reviewed [checkout callback contract](https://www.infinitepay.io/checkout-documentacao). The live verifier and handler are now explicitly unsupported pending reviewed authentication and payment lookup bound to merchant, order and amount. Offline fixtures still require an explicit mock secret. This contains the unsupported framework path; it does not assert that the provider lacks other API products. Additional Lemon Squeezy review found that the legacy parser accepted missing event/object kinds, serialized absent customer/variant IDs as `null`, and mapped `on_trial` to `Unpaid`. A dedicated subscription parser now validates lifecycle, store/mode/identity and expiry, separately from invoice events. Three real-HMAC regressions failed against the old code and pass after the correction. Provider [object](https://docs.lemonsqueezy.com/api/subscriptions/the-subscription-object) and [event](https://docs.lemonsqueezy.com/help/webhooks/event-types) contracts define the boundary; this is not a live-account test or durable reconciliation. - Preserve source compatibility in v12.1: additive constructors, builders, extension contracts and explicit deprecations. A mandatory trait method, changed public struct literal or silently changed identifier meaning can break applications even when the function name stays the same. - Containing an invalid live operation may change runtime behavior; document the failure and upgrade path. Do not retain fabricated values for the sake of apparent compatibility. - Generator fixes affect newly generated code. Existing applications need reviewable source/database migrations, collision reports and rollback or forward-recovery guidance. Do not regenerate over customized code. - A webhook replay claim alone is not atomic domain processing. The database commit must determine whether processing completed; failed work must remain recoverable. A timeout after provider mutation remains an unknown outcome. - Test-mode and offline mocks remain useful. Production configuration must reject a mock as payment evidence; a success redirect never grants access. - Allocation to v12.1 is a target, not evidence of merge or publication. Carry reviewed fixes into v13 and verify their presence by commit, not branch name. ## Boundaries that are not framework defects `RULLST-002` is the umbrella reference to SAAS-001–015, not a sixteenth independent payment defect. Studio's disconnected AI playground is a documented limitation. Adapters that explicitly reject unsupported live methods are also documented capability boundaries, not automatically broken integrations. The reports attribute Docker dependency-copy mistakes, assistant formatting, proxy/origin behavior, unsafe example HTML and application error disclosure to the examples repository. Keep their regression work there and contribute only reusable fixes upstream. A paid file already committed publicly cannot become exclusive through route authorization; private artifact storage is application work. No private Academy implementation or learner data is copied into this public planning document. ## Delivery slices and required evidence 1. **Compatible containment and small regressions:** SAAS-005/006/011/012/015; contain SAAS-001/002/007 until their request contracts are complete; SAAS-003 now has its typed product contract. 2. **One complete Stripe journey:** SAAS-004/008/010 plus provider-scoped persistence from SAAS-009; opt-in SAAS-014 only when payment and subscription evidence are distinct. Test retries, refunds and disputes as well as success. 3. **Configuration consistency:** RULLST-001 and SAAS-013, with explicit migration and feature-consumer coverage. These can proceed independently of gateways. 4. **v13 capabilities:** finish additional providers one operation at a time, then multi-provider reconciliation, portals and Wise payouts. Retain the operation-level capability matrix and sanitized provider evidence. Every implementation slice needs targeted negatives, materialized generator coverage, compatible consumer checks and the repository release gates. Hosted checks should handle cold native/all-feature matrices whose disk peak cannot be bounded locally. Local tests must respect the disk reserve in `AGENTS.md`. Provider sandbox evidence is separate from Rust tests; no live payment, payout, deployment or credential change is part of this planning review. ## Provider contracts consulted on 17 September 2026 The request discrepancies above are consistent with the provider-owned [Lemon Squeezy checkout](https://docs.lemonsqueezy.com/api/checkouts/create-checkout), [Paddle transaction](https://developer.paddle.com/api-reference/transactions/create-transaction/), [Polar checkout](https://polar.sh/docs/features/checkout/session) and [Wise transfer](https://docs.wise.com/api-reference/transfer) contracts. Stripe documents overlapping endpoint secrets and multiple signatures in its [webhook guide](https://docs.stripe.com/webhooks). These references support contract review; they do not prove account eligibility or acceptance of a Rullst request. --- # Per-Commit Quality Scorecard Source: https://rullst.github.io/Rullst/book/quality-scorecard.html # Quality Scorecard The published v12 source, `eb11f892ae28f076e7a83c38a635316c6ed89028`, received **94/100 (A)** in [Rust CI run 34895536764](https://github.com/Rullst/Rullst/actions/runs/34895536764). The artifact `quality-scorecard-eb11f892ae28f076e7a83c38a635316c6ed89028` records all sixteen crate scores and the gates used to award them. These are repository-owned audit scores. They measure the stated engineering criteria, not feature completeness, security certification, provider homologation or superiority over other frameworks. ## How scores are awarded The versioned [audit policy](https://github.com/Rullst/Rullst/blob/main/.github/quality-scorecard-policy.json) sets reviewed per-crate ceilings. Rust CI constrains those values using its actual results; a green run cannot invent additional points. | Dimension | Weight | Evidence | | :--- | ---: | :--- | | API and architecture | 20 | Explicitness, cohesion and public boundaries, constrained by compile/lint, feature and MSRV gates | | Verification depth | 25 | Reviewed test depth, constrained by cross-platform workspace results | | Security and failure design | 20 | Fail-closed, error and secret boundaries, constrained by lint and specialist checks | | Documentation and DX | 15 | Reviewed guides, examples, features, migrations and evidence links | | Operations and release | 20 | Reviewed durability, live-service and recovery contracts, constrained by relevant gates | Failed, cancelled or skipped applicable gates suppress the dimensions they prove. A targeted diagnostic run cannot substitute for the full CI matrix. See [workflow triggers and scopes](https://rullst.github.io/Rullst/book/workflows.html) for when the report is emitted. Grades are **A+ 97–100**, **A 90–96**, **B 80–89**, **C 70–79**, **D 60–69**, and **F below 60**. ## Stable v12 results The v12 requirement was A or better for fifteen non-IoT packages and B or better for IoT. The stable-source run met it. The policy was reviewed on September 3, 2026; the later release run applied its conditioning gates rather than claiming a new independent expert audit. | Crate | Current audited score | Grade | Principal remaining evidence boundary | | :--- | ---: | :---: | :--- | | `rullst-core` | 96 | A | Dependency operations, distributed deployment and host authorization | | `rullst-orm` | 96 | A | Online snapshot isolation, managed/PITR backup, vendor operations and application writer/tenant/key policy | | `rullst-security` | 96 | A | Trusted rollback checkpoints, external SIEM delivery, independent audit and certification | | `rullst-connect` | 95 | A | Remote-provider leases/reconciliation, key/directory/backup operations, multi-host refresh and provider conformance | | `rullst` | 96 | A | Whole-file recovery/backup operations, multi-host coordination and maturity inherited from opt-in domain crates | | `rullst-auth` | 95 | A | Shared ceremonies, multi-host state, refresh workflow and normative WebAuthn conformance | | `rullst-mail` | 95 | A | Authoritative malware/CDR inspection, multi-host operations and inbox/provider evidence | | `rullst-messaging` | 96 | A | Remote protocols/replication, full metadata encryption and provider operations | | `cargo-rullst` | 95 | A | Production deployment, provider accounts and real-application acceptance | | `rullst-ai` | 95 | A | Exact live-model results, non-compatible streaming/provider loops, durable audit receiver operations and external retrievers | | `rullst-studio` | 94 | A | Durable/OTLP storage, key operations and shared operator authorization | | `rullst-capital` | 93 | A | Live provider-account acceptance, InfinitePay live validation and authoritative outbox/reconciliation | | `rullst-orm-macros` | 95 | A | Compiler/ecosystem compatibility beyond the tested matrix | | `rullst-nexus` | 95 | A | Host identity/domain policy, global/custom-route authorization, immutable audit delivery and production operations | | `rullst-macros` | 94 | A | Real browser/network ecosystems and host identity policy remain external | | `rullst-iot` | 83 | B | Concrete transport/hardware storage, flashing and bootloader evidence | | **Repository (equal-crate aggregate)** | **94** | **A** | **1,509/1,600; exact score remains conditional on the SHA's gates** | ## What “ceiling reached” meant The v12 campaign reached its reviewed target of **1,509/1,600 points**. A zero gap means that campaign's target was met. It does not mean further improvement is impossible, that every roadmap item shipped, or that a model has a permanent capability ceiling. The earlier planning tables and interim scores are preserved in the [immutable pre-publication record](https://github.com/Rullst/Rullst/blob/v12.0.0/docs/src/quality-scorecard.md). Future work can earn different scores only after the policy, implementation and evidence are reviewed together. ## v13 privacy candidate The 20 September policy update retains the sixteen existing ceilings and adds `rullst-privacy` to the seventeen-package candidate inventory. Its initial ceiling is **85/100**, below the unchanged 90-point RC planning floor and 95-point local target. The report remains observational; these planning values grant no release approval and are not a published v13 result. The reviewed dimensions are API 19/20 (explicit optional contracts), verification 23/25 (restart, concurrency, real PostgreSQL and generated HTTP consumers), security 19/20 (bounded, bound and fail-closed decisions), documentation 14/15 (scoped methods, setup and recovery obligations), and operations 10/20. Pending installed-distribution acceptance, first registry publication/ownership and operator recovery evidence withhold operations credit. Live providers and verified guardianship remain outside the implemented scope. The workspace and generated CLI test matrix conditions security/operations credit; the local Verus pilot is separate evidence and does not increase this report's score. This score predates the first publication of `rullst-privacy` in the 13.0.0-alpha.1 prerelease on October 11, 2026. ## Maintaining the score The September 22 package-preparation candidate adds Supervision and Media to the nineteen-package scorecard inventory while retaining every earlier ceiling. Each new package starts with the same conservative **85/100 ceiling** as Privacy: API 19, verification 23, security 19, documentation 14 and operations 10. The 90-point RC planning floor and 95-point local target remain unchanged. These are reviewed policy limits, not awarded scores or independent audits. Supervision's evidence covers its optional domain contracts, shared-local SQLite state and minimized browser observations; its generated LMS consumer was removed with `make:supervision`. It does not verify guardianship, infer cheating as fact or control other device applications. Media's evidence covers the Bunny lifecycle, protocol failures, browser upload, durable recovery and an extracted consumer; actual Bunny/CDN interoperability and application entitlement policy remain outside that evidence. Both withhold operations credit for ordinary-release archive acceptance, first registry registration and deployment recovery. Their current hosted gates must pass before the report can award the applicable dimensions. Labs remains outside the publication scorecard. Its isolation acceptance was removed with the runner candidate; execution evidence now belongs to each application-owned runner. For every relevant change, retain the commit and workflow result, identify which gates and evidence changed, and report capability progress separately. New source does not inherit the stable release's score automatically. Provider acceptance, physical devices, store publication, independent review and production operations remain separately scoped evidence. Use the [capability status](https://rullst.github.io/Rullst/book/capability-status.html), [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html), and [release audit](https://rullst.github.io/Rullst/book/v12-release-audit.html) for those boundaries. --- # OpenSSF Scorecard Evidence & Improvements Source: https://rullst.github.io/Rullst/book/openssf-scorecard.html # OpenSSF Scorecard: evidence and improvement plan The [public report](https://api.scorecard.dev/projects/github.com/Rullst/Rullst) is separate from Rullst's [per-commit quality scorecard](https://rullst.github.io/Rullst/book/quality-scorecard.html). On 27 September 2026 at 09:52 UTC, OpenSSF Scorecard v5.5.0 reported **8.2/10** for `6c926f95b5e6ba700ac7b094f30144c96d2b5fb7`. The README badge follows the live report; this dated snapshot is not a promised future score or certification. ## Remaining checks | Check | Snapshot | Evidence and next action | | --- | --- | --- | | Signed-Releases | 0/10 | GitHub stores genuine release attestations, but the release assets do not include portable signature bundles. The release workflow change for v12.1.2 and v13 preserves the original Sigstore bundle and verifies it before attaching it to future releases. Acceptance in a real tag workflow remains pending. | | Code-Review | 0/10 | The scanner found no approved changesets among 17 sampled changesets. Arrange independent human review, especially for authentication, tenant boundaries and release infrastructure. AI analysis is additional evidence, not a second human reviewer. | | Branch-Protection | Unavailable (-1) | The workflow token cannot read classic branch-protection settings. Protected `main` and `v12` do exist. A public ruleset now mirrors both branches’ existing requirements without adding a token or removing classic protections. Anonymous REST checks confirmed visibility and all 46 application-bound checks. The follow-up scanner recognized the rules and assigned 4/10; required human approvals remain absent. | | SAST | 8/10 | The scanner detected CodeQL but credited only 12 of 30 commits. Both language analyses and GitHub's CodeQL findings check are required for merge. A historical first-page detection limitation is documented in the [delivery record](https://rullst.github.io/Rullst/book/v13-delivery-plan.html); a fresh review of this snapshot found successful CodeQL suites on the heads of all 17 merged PRs associated with its 30 commits. All suites appeared within the first 30 results in the maintainer’s query, so the earlier pagination explanation does not establish the cause of this discrepancy. Scanner-token visibility/caching remains unconfirmed. | | CII-Best-Practices | 5/10 | The project was granted the Silver badge on 10 October 2026. The continuity requirement that had blocked it was met on 8 October: a successor maintainer holds organization ownership, release approval and crate ownership (see the [governance policy](https://github.com/Rullst/Rullst/blob/main/GOVERNANCE.md#continuity)). This score predates the badge; a later scan must confirm the new score. | | Binary-Artifacts | 9/10 | The finding identified the Labs runner's trusted Rust/Wasm compatibility fixture (`rullst-labs-runner/tests/fixtures/checked_sum.wasm`), which Labs CI reproduced byte-for-byte with pinned Rust. The fixture, its reproduction job and the Scorecard annotation were removed with the runner candidate for 13.0, leaving no checked-in executable; a later scan must confirm the new score. | The other eleven checks scored 10 in this snapshot. No check is disabled and no badge score is hardcoded as part of this work. An unavailable check is missing visibility, not a passing check. Making it observable can lower the aggregate score while improving the accuracy of the report. The [follow-up run at 19:07 UTC](https://github.com/Rullst/Rullst/actions/runs/36343168687) reported **7.9/10** for `a8a78aa2ac79654e366930ae581cc2829922d9f4` after public ruleset activation. Branch-Protection now contributes 4/10 instead of being excluded as unavailable; no existing protection was removed. SAST remained 8/10 (13 of 30 commits detected). Signed-Releases remained 0 because the portable provenance export has not yet been published. These observations do not establish a future release score, and badge improvement is not a publication prerequisite. ## Portable release attestations The release workflow already signs the verified crate archives, native CLI artifacts and release evidence in a job that neither checks out source nor executes downloaded artifacts. The added export copies that action's original bundle to `rullst-release.sigstore.json`; it does not invent, re-sign or alter the attestation. Its unchanged signed DSSE envelope is also exported as one JSON line in `rullst-release.intoto.jsonl`, for in-toto consumers. Both jobs compare that export against the original bundle and reject a mismatch. The signing job verifies the exported bundle before registry publication. An immutable workflow artifact then transports it to the GitHub release job, which independently verifies every attested file after downloading it against the bundle, the expected repository, exact release workflow/tag identity, source and signer commits, GitHub's OIDC issuer, and hosted-runner requirement. Any verification failure stops GitHub release creation. This makes the signature, certificate and transparency evidence available beside the artifacts, including for consumers retaining local release evidence. GitHub's attestation service remains available; the CLI's existing verification policy is unchanged. [GitHub CLI](https://cli.github.com/manual/gh_attestation_verify) supports verification with a downloaded bundle: ```sh # Supply an independently trusted tag and full commit, not values read only # from an unverified download. ARTIFACT is a downloaded crate or CLI file. gh attestation verify "$ARTIFACT" \ --bundle rullst-release.sigstore.json \ --repo Rullst/Rullst \ --cert-identity "https://github.com/Rullst/Rullst/.github/workflows/release.yml@refs/tags/$RELEASE_TAG" \ --cert-oidc-issuer https://token.actions.githubusercontent.com \ --source-ref "refs/tags/$RELEASE_TAG" \ --source-digest "$RELEASE_COMMIT" \ --signer-digest "$RELEASE_COMMIT" \ --deny-self-hosted-runners ``` A local bundle does not by itself supply the verifier's trusted roots. Follow the GitHub CLI instructions when preparing completely offline verification. Scorecard v5.5.0 [recognizes `.sigstore.json` release assets](https://github.com/ossf/scorecard/blob/v5.5.0/probes/releasesAreSigned/impl.go). It also [recognizes `.intoto.jsonl` provenance](https://github.com/ossf/scorecard/blob/v5.5.0/probes/releasesHaveProvenance/impl.go). Those filename heuristics are weaker than cryptographic verification, which is why the release job verifies the actual bundle and artifacts. The score samples up to five releases, so one future release does not establish a perfect history. Historical releases and their attestations are not rewritten by this change. ## Sequencing and acceptance These release improvements are included in v12.1.2 preparation. The prior source `273db60cae95c397401bdd5a2d28ca88de96e7fc` passed 28/28 controls, but that is historical evidence for that SHA. Release/provenance and documentation changes require corresponding admission of the new source. Use the existing fuzz equivalence policy; do not assume that unchanged Rust files imply reusable evidence, alter campaign duration, or weaken a gate to save a run. Before merging, require workflow lint, documentation checks and protected CI. Rehearse the verifier with a genuine published artifact/bundle and confirm rejection of changed bytes, an incorrect workflow identity and an incorrect source commit. Final end-to-end acceptance requires a separately authorized tag release, an attached bundle and successful consumer verification. Only a later public Scorecard report can establish a score increase. Independent human review is still rare: there is one active maintainer, and the successor maintainer's approvals count only for changes they actually review. Badge declarations and actual continuity arrangements remain maintainer decisions. The [official check definitions](https://github.com/ossf/scorecard/blob/main/docs/checks.md) and [restricted authentication guidance](https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md) describe their criteria. Do not fabricate approvals or weaken protections to improve a numerical score. bestpractices.dev granted the Silver badge on 10 October 2026, with every criterion answered. The [Silver evidence guide](https://rullst.github.io/Rullst/book/openssf-best-practices.html) records the answer, justification and evidence for each criterion. Keep it current when an answer changes. --- # OpenSSF Best Practices: Silver Evidence Source: https://rullst.github.io/Rullst/book/openssf-best-practices.html # OpenSSF Best Practices: Silver Evidence Rullst holds the **Silver** [OpenSSF Best Practices badge](https://www.bestpractices.dev/projects/13321) (project 13321), granted on 10 October 2026. This page is the evidence guide for the Silver form, with one row per Silver criterion: - the answer to select; - a justification ready to paste; - the evidence link. Only a project owner can edit the form. Keep this guide and the form in agreement when an answer changes. The criteria list was taken from the [official Silver criteria](https://www.bestpractices.dev/en/criteria/1) on 9 October 2026: 55 criteria (44 MUST, 10 SHOULD, 1 SUGGESTED). Several also appear in the passing form and are already answered there. **How to use it.** Open the project's Silver form, select the answer in the second column and paste the justification. Where the form requires a URL (marked "URL required"), paste the evidence link after the justification. Pages added together with this guide (the assurance case, release verification and reproducible archives) resolve only after the change is merged to `main` and the book is published. ## Blocking These MUST criteria are not met yet: | Criterion | What is missing | | --- | --- | | `documentation_achievements` | The README does not show the Best Practices badge yet. [PR #447](https://github.com/Rullst/Rullst/pull/447) adds it; merge it, then answer **Met** with the justification in the Basics table. | Owner confirmations recorded on 9 October 2026: - **`vulnerability_report_credit`:** the only advisory in the last 12 months (application-key configuration, fixed in 12.1.1) was opened by the maintainer from the project's own review; no outside reporter needs credit. - **`documentation_current`** and **`documentation_roadmap`:** `SECURITY.md` names 12.3.0, and `ROADMAP.md` has a dated twelve-month section. - **`sites_password_security`:** the demo applications on `rullst.win` are project sites. They are built from the Rullst starters, which hash passwords with Argon2id (`rullst-auth`); the answer is Met. ## Basics | Criterion | Answer | Justification to paste | Evidence | | --- | --- | --- | --- | | `achieve_passing` | Met | Rullst holds the passing badge (project 13321). | [bestpractices.dev/projects/13321](https://www.bestpractices.dev/projects/13321) | | `contribution_requirements` (URL required) | Met | CONTRIBUTING.md states the requirements: tests for new functionality and regression tests for fixes, Conventional Commits, `cargo fmt`, strict Clippy and the full test suite, and the pull-request template. | [CONTRIBUTING.md](https://github.com/Rullst/Rullst/blob/main/CONTRIBUTING.md#pull-requests--commit-guidelines) | | `dco` (URL required) | Unmet | Not adopted. Almost all changes come from the single maintainer, and contributors license their work under MIT through GitHub's terms of service (inbound=outbound), which is not a per-contribution assertion. Adopting it means requiring `git commit -s` in CONTRIBUTING.md and a DCO check on pull requests. | [CONTRIBUTING.md](https://github.com/Rullst/Rullst/blob/main/CONTRIBUTING.md) | | `governance` (URL required) | Met | GOVERNANCE.md documents who decides, how proposals are accepted, rejected or deferred, the change and release controls, and continuity. | [GOVERNANCE.md](https://github.com/Rullst/Rullst/blob/main/GOVERNANCE.md) | | `code_of_conduct` (URL required) | Met | Rullst adopts the Contributor Covenant 2.1 in the standard location. | [CODE_OF_CONDUCT.md](https://github.com/Rullst/Rullst/blob/main/CODE_OF_CONDUCT.md) | | `roles_responsibilities` (URL required) | Met | GOVERNANCE.md names the maintainer and release decision maker, the successor maintainer and the contributor role, with each role's duties and limits (including AI assistants). | [GOVERNANCE.md, roles](https://github.com/Rullst/Rullst/blob/main/GOVERNANCE.md#roles-and-decisions) | | `access_continuity` (URL required) | Met | Since 8 October 2026 a successor maintainer holds GitHub organization ownership (with 2FA), release-environment approval, ownership of every crate on crates.io and recovery access to the security inbox, verified through the APIs and a test issue (#440) and re-checked yearly. | [GOVERNANCE.md, continuity](https://github.com/Rullst/Rullst/blob/main/GOVERNANCE.md#continuity), [successor checklist](https://github.com/Rullst/Rullst/blob/main/docs/successor.md) | | `bus_factor` (URL required) | Unmet | There is one active maintainer. The successor holds every credential needed to continue the project, but does not maintain it day to day, so fewer than two people can currently carry the work. | [GOVERNANCE.md](https://github.com/Rullst/Rullst/blob/main/GOVERNANCE.md#roles-and-decisions) | | `documentation_roadmap` (URL required) | Met | ROADMAP.md describes what is planned (v12 maintenance, the v13 line and its priorities, ideas after 13.0) and what is not (abandoned milestones such as M39, work assigned to a separate programme). The v13 maintenance scope records what was removed and why. | [ROADMAP.md](https://github.com/Rullst/Rullst/blob/main/ROADMAP.md), [v13 maintenance scope](https://rullst.github.io/Rullst/book/v13-maintenance-scope.html) | | `documentation_architecture` (URL required) | Met | The framework specification documents the directory conventions, crates and capabilities and the core APIs; the architecture-decisions guide and security architecture describe the main components and their boundaries. | [Framework spec](https://rullst.github.io/Rullst/book/spec.html), [architecture choices](https://rullst.github.io/Rullst/book/architecture-decisions.html) | | `documentation_security` (URL required) | Met | SECURITY.md, the security architecture, the security-layers guide and the claims ledger state what users can and cannot expect, including the limits of each control. | [Security architecture](https://rullst.github.io/Rullst/book/security-architecture.html), [which layer to use](https://rullst.github.io/Rullst/book/security-layers.html), [SECURITY.md](https://github.com/Rullst/Rullst/blob/main/SECURITY.md) | | `documentation_quick_start` (URL required) | Met | The README quickstart and the "Start here" and "Zero to a complete app" guides take a new user from installation to a running application. | [README quickstart](https://github.com/Rullst/Rullst#quickstart), [Start here](https://rullst.github.io/Rullst/book/start-here.html) | | `documentation_current` | Met | Guide code is compiled as doctests, and every documentation change runs the book build, offline link and anchor checks on Markdown and rendered HTML, a browser smoke test and a spell check. Documentation is updated with the behavior it describes. | [documentation.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/documentation.yml), [book doctests](https://github.com/Rullst/Rullst/blob/main/rullst/src/book_doctests.rs) | | `documentation_achievements` (URL required) | Unmet until PR #447 merges | After PR #447: the README front page shows and links the OpenSSF Best Practices badge next to the OpenSSF Scorecard badge. | [README](https://github.com/Rullst/Rullst#readme) | | `accessibility_best_practices` | Met | Browser smoke tests of the documentation site check the skip link, ARIA states, keyboard navigation and reduced motion. `cargo rullst audit --report` checks image text alternatives, form labels and page language in generated applications. No formal WCAG conformance is claimed. | [site-browser-smoke.mjs](https://github.com/Rullst/Rullst/blob/main/.github/site-browser-smoke.mjs), [audit report](https://rullst.github.io/Rullst/book/security-report.html) | | `internationalization` | Unmet | Documentation, CLI and generated starters are English only. The framework has a few locale hooks (account mail in English, Portuguese and Spanish; a configurable HTML `lang`), but no general message-catalog localization. | [account mail](https://rullst.github.io/Rullst/book/account-mail-v12-1.html) | | `sites_password_security` | Met | Project sites that keep accounts are the demo applications on rullst.win; they are built from the Rullst starters, whose sign-in stores only Argon2id password hashes (rullst-auth). The repository, releases, book and crates.io use GitHub/crates.io sign-in and store no passwords. | [rullst-auth](https://github.com/Rullst/Rullst/tree/main/rullst-auth) | ## Change Control | Criterion | Answer | Justification to paste | Evidence | | --- | --- | --- | --- | | `maintenance_or_update` | Met | v12 is maintained on a protected branch with patch and minor releases while v13 develops on `main`. Deprecated APIs stay for at least one minor release, every major has a migration guide, and `cargo rullst upgrade` previews and applies upgrades. | [Compatibility policy](https://rullst.github.io/Rullst/book/compatibility-policy.html), [v13 migration](https://rullst.github.io/Rullst/book/migration-v13.html) | ## Reporting | Criterion | Answer | Justification to paste | Evidence | | --- | --- | --- | --- | | `report_tracker` | Met | Individual issues are tracked in GitHub Issues with bug and feature templates; the bug template sends security reports to private disclosure. | [GitHub Issues](https://github.com/Rullst/Rullst/issues) | | `vulnerability_report_credit` (URL required) | Met | The vulnerabilities fixed in the last 12 months were found by the project's own reviews, so there was no outside reporter to credit. SECURITY.md commits to crediting reporters in the changelog and advisories unless they ask for anonymity. | [SECURITY.md](https://github.com/Rullst/Rullst/blob/main/SECURITY.md) | | `vulnerability_response_process` (URL required) | Met | SECURITY.md documents private reporting (email or GitHub private vulnerability reporting), what to include, response and patch targets by severity, and credit; the advisory-exceptions policy covers severity, ownership and temporary exceptions. | [SECURITY.md](https://github.com/Rullst/Rullst/blob/main/SECURITY.md), [advisory exceptions](https://rullst.github.io/Rullst/book/security-advisory-exceptions.html) | ## Quality | Criterion | Answer | Justification to paste | Evidence | | --- | --- | --- | --- | | `coding_standards` (URL required) | Met | Rust code follows the standard Rust style (rustfmt defaults) and Clippy. CONTRIBUTING.md and AGENTS.md add project rules, such as no panics in production paths, parameterized SQL and Conventional Commits. | [CONTRIBUTING.md](https://github.com/Rullst/Rullst/blob/main/CONTRIBUTING.md#pull-requests--commit-guidelines), [AGENTS.md](https://github.com/Rullst/Rullst/blob/main/AGENTS.md) | | `coding_standards_enforced` | Met | Every pull request runs `cargo fmt --all -- --check` and `cargo clippy --workspace --all-targets --all-features -- -D warnings`, plus zero-panic Clippy gates on library code. | [ci.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/ci.yml), [zero-panics.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/zero-panics.yml) | | `build_standard_variables` | Met | Builds use Cargo, which honors `RUSTFLAGS`, `CARGO_ENCODED_RUSTFLAGS` and linker settings. There are no first-party build scripts or Makefiles that replace them; the only native C/C++ code (optional bundled DuckDB) is compiled by the `cc` crate, which reads `CC`, `CFLAGS`, `CXX` and `CXXFLAGS`. | [Cargo.toml](https://github.com/Rullst/Rullst/blob/main/Cargo.toml) | | `build_preserve_debug` | Met | Debug information follows Cargo profiles, and the workspace does not force stripping. It can be requested with standard settings such as `CARGO_PROFILE_RELEASE_DEBUG=true`; the test profile omits DWARF by default to bound disk use and honors `CARGO_PROFILE_TEST_DEBUG`. | [Cargo.toml](https://github.com/Rullst/Rullst/blob/main/Cargo.toml) | | `build_non_recursive` | Met | One Cargo workspace resolves and builds a single dependency graph; there are no recursive Makefiles or sub-builds. | [Cargo.toml](https://github.com/Rullst/Rullst/blob/main/Cargo.toml) | | `build_repeatable` | Met | Published `.crate` archives are bit-for-bit reproducible from the same commit and pinned toolchain. CI repackages two crates from a fresh checkout on every pull request, the release job reproduces every archive before publication, and an independent rebuild matched the crates.io checksum of `rullst-macros` 12.2.0. Native CLI binaries are attested but not claimed reproducible. | [Reproducible crate archives](https://rullst.github.io/Rullst/book/reproducible-builds.html) | | `installation_common` | Met | The CLI installs with `cargo install cargo-rullst --version =13.0.0-alpha.4 --locked` (Cargo skips prereleases, so a plain `cargo install cargo-rullst` selects the end-of-life 12.x CLI) and uninstalls with `cargo uninstall cargo-rullst`; libraries are added as normal Cargo dependencies with the same exact version. | [Getting started](https://rullst.github.io/Rullst/book/1-getting-started.html) | | `installation_standard_variables` | Met | `cargo install` honors `CARGO_INSTALL_ROOT`, `--root` and `CARGO_HOME`, Cargo's conventions for the installation location. Libraries are never installed system-wide. | [Getting started](https://rullst.github.io/Rullst/book/1-getting-started.html) | | `installation_development_quick` | Met | After cloning, rustup installs the toolchain pinned in `rust-toolchain.toml`, and `cargo build` and `cargo test` work; CONTRIBUTING.md lists the steps. | [CONTRIBUTING.md, development setup](https://github.com/Rullst/Rullst/blob/main/CONTRIBUTING.md#development-setup) | | `external_dependencies` (URL required) | Met | The Cargo manifests and `Cargo.lock` list every dependency with exact versions and checksums; each release also attaches a CycloneDX SBOM. | [Cargo.lock](https://github.com/Rullst/Rullst/blob/main/Cargo.lock), [Cargo.toml](https://github.com/Rullst/Rullst/blob/main/Cargo.toml) | | `dependency_monitoring` | Met | Dependabot checks Cargo and GitHub Actions weekly for `main` and `v12`. `cargo audit` runs on every push and pull request and daily, `cargo deny` on every push and pull request, and a release stops on any advisory. | [dependabot.yml](https://github.com/Rullst/Rullst/blob/main/.github/dependabot.yml), [audit.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/audit.yml), [cargo-deny.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/cargo-deny.yml) | | `updateable_reused_components` | Met | Dependencies come from crates.io through Cargo and update with `cargo update` or Dependabot. The only vendored files are versioned htmx and Pico CSS browser assets (the version is in the file name), which are updated by hand. | [Cargo.toml](https://github.com/Rullst/Rullst/blob/main/Cargo.toml) | | `interfaces_current` | Met | CI builds with `-D warnings`, so calling a deprecated dependency API fails the build. Every remaining `allow(deprecated)` is in a test of one of Rullst's own deprecated compatibility APIs. | [ci.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/ci.yml) | | `automated_integration_testing` | Met | Every push and pull request to `main`, `v12` and `v13` runs the automated test suite in GitHub Actions and reports the result on the commit; a nightly run covers Linux, macOS and Windows. | [ci.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/ci.yml) | | `regression_tests_added50` | Met | CONTRIBUTING.md requires a regression test for bug fixes. Of 1,139 `fix` commits on `main` and `v12` from 9 April to 9 October 2026, 972 (85%) changed or added tests. | [CONTRIBUTING.md](https://github.com/Rullst/Rullst/blob/main/CONTRIBUTING.md#pull-requests--commit-guidelines) | | `test_statement_coverage80` | Met | Line coverage measured with cargo-llvm-cov was 92.56% on `main` (Codecov, 9 October 2026), and CI fails below 90%. | [Codecov](https://app.codecov.io/gh/Rullst/Rullst), [coverage.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/coverage.yml) | | `test_policy_mandated` | Met | CONTRIBUTING.md: "Major new functionality must include automated tests of its public behavior, failure cases and relevant trust boundaries." | [CONTRIBUTING.md](https://github.com/Rullst/Rullst/blob/main/CONTRIBUTING.md#pull-requests--commit-guidelines) | | `tests_documented_added` | Met | The test requirement is in CONTRIBUTING.md and in the pull-request template checklist. | [PULL_REQUEST_TEMPLATE.md](https://github.com/Rullst/Rullst/blob/main/.github/PULL_REQUEST_TEMPLATE.md) | | `warnings_strict` | Met | All CI builds use `-D warnings`; Clippy runs on all targets and features with `-D warnings`, Core denies `clippy::unwrap_used` and `clippy::expect_used`, and zero-panic gates deny panicking calls in library code. | [ci.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/ci.yml) | ## Security | Criterion | Answer | Justification to paste | Evidence | | --- | --- | --- | --- | | `implement_secure_design` | Met | The assurance case shows how each Saltzer and Schroeder principle is applied, with code: least privilege in workflows and API tokens, fail-safe defaults, complete mediation by the router-wide security baseline, separation of privilege in the release pipeline. | [Assurance case](https://rullst.github.io/Rullst/book/assurance-case.html#secure-design-principles) | | `crypto_weaknesses` | Met | Defaults are AES-256-GCM, HMAC-SHA256, Ed25519, ECDSA P-256 and Argon2id, with TLS through rustls only. SHA-1 appears only as HMAC-SHA1 for TOTP, as RFC 6238 and authenticator apps require. | [Assurance case](https://rullst.github.io/Rullst/book/assurance-case.html#countering-common-implementation-weaknesses) | | `crypto_algorithm_agility` | Unmet | Rullst deliberately uses one modern algorithm per purpose (AES-256-GCM, Argon2id, HMAC-SHA256, Ed25519; JWTs use HS256 only). Stored ciphertext carries key IDs, so keys rotate without code changes, but changing an algorithm needs a release; TLS negotiates among several rustls cipher suites. | [Vault](https://rullst.github.io/Rullst/book/tutorials/15-rullst-vault-encryption.html) | | `crypto_credential_agility` | Met | Keys and credentials are read from the environment or configuration (`APP_KEY`, `RULLST_ENCRYPTION_KEY` or a keyring), never compiled in. Keyrings with key IDs allow rotation, and documented placeholder keys are refused. | [Assurance case](https://rullst.github.io/Rullst/book/assurance-case.html#countering-common-implementation-weaknesses) | | `crypto_used_network` | Met | Provider integrations use HTTPS through rustls, AI provider URLs must be HTTPS (or literal loopback for local models), and remote PostgreSQL requires `sslmode=verify-full`; the security headers add HSTS. | [Assurance case](https://rullst.github.io/Rullst/book/assurance-case.html) | | `crypto_tls12` | Met | TLS is provided by rustls, which supports TLS 1.2 and 1.3 only. | [Cargo.lock](https://github.com/Rullst/Rullst/blob/main/Cargo.lock) | | `crypto_certificate_verification` | Met | rustls verifies certificates by default, and no first-party code disables verification (no `danger_accept_invalid_certs` or custom no-op verifier). | [Assurance case](https://rullst.github.io/Rullst/book/assurance-case.html#countering-common-implementation-weaknesses) | | `crypto_verification_private` | Met | Because certificate verification is never disabled, the TLS handshake is verified before any API key or bearer token is sent in an HTTP header. | [Assurance case](https://rullst.github.io/Rullst/book/assurance-case.html) | | `signed_releases` | Met | Since v12.0.0, every crate archive and evidence file (and, from v12.1.0, every CLI binary) is signed as a GitHub artifact attestation through Sigstore keyless signing, so no long-lived private key exists on any distribution site. From v12.1.2 the Sigstore bundle is attached; SECURITY.md documents verification with `gh attestation verify`. | [SECURITY.md, verifying a release](https://github.com/Rullst/Rullst/blob/main/SECURITY.md#verifying-a-release) | | `version_tags_signed` | Unmet | Release tags are annotated but not signed; their integrity relies on protected branches, release admission of the exact commit and the attested artifacts. To start, sign tags with `git tag -s` using a GPG or SSH key registered with GitHub (or keyless with gitsign) and add `git verify-tag` to release admission. | [release.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/release.yml) | | `input_validation` | Met | Requests are parsed into typed structures with `ValidatedJson` and `ValidatedForm`; identifiers, paths, storage keys and operators are checked against allowlists, and invalid input is rejected with a typed error. | [Forms and validation](https://rullst.github.io/Rullst/book/tutorials/07-forms-and-validation.html), [assurance case](https://rullst.github.io/Rullst/book/assurance-case.html#countering-common-implementation-weaknesses) | | `hardening` | Met | Secure headers (HSTS, nonce CSP, frame denial, COOP, COEP, CORP), `unsafe` limited to five reviewed files, `forbid(unsafe_code)` in four crates, zero-panic Clippy gates, bounded JSON size and depth, and fail-closed defaults. | [Security architecture](https://rullst.github.io/Rullst/book/security-architecture.html) | | `assurance_case` (URL required) | Met | The assurance case summarizes the threat models, lists the trust boundaries, argues the secure design principles and maps common weaknesses to mechanisms and named tests, with an explicit limits section. | [Assurance case](https://rullst.github.io/Rullst/book/assurance-case.html) | ## Analysis | Criterion | Answer | Justification to paste | Evidence | | --- | --- | --- | --- | | `static_analysis_common_vulnerabilities` | Met | CodeQL analyzes Rust and JavaScript/TypeScript after every push to `main`, on pull requests to the maintained lines and weekly. Clippy, `cargo audit`, `cargo deny` and the IDOR route audit also run in CI. | [codeql.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/codeql.yml) | | `dynamic_analysis_unsafe` | Met | Rust is memory-safe outside `unsafe`, which is limited to five reviewed OS-integration files. AddressSanitizer and ThreadSanitizer run nightly on twelve crates; Miri, Kani and 42 fuzz targets run as on-demand campaigns. | [sanitizers.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/sanitizers.yml), [fuzzing.yml](https://github.com/Rullst/Rullst/blob/main/.github/workflows/fuzzing.yml) | ## Summary | Answer | Count | Criteria | | --- | ---: | --- | | Met | 49 | All others | | Unmet | 6 | `documentation_achievements` (MUST, until PR #447 merges), `dco`, `bus_factor`, `internationalization`, `crypto_algorithm_agility` (SHOULD), `version_tags_signed` (SUGGESTED) | Once PR #447 is merged, every MUST criterion is answered Met. The unmet SHOULD and SUGGESTED criteria do not block Silver if their justifications are filled in. See also the [OpenSSF Scorecard evidence](https://rullst.github.io/Rullst/book/openssf-scorecard.html). --- # v12 Stable Release Audit Source: https://rullst.github.io/Rullst/book/v12-release-audit.html # v12 stable release audit Status: **Rullst `12.0.0` was published on September 15, 2026** from immutable tag [`v12.0.0`](https://github.com/Rullst/Rullst/releases/tag/v12.0.0) at commit [`eb11f892ae28f076e7a83c38a635316c6ed89028`](https://github.com/Rullst/Rullst/commit/eb11f892ae28f076e7a83c38a635316c6ed89028). The protected [release workflow](https://github.com/Rullst/Rullst/actions/runs/34955468167) completed its admission, artifact verification, attestation, publication preflight, sixteen-package Trusted Publishing, registry verification, and GitHub release jobs successfully. ### Stable-source verification receipts These successful runs all identify `eb11f892` as their source: | Evidence | Workflow run | | :--- | :--- | | Multi-platform CI and 94/A repository scorecard | [34895536764](https://github.com/Rullst/Rullst/actions/runs/34895536764) | | LLVM line coverage: 90.3220% repository, 90.6186% framework libraries | [34895523751](https://github.com/Rullst/Rullst/actions/runs/34895523751) | | Documentation | [34895523669](https://github.com/Rullst/Rullst/actions/runs/34895523669) | | Full 40-target fuzz campaign | [34895536740](https://github.com/Rullst/Rullst/actions/runs/34895536740) | | Bounded Kani harnesses | [34895536646](https://github.com/Rullst/Rullst/actions/runs/34895536646) | | Scoped Miri checks | [34895536650](https://github.com/Rullst/Rullst/actions/runs/34895536650) | | Property tests | [34895536923](https://github.com/Rullst/Rullst/actions/runs/34895536923) | | Sanitizers | [34951576777](https://github.com/Rullst/Rullst/actions/runs/34951576777) | The release admission policy lists the complete required workflow inventory. The informational mutation campaign below belongs to its separately recorded earlier SHA; it is not a fresh mutation result for the stable commit. Each hosted result remains evidence for its recorded commit, inventory, and environment rather than every future tree. This repository-owned audit is not an independent certification. ## Baseline and method The audit began on candidate `a6b3bc8a`, which included the now-integrated second-computer CLI findings, fail-closed SQLite schema validation for automatic migration generation, and the corrected mutation inventory. All 22 applicable push workflows passed on that SHA. The immutable RC tag is `v12.0.0-rc.1` at `2b19567e8d184ad3ceeebae812de71b06d32e484`. Stable preparation then synchronized package versions, public documentation, site copy and release evidence and added bounded correctness/hardening changes to CSRF composition, session-cookie handling, generated registration/billing/container contracts and Studio's browser assets. Those runtime changes earned fresh evidence before the final tag; the stable publication did not inherit an RC pass merely by description. The obsolete temporary CLI handoff file was retired after integration and remains available in Git history. Review covers every published crate. IoT receives only a light triage under the owner's explicit v12 exception. Each deep review traces public inputs through validation, state and side effects, compares documented behavior, and adds negative regressions for reproduced defects. Existing tests alone do not close a finding. Regressions are run before and after corrections when feasible. This is repository-owned code review and testing, not an independent security assessment, provider certification, or a claim that every line or deployment configuration has been exhaustively analyzed. Tests are serialized on the memory-limited local machine. The final local workspace gate completed after the correction batch. ## Evidence-bound repository grade The authoritative scoring method remains the [quality scorecard](https://rullst.github.io/Rullst/book/quality-scorecard.html); this audit does not create a second, more flattering grading system. Under that policy, the reviewed implementation supports a maximum local aggregate of **94/A** when all conditioning gates are green. Each of the 15 active non-IoT crates has an A ceiling, while `rullst-iot` retains the owner-approved **83/B** exception because physical device and boot-chain evidence is outside the v12 campaign. That is an evidence-bounded engineering grade, not a security certification, feature-completeness percentage, independent audit result, or release authorization. A candidate earns the recorded grade only when its exact SHA passes the applicable scorecard constraints. The RC met those conditions only after its automatic and manual gates, packaging checks, documentation review, and explicit GO decision completed. Stable v12 subsequently passed its own conditioning gates at `eb11f892`, as recorded above; neither result authorizes unverified future changes. ## Post-publication documentation closeout The maintenance cleanup separates the published v12 record from the v13 backlog, replaces oversized historical planning pages with English archive pointers, and preserves the original claims and evidence under the immutable `v12.0.0` tag. The temporary `CLIFIX.md` handoff is retired; the frozen 190-claim historical classification remains machine-checked independently of the shortened release page. README and landing-page examples are sourced from the independently maintained [examples repository](https://github.com/Rullst/examples), not advertised as fresh stable-version or platform certification. Local documentation builds, source-link/anchor checks, spelling checks, and real-browser layout/accessibility checks validate the edited presentation. They do not establish that every tutorial or external provider was newly tested end to end in production. ## Coverage ledger | Crate / surface | Review scope | Current status | | --- | --- | --- | | `rullst-orm` | Projection identifiers, empty-set predicates, tenant/global scopes, transactions, policy mutations, nested queries and search | Reproduced isolation/transaction defects corrected; focused default/strict-SQLite/Redis regressions and the final all-feature workspace gate are green; live external-backend matrices remain release evidence | | `rullst-orm-macros` | Generated SQL bindings, parser diagnostics, portable identifiers and scope generation | Corrected generated contracts; 43 unit tests, one smoke test and 24 compile-fail cases green | | `rullst-core` | HTTP security composition, CSRF, lifecycle and development state ownership | CSRF/security composition 21 tests green; four reload tests and actual Node client behavior tests green | | `cargo-rullst` | Remote CLI handoff, public profile accuracy, supervised restart, generated contracts | Supervisor, dashboard, command-behavior, public-profile and materialized blueprint gates are green; snapshot launch retries bounded transient Linux executable-busy races; automatic SQLite migration generation now rejects unsafe database-owned identifiers before writing Rust source | | `rullst-auth` | JWT expiry/revocation, encrypted sessions, role guards, passkey/SQLite cancellation | Corrections green: 60 library tests and five durable JWT integrations | | `rullst-security` | WebSocket origin enforcement, middleware readiness, bounded redaction, crypto/input policies | 159 library tests and two Tower tests green; final rate-limit run passed 11 tests including two added afterward (161 library cases now) | | `rullst-connect` | OIDC claims/nonce, refresh semantics, callback state and token lifetimes | Corrections green: 204 library tests with Axum-session and SQLite features | | `rullst-capital` | Provider side effects, pricing, charge binding, authenticated payload schema and signature protocols | 99 library plus 22 integration tests green with Actix; one later Actix duplicate-header regression also green | | `rullst-nexus` | Admin transport/origin/authorization and tenant/audit boundaries | Corrections green: 50 library plus 11 integration tests, including real SQLite tenant/audit cases; the default coverage pass now includes those cases and its shared-pool race was removed, with 10 consecutive parallel integration runs green | | `rullst-studio` | Local operator boundary, handoff layout/telemetry changes, dynamic HTML | Static boundary review found no additional reproduced defect; 48 library tests, integrations and the final workspace gate are green | | `rullst-macros` | Escaping/raw HTML, generated handler/runtime contracts | Static trust-context review completed; no new reproduced defect; final all-feature workspace tests and doctests are green | | `rullst-ai` | Provider/mock separation, redirect handling, response limits and tool-policy boundaries | 102 library tests green, including real local HTTP regressions for all five native transports | | `rullst-mail` | Provider side effects, transport limits, attachments/headers, suppression and delivery evidence | 98 library tests green with SQLite, including actual HTTP and cancelled-write regressions | | `rullst-messaging` | Publication/lease/retry/idempotency, local durability and outbox composition | Cancellation defect corrected; focused SQLite evidence and final all-feature workspace coverage, including the optional ORM outbox relay, are green | | `rullst` | Facade feature wiring, composed subsystem and tutorial contracts | Static facade/feature review; existing verified-TLS composition retained; final composed all-feature workspace tests and doctests are green | | `rullst-iot` | Manifest/public capability honesty only; no hardware or deep audit | Light review complete; README/manifest agree on helper/simulator/transport boundaries; approved scope exception | Counts above describe separate focused runs and overlap; do not sum them into an invented coverage metric. In addition, the complete workspace test suite passed with all features, and workspace Clippy passed with all targets, all features and `-D warnings`. ## Reproduced defects and correction boundaries | Area | Observed failure | Correction and remaining boundary | | --- | --- | --- | | ORM projections | Safe-looking `select`/`pluck` accepted SQL expressions | Validate safe projection identifiers; deliberate raw SQL remains a caller-owned escape hatch | | ORM membership and scopes | Empty `IN` matched all rows; `OR` escaped tenant/global/soft-delete constraints | Empty sets are false predicates; group mandatory scopes separately and preserve nested query errors | | ORM search | Local and external Scout paths bypassed model scopes; empty external results could select ID zero | Start from the scoped query, reject missing context before provider calls and bind provider IDs through empty-aware membership | | ORM transactions | `pluck`, streaming/eager paths or mutation callbacks could bypass/wait on an already borrowed transaction | Use the managed executor; release query locks before callbacks/eager work where supported; unsupported mutation-callback reentry returns a typed validation error instead of hanging | | ORM mutations/macros | Bulk delete bypassed model policy; keyset iteration could escape its cursor; unsupported identifiers reached malformed generated code | Reject unauthorized bulk mutation, group keyset predicates, propagate errors and emit compile diagnostics for unsupported identifiers/scopes | | Durable local stores | Cancellation during manual `BEGIN` left uncommitted state in a pooled connection | SQLx RAII transactions in Auth JWT/passkey, Mail suppression and Messaging; cancellation racing dispatched commit still requires reconciliation | | Authentication | Expired revoked JWT could become accepted during skew allowance | Enforce hard expiry independently of permitted clock skew; no distributed revocation claim | | OAuth/OIDC | Missing claims, nonce downgrade, refresh-token confusion, empty callback state and invalid lifetimes | Strict provider-specific validation and checked positive bounded lifetimes; real accounts and distributed one-shot callback storage still need external evidence | | Security middleware | HTTP/2 CONNECT bypassed WebSocket origin policy; cloned Tower services lost acquired readiness | Apply the origin guard to the extended method and call the ready service instance | | Abuse controls/logs | Reset zero-limit admission, counter overflow, concurrent capacity escape and redaction suffix leakage | Checked bounded admission, atomic capacity/reclamation and bounded fail-closed redaction; controls remain process-local | | Core CSRF | Empty proofs accepted; valid split Cookie fields rejected; duplicate proofs ambiguous | Nonempty bounded unique tokens, multi-field cookie parsing and exact supported form media type; unsigned double-submit is not a session-signed CSRF scheme | | Production CSRF composition | An application router with an explicit CSRF layer was wrapped by the `Server` production baseline, so a first GET emitted two different cookies and the rendered form echoed only one; browsers retained the other and valid submissions failed with 403 | A private request marker makes nested framework/application CSRF composition idempotent while preserving validation at the outer boundary; the exact two-layer regression requires one matching cookie and a successful matching POST. Applications still own HTTPS, session authentication and exact exemptions | | Examples Omni/HTML/COEP report | A generated Tauri package used colliding Windows MSVC PDB target names; `html!` rejected source comments; the deployed LMS needed reviewed cross-origin media behavior | Give the Omni library a distinct `rullst_omni_lib` target and compile-test the emitted entrypoint; strip bounded source comments in the macro parser; retain `require-corp` by default while exposing a closed, validated Core COEP application setting. Cross-origin media still requires matching CSP, server headers and deployed-browser evidence | | Nexus operator access | An absolute HTTPS URI impersonated verified TLS; local Host/Origin boundary incomplete | Require the private verified-transport capability and validate local browser Host/Origin; deployment proxies must supply the correct trusted adapter | | Capital live operations | Fabricated portals/no-op mutations, four hardcoded prices and undocumented mock aliases | Explicit unsupported errors for unimplemented live behavior; deterministic mocks only through documented mock credentials; consult the crate's provider-method matrix | | Capital receipts/webhooks | Incomplete authenticated payloads inferred active/paid; charge identity insufficiently bound; Polar/MP body-only signatures did not represent their protocols | Validate required event/status/charge bindings; bounded Polar header-based Standard Webhooks verification; incompatible legacy live signature paths fail closed, including MP until its full provider verification is implemented | | AI/Mail transports | Redirects forwarded private request content; AI JSON unbounded; suppression cancellation leaked state | Pooled redirect-disabled clients, connection/request budgets, bounded native AI responses and SQLx rollback ownership; native custom endpoints remain trusted operator configuration | | Public DLL reload | Windows LMS loaded an independent ORM/runtime state and unsafe cross-runtime workarounds were proposed | Remove public DLL generation and use directly linked supervised restart; retained legacy loader is experimental and not a stable Rust ABI | | Automatic migration generation | SQLite table and column names reached a metadata query and generated Rust comments/strings without validation | Reuse the strict bounded database-identifier policy, bind the SQLite metadata lookup and reject the complete schema before generating any file; regressions cover newline-bearing table and column identifiers | | Release coverage | The earlier final-main LLVM artifact reported 78,962/87,941 lines (89.7897%) while the upload job itself stayed green; default Nexus SQLite/audit paths were omitted | Nexus is now included in the merged default-profile pass and exact 90% whole-repository and framework-library floors run before upload. PR #183's hosted artifact reported 79,349/87,941 lines (90.2298%) overall and 59,767/66,004 (90.5506%) across 435 governed framework-library files; the frozen release SHA must repeat this gate | | Fuzz campaign | Earlier hosted runs found complete-tree rendering in unsupported-union and missing-ID model diagnostics. On `36411ea1`, 39 of 40 targets completed their full 5.5-hour campaigns; `fuzz_parser` alone found a third valid derive tree where unconditional `Field::span()` validation exceeded the ten-second per-input limit | Anchor model, relation, field and unsupported-type diagnostics to bounded identifiers instead of rendering complete syntax trees; retain all three discovered shapes in the parser corpus. The latest exact ASan reproducer improved from a repeatable 12.1-second timeout to about 30 milliseconds locally; a fresh five-minute local campaign completed 1,740,804 executions without a finding, and all 43 macro unit tests plus 24 compile-fail cases are green. Hosted diagnostic run 34495340300 then completed 1,541,970 executions in 301 seconds on corrected code commit `40c1b083`, with no finding. The subsequent complete 40-target campaign passed on `7697fb8a` in run `34642351302`; its evidence is carried only across the bounded final delta described below | The parser correction also passed the exact local workspace gates: `cargo test --workspace --all-features` and `cargo clippy --workspace --all-features -- -D warnings`. This is local evidence only and does not replace the hosted final-candidate matrix. The adversarial regressions use local databases, mock keys, signed synthetic tokens and loopback HTTP servers—not real credentials or real payment requests. Provider capability corrections are observable behavior changes: callers must handle explicit errors where previous code returned misleading success. ## AI-assisted security review evidence A read-only Codex Security deep scan inspected representative repository surfaces and preserved a **partial**, not exhaustive, report after reaching its configured budget. It reported no Critical or High finding and independently validated one Low-severity, Medium-confidence generated-source injection path in `make:migration:auto`. The finding required control of a selected SQLite schema, explicit generator invocation and a later insufficiently reviewed build, but its possible impact justified correction before the RC. The follow-up working-tree scan reviewed both changed production files but reached its smaller budget during threat-model construction, before final validation. It produced no report and is deliberately not counted as a pass. The release evidence for the correction is therefore the source trace, the two crafted-SQLite regressions, the complete all-feature workspace tests, strict workspace Clippy and the subsequent hosted candidate gates. This AI-assisted review is neither an independent audit nor a claim of complete repository coverage. ## Website, README and first-run documentation The organization root website and the framework Pages site were different deployments. The old organization site still described `main` as v5 and `dev` as v12, and its privacy page asserted unverified worldwide legal compliance. The source correction prepared matching copy for both entry points. This repository's [stable Pages deployment](https://github.com/Rullst/Rullst/actions/runs/34895523735) passed; the organization-root deployment remains a separately operated site. The RC copy presented v12.0.0-rc.1 as an explicit evaluation candidate and marked v5 end-of-life. The stable documentation presents `12.0.0` as the supported v12 line while retaining application-specific security and deployment limits. The landing uses local CSS/JavaScript/images, finite reduced-motion-aware animation, thirteen owner-supplied social links and a concrete privacy notice. No analytics, social embeds or browser storage were added. Benchmark templates replace remote fonts with system fonts and pin Chart.js with integrity metadata; the remaining jsDelivr request is disclosed. The README preserves the top and bottom dedication, workflow dashboard, genuine coverage/Scorecard badges and evidence boundaries. It corrects the obsolete frontend-profile advertisement. The [beginner learning page](https://rullst.github.io/Rullst/book/start-here.html) links existing authoritative tutorials instead of creating another competing API reference. Initial guides clarify matching CLI installation, optional persistence, first-build time, actual generator paths and how to verify a visible result. Verified locally for the stable-promotion worktree: `mdbook build docs`, `python3 .github/validate-site.py`, `node --check docs/site.js`, and `node .github/site-browser-smoke.mjs`. The Chromium test passed desktop, 390/320-pixel layouts, keyboard/mobile menu behavior, clipboard success/denial, privacy disclosure, reduced motion, no-JavaScript navigation and no external landing requests or browser storage. The RC organization-site export previously passed the same smoke suite with `--organization-site`; the stable copy still requires its separate deployment receipt. This is not a WCAG or cross-browser certification. ## Development reload decision The public v12 development loop uses **supervised process restart**. Both `cargo rullst dev` and `cargo rullst dash` enable it automatically; plain `cargo run` remains ordinary execution. The wizard no longer asks for a DLL profile, and the legacy scaffold flag fails with migration guidance. A successful build precedes stopping the existing child. Compilation errors leave that child serving. Each process runs an owned executable snapshot, so a Windows executable lock does not prevent the next Cargo build. The browser refreshes through a same-origin generation probe after a new server responds. State in memory resets; this does not promise zero-downtime deployment. The [tutorial](https://rullst.github.io/Rullst/book/tutorials/51-authenticated-hot-reload.html) explains the contract. The v13 decision is evidence-driven: compare measured reload time, failure recovery, process cleanup, memory and state ownership across databases and operating systems before considering a different architecture. ## Pre-stable evidence retained - Approved code candidate `a6b3bc8a` passed all 22 applicable automatic push workflows. Codecov reported 91.49% repository coverage and 100% patch coverage; the separately enforced framework-library component also passed 90%. - The dependency, code-scanning and secret-scanning APIs each reported zero open alerts after the hosted candidate completed. - Sixteen exact `12.0.0-rc.1` archives passed metadata/topological preflight, content audit and Cargo package verification. An extracted consumer compiled, the packaged CLI installed and reported the correct version, and all six installed-CLI blueprints generated and compiled without monorepo paths. - The complete 40-target hosted fuzz campaign passed on parent `7697fb8a` in run `34642351302`. It remains historical RC evidence. Core/Auth and generated-application runtime changes subsequently required a fresh campaign; stable-source run `34895536740` passed all forty targets. - The full mutation campaign is complete. The originating run `34688592153` left six of eighty shards incomplete; exact-SHA recovery run `34738841341` completed eleven of twelve halves before GitHub twice shut down the runner at the same resource-intensive ORM mutant. Finalization run `34761010296` split only that remaining half again, bounded its internal test timeout and passed both fragments, the aggregate and the evidence boundary. The verified content-addressed artifact reports **14,391/14,391 candidates classified** across 87 complete artifacts: 8,705 caught, 3,553 missed, 76 timed out and 2,057 unviable, for a conservative caught percentage of 70.57%. Its source is `1ffdf0a72577d1a974b4e87e2bf207f4a3580243`; inventory SHA-256 is `986d5cc1de71f7c8afbae7823a8fca53304b84f8e81b8f9e2d1fd3949c15ef80`. Mutation remains informational: full classification is valid sensitivity evidence, while surviving and timed-out mutants remain explicit follow-up findings rather than being mislabeled as caught. - The owner supplied explicit GO for `v12.0.0-rc.1` on September 12, 2026 after reviewing the package graph and topological plan. The later stable closeout repeated the applicable exact-SHA gates and completed the protected publication pipeline. The 94/A repository score remains supporting evidence, not a security certification. ## RC lineage and stable publication receipt - Tag `v12.0.0-rc.1` resolves to `2b19567e8d184ad3ceeebae812de71b06d32e484`. The release workflow's verify and publish jobs succeeded on that SHA. All sixteen packages were published in topological order, indexed with the verified archive checksums and exposed through docs.rs; the owner also completed a separate-machine CLI smoke test. - The GitHub release is marked as a prerelease and contains all sixteen crate archives, checksum files, Cargo metadata/lockfile, Cargo Audit JSON, CycloneDX 1.5 SBOM, bounded security evidence, advisory policy, ownership evidence and tag context. GitHub's SHA-pinned build-provenance attestation verified for the archives and evidence bundle. - The overall historical run is red only because the additional reusable SLSA generator failed during environment detection: its transitive `detect-workflow-js@v2.1.0` reference violated this repository's full-SHA Action policy. It never generated or uploaded provenance. It is not counted as release evidence, and no SLSA level is claimed. Stable preparation removed that redundant external job while retaining the successful SHA-pinned GitHub attestation already produced inside the verified job. - All package names are registered. Trusted Publishing is configured by the owner for all sixteen packages, the temporary bootstrap secret was removed, and the exposed bootstrap token was revoked. Stable publication used the protected `crates-io` environment and short-lived OIDC credentials; the bootstrap path remains inactive. - Stable `12.0.0` promotes the audited RC lineage plus the declared post-RC corrections: idempotent nested CSRF composition, stricter session-cookie parsing/logout, transactional generated LMS registration, POST/CSRF billing mutations, hardened generated containers, self-contained Studio assets, collision-free Omni targets, source-comment parsing and explicit validated COEP application policy. - Tag `v12.0.0` resolves to `eb11f892ae28f076e7a83c38a635316c6ed89028`. Release run `34955468167` reproduced and verified the archives, created SHA-pinned build-provenance attestations, published all sixteen packages in topological order, verified registry availability and checksums, and created the non-prerelease GitHub release. ## Residual limitations for the next reviewer - HTML escaping does not make `RawHtml`, custom escaping implementations, JavaScript contexts or URL policies safe automatically. RPC parameters are untrusted inputs, not identity assertions. - Some direct session/passkey helpers depend on caller input-size bounds even when HTTP middleware limits requests. Cookie isolation matters for the unsigned double-submit scheme. Local rate controls are not distributed limits. - OIDC fixtures do not establish live-account acceptance, JWKS stampede resistance or multi-host atomic callback consumption. - NFS-e remains preparation/offline evidence, not official fiscal authorization. Payment fixtures do not homologate providers or fully model every event schema. - SQLite cancellation tests prove rollback ownership before commit; they cannot make cancellation during a dispatched commit into exactly-once knowledge. - Some older route smoke tests accept unavailable-database responses and prove route presence only. Real SQLite behavioral tests are identified separately. - IoT received only the agreed manifest/README review. No physical devices, browser WebAuthn ceremony, store signing, live provider or deployment proxy certification was performed. ## Evidence handling Confirmed security defects remain local until corrections and regression evidence are ready for a coordinated commit. Findings are classified by impact, not by whether the previous scorecard happened to be green. Provider behavior that cannot be completed within the current v12 contract must fail explicitly and be documented as unsupported; mock success is not live-provider evidence. ## Repeatable focused verification receipts These focused commands and the final local preflight succeeded during the September 5–6 correction batch. The September 8 PR #183 tree, merged as `adb83c8b`, independently repeated `cargo test --workspace --all-features`, strict all-feature workspace Clippy, formatting and diff checks successfully: ```bash CARGO_BUILD_JOBS=1 cargo test -p cargo-rullst --lib -- --test-threads=1 CARGO_BUILD_JOBS=1 cargo test -p rullst-core --lib server::dev_reload -- --test-threads=1 node rullst-core/src/server/dev_reload/client_tests.cjs CARGO_BUILD_JOBS=1 cargo test -p rullst-nexus --lib --tests -- --test-threads=1 CARGO_BUILD_JOBS=1 cargo test -p rullst-security --lib rate_limit::tests -- --test-threads=1 CARGO_BUILD_JOBS=1 cargo test -p rullst-ai --lib CARGO_BUILD_JOBS=1 cargo test -p rullst-mail --features sqlite --lib CARGO_BUILD_JOBS=1 cargo test -p rullst-connect --features axum-session,sqlite --lib CARGO_BUILD_JOBS=1 cargo test -p rullst-capital --features actix --lib --tests CARGO_BUILD_JOBS=1 cargo test -p rullst-capital --features actix --lib middleware_rejects_duplicate_standard_webhook_headers CARGO_BUILD_JOBS=1 cargo test -p rullst-messaging --features sqlite -- --test-threads=1 --quiet CARGO_BUILD_JOBS=1 cargo clippy -p cargo-rullst -p rullst-core --lib -- -D warnings CARGO_BUILD_JOBS=1 cargo clippy --workspace --all-targets --keep-going -- -D warnings CARGO_BUILD_JOBS=1 cargo test --workspace --all-features CARGO_BUILD_JOBS=1 cargo clippy --workspace --all-targets --all-features -- -D warnings cargo fmt --all -- --check git diff --check bash .github/check-historical-roadmap-ledger.sh bash .github/check-crate-architecture.sh ``` ORM default/strict-SQLite/Redis and macro compile-fail results are recorded in the coverage ledger. Live-service backend verification remains distinct from the passing all-feature compile, lint and local workspace gates. --- # v12 Partial Release Recovery Source: https://rullst.github.io/Rullst/book/release-recovery.html # Partial-publication and recovery runbook This runbook covers an interrupted multi-crate publication. A crates.io upload is irreversible: a version can be yanked but cannot be deleted or replaced with different bytes. Recovery therefore resumes forward from verified artifacts; it never tries to overwrite an accepted version. ## Preconditions - Freeze merges and deployments that consume the affected prerelease. - Preserve the release workflow run, tag, commit, `checksums.txt`, attestation, SBOM, audit output and every `.crate` archive. - Revoke a registry token immediately if credential compromise is suspected. - Assign an incident owner and record times, commands, registry responses and affected package versions. ## Name ownership and first-publication credentials The machine-readable policy in `.github/crates-ownership-policy.json` separates the expected crates.io owner (`venelouis`) from the GitHub identity trusted to run the release (`Rullst/Rullst`). The bootstrap allowlist is not an ownership claim: it is the reviewed set of names that may still return `404` before their first publication. `.github/check-crates-ownership.sh` runs once in the verification job and again immediately before publication. Every registered package must include `venelouis` among its owners. Every unregistered package must be present in the bootstrap allowlist. A name registered by another owner, an unexpected missing name, an API error, or a malformed policy stops the release. The following one-time bootstrap procedure was completed before the first v12 RC and again for the four new v13 names (see below). All twenty package names are now registered, so future releases must use Trusted Publishing and must not recreate a bootstrap secret unless a new, reviewed package name is introduced: **Historical procedure:** the current `release.yml` has removed this token fallback and refuses every unregistered package, including on prerelease tags. Adding a name to the allowlist does not enable publication. Do not set the old secret and expect the steps below to work on the current workflow. A new name needs a reviewed change that restores the fallback, merged before the release admission, as [PR #452](https://github.com/Rullst/Rullst/pull/452) did. 1. Protect the GitHub `crates-io` environment with required review and tag deployment rules. 2. Configure Trusted Publishing for every already-registered package using GitHub owner `Rullst`, repository `Rullst`, workflow `release.yml`, and environment `crates-io`. 3. Create a shortest-lived crates.io token with the `publish-new` endpoint scope. Restrict its crate-name scope to the reviewed bootstrap names if the crates.io UI permits that combination. It does not need `publish-update`: registered packages are published with the OIDC credential. 4. Store it only as the `CRATES_IO_BOOTSTRAP_TOKEN` secret in the protected `crates-io` environment. Never put it in repository secrets, logs, command history, documentation, or a local Cargo credentials file. 5. Start the tag workflow only after reviewing the generated ownership evidence. The publish loop selects the bootstrap token only for packages classified as `unregistered-reviewed-bootstrap`; registered packages use the short-lived Trusted Publishing token. 6. After all new packages are indexed, configure the same Trusted Publisher for each of them, enable crates.io's trusted-publishing-only protection, revoke the bootstrap token, delete the GitHub environment secret, and remove the names from the bootstrap allowlist in a reviewed change. Trusted Publishing cannot be configured before a crate's first release. The one-time token is therefore an explicit, bounded exception, not a permanent fallback. See the official [crates.io Trusted Publishing announcement](https://blog.rust-lang.org/2025/07/11/crates-io-development-update-2025-07/) and [Cargo publishing rules](https://doc.rust-lang.org/cargo/reference/publishing.html). ### v13 package registration `rullst-privacy`, `rullst-supervision`, `rullst-media` and `rullst-labs` were first published in 13.0.0-alpha.1 on October 11, 2026, from release run [38151068563](https://github.com/Rullst/Rullst/actions/runs/38151068563). The [crates.io documentation](https://crates.io/docs/trusted-publishing) still requires an API token for a first publication. The first attempt (run 38121092958) refused before uploading anything, because the publish job had no first-publication path. Registering the names outside the workflow was not possible: `rullst-media` depends on `rullst-core` at the same prerelease version. [PR #452](https://github.com/Rullst/Rullst/pull/452) restored the fallback above for that release: - The maintainer created a short-lived `publish-new` token and stored it only as the `crates-io` environment secret. - All twenty archives matched the release checksums on crates.io. A reviewed change after that release reverted the fallback and emptied the allowlist. The maintainer then completed step 6 for the four crates: - set the Trusted Publisher to `Rullst/Rullst`, `release.yml`, environment `crates-io`; - enable trusted-publishing-only updates; - revoke the token and delete the environment secret. The next release's ownership check verifies that `venelouis` owns all twenty names. **Lesson:** check for unregistered names before the admission. The release verification job accepted reviewed bootstrap names, so the refusal surfaced only in the publish job, two hours into the run. - `.github/release-preflight.py` now refuses an unregistered name before the admission is dispatched. - The verification job refuses it before packaging. A change that restores the fallback must also remove that early refusal. ## Determine the exact state For each package in `.github/release-order.json`: 1. query `https://crates.io/api/v1/crates/<crate>/<version>`; 2. classify it as not indexed, indexed with the expected checksum, or indexed with an unexpected checksum; 3. compare the registry checksum to the retained verified archive; 4. verify that every already-published internal dependency precedes its dependent in the release order. Do not infer success only from the exit status of `cargo publish`. Registry indexing is asynchronous, so poll with a bounded timeout. The release workflow performs this check after each package. ## Recovery decisions ### Nothing was accepted Correct the failure on a new commit. If the tag or package content must change, use a new prerelease version such as `12.0.0-rc.2`. Never move a published or publicly attested tag to unrelated content. ### A prefix was accepted with matching checksums Keep the accepted packages. Re-run the release workflow from the same immutable tag and verified artifact set. Its resume logic skips only versions whose crates.io checksum equals the retained archive and continues at the first missing package. If source, manifest or artifact content needs any change, bump **all** publishable packages and internal requirements atomically to a new prerelease. Do not mix rebuilt bytes into the interrupted version. ### A checksum is unexpected Stop immediately. Do not publish dependents. Preserve evidence, revoke credentials, contact crates.io support and treat the event as a potential supply-chain incident. Yank the affected version when that reduces user risk, but remember that yanking does not remove downloaded bytes. ### A dependency never becomes index-visible Stop before its dependents. Retain the successful upload response and poll the API/index within the bounded operational window. If crates.io reports an incident, wait for service recovery; do not reorder the DAG or weaken checksum verification. ## Consumer mitigation - Yank a broken or unsafe prerelease and publish a corrected new prerelease. - State exactly which packages/versions were accepted, which were yanked and which replacement users should select. - Never describe yanking as deletion or as proof that no consumer downloaded the artifact. - For a stable vulnerability, follow the advisory SLA and coordinated disclosure policy in [Security advisory exceptions](https://rullst.github.io/Rullst/book/security-advisory-exceptions.html). ## Completion evidence Recovery is complete only when: - every version in the tagged release inventory is indexed with its retained checksum (16 packages for v12; 20 in the current v13 candidate); - a clean consumer resolves only registry packages and compiles; - documentation/index pages are reachable; - the incident timeline and any yanks/replacements are recorded; - temporary credentials are revoked and Trusted Publishing is restored; - the next release includes a regression for the initiating failure. This process is package recovery, not application rollback. Database migrations, traffic rollback, secrets and deployed binaries require a separate application-specific operational plan. --- # Website Maintenance & Privacy Boundaries Source: https://rullst.github.io/Rullst/book/website-maintenance.html # Website maintenance and privacy boundaries The public entry points have different deployment sources: | Entry point | Repository source | Purpose | | --- | --- | --- | | `https://rullst.github.io/` | `Rullst/Rullst.github.io` | Organization landing page and standalone privacy notice | | `https://rullst.github.io/Rullst/` | This repository's `pages.yml` workflow | Matching landing page, mdBook, images and benchmark dashboards | | `gh-pages` branch in this repository | Benchmark workflow data | Criterion history consumed by the Pages build; not a development branch | The landing design and copy have one editable source: `docs/home_template.html`, `docs/site.css` and `docs/site.js`. Preserve the footer dedication and the release notice (v13 prerelease, v12 and v5 end of life). The install command must pin the current `cargo-rullst` version; the validator checks it. Use actual source and release evidence for claims; do not hardcode aspirational coverage, scorecard, speed or certification values. ## Validate before deployment From the framework checkout, using Node 24 and a locally installed Chromium (`google-chrome`, or set `CHROME_BIN`): ```bash mdbook build docs python3 .github/validate-site.py python3 .github/test-llms-txt.py node --check docs/site.js node .github/site-browser-smoke.mjs ``` The Pages build also writes `llms.txt` (an index of every book page in `SUMMARY.md` order) and `llms-full.txt` (their Markdown, with mdBook include directives expanded; pages over 64 KiB other than the spec and the CLI reference are listed under `## Optional`) to the site root with `.github/generate-llms-txt.py`. They are generated on every deployment and never edited by hand; preview them with `python3 .github/generate-llms-txt.py --out <directory>`. `validate-site.py` generates both files and fails when either is missing a `SUMMARY.md` page or exceeds its size bound (128 KiB and 8 MiB). The browser test checks desktop and 390/320-pixel layouts, keyboard and mobile navigation, clipboard success/denial, privacy disclosure, reduced motion, no-JavaScript navigation, resource failures, CSP errors, external requests and browser storage. It is not a complete accessibility audit or cross-browser certification. Optional `--screenshots /absolute/output/directory` records viewport previews without adding binary artifacts to the repository. ### Keep cached assets in sync with the page The landing references CSS and JavaScript with a content version, such as `./assets/site.css?v=<first-16-hex-digits-of-SHA-256>`. After editing either asset, compute its SHA-256 and update its reference in `home_template.html`. The validator and organization exporter reject stale versions. This is cache invalidation, not an artifact-signing or browser integrity guarantee. The browser regression check first stores an old unversioned stylesheet and script in a real four-hour browser cache, confirms they are reused, then loads the new landing and checks the three illustrated module cards' actual styles. The organization privacy page shares the versioned stylesheet. Keep query parameters in any CDN cache key; an intermediary configured to ignore them requires a cache-policy change or content-hashed asset filenames instead. Previously cached HTML can still show the previous design until refreshed. The source landing has no analytics, social embeds, remote fonts, cookies or local/session storage. CSS and JavaScript are local; ambient, hero, workflow and scroll-reveal motion is finite, progressively enhanced and disabled by the reduced-motion preference. Content stays visible when JavaScript is unavailable. Clipboard access follows an explicit button click and copies only the displayed command. Hosting still processes requests. The privacy notice links GitHub's statement and does not promise control of its logs or retention. Benchmark dashboards load integrity-pinned Chart.js from jsDelivr; documentation can store display preferences and contain external content. Do not extend the landing's narrower description to those surfaces or to user-built applications. New tracking, embeds, forms or storage require a fresh privacy review before deployment. ## Keep the organization website synchronized Preserve any work in the separate website checkout before running the exporter: ```bash node .github/export-organization-site.mjs /path/to/clean/Rullst.github.io node .github/site-browser-smoke.mjs --organization-site /path/to/Rullst.github.io ``` The exporter refuses a dirty tree or wrong repository. It replaces only the five known website files, derives `privacy.html` from the landing notice and does not commit, push or deploy. Review the diff and deploy matching framework documentation first, then the organization website. Keep independent commit and deployment receipts for each repository. A framework push alone does not update the organization's root landing page. The footer's thirteen destination links are the owner's supplied community list. Instagram, TikTok and YouTube handles are normalized to full profile URLs. These are normal links, not embedded feeds or a claim that account availability has been independently verified. Recheck ownership and links when the owner changes that list. --- # Rullst Academy Product Programme Source: https://rullst.github.io/Rullst/book/rullst-academy.html # Rullst Academy product programme > **Status:** proposed reference product, not a shipped framework capability. > Academy must live in a separate repository and depend only on published > Rullst packages. Its operation is evidence for the framework; its product > content and deployment are not part of the `rullst` crate. Rullst Academy is a web-first platform for learning Rust and Rullst through short explanations, authoritative interactive exercises, review sessions and small practical projects. The target is an enjoyable learning product, not a claim that a framework can automatically produce good pedagogy or a universally better alternative to an existing course. ## Product boundary The browser-accessible web application is canonical. Server-side code owns identity, school scope, authorization, course versions, grading, progress, achievements and audit. Installing the application as a Progressive Web App (`cargo rullst make:pwa`) does not create a second security or business model. The Academy repository must: - pin one exact Rullst RC or stable release and contain no monorepo path dependencies; - use public CLI scaffolds and escape hatches instead of private framework modules; - keep curriculum, content, product UI, deployment and learner support in the application; - contribute only proven, reusable abstractions back to optional framework crates; - provide application-level upgrade, backup/restore and recovery feedback for future releases; this proposed product is not a prerequisite for the already published v12 release. ## Focused first release The first useful Academy release should complete one narrow journey well: 1. A learner creates an account, joins the appropriate school and enrolls in a published course. 2. The learner reads or watches an accessible lesson and resumes recorded progress. 3. A server-authoritative quiz, matching or typed-recall activity records an idempotent result without accepting client-authored points. 4. The review queue schedules a later activity and an achievement or certificate is derived from persisted rules. 5. A practical project is submitted to a constrained runner and receives deterministic test feedback. 6. An instructor drafts, reviews and publishes content; an administrator can audit the resulting events without crossing school boundaries. Polish should concentrate on this journey: responsive SSR, keyboard operation, visible focus, reduced motion, clear errors, fast navigation and useful empty states. Payment, social features, advanced gamification, native-store releases, offline synchronization and a large course catalogue are not prerequisites for the first release. ## Initial curriculum and projects Two short tracks are enough to validate the product: | Track | Initial lessons | Practical outcome | | :--- | :--- | :--- | | Rust foundations | ownership and borrowing, structs/enums, pattern matching, errors, iterators, async and tests | a tested command-line application that persists bounded data | | Rullst web development | project creation, routes/SSR, forms, ORM/migrations, authentication/authorization, queues and deployment preparation | a secure small web application with owner-only CRUD and background work | Every exercise must have a versioned ruleset, bounded input/output and retained test identity. Curriculum authors, not an LLM, define the expected concepts, tests, hints and completion conditions. ## Safe practical-code runner Learner code is hostile input. It must never execute inside the Academy web process or through a mounted host Docker socket. The application-owned runner must use a disposable rootless container or stronger microVM boundary with: - no network by default and no cloud/application credentials; - read-only base images and a fresh writable workspace per attempt; - explicit CPU, memory, process, file, disk, output and wall-clock limits; - a reviewed Rust toolchain/dependency policy and immutable image digest; - bounded compile/test logs with control-character handling and secret redaction; - an idempotent submission identity, queued execution, cancellation and terminal retry/dead-letter policy; - cleanup after success, failure, timeout and worker restart; - adversarial escape, fork-bomb, filesystem, network and output-amplification tests before public use. The runner returns structured test evidence. It does not grant points directly; the Academy service binds that evidence to the authenticated learner, project version and server-owned scoring policy. Rullst does not ship this runner; with `rullst-labs`, it implements the [Labs controller contract](https://rullst.github.io/Rullst/book/labs-runner-contract.html). ## Local AI mascot Privacy and proportional age assurance are v13 P0 dependencies for the applicable learner journeys; follow the shared [privacy programme](https://rullst.github.io/Rullst/book/privacy-age-assurance-roadmap.html). Academy must decide its audience and processing purposes, validate any guardian relationship, and implement actual export/deletion adapters. The v12 Academy scaffold's age bands and consent records, retired in v13, are not independent age or guardianship verification. Reading lessons must not acquire a camera requirement without a justified policy. The mascot can be a friendly tutor backed by the existing Ollama path in `rullst-ai`, with a deterministic offline fallback for tests. It should use a bounded RAG corpus containing version-pinned official Rust material, Rullst documentation and Academy-authored hints. The mascot may explain an error, ask a guiding question, retrieve a relevant lesson or suggest the next exercise. It must not: - authoritatively grade code, invent completion or change persisted points; - execute arbitrary tools or learner code outside the sandbox boundary; - retrieve another learner's conversation, submission or school data; - receive secrets, raw session values or unnecessary personal data; - present an uncited generated statement as official Rust or Rullst behavior. Responses should carry source references and a visible “local AI tutor” label. Provider/model unavailability must leave the curriculum and deterministic grader usable. Prompt-injection regressions, context limits, tenant binding, PII masking and secret-minimized audit remain mandatory. ## What exists and what remains application work | Boundary | Current reusable foundation | Academy must still prove | | :--- | :--- | :--- | | Learning domain | The LMS example's (`examples/lms`, the former v13 LMS starter) catalog, modules, lessons, accessible player, enrollment and owner-bound progress; the v12 Academy scaffold's activities, quizzes, review, completion, certificates, leaderboard and automation were retired | those learning features, coherent product UX, content quality, complete authorship and browser E2E | | Identity and schools | Session/RBAC helpers; the LMS example has no schools (the v12 school-scoped contracts were retired) | schools and memberships, account recovery, invitations, device/session policy and every cross-school negative | | AI tutor | guarded providers, Ollama fallback, bounded tenant-aware RAG and audit contracts | curated corpus, pedagogy, model evaluation, capacity and user-facing failure behavior | | Practical projects | queues, outbox and bounded messaging foundations; the `rullst-labs` job and grading contracts (13.0.0-alpha.1 prerelease) | isolated runner, immutable images, resource policy and escape testing | | Media | bounded accessible lesson metadata, captions and transcripts; the opt-in `rullst-media` Bunny Stream candidate (13.0.0-alpha.1 prerelease) | upload, storage, scanning, transcoding, caption quality and retention | | Operations | health/readiness, telemetry, deploy scaffolds and upgrade assistant | production topology, TLS/proxy identity, backup/restore, rollback, alerts and incident response | ## Repository and release boundary Academy will be developed in a separate repository and conversation. This document records only the intended boundary and the reusable framework foundation that already exists; it does not authorize further Academy work in the Rullst repository and it is not a framework release gate. - **v12.0:** close the framework independently through its coverage, CI/package/security, upgrade and release-candidate gates. Academy does not need to exist or run against the RC. - **v12.0.x:** maintenance only for confirmed framework defects and security fixes, without an Academy capability programme. - **v13:** the next feature line. Reusable improvements discovered by the future external Academy may be proposed with their own bounded contracts; research-heavy or breaking work remains explicitly governed by v13 criteria. The open canonical milestones in the [current roadmap](https://rullst.github.io/Rullst/book/roadmap.html) belong to the long-term v13 horizon. They are not release blockers for v12.0, and Academy itself is not part of that milestone denominator. ## Acceptance evidence Before Academy can be treated as release evidence, record all of the following against immutable application and framework SHAs: - the complete learner/instructor/admin journey on PostgreSQL, with SQLite kept as a local profile only when its declared limits are acceptable; - anonymous, cross-user, cross-role and cross-school denial tests; - browser accessibility and responsive-layout checks for the primary journey; - sandbox abuse tests and bounded compile/test output; - backup restoration, forward migration, rollback and framework-upgrade drills; - dependency-only installation from crates.io with no Rullst path overrides; - load and failure tests with unavailable AI, mail, cache and worker services; - a documented human GO/NO-GO decision and remaining product risks. Related reusable guides include [accessible Academy media](https://rullst.github.io/Rullst/book/tutorials/45-accessible-academy-media.html), [tenant-bound RAG](https://rullst.github.io/Rullst/book/tutorials/41-tenant-bound-rag.html), and the [assisted upgrade workflow](https://rullst.github.io/Rullst/book/tutorials/36-assisted-framework-upgrades.html). --- # Maybe SaaS: Product Incubation Strategy Source: https://rullst.github.io/Rullst/book/maybesaas.html # Maybe SaaS: Incubating Products Built with Rullst > **Status: strategy proposal, not an implementation claim.** None of the > products named in this document exists merely because it is described here. > The [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html) remains the evidence source for > what the framework implements today. Some of Rullst's most ambitious ideas need more than another module in a web framework. They require a continuously operated service, official homologation, named hardware, vendor interoperability, incident response, or a dedicated security program. Those ideas can become separate products **built with Rullst**, while the framework remains a reusable and honest foundation. This is not automatically an open-core strategy, and it does not require every advanced capability to become paid software. It is an incubation boundary: use the smallest delivery model that can provide credible evidence and operations. ## The four possible delivery models | Model | Use it when | What stays in the Rullst framework repository | | :--- | :--- | :--- | | **Framework crate** | The capability is a reusable local library and does not require a vendor-operated control plane. | Public traits, types, adapters, deterministic mocks, documentation, and contract tests. | | **Open-source reference application** | Users need a deployable example or can reasonably self-host the whole capability. | The stable client contract and an example; the application can have its own repository and release cycle. | | **Managed SaaS or private control plane** | The value depends on durable shared state, continuously updated provider rules, tenant operations, monitoring, or 24/7 availability. | A provider-neutral client/SDK, an explicit remote adapter, offline mocks, and a self-hosted escape hatch where practical. | | **Conformance or hardware program** | Correctness depends on official test environments, physical devices, audited cryptography, interoperability matrices, or certification. | Interfaces and test vectors. Passing named external suites is required before a production claim. | A capability can use more than one model. For example, an IoT platform can have an open device SDK, a managed control plane, and a physical hardware conformance lab. ## Recommended boundary ```text Application built with Rullst | | typed client contract v Framework adapter + deterministic offline mock | | explicit opt-in; never silent fallback v Separately deployed product or customer-managed service | +-- durable state and tenant isolation +-- provider, fiscal, or device interoperability +-- monitoring, audit, support, and incident response +-- independent conformance evidence where required ``` The framework must remain useful without the managed product. Remote services must not become hidden requirements for routing, ORM, authentication, or local development. Live configuration must also fail closed instead of silently falling back to a mock. ## Candidate incubation programs The names below are working descriptions, not announced product names. ### 1. Fiscal Cloud for NFS-e **Best initial form:** a dedicated fiscal program, followed by a managed SaaS and a self-hostable/private deployment if the operating model proves viable. Rullst v13 removed its bounded local NFS-e preparation module (DPS 1.01 builder, checksum-pinned XSD validation, PKCS#12 XMLDSig and mTLS client preparation), which was never validated with a real municipality; its source remains in git history as a starting point. A live product would be responsible for substantially more: - official schemas, municipality/national variations, rejection codes, and protocol updates; - PKCS#12 or delegated certificate custody, rotation, access control, and audit; - official request/response envelopes, retries, reconciliation, cancellation, substitution, and immutable evidence around the local crypto/schema core; - durable idempotency and a complete issuance state machine; - official homologation environments, operational monitoring, and specialized support. The fiscal engine should be a separate product with its own lifecycle, because protocol and legal maintenance must not be coupled to releases of the framework suite; `rullst-capital` no longer ships fiscal types. It must not advertise legal or tax compliance without qualified review and current official evidence. **Why it is attractive:** it solves a difficult Brazilian SaaS problem and would exercise queues, cryptography, observability, billing, multi-tenancy, and failure recovery in a real Rullst application. **Why it is risky:** certification, certificate custody, protocol drift, and financially consequential failures make this much more than an HTTP adapter. ### 2. IoT Device and OTA Control Plane **Best initial form:** open device SDK plus a hardware conformance program; managed SaaS only after selecting and testing named device families. Rullst currently provides `no_std` telemetry/frame foundations and an Ed25519-signed OTA manifest verification gate. A credible platform would add: - device registry, provisioning, fleet inventory, and tenant isolation; - MQTT/CoAP/LoRaWAN interoperability against named brokers and devices; - durable anti-rollback counters, staged rollouts, download resumption, boot slots, health confirmation, and rollback orchestration; - signed firmware provenance, software bills of materials, revocation, and incident response; - physical test rigs covering power loss, partial writes, clock faults, poor networks, and recovery paths. The device-side verifier should remain small, auditable, and independent of the SaaS. A customer must not brick a fleet merely because the control plane is temporarily unavailable. ### 3. Key Management, HSM, and Post-Quantum Interoperability **Best initial form:** adapter crates and a conformance program, **not a new cryptographic SaaS first**. HSM and post-quantum work is worthwhile only for named protocols, devices, and threat models. The safe path is to integrate audited libraries and standards, then validate them against PKCS#11 services, cloud KMS/HSM providers, secure elements, and published test vectors. Rullst must not invent cryptographic primitives or use a generic “quantum-safe” badge. A managed orchestration service could eventually handle key policy, rotation, attestation inventory, and audit workflows. Actual private-key operations should remain inside the selected HSM/KMS boundary whenever possible. On-premise and bring-your-own-key modes are likely requirements, not optional extras. ### 4. Enterprise Identity Gateway **Best initial form:** a separate identity service or private control plane, with a dedicated crate boundary if reusable protocols are added. `rullst-connect` is currently focused on OAuth2/OIDC and social identity. Enterprise SAML, SCIM provisioning, organization/domain discovery, directory synchronization, delegated administration, policy evaluation, and tenant audit are a coherent product of their own. They should not be represented as a few extra provider flags in the OAuth crate. The framework can expose authentication/session integration and typed identity events. The gateway can operate federation metadata, provisioning jobs, enterprise connectors, and tenant-specific policy. A private deployment option is important for organizations that cannot send identity metadata to a shared SaaS. ### 5. Security Operations Control Plane **Best initial form:** a self-hostable reference service before any managed SaaS claim. Rullst Security, Radar, Studio, and Nexus already provide useful local security and telemetry foundations. A separate control plane could aggregate signed events from multiple applications, manage distributed rate-limit policy, deliver alerts to SIEM destinations, retain audit evidence, and coordinate incident response. This product would need explicit data-retention controls, regional placement, tenant isolation, end-to-end authentication, redaction, bounded ingestion, and an unavailable state that never weakens an application's local defenses. Claims such as autonomous blocking, zero leakage, or complete OWASP coverage would still require narrow definitions and independent evidence. ### 6. Messaging and Remote Storage **Best initial form:** provider-neutral crates and conformance suites before a managed service. Kafka, RabbitMQ, Redis Streams, S3, and R2 are integrations with mature services. Rullst now has a coherent bounded `rullst-messaging` contract for idempotency, groups, leases, retry and dead letters, with a deterministic process-local broker, canonical envelope/trace contracts and opt-in durable local SQLite state with explicit encrypted content. The 13.0.0-alpha.1 prerelease candidates add a standalone Redis Streams profile (`messaging-redis`) and private S3-compatible object storage with resumable multipart uploads (`storage-s3`, `storage-multipart`), tested against disposable local services. Other remote adapters still need provider-specific conformance, backpressure and deterministic mock evidence, and live provider interoperability remains unvalidated. Building another broker or object store as a SaaS would add little value until Rullst applications reveal a concrete unmet need. ## What should remain inside the framework Even when a separate product exists, Rullst should own: - stable, provider-neutral traits and serializable contract types; - feature-gated client adapters with bounded timeouts and typed errors; - deterministic offline mocks selected only by explicit mock configuration; - local development and self-hosted paths where they are practical; - contract tests that every official or third-party provider must pass; - telemetry hooks that expose availability without fabricating success; - migration and escape-hatch documentation that prevents vendor lock-in. The separate product should own production tenancy, billing, durable global state, operational dashboards, on-call response, external-provider drift, certification evidence, and service-specific data governance. ## Promotion gates An incubated idea must not be promoted from roadmap to implemented because a demo or one happy-path adapter exists. Before a public production claim, require the applicable gates: 1. a named owner and a documented support/release policy; 2. a threat model, abuse cases, tenant-isolation tests, and data classification; 3. a versioned API contract, deterministic mock, and self-host/exit strategy; 4. end-to-end tests against named providers, devices, or official environments; 5. migrations, backup/restore, disaster recovery, observability, and SLOs; 6. bounded retries, idempotency, reconciliation, and failure-injection tests; 7. security review and, for cryptographic/fiscal work, independent specialist validation; 8. a private beta with real operators before general-availability language; 9. evidence linked from the capability ledger and the relevant roadmap. Passing framework unit tests is necessary, but it cannot substitute for these external and operational gates. ## Suggested order of investment 1. **Shared foundations:** distributed idempotency/rate limiting, canonical Security contracts, complete scaffold validation, and reproducible releases. 2. **One reference product:** build and operate a narrowly scoped Rullst service to validate deployment, multi-tenancy, telemetry, upgrades, and support. 3. **Fiscal discovery or enterprise identity:** choose one based on access to qualified domain partners and real design customers; do not start both as production programs simultaneously. 4. **IoT control plane:** proceed only with named hardware, a physical lab, and a partner willing to test real update failures. 5. **HSM/PQC:** integrate audited standards for concrete use cases after the threat model exists; keep speculative branding out of production claims. The first reference product does not need to be the most spectacular one. Its purpose is to prove that applications built with Rullst can be upgraded, observed, secured, and operated for long periods. That operational evidence is more valuable to the framework than another unchecked feature list. ## Strategic conclusion Yes, a separate SaaS built entirely with Rullst can be an excellent direction. It creates a demanding real customer of the framework and can finance deeper engineering. The separation is successful only if it protects both sides: - the framework stays open, portable, provider-neutral, and truthful; - the product can evolve at the cadence required by its domain; - neither side claims external certification or production readiness without evidence; - every managed convenience has an explicit contract and a credible exit path. The goal is not to move unfinished features behind a hosted API. It is to give the ideas that require operations, certification, or hardware the independent engineering program they need to become real. --- # Historical Documentation Sources Source: https://rullst.github.io/Rullst/book/original-documentation-audit.html # Historical Documentation Sources Rullst's original ambition and the subsequent audit remain recoverable from immutable source snapshots: | Historical record | Source | | :--- | :--- | | Documentation before the first repository audit, August 24, 2026 | [Commit 96222fbd31bec3d20bc50db68c41bb85ca595779](https://github.com/Rullst/Rullst/tree/96222fbd31bec3d20bc50db68c41bb85ca595779) | | Original audit and later annotations | [Archived gpt.md](https://github.com/Rullst/Rullst/blob/v12.0.0/gpt.md) | | Preservation analysis and recovered ambitions | [Complete preservation record](https://github.com/Rullst/Rullst/blob/v12.0.0/docs/src/original-documentation-audit.md) | | Historical 190-claim classification | [Complete v12 development ledger](https://github.com/Rullst/Rullst/blob/v12.0.0/docs/src/v12.md) | For current APIs, use the [specification](https://rullst.github.io/Rullst/book/spec.html). For unfinished ideas, use the [roadmap](https://rullst.github.io/Rullst/book/roadmap.html) and [capability ledger](https://rullst.github.io/Rullst/book/capability-ledger.html). Historical scores and sample code are not current release instructions. --- # Archived v12 Hardening Tracker Source: https://rullst.github.io/Rullst/book/hardening-status.html # Archived v12 Hardening Tracker The pre-release tracker has been retired after publication of Rullst 12.0.0. Its intermediate scores, Portuguese notes, and open release checklist described earlier commits. They must not be read as the current release status. - [Read the complete tracker frozen at v12.0.0](https://github.com/Rullst/Rullst/blob/v12.0.0/docs/src/hardening-status.md). - [v12.0.0 release receipt and review findings](https://rullst.github.io/Rullst/book/v12-release-audit.html). - [Current capability boundaries and remaining work](https://rullst.github.io/Rullst/book/capability-ledger.html). - [Security controls and their tests](https://rullst.github.io/Rullst/book/v12-security-claims.html). This short page preserves the published URL. Open capability work remains in the capability ledger and roadmap; archiving the tracker does not close it. --- # Archived Framework Comparison Source: https://rullst.github.io/Rullst/book/comparatives.html # Archived Framework Comparison The August 27, 2026 comparison is a dated research snapshot. Its competitor claims and pre-release recommendations are no longer maintained as current product guidance. - [Read the complete historical comparison](https://github.com/Rullst/Rullst/blob/v12.0.0/docs/src/comparatives.md). - [Evaluate Rullst's current strengths and tradeoffs](https://rullst.github.io/Rullst/book/why-Rullst.html). - [Check the implemented scope and limitations](https://rullst.github.io/Rullst/book/capability-ledger.html). - [Review reproducible performance methodology](https://rullst.github.io/Rullst/book/tutorials/35-high-performance-benchmarking.html). - [Follow the current v13 comparative evaluation plan](https://rullst.github.io/Rullst/book/v13-framework-comparison.html). Any new comparison needs dated primary sources or equivalent measured workloads. This archive preserves the original research without renewing its claims.