laravel/jetstream
Laravel Jetstream is a starter kit for Laravel 11.x and prior that scaffolds authentication, registration, profile management, API tokens, teams, and session management using Livewire or Inertia with Tailwind CSS.
Check the compatibility window first. The README states Jetstream is the starter kit for Laravel 11.x and prior; for Laravel 12+ the recommendation is the new kits at laravel.com/starter-kits. Jetstream is effectively in maintenance — it's the right call for an existing Laravel ≤11 app that already uses it, or for a team that specifically wants the Livewire/Inertia + teams + Sanctum-tokens bundle. It is not a sensible greenfield choice in 2026.
composer require laravel/jetstream
php artisan jetstream:install livewire # or: inertia
npm install && npm run build
php artisan migrate
The installer accepts feature flags (--teams, --api, --dark, --verification, --ssr, --pest, --composer=..., --force); the stack argument is validated, so a typo fails fast rather than half-scaffolding. After install, verify config/jetstream.php rather than trusting your mental model of what was generated.
Where to look first, in this order:
config/jetstream.php — which features are on: Features::termsAndPrivacyPolicy(), profilePhotos(), api(), teams(['invitations' => true]), accountDeletion(), plus profile_photo_disk.config/fortify.php — registration, email verification, 2FA, password reset, and the features list that gates the auth routes. Jetstream delegates all authentication to Fortify; half of "Jetstream isn't doing X" is actually a Fortify config question.app/Providers/JetstreamServiceProvider.php — roles, permissions, API token defaults, model overrides.app/Providers/FortifyServiceProvider.php — views, login rate limiter, action bindings.app/Actions/Fortify/* and app/Actions/Jetstream/* — the mutation logic you'll actually be editing.First useful task: log in, open /user/profile, and trace one request end-to-end — e.g. change your name and watch UpdateUserProfileInformation fire, then confirm the HasProfilePhoto trait drives the avatar. That single loop teaches you the Jetstream/Fortify/Actions split faster than reading the docs.
php artisan route:list --path=user
php artisan route:list --path=teams
Customize by swapping actions, not by editing vendor code. Every Jetstream/Fortify action is resolved from the container, so override with a binding:
// app/Providers/JetstreamServiceProvider.php
public function boot(): void
{
$this->app->singleton(
\App\Actions\Fortify\CreateNewUser::class,
\App\Actions\Fortify\CreateNewUserWithConsent::class
);
}
This is the standard hook for "add a field to registration", "assign a default role", "notify Slack on team creation".
Roles and permissions are declared, not discovered. In JetstreamServiceProvider::boot():
Jetstream::defaultApiTokenPermissions(['read']);
Jetstream::permissions(['read', 'create', 'update', 'delete']);
Jetstream::role('admin', 'Administrator', ['read', 'create', 'update', 'delete'])
->description('Full access to the team.');
Jetstream::role('editor', 'Editor', ['read', 'create', 'update'])
->description('Can write content.');
Then check with $user->hasTeamPermission($team, 'update') and $user->tokenCan('update') for API tokens. Use Jetstream::formatPermissionsUsing(fn ($p) => strtolower($p)) if you have any risk of casing drift between role definitions and tokenCan checks — it's a cheap guard against a confusing class of bug.
Teams: gate the whole app on Jetstream::hasTeamFeatures(). Don't sprinkle if (config(...)) around; use the facade:
if (Jetstream::hasTeamFeatures()) {
// $user->currentTeam, ->personalTeam, ->allTeams(),
// ->switchTeam($team), ->teamRole($team), ->ownsTeam($team)
}
The Laravel\Jetstream\HasTeams trait supplies all of that. current_team_id is what most of your authorization should key off, and team-scoped routes should sit behind auth:sanctum + your own team-membership middleware, not just auth.
API tokens: Sanctum-backed, only available when Features::api() is enabled. Issue and check with $user->createToken('name', ['read']) and $request->user()->tokenCan('read'). Protect API routes with auth:sanctum — a plain auth guard will silently accept session auth and confuse your token tests.
Profile photos: HasProfilePhoto gives you profilePhotoUrl, updateProfilePhoto, deleteProfilePhoto. Point profile_photo_disk at a public disk and make sure php artisan storage:link ran (the installer does it, but container rebuilds lose the symlink). For private disks you'll need to override profilePhotoUrl to return a temporary URL.
Stack-specific extension points:
| Livewire stack | Inertia stack | |
|---|---|---|
| Layout | resources/views/layouts/app.blade.php |
resources/js/Layouts/AppLayout.vue |
| Screens | class-based Livewire components + resources/views/livewire |
resources/js/Pages/** |
| Nav | Blade x-nav-link |
JetstreamNavLink / slot props |
| Shared data | Blade props | ShareInertiaData → $page.props |
For Inertia, remember shared props are camel-cased and namespaced: $page.props.auth.user, $page.props.jetstream.hasTeamFeatures, $page.props.jetstream.canManageApiTokens.
Testing: Jetstream scaffolds Pest 2 (v3.1.0+) or PHPUnit feature tests for registration, password reset, 2FA, teams, and API tokens. Keep them — they're your regression net when you swap actions. Feature-conditional tests are skipped when the feature is off, so php artisan test stays green in trimmed-down installs. In your own tests, RefreshDatabase + $this->actingAs($user) is the pattern; for team-scoped assertions set $user->current_team_id explicitly rather than relying on login-time defaults.
loadViewsFrom was removed in 3.0.1, so resources/views/vendor/jetstream overrides no longer work — the files are simply yours to edit. Consequence: package upgrades won't bring view fixes. Re-running the installer with --force will overwrite your edits; diff first.<x-jet-input> is dead. The "Jet" component prefix was removed in 2.12.0 — it's <x-input>, <x-button>, <x-dialog-modal>. Every tutorial written before 2022 uses the old names.$page.user → $page.auth.user in Inertia as of 3.0.0. Combined with the userPage → usePage typo fix in 3.1.0, most copy-pasted Inertia snippets from blogs are broken on a current install.teams, team_user, and team_invitations tables plus current_team_id on users. Publish and re-run migrations (--tag=jetstream-migrations) rather than assuming Features::teams() is enough; a missing column surfaces as a null currentTeam and authorization that passes when it shouldn't.Jetstream::ignoreMigrations() must be called in register(), before the package's migration paths are loaded. Put it in boot() and it silently does nothing.implements MustVerifyEmail on App\Models\User and enable the Fortify feature. Only one of the two, and the verified middleware either 403s everyone or lets everyone through.password.confirm is in the middleware group, 2FA setup appears broken until you've confirmed within password_timeout. The refresh-on-two-factor-challenge bug was fixed in 3.1.1 — pin at least that if you see the challenge page looping.route() inside setup() is a known historical foot-gun (fixed 2.8.5); prefer the usePage().props route helpers or route() inside render, and make sure @routes is present in your root Blade.php artisan config:clear (or config:cache in prod). A stale cached config is the #1 cause of "the feature is enabled but the UI is missing".darkMode: 'class' in tailwind.config.js plus the dark: variants. Upgrading from 2.x gives you the toggle without the styles — plan a Tailwind pass, not just a composer bump.$user->delete(). The DeleteUser action handles team cleanup and personal access tokens; if you own users with teams, decide the semantics explicitly. Also note the logout redirect after deletion follows the Fortify-configured redirect (2.13.0).@livewire('profile.update-profile-information-form') breaks silently-ish if you relocate a view. Keep the namespace mapping in mind before renaming files.How can I help you explore Laravel packages today?