laravel/breeze
Laravel Breeze is a minimal starter kit that scaffolds authentication and a clean UI with Blade, Vue, or React, including login, registration, password reset, email verification, and profile pages to speed up new Laravel apps. Supports Laravel 11.x and prior.
Breeze is a scaffolder, not a runtime package. You install it, run one Artisan command, it copies controllers, routes, views/components, tests and config into your app, and then it is essentially inert. That mental model drives everything below.
Minimal path for a fresh Laravel 11/12 app:
composer require laravel/breeze --dev
php artisan breeze:install blade --dark --pest
php artisan migrate
npm install && npm run dev
Pick the stack you actually ship: blade, livewire, livewire-functional (Volt), react, vue, or api. Flags worth knowing: --typescript, --ssr (Inertia SSR), --dark, --pest, --eslint/--prettier, and a package manager flag (--npm, --pnpm, --bun, --deno).
Look at these first, in this order:
routes/auth.php — the entire auth surface (login, register, password reset, email verification, password confirmation). routes/web.php just requires it.app/Http/Controllers/Auth/* — one small controller per concern; no inheritance, no magic.app/Http/Requests/Auth/LoginRequest.php — validation, authenticate(), and the throttleKey() rate limiter.resources/views/auth/* (Blade/Livewire) or resources/js/Pages/Auth/* (Inertia) — the UI you will actually restyle.tests/Feature/Auth/* — Breeze ships working Pest/PHPUnit feature tests; this is your fastest way to understand intended behavior.First real use case: get login working, then immediately change the post-login destination. In AuthenticatedSessionController::store():
return redirect()->intended(route('dashboard', absolute: false));
Change that one line and you've already validated the whole install.
Treat generated files as your code. All customization is editing generated files, not configuring a package. That means normal Laravel patterns apply — FormRequests, policies, middleware, events.
Disabling registration is route removal, not a config flag:
// routes/auth.php — comment out the two registration routes
// Route::get('register', [RegisteredUserController::class, 'create'])->name('register');
// Route::post('register', [RegisteredUserController::class, 'store']);
Then remove the "Register" links from resources/views/auth/login.blade.php and the Inertia login page, otherwise you ship dead links.
Extending registration with extra columns is three coordinated edits — migration + User::$fillable, RegisteredUserController::store() validation/creation, and the form. Same shape for the profile: ProfileController::update() plus ProfileUpdateRequest.
Protecting routes uses stock middleware aliases Breeze registers:
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', fn () => view('dashboard'))->name('dashboard');
});
verified only does anything once App\Models\User implements MustVerifyEmail — check this explicitly, it's a frequent "verification emails never send" cause.
Authorization is deliberately absent. There are no roles, teams, or API tokens (that's Jetstream/Filament territory). Add is_admin/roles yourself and gate in AuthServiceProvider/policies. Breeze leaves you a clean, unopinionated base precisely so this stays simple.
API stack is a different animal: no frontend, Sanctum installed, routes/api.php plus a POST /api/login-style token endpoint. For a first-party SPA, use cookie-based Sanctum instead of tokens and register the frontend origin in SANCTUM_STATEFUL_DOMAINS (Breeze already seeds 127.0.0.1:3000).
Inertia stacks: shared auth state arrives via HandleInertiaRequests::share() under auth.user. Consume it with usePage().props.auth.user and keep resources/js/types/index.d.ts in sync with the users table whenever you add columns — the shipped User type is a snapshot, and stale types are the #1 TypeScript friction point.
Testing pattern: copy Breeze's feature tests as the template for anything auth-adjacent.
$this->actingAs($user)->get('/dashboard')->assertOk();
$this->post('/login', ['email' => $user->email, 'password' => 'password'])
->assertRedirect(route('dashboard', absolute: false));
Reusing across projects: if you scaffold more than a couple of apps a year, commit the post-customization state into an internal skeleton repo rather than re-running breeze:install each time. The installer's value drops to near-zero after the first run, and re-running it is destructive.
breeze:install writes stubs over existing paths without meaningful merging. Always scaffold on a clean branch with a clean working tree, commit immediately after, and diff before you customize. Never run it casually in a live project.--dev matters. Breeze has no runtime footprint; keep it in require-dev and you can legitimately composer remove laravel/breeze once scaffolding is done without touching app behavior.laravel.com/starter-kits first — Breeze is now the "legacy-compatible" option, not the default recommendation.bootstrap.js import for Laravel 13+. If you upgrade Laravel after scaffolding, check resources/js/app.js for a stale import that silently breaks the build.LoginRequest::throttleKey() uses the cache. If you see unexpected 429s, clear the limiter (RateLimiter::clear($key)) or use the array cache driver in phpunit.xml. Also note the 5-attempt lockout is per email+IP, so parallel test runs can interfere.config('auth.password_timeout') (default 10800s) governs password.confirm. Tests that expect re-confirmation will pass immediately if a prior test confirmed the password in the same session.APP_URL — a wrong APP_URL produces invalid-signature errors that look like code bugs.@import "tailwindcss" in app.css, no tailwind.config.js); older ones use a JS config file. Confirm which you got before copying tutorials. Dark mode requires installing with --dark and the dark class strategy being wired up.--pnpm/--bun/--deno affects lockfiles and the installer's flushNodeModules() behavior; mixing a stale package-lock.json with a bun.lock is a reliable way to get "works on my machine" builds.--ssr adds a separate SSR entry and import.meta.env.SSR guards. Several changelog entries were dedicated to hydrateRoot and Ziggy prop typing — if SSR routes 500 on build, check those first.#[Validate] attributes on the component; wire:model vs wire:model.live changes when validation fires. Password fields need explicit clearing on success (multiple fixes landed for this across stacks) — verify it yourself.changelog/ before starting each new project and port anything relevant manually.How can I help you explore Laravel packages today?