Designing Minimal Team Management with Laravel Jetstream
Use Laravel Jetstream’s built‑in team tables, models, and Gate policies to add team creation and invitations without writing custom migrations or controllers.
04 Jul 2026, 08:23 UTC

Problem: Adding multi‑tenant team support without extra code
When building a SaaS prototype you often need a way for users to create teams, invite teammates, and scope resources to a team. Writing custom migrations, controllers, and policies adds maintenance overhead. Laravel Jetstream already ships a team feature, but it is only available in the "stack" (Livewire or Inertia) installation, not the minimal API‑only setup.
Takeaway
Leverage Jetstream’s built‑in Team model, pivot table, and Gate policies. With only the full‑stack Jetstream installed and the default mail driver configured, you get a functional team workflow—creation, invitation, removal—without writing any migrations, controllers, or authorization logic.
Requirements
- Laravel 10.x (Jetstream 4.x compatible)
- Jetstream installed with the
livewireorinertiastack (theapistack omits team views and routes) - A configured mail driver (SMTP, Mailgun, Sendgrid, etc.) that can send outgoing mail
- Database connection that supports migrations (MySQL, PostgreSQL, SQLite)
Smallest Suitable Design
The design consists of three layers that Jetstream already provides:
- Data model –
teamstable andteam_userpivot table, plus theApp\Models\TeamEloquent model and theteam()relationship onUser. - Application logic – Jetstream’s
TeamController(Livewire componentjetstream::teams.indexor Inertia page) handles creation, listing, and deletion. Invitations are managed by theTeamInvitationmodel and theInviteTeamMemberaction. - Trust boundary – Laravel Gate policies defined in
App\Providers\JetstreamServiceProviderrestrict invitation and removal to users whose pivot record has theownerrole.
No extra migrations, controllers, or policy files are required; you only need to ensure the existing ones are executed and the mail environment is set.
Trust / Data Boundaries
The team feature enforces two boundaries:
- Database boundary – A user can only see teams where a row exists in
team_userwith theiruser_id. Theteam()relationship scopes queries automatically. - Authorization boundary – The Gate ability
updateon theTeammodel checks$user->teamUsers->where('team_id', $team->id)->first()->role === 'owner'. If the check fails, Jetstream returns a 403 response.
These boundaries are enforced at the model and controller level; bypassing them would require directly manipulating the database or disabling Gate checks, both of which are operational red flags.
Operational Checks
After deploying or updating the application, verify the following:
- Migrations – Run
php artisan migrateand confirm the presence ofteamsandteam_usertables:
# Run in the project root, requires SSH or local terminal with php & composer installed
php artisan migrate
# Then inspect schema (example for MySQL)
mysql -u root -p -e "SHOW TABLES LIKE 'teams'; SHOW TABLES LIKE 'team_user';"
- Mail configuration – Ensure
MAIL_MAILER,MAIL_HOST,MAIL_PORT,MAIL_USERNAME,MAIL_PASSWORD(or equivalent) are set in.env. Test with:
php artisan tinker
>>> Mail::raw('test', function ($message) {
$message->to('you@example.com')->subject('Jetstream mail test');
});
If the command throws an exception, the mail driver is misconfigured and invitations will fail silently.
- Route accessibility – Authenticate as any user and visit
/teams(Livewire) or/teams(Inertia). You should see the team listing page without a 404.
Failure Modes
- Migration failure – If the
teamsorteam_usertables are missing, Jetstream’s controllers will throw a "Base table or view not found" error. The feature is effectively disabled until migrations succeed. - Email service outage – Invitations rely on the configured mail driver. If the driver cannot connect (e.g., SMTP auth failure), the invitation is stored but the email is never sent. Users see a success flash but never receive the link, leading to confusion. Monitor mail logs (
mail.logor external service dashboard) for delivery errors. - Permission misconfiguration – Accidentally removing the
ownerrole from the pivot record (e.g., via a custom script) allows non‑owners to pass the Gate check and invite members. Regularly audit theteam_user.rolecolumn; it should only contain 'owner' or 'member'.
Conditions That Would Change the Design
- If you need custom team attributes (e.g., a
subscription_plancolumn), you would add a migration to theteamstable and extend theTeammodel with an accessor or mutator. - If you want to replace Jetstream’s invitation flow with a custom notification (e.g., Slack), you would override the
InviteTeamMemberaction inApp\Actions\Jetstream\InviteTeamMemberand keep the existing Gate policies. - If you decide to move to a micro‑service architecture where team data lives in a separate service, you would replace Jetstream’s Eloquent models with API clients and re‑implement the Gate checks as middleware.
Practical Verification Steps
- Install Jetstream with stack:
composer require laravel/jetstreamfollowed byphp artisan jetstream:install livewire(or inertia). - Run migrations:
php artisan migrate. - Register two users via the UI.
- Log in as the first user, create a team, and verify it appears in the list.
- Use the invitation form to invite the second user’s email.
- Check the email inbox (or mail trap) for the invitation link; follow it to confirm the second user can accept and appears in the team’s member list.
- Log in as the second user (non‑owner) and attempt to invite a third user; expect a 403 response or UI disabled state.
Limitations
Jetstream’s team feature assumes a single‑owner model per team. If your SaaS requires multiple owners or role‑based permissions beyond owner/member, you will need to extend the pivot table and adjust the Gate policies. The feature also depends on Jetstream’s provided views; a headless API‑only project would need to build its own endpoints or adopt Jetstream’s API routes (which are not included by default).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.