Build a Posts Feature
This guided example follows one complete feature from URL to persisted data.
It uses the working blog generated by xr new --auth, so you can read the
source beside a running app rather than assemble unrelated snippets.
- Start with a running example
- 1. Define the boundary in the router
- 2. Make invalid input impossible to reach the action
- 3. Keep the model and schema together
- 4. Keep controller actions linear
- 5. Render a narrow view model
- 6. Verify the request boundary
- Reuse this sequence
Start with a running example
xr new examples/posts-guide --auth
cd examples/posts-guide
cargo run -- migrate
xr dev
Open http://127.0.0.1:34187, register an account, and create a post. The
starter is deliberately small, but the feature has the same layers a larger
app uses:
| Responsibility | Larust location | Laravel analogue |
|---|---|---|
| Public URLs and access rules | routes/web.rs |
routes/web.php + route middleware |
| Typed input contract | app/Http/Requests/store_post_request.rs |
Form Request |
| Persisted data | app/Models/post.rs + database/migrations/ |
Eloquent model + migration |
| Orchestration | app/Http/Controllers/post_controller.rs |
Controller action |
| HTML | resources/views/posts/*.blade.xr |
Blade view |
The names transfer on purpose. The difference is that routes, fields, and handlers are direct Rust references the compiler can verify.
1. Define the boundary in the router
Readers may view posts. Creating one requires an authenticated session, so write the public routes first and put write routes in a middleware group:
use larust_http::{Route, Router};
use larust_http::axum::middleware::from_fn;
use larust_support::auth::require_auth;
pub fn routes() -> Router {
Route::get("/posts", PostController::index).name("posts.index")
.get("/posts/{post}", PostController::show).name("posts.show")
.group("", |r| {
r.middleware(from_fn(require_auth))
.get("/posts/create", PostController::create).name("posts.create")
.post("/posts", PostController::store).name("posts.store")
})
}
| Laravel | Larust | Why it matters |
|---|---|---|
Route::middleware('auth')->group(...) |
.group("", \|r\| r.middleware(from_fn(require_auth))...) |
Larust passes the middleware function directly, so there is no string alias registry to maintain. |
{post} implicit binding |
post: Post handler argument |
A missing record becomes a 404 before the handler runs. |
| Route name | .name("posts.show") |
Link helpers can refer to a stable name instead of duplicating paths. |
Use a group when a subset shares a concern. Use top-level middleware only
when every route should inherit it. Routing
explains prefixes, nested groups, and when to use .merge() instead.
2. Make invalid input impossible to reach the action
A Form Request is both the validation declaration and the typed input your controller receives:
use larust_support::FormRequest;
#[derive(FormRequest)]
pub struct StorePostRequest {
#[validate(required, length(max = 255))]
pub title: String,
#[validate(required, length(max = 50_000))]
pub content: String,
}
pub async fn store(request: StorePostRequest) -> Result<impl IntoResponse, AppError> {
let input = request.validated();
// Execution reaches this line only after the request was validated.
}
| Laravel rule | Larust rule | Result |
|---|---|---|
'title' => 'required|max:255' |
#[validate(required, length(max = 255))] |
The rule is declared with the field. |
$request->validated() |
request.validated() |
Both mark the safe boundary clearly. |
| Validation exception | 422 extractor rejection |
The controller action is not called for invalid input. |
Form Requests currently read HTML form and multipart bodies. For JSON APIs,
use Axum’s Json<T> extractor and apply validation suitable for that API.
The Form Requests reference
lists every supported rule and current limitation.
3. Keep the model and schema together
The generated post model maps real Rust fields to the database table:
use larust_support::orm::sqlx;
use larust_support::Model;
#[derive(Model, sqlx::FromRow)]
#[table("posts")]
pub struct Post {
#[primary_key]
pub id: i64,
pub user_id: i64,
pub title: String,
pub content: String,
}
The migration remains plain SQL:
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
content TEXT NOT NULL
);
Laravel comparison: #[derive(Model)] provides Eloquent-shaped model
operations, including create, find, update, and delete. Unlike
Eloquent, fields live on the Rust struct instead of a dynamic property bag;
a misspelled field is a build error.
For a new resource, begin with:
xr make:model Task --migration
xr make:controller TaskController --resource
xr make:request StoreTaskRequest
Then replace the generator’s intentionally minimal stubs with the fields and
behavior your app needs. Run xr migrate after editing the SQL. Continue to
Models & Relationships for
relationships and Migrations & the Query Builder
for backend-specific setup.
4. Keep controller actions linear
The store action declares each dependency it needs: session state, authentication, and validated input. It then persists and redirects:
pub async fn store(
session: Session,
Auth(user): Auth<User>,
request: StorePostRequest,
) -> Result<impl IntoResponse, AppError> {
let input = request.validated();
let post = Post::create(NewPost {
user_id: user.id,
title: input.title,
content: input.content,
}).await?;
Ok(larust_support::redirect()
.route("posts.index")?
.with(&session, "success", "Post published.")
.await)
}
There is no controller base class or implicit request object. Session,
Auth<User>, Post, and StorePostRequest are extractors. If one cannot be
resolved—because the visitor is signed out, the model is absent, or validation
fails—the action does not execute.
That is the useful translation from Laravel: the responsibilities are familiar, but the values and dependencies are explicit.
5. Render a narrow view model
Pass only the values the template needs:
Ok(larust_support::view!("posts.show", {
id: post.id,
title: post.title,
content: post.content,
}))
@extends('layouts.app')
@section('content')
<article>
<h1></h1>
<div><!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<link rel="stylesheet" href="/assets/css/just-the-docs-default.css">
<link rel="stylesheet" href="/assets/css/just-the-docs-head-nav.css" id="jtd-head-nav-stylesheet">
<style id="jtd-nav-activation">
.site-nav > ul.nav-list:first-child > li > a,
.site-nav > ul.nav-list:first-child > li > ul > li:not(:nth-child(1)) > a,
.site-nav > ul.nav-list:first-child > li > ul > li > ul > li a {
background-image: none;
}
.site-nav > ul.nav-list:not(:first-child) a,
.site-nav li.external a {
background-image: none;
}
.site-nav > ul.nav-list:first-child > li:nth-child(7) > ul > li:nth-child(1) > a {
font-weight: 600;
text-decoration: none;
}.site-nav > ul.nav-list:first-child > li:nth-child(7) > button svg,
.site-nav > ul.nav-list:first-child > li:nth-child(7) > ul > li:nth-child(1) > button svg {
transform: rotate(-90deg);
}.site-nav > ul.nav-list:first-child > li.nav-list-item:nth-child(7) > ul.nav-list,
.site-nav > ul.nav-list:first-child > li.nav-list-item:nth-child(7) > ul.nav-list > li.nav-list-item:nth-child(1) > ul.nav-list {
display: block;
}
</style>
<script src="/assets/js/vendor/lunr.min.js"></script>
<script src="/assets/js/just-the-docs.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.8.0 -->
<title>Authentication & Authorization | Larust</title>
<meta name="generator" content="Jekyll v3.10.0" />
<meta property="og:title" content="Authentication & Authorization" />
<meta property="og:locale" content="en_US" />
<meta name="description" content="A Laravel-shaped web framework for Rust. Real, compiled, type-checked Rust underneath - Axum, sqlx, tower-sessions - with the directory layout, routing style, validation, templates, ORM vocabulary, and CLI a Laravel developer already knows." />
<meta property="og:description" content="A Laravel-shaped web framework for Rust. Real, compiled, type-checked Rust underneath - Axum, sqlx, tower-sessions - with the directory layout, routing style, validation, templates, ORM vocabulary, and CLI a Laravel developer already knows." />
<link rel="canonical" href="https://docs.larust.dev/digging-deeper/authentication-and-authorization/" />
<meta property="og:url" content="https://docs.larust.dev/digging-deeper/authentication-and-authorization/" />
<meta property="og:site_name" content="Larust" />
<meta property="og:image" content="https://docs.larust.dev/assets/og-image.png" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:image" content="https://docs.larust.dev/assets/og-image.png" />
<meta property="twitter:title" content="Authentication & Authorization" />
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"WebPage","description":"A Laravel-shaped web framework for Rust. Real, compiled, type-checked Rust underneath - Axum, sqlx, tower-sessions - with the directory layout, routing style, validation, templates, ORM vocabulary, and CLI a Laravel developer already knows.","headline":"Authentication & Authorization","image":"https://docs.larust.dev/assets/og-image.png","url":"https://docs.larust.dev/digging-deeper/authentication-and-authorization/"}</script>
<!-- End Jekyll SEO tag -->
<!-- Included last inside <head> (see just-the-docs' own head.html). -->
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;650;700;800&display=swap" rel="stylesheet">
</head>
<body>
<a class="skip-to-main" href="#main-content">Skip to main content</a>
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
<symbol id="svg-link" viewBox="0 0 24 24">
<title>Link</title>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</symbol>
<symbol id="svg-menu" viewBox="0 0 24 24">
<title>Menu</title>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu">
<line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</symbol>
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
<title>Expand</title>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</symbol>
<!-- Feather. MIT License: https://github.com/feathericons/feather/blob/master/LICENSE -->
<symbol id="svg-external-link" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link">
<title id="svg-external-link-title">(external link)</title>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line>
</symbol>
<symbol id="svg-doc" viewBox="0 0 24 24">
<title>Document</title>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline>
</svg>
</symbol>
<symbol id="svg-search" viewBox="0 0 24 24">
<title>Search</title>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search">
<circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</symbol>
<!-- Bootstrap Icons. MIT License: https://github.com/twbs/icons/blob/main/LICENSE.md -->
<symbol id="svg-copy" viewBox="0 0 16 16">
<title>Copy</title>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
</svg>
</symbol>
<symbol id="svg-copied" viewBox="0 0 16 16">
<title>Copied</title>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard-check-fill" viewBox="0 0 16 16">
<path d="M6.5 0A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3Zm3 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3Z"/>
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1A2.5 2.5 0 0 1 9.5 5h-3A2.5 2.5 0 0 1 4 2.5v-1Zm6.854 7.354-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 0 1 .708-.708L7.5 10.793l2.646-2.647a.5.5 0 0 1 .708.708Z"/>
</svg>
</symbol>
</svg>
<header class="side-bar">
<div class="site-header">
<a href="/" class="site-title lh-tight">
Larust
</a>
<button id="menu-button" class="site-button btn-reset" aria-label="Menu" aria-expanded="false">
<svg viewBox="0 0 24 24" class="icon" aria-hidden="true"><use xlink:href="#svg-menu"></use></svg>
</button>
</div>
<nav aria-label="Main" id="site-nav" class="site-nav">
<ul class="nav-list"><li class="nav-list-item"><a href="/" class="nav-list-link">Home</a></li><li class="nav-list-item"><button class="nav-list-expander btn-reset" aria-label="Getting Started submenu" aria-expanded="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#svg-arrow-right"></use></svg>
</button><a href="/getting-started/" class="nav-list-link">Getting Started</a><ul class="nav-list"><li class="nav-list-item"><a href="/getting-started/installation/" class="nav-list-link">Installation</a></li><li class="nav-list-item"><a href="/getting-started/your-first-app/" class="nav-list-link">Your First App</a></li><li class="nav-list-item"><a href="/getting-started/build-a-posts-feature/" class="nav-list-link">Build a Posts Feature</a></li><li class="nav-list-item"><a href="/getting-started/project-structure/" class="nav-list-link">Project Structure</a></li></ul></li><li class="nav-list-item"><a href="/coming-from-laravel/" class="nav-list-link">Coming from Laravel</a></li><li class="nav-list-item"><a href="/coming-from-rust/" class="nav-list-link">Coming from Rust</a></li><li class="nav-list-item"><button class="nav-list-expander btn-reset" aria-label="The Basics submenu" aria-expanded="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#svg-arrow-right"></use></svg>
</button><a href="/the-basics/" class="nav-list-link">The Basics</a><ul class="nav-list"><li class="nav-list-item"><a href="/the-basics/routing/" class="nav-list-link">Routing</a></li><li class="nav-list-item"><a href="/the-basics/controllers-and-requests/" class="nav-list-link">Controllers & Requests</a></li><li class="nav-list-item"><a href="/the-basics/views-and-templates/" class="nav-list-link">Views & Templates</a></li><li class="nav-list-item"><a href="/the-basics/middleware-sessions-and-csrf/" class="nav-list-link">Middleware, Sessions & CSRF</a></li><li class="nav-list-item"><a href="/the-basics/error-handling/" class="nav-list-link">Error Handling</a></li></ul></li><li class="nav-list-item"><button class="nav-list-expander btn-reset" aria-label="Database submenu" aria-expanded="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#svg-arrow-right"></use></svg>
</button><a href="/database/" class="nav-list-link">Database</a><ul class="nav-list"><li class="nav-list-item"><a href="/database/migrations-and-query-builder/" class="nav-list-link">Migrations & the Query Builder</a></li><li class="nav-list-item"><a href="/database/models-and-relationships/" class="nav-list-link">Models & Relationships</a></li></ul></li><li class="nav-list-item"><button class="nav-list-expander btn-reset" aria-label="Digging Deeper submenu" aria-expanded="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#svg-arrow-right"></use></svg>
</button><a href="/digging-deeper/" class="nav-list-link">Digging Deeper</a><ul class="nav-list"><li class="nav-list-item"><a href="/digging-deeper/authentication-and-authorization/" class="nav-list-link">Authentication & Authorization</a></li><li class="nav-list-item"><a href="/digging-deeper/mail-and-notifications/" class="nav-list-link">Mail & Notifications</a></li><li class="nav-list-item"><a href="/digging-deeper/events-queues-and-scheduling/" class="nav-list-link">Events, Queues, Scheduling & Commands</a></li><li class="nav-list-item"><a href="/digging-deeper/file-storage-and-cache/" class="nav-list-link">File Storage & Cache</a></li><li class="nav-list-item"><a href="/digging-deeper/reactive-components/" class="nav-list-link">Reactive Components</a></li><li class="nav-list-item"><a href="/digging-deeper/image-optimization/" class="nav-list-link">Image Optimization</a></li><li class="nav-list-item"><a href="/digging-deeper/realtime-and-broadcasting/" class="nav-list-link">Realtime & Broadcasting</a></li><li class="nav-list-item"><a href="/digging-deeper/plugins/" class="nav-list-link">Plugins</a></li><li class="nav-list-item"><a href="/digging-deeper/localization/" class="nav-list-link">Localization</a></li></ul></li><li class="nav-list-item"><button class="nav-list-expander btn-reset" aria-label="Optional Features submenu" aria-expanded="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#svg-arrow-right"></use></svg>
</button><a href="/optional-features/" class="nav-list-link">Optional Features</a><ul class="nav-list"><li class="nav-list-item"><a href="/optional-features/cashier/" class="nav-list-link">Cashier</a></li><li class="nav-list-item"><a href="/optional-features/sanctum/" class="nav-list-link">Sanctum</a></li><li class="nav-list-item"><a href="/optional-features/permissions/" class="nav-list-link">Permissions</a></li><li class="nav-list-item"><a href="/optional-features/shield/" class="nav-list-link">Shield</a></li><li class="nav-list-item"><a href="/optional-features/passport/" class="nav-list-link">Passport</a></li><li class="nav-list-item"><a href="/optional-features/pennant/" class="nav-list-link">Pennant</a></li><li class="nav-list-item"><a href="/optional-features/scout/" class="nav-list-link">Scout</a></li><li class="nav-list-item"><a href="/optional-features/reverb/" class="nav-list-link">Reverb</a></li><li class="nav-list-item"><a href="/optional-features/socialite/" class="nav-list-link">Socialite</a></li><li class="nav-list-item"><a href="/optional-features/db/" class="nav-list-link">Db</a></li><li class="nav-list-item"><a href="/optional-features/sitemap/" class="nav-list-link">Sitemap</a></li></ul></li><li class="nav-list-item"><a href="/cli-reference/" class="nav-list-link">CLI Reference</a></li><li class="nav-list-item"><a href="/testing/" class="nav-list-link">Testing</a></li><li class="nav-list-item"><a href="/deployment-and-desktop-apps/" class="nav-list-link">Deployment & Desktop Apps</a></li><li class="nav-list-item"><a href="/converting-a-laravel-app/" class="nav-list-link">Converting a Laravel App</a></li><li class="nav-list-item"><a href="/architecture-overview/" class="nav-list-link">Architecture Overview</a></li><li class="nav-list-item"><a href="/faq/" class="nav-list-link">FAQ</a></li></ul>
</nav>
<div class="d-md-block d-none site-footer">
This site uses <a href="https://github.com/just-the-docs/just-the-docs">Just the Docs</a>, a documentation theme for Jekyll.
</div>
</header>
<div class="main" id="top">
<div id="main-header" class="main-header">
<div class="search" role="search">
<div class="search-input-wrap">
<input type="text" id="search-input" class="search-input" tabindex="0" placeholder="Search Larust" autocomplete="off">
<label for="search-input" class="search-label">
<span class="sr-only">Search Larust</span>
<svg viewBox="0 0 24 24" class="search-icon" aria-hidden="true"><use xlink:href="#svg-search"></use></svg>
</label>
</div>
<div id="search-results" class="search-results"></div>
</div>
<nav aria-label="Auxiliary" class="aux-nav">
<ul class="aux-nav-list">
<li class="aux-nav-list-item">
<a href="https://larust.dev" class="site-button"
target="_blank" rel="noopener noreferrer"
>
larust.dev
</a>
</li>
<li class="aux-nav-list-item">
<a href="https://github.com/wallabydesigns/Larust" class="site-button"
target="_blank" rel="noopener noreferrer"
>
Larust on GitHub
</a>
</li>
</ul>
</nav>
</div>
<div class="main-content-wrap">
<nav aria-label="Breadcrumb" class="breadcrumb-nav">
<ol class="breadcrumb-nav-list">
<li class="breadcrumb-nav-list-item"><a href="/digging-deeper/">Digging Deeper</a></li>
<li class="breadcrumb-nav-list-item"><span>Authentication & Authorization</span></li>
</ol>
</nav>
<div id="main-content" class="main-content">
<main>
<h1 class="no_toc" id="authentication--authorization">
<a href="#authentication--authorization" class="anchor-heading" aria-labelledby="authentication--authorization"><svg viewBox="0 0 16 16" aria-hidden="true"><use xlink:href="#svg-link"></use></svg></a> Authentication & Authorization
</h1>
<ol id="markdown-toc">
<li><a href="#authenticatable" id="markdown-toc-authenticatable"><code class="language-plaintext highlighter-rouge">Authenticatable</code></a></li>
<li><a href="#logging-in-and-out" id="markdown-toc-logging-in-and-out">Logging in and out</a></li>
<li><a href="#reading-the-current-user" id="markdown-toc-reading-the-current-user">Reading the current user</a></li>
<li><a href="#route-guards" id="markdown-toc-route-guards">Route guards</a></li>
<li><a href="#multiple-guards" id="markdown-toc-multiple-guards">Multiple guards</a></li>
<li><a href="#authorization-policyu" id="markdown-toc-authorization-policyu">Authorization: <code class="language-plaintext highlighter-rouge">Policy<U></code></a></li>
<li><a href="#next" id="markdown-toc-next">Next</a></li>
</ol>
<h2 id="authenticatable">
<a href="#authenticatable" class="anchor-heading" aria-labelledby="authenticatable"><svg viewBox="0 0 16 16" aria-hidden="true"><use xlink:href="#svg-link"></use></svg></a> <code class="language-plaintext highlighter-rouge">Authenticatable</code>
</h2>
<p>Any type your app wants to log in as implements one small trait:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span> <span class="n">Authenticatable</span> <span class="k">for</span> <span class="n">User</span> <span class="p">{</span>
<span class="k">fn</span> <span class="nf">auth_id</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">)</span> <span class="k">-></span> <span class="nb">i64</span> <span class="p">{</span> <span class="k">self</span><span class="py">.id</span> <span class="p">}</span>
<span class="k">async</span> <span class="k">fn</span> <span class="nf">find_for_auth</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nb">i64</span><span class="p">)</span> <span class="k">-></span> <span class="nb">Result</span><span class="o"><</span><span class="nb">Option</span><span class="o"><</span><span class="k">Self</span><span class="o">></span><span class="p">,</span> <span class="n">AppError</span><span class="o">></span> <span class="p">{</span>
<span class="nn">User</span><span class="p">::</span><span class="nf">find</span><span class="p">(</span><span class="n">id</span><span class="p">)</span><span class="k">.await</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">--auth</code>-scaffolded apps generate this for you already.</p>
<h2 id="logging-in-and-out">
<a href="#logging-in-and-out" class="anchor-heading" aria-labelledby="logging-in-and-out"><svg viewBox="0 0 16 16" aria-hidden="true"><use xlink:href="#svg-link"></use></svg></a> Logging in and out
</h2>
<p>Password hashing (argon2) and session storage are handled for you;
logging in is: verify the password, then store the user’s id in the
session (rotating the session id on success, preventing session
fixation):</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> <span class="nf">login</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">LoginRequest</span><span class="p">)</span> <span class="k">-></span> <span class="nb">Result</span><span class="o"><</span><span class="k">impl</span> <span class="n">IntoResponse</span><span class="p">,</span> <span class="n">AppError</span><span class="o">></span> <span class="p">{</span>
<span class="k">let</span> <span class="n">validated</span> <span class="o">=</span> <span class="n">request</span><span class="nf">.validated</span><span class="p">();</span>
<span class="k">let</span> <span class="n">user</span> <span class="o">=</span> <span class="nn">User</span><span class="p">::</span><span class="nf">query</span><span class="p">()</span><span class="nf">.where_eq</span><span class="p">(</span><span class="s">"email"</span><span class="p">,</span> <span class="o">&</span><span class="n">validated</span><span class="py">.email</span><span class="p">)</span><span class="nf">.first</span><span class="p">()</span><span class="k">.await</span><span class="o">?</span>
<span class="nf">.filter</span><span class="p">(|</span><span class="n">user</span><span class="p">|</span> <span class="nn">larust_support</span><span class="p">::</span><span class="nn">auth</span><span class="p">::</span><span class="nf">verify_password</span><span class="p">(</span><span class="o">&</span><span class="n">user</span><span class="py">.password_hash</span><span class="p">,</span> <span class="o">&</span><span class="n">validated</span><span class="py">.password</span><span class="p">)</span><span class="nf">.unwrap_or</span><span class="p">(</span><span class="k">false</span><span class="p">))</span>
<span class="nf">.ok_or_else</span><span class="p">(||</span> <span class="nn">AppError</span><span class="p">::</span><span class="n">Http</span> <span class="p">{</span> <span class="n">status</span><span class="p">:</span> <span class="nn">StatusCode</span><span class="p">::</span><span class="n">UNPROCESSABLE_ENTITY</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="s">"Invalid credentials."</span><span class="nf">.into</span><span class="p">()</span> <span class="p">})</span><span class="o">?</span><span class="p">;</span>
<span class="c1">// Stores the user's id in the session and rotates the session id</span>
<span class="c1">// (preventing session fixation) - Laravel's `Auth::login($user)`.</span>
<span class="nn">larust_support</span><span class="p">::</span><span class="nn">auth</span><span class="p">::</span><span class="nf">login</span><span class="p">(</span><span class="o">&</span><span class="n">session</span><span class="p">,</span> <span class="o">&</span><span class="n">user</span><span class="p">)</span><span class="k">.await</span><span class="o">?</span><span class="p">;</span>
<span class="nf">Ok</span><span class="p">(</span><span class="nf">redirect</span><span class="p">()</span><span class="nf">.route</span><span class="p">(</span><span class="s">"posts.index"</span><span class="p">)</span><span class="o">?</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p class="note">Password checks always run at the same cost regardless of whether the
email matched a real user - a mismatched-email short-circuit is a real,
if minor, timing side-channel this framework deliberately avoids, even
though the error message looks identical either way.</p>
<h2 id="reading-the-current-user">
<a href="#reading-the-current-user" class="anchor-heading" aria-labelledby="reading-the-current-user"><svg viewBox="0 0 16 16" aria-hidden="true"><use xlink:href="#svg-link"></use></svg></a> Reading the current user
</h2>
<p>Three ways, depending on what you need:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 1. Require login - a real extractor. No session, or a stale/deleted</span>
<span class="c1">// user id, rejects the request before your handler runs.</span>
<span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> <span class="nf">create</span><span class="p">(</span><span class="nf">Auth</span><span class="p">(</span><span class="n">user</span><span class="p">):</span> <span class="n">Auth</span><span class="o"><</span><span class="n">User</span><span class="o">></span><span class="p">)</span> <span class="k">-></span> <span class="nb">Result</span><span class="o"><</span><span class="k">impl</span> <span class="n">IntoResponse</span><span class="p">,</span> <span class="n">AppError</span><span class="o">></span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
<span class="c1">// 2. Optional - the route works either way, you just branch on it.</span>
<span class="k">let</span> <span class="n">viewer</span><span class="p">:</span> <span class="nb">Option</span><span class="o"><</span><span class="n">User</span><span class="o">></span> <span class="o">=</span> <span class="nn">larust_support</span><span class="p">::</span><span class="nn">auth</span><span class="p">::</span><span class="nn">user</span><span class="p">::</span><span class="o"><</span><span class="n">User</span><span class="o">></span><span class="p">(</span><span class="o">&</span><span class="n">session</span><span class="p">)</span><span class="k">.await</span><span class="o">?</span><span class="p">;</span>
<span class="c1">// 3. Just a yes/no, when you don't need the user itself (e.g. a nav bar).</span>
<span class="k">let</span> <span class="n">is_authenticated</span> <span class="o">=</span> <span class="nn">larust_support</span><span class="p">::</span><span class="nn">auth</span><span class="p">::</span><span class="nf">check</span><span class="p">(</span><span class="o">&</span><span class="n">session</span><span class="p">)</span><span class="k">.await</span><span class="o">?</span><span class="p">;</span>
</code></pre></div></div>
<h2 id="route-guards">
<a href="#route-guards" class="anchor-heading" aria-labelledby="route-guards"><svg viewBox="0 0 16 16" aria-hidden="true"><use xlink:href="#svg-link"></use></svg></a> Route guards
</h2>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">route</span><span class="nf">.group</span><span class="p">(</span><span class="s">""</span><span class="p">,</span> <span class="p">|</span><span class="n">r</span><span class="p">:</span> <span class="n">Router</span><span class="p">|</span> <span class="p">{</span>
<span class="n">r</span><span class="nf">.middleware</span><span class="p">(</span><span class="nn">axum</span><span class="p">::</span><span class="nn">middleware</span><span class="p">::</span><span class="nf">from_fn</span><span class="p">(</span><span class="n">require_auth</span><span class="p">))</span>
<span class="nf">.get</span><span class="p">(</span><span class="s">"/posts/create"</span><span class="p">,</span> <span class="nn">PostController</span><span class="p">::</span><span class="n">create</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">require_auth</code> redirects a guest to the login page; <code class="language-plaintext highlighter-rouge">redirect_authenticated</code>
does the inverse (Laravel’s <code class="language-plaintext highlighter-rouge">guest</code> middleware) - bouncing an
already-logged-in user away from <code class="language-plaintext highlighter-rouge">/login</code>/<code class="language-plaintext highlighter-rouge">/register</code>. Both are plain
functions, attached like any other middleware - see
<a href="../../the-basics/routing#groups-and-group-scoped-middleware">Routing</a>.</p>
<h2 id="multiple-guards">
<a href="#multiple-guards" class="anchor-heading" aria-labelledby="multiple-guards"><svg viewBox="0 0 16 16" aria-hidden="true"><use xlink:href="#svg-link"></use></svg></a> Multiple guards
</h2>
<p>Laravel’s <code class="language-plaintext highlighter-rouge">guard('admin')</code> concept: a second <code class="language-plaintext highlighter-rouge">Authenticatable</code> type, logged
in independently of the first, in the same browser session. Name it by
overriding one associated constant - the only thing distinguishing a guard
from the default:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span> <span class="n">Authenticatable</span> <span class="k">for</span> <span class="n">Admin</span> <span class="p">{</span>
<span class="k">const</span> <span class="n">GUARD</span><span class="p">:</span> <span class="o">&</span><span class="k">'static</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"admin"</span><span class="p">;</span>
<span class="k">fn</span> <span class="nf">auth_id</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">)</span> <span class="k">-></span> <span class="nb">i64</span> <span class="p">{</span> <span class="k">self</span><span class="py">.id</span> <span class="p">}</span>
<span class="k">async</span> <span class="k">fn</span> <span class="nf">find_for_auth</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nb">i64</span><span class="p">)</span> <span class="k">-></span> <span class="nb">Result</span><span class="o"><</span><span class="nb">Option</span><span class="o"><</span><span class="k">Self</span><span class="o">></span><span class="p">,</span> <span class="n">AppError</span><span class="o">></span> <span class="p">{</span>
<span class="nn">Admin</span><span class="p">::</span><span class="nf">find</span><span class="p">(</span><span class="n">id</span><span class="p">)</span><span class="k">.await</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Every function above has a <code class="language-plaintext highlighter-rouge">_for::<U></code> counterpart that targets a specific
guard instead of the default (<code class="language-plaintext highlighter-rouge">"web"</code>) one:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">larust_support</span><span class="p">::</span><span class="nn">auth</span><span class="p">::</span><span class="nf">login</span><span class="p">(</span><span class="o">&</span><span class="n">session</span><span class="p">,</span> <span class="o">&</span><span class="n">admin</span><span class="p">)</span><span class="k">.await</span><span class="o">?</span><span class="p">;</span> <span class="c1">// stores it under "admin"'s own slot</span>
<span class="nn">larust_support</span><span class="p">::</span><span class="nn">auth</span><span class="p">::</span><span class="nn">check_for</span><span class="p">::</span><span class="o"><</span><span class="n">Admin</span><span class="o">></span><span class="p">(</span><span class="o">&</span><span class="n">session</span><span class="p">)</span><span class="k">.await</span><span class="o">?</span><span class="p">;</span> <span class="c1">// is *this* guard logged in?</span>
<span class="nn">larust_support</span><span class="p">::</span><span class="nn">auth</span><span class="p">::</span><span class="nn">logout_for</span><span class="p">::</span><span class="o"><</span><span class="n">Admin</span><span class="o">></span><span class="p">(</span><span class="o">&</span><span class="n">session</span><span class="p">)</span><span class="k">.await</span><span class="o">?</span><span class="p">;</span> <span class="c1">// clears only "admin"'s slot</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">user::<U></code> and the <code class="language-plaintext highlighter-rouge">Auth<U></code> extractor already read through <code class="language-plaintext highlighter-rouge">U::GUARD</code>
automatically - <code class="language-plaintext highlighter-rouge">Auth<Admin></code> and <code class="language-plaintext highlighter-rouge">Auth<User></code> resolve independent login
state on the same request without any extra ceremony:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> <span class="nf">dashboard</span><span class="p">(</span><span class="nf">Auth</span><span class="p">(</span><span class="n">admin</span><span class="p">):</span> <span class="n">Auth</span><span class="o"><</span><span class="n">Admin</span><span class="o">></span><span class="p">)</span> <span class="k">-></span> <span class="k">impl</span> <span class="n">IntoResponse</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
<span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> <span class="nf">profile</span><span class="p">(</span><span class="nf">Auth</span><span class="p">(</span><span class="n">user</span><span class="p">):</span> <span class="n">Auth</span><span class="o"><</span><span class="n">User</span><span class="o">></span><span class="p">)</span> <span class="k">-></span> <span class="k">impl</span> <span class="n">IntoResponse</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
</code></pre></div></div>
<p>Route guards follow the same pattern - <code class="language-plaintext highlighter-rouge">require_auth_for::<Admin></code> and
<code class="language-plaintext highlighter-rouge">redirect_authenticated_for::<Admin></code> sit next to the default-guard
<code class="language-plaintext highlighter-rouge">require_auth</code>/<code class="language-plaintext highlighter-rouge">redirect_authenticated</code>, so an admin section can require
its own login independently of the regular user guard:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">route</span><span class="nf">.group</span><span class="p">(</span><span class="s">"/admin"</span><span class="p">,</span> <span class="p">|</span><span class="n">r</span><span class="p">:</span> <span class="n">Router</span><span class="p">|</span> <span class="p">{</span>
<span class="n">r</span><span class="nf">.middleware</span><span class="p">(</span><span class="nn">axum</span><span class="p">::</span><span class="nn">middleware</span><span class="p">::</span><span class="nf">from_fn</span><span class="p">(</span><span class="nn">require_auth_for</span><span class="p">::</span><span class="o"><</span><span class="n">Admin</span><span class="o">></span><span class="p">))</span>
<span class="nf">.get</span><span class="p">(</span><span class="s">"/dashboard"</span><span class="p">,</span> <span class="nn">AdminController</span><span class="p">::</span><span class="n">dashboard</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">require_auth_for::<Admin></code> redirects a guest to a route named
<code class="language-plaintext highlighter-rouge">"admin.login"</code> (<code class="language-plaintext highlighter-rouge">"{guard}.login"</code>, not the plain <code class="language-plaintext highlighter-rouge">"login"</code>
<code class="language-plaintext highlighter-rouge">require_auth</code> uses) - name your admin login route accordingly:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">route</span><span class="nf">.post</span><span class="p">(</span><span class="s">"/admin/login"</span><span class="p">,</span> <span class="nn">AdminAuthController</span><span class="p">::</span><span class="n">login</span><span class="p">)</span><span class="nf">.name</span><span class="p">(</span><span class="s">"admin.login"</span><span class="p">)</span>
</code></pre></div></div>
<p>A single-guard app never has to think about any of this - a plain <code class="language-plaintext highlighter-rouge">User</code>
that never overrides <code class="language-plaintext highlighter-rouge">GUARD</code> keeps using the exact <code class="language-plaintext highlighter-rouge">"_auth_user_id"</code>
session key it always has, and every non-<code class="language-plaintext highlighter-rouge">_for</code> function above keeps
working unchanged. Only the second <code class="language-plaintext highlighter-rouge">Authenticatable</code> type needs a
<code class="language-plaintext highlighter-rouge">GUARD</code> override; forgetting one collides with the default guard’s slot
rather than failing at compile time, so name every guard beyond the first.</p>
<h2 id="authorization-policyu">
<a href="#authorization-policyu" class="anchor-heading" aria-labelledby="authorization-policyu"><svg viewBox="0 0 16 16" aria-hidden="true"><use xlink:href="#svg-link"></use></svg></a> Authorization: <code class="language-plaintext highlighter-rouge">Policy<U></code>
</h2>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span> <span class="n">Policy</span><span class="o"><</span><span class="n">User</span><span class="o">></span> <span class="k">for</span> <span class="n">Post</span> <span class="p">{</span>
<span class="k">fn</span> <span class="nf">view_any</span><span class="p">(</span><span class="n">_user</span><span class="p">:</span> <span class="o">&</span><span class="n">User</span><span class="p">)</span> <span class="k">-></span> <span class="nb">bool</span> <span class="p">{</span> <span class="k">true</span> <span class="p">}</span>
<span class="k">fn</span> <span class="nf">view</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">,</span> <span class="n">_user</span><span class="p">:</span> <span class="o">&</span><span class="n">User</span><span class="p">)</span> <span class="k">-></span> <span class="nb">bool</span> <span class="p">{</span> <span class="k">true</span> <span class="p">}</span>
<span class="k">fn</span> <span class="nf">create</span><span class="p">(</span><span class="n">_user</span><span class="p">:</span> <span class="o">&</span><span class="n">User</span><span class="p">)</span> <span class="k">-></span> <span class="nb">bool</span> <span class="p">{</span> <span class="k">true</span> <span class="p">}</span>
<span class="k">fn</span> <span class="nf">update</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">,</span> <span class="n">user</span><span class="p">:</span> <span class="o">&</span><span class="n">User</span><span class="p">)</span> <span class="k">-></span> <span class="nb">bool</span> <span class="p">{</span> <span class="k">self</span><span class="py">.user_id</span> <span class="o">==</span> <span class="n">user</span><span class="py">.id</span> <span class="p">}</span>
<span class="k">fn</span> <span class="nf">delete</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">,</span> <span class="n">user</span><span class="p">:</span> <span class="o">&</span><span class="n">User</span><span class="p">)</span> <span class="k">-></span> <span class="nb">bool</span> <span class="p">{</span> <span class="k">self</span><span class="py">.user_id</span> <span class="o">==</span> <span class="n">user</span><span class="py">.id</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Five methods, same names and meaning as Laravel’s own policy abilities.
Every one has a matching <code class="language-plaintext highlighter-rouge">authorize_*</code> default method that converts
straight to a 403:</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">Post</span><span class="p">::</span><span class="nf">authorize_create</span><span class="p">(</span><span class="o">&</span><span class="n">user</span><span class="p">)</span><span class="o">?</span><span class="p">;</span> <span class="c1">// fn create, static</span>
<span class="nn">larust_support</span><span class="p">::</span><span class="nn">auth</span><span class="p">::</span><span class="nf">authorize</span><span class="p">(</span><span class="n">post</span><span class="nf">.can_manage</span><span class="p">(</span><span class="o">&</span><span class="n">user</span><span class="p">)</span><span class="k">.await</span><span class="o">?</span><span class="p">)</span><span class="o">?</span><span class="p">;</span> <span class="c1">// ad hoc bool, same helper underneath</span>
<span class="n">post</span><span class="nf">.authorize_update</span><span class="p">(</span><span class="o">&</span><span class="n">user</span><span class="p">)</span><span class="o">?</span><span class="p">;</span> <span class="c1">// fn update, instance</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">xr make:policy Post</code> generates the trait impl skeleton for you
(<code class="language-plaintext highlighter-rouge">--user <Type></code> if your <code class="language-plaintext highlighter-rouge">Authenticatable</code> isn’t named <code class="language-plaintext highlighter-rouge">User</code>). Policy
methods are deliberately synchronous - if a real check needs an <code class="language-plaintext highlighter-rouge">await</code>
(a database lookup for a moderator role, say, as the reference app’s own
<code class="language-plaintext highlighter-rouge">Post::can_manage</code> does), write that as an ordinary <code class="language-plaintext highlighter-rouge">async fn</code> on the
model itself and call it from the controller instead of trying to force
it through <code class="language-plaintext highlighter-rouge">Policy</code>’s own methods.</p>
<h2 id="next">
<a href="#next" class="anchor-heading" aria-labelledby="next"><svg viewBox="0 0 16 16" aria-hidden="true"><use xlink:href="#svg-link"></use></svg></a> Next
</h2>
<p><a href="../../optional-features/sanctum">Sanctum</a>, <a href="../../optional-features/permissions">Permissions</a>,
and <a href="../../optional-features/shield">Shield</a> cover the optional,
opt-in parts of auth - API tokens, roles/permissions, and resource-scoped
permission bundles. <a href="../../digging-deeper/mail-and-notifications">Mail & Notifications</a>
covers reaching users outside the request/response cycle.</p>
</main>
<hr>
<footer>
<p><a href="#top" id="back-to-top">Back to top</a></p>
<p class="text-small mb-0">Larust is source-available on <a href="https://github.com/wallabydesigns/Larust">GitHub</a>. Not affiliated with Laravel or Laravel Holdings Inc.</p>
<div class="d-flex mt-2">
<p class="text-small mb-0">
<a href="https://github.com/wallabydesigns/Larust/edit/main/docs/digging-deeper/authentication-and-authorization.md" id="edit-this-page">Edit this page on GitHub</a>
</p>
</div>
<div class="d-md-none mt-4 fs-2">
This site uses <a href="https://github.com/just-the-docs/just-the-docs">Just the Docs</a>, a documentation theme for Jekyll.
</div>
</footer>
</div>
</div>
<div class="search-overlay"></div>
</div>
</body>
</html>
</div>
</article>
@endsection
Blade directive names are intentionally familiar. Expressions inside
`` are Rust expressions, and .blade.xr templates are parsed at
build time instead of interpreted per request. Views & Templates
covers layouts, escaping, components, and reactive templates.
6. Verify the request boundary
Run the focused tests while developing the feature:
cargo test --test store_post_test
cargo test --workspace
A useful feature test exercises the behavior a user sees: create the required
user state, submit a form to /posts, assert the redirect, then assert that
the persisted post contains the submitted data. This protects the route,
authentication, validation, controller, and database contract together.
The Testing guide shows the project’s TestClient helpers
and database-isolation patterns. Test the unhappy path too: an anonymous write
should redirect or reject, and an invalid title should return a 422 without
creating a row.
Reuse this sequence
Every conventional Larust feature can begin with the same checklist:
- Add the route and its access boundary.
- Declare validated input.
- Change the model and migration together.
- Keep the action explicit: extract, authorize, persist, respond.
- Render only the data the template needs.
- Test the public request contract.
For a richer implementation of this feature, explore the repository’s
demo/ app. It
extends the same foundation with policies, tags, comments, events,
notifications, and reactive components.