Enabling Team Multi-Tenancy in Laravel Jetstream with Livewire
Enable Jetstream's Teams feature for multi-tenant apps: run migrations, update the User model, configure roles, and prevent cross-team data leakage.
16 Jan 2026, 08:40 UTC

Desired Outcome
Multi-tenant Laravel applications need to isolate data by organization, but turning on Jetstream’s Teams feature after an initial installation introduces migration, model, and session-scoping steps that can silently expose records across tenants if missed. This guide shows how to enable Teams on the Livewire stack, configure basic member roles, and verify that the active team context stays isolated.
By the end of this procedure, authenticated users should be able to create or belong to multiple teams, switch their active team from the dashboard, invite collaborators by email with an assigned role, and see only the records that belong to their currently selected team. Jetstream stores these relationships in a pivot table—an intermediate database table that connects two models, in this case users and teams—and tracks the selected team in the session.
Prerequisites
- A Laravel application with Jetstream already installed using the Livewire stack.
- A configured database connection and Artisan CLI access.
- The default
App\Models\Usermodel available for editing. - Basic familiarity with Eloquent, Laravel’s ORM, and service providers.
Enable the Teams Feature
Jetstream gates optional features behind a features array. Open config/jetstream.php and ensure the Teams feature is listed:
'features' => [
Features::profilePhotos(),
Features::api(),
Features::teams(),
],
If your installation predates the Teams feature, the corresponding migration files may not have been published. Publish them explicitly:
php artisan vendor:publish --tag=jetstream-migrations
Run the migrations to create the teams, team_user, and team_invitations tables:
php artisan migrate
Risk: Running migrate alters database state. On production, take a backup first or run inside a maintenance window.
Update the User Model
The User model must opt in to team behavior. Open app/Models/User.php and add the HasTeams trait:
namespace App\Models;
use Laravel\Jetstream\HasTeams;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasTeams;
// ...
}
A trait in PHP is a mechanism for reusing methods across classes. HasTeams adds the relationship methods and the currentTeam accessor that Jetstream’s UI relies on.
Configure Team Roles
Jetstream’s default team model uses a simple string-based role system stored in the team_user pivot table. Register the roles your application needs in app/Providers/JetstreamServiceProvider.php:
use Laravel\Jetstream\Jetstream;
public function boot()
{
Jetstream::role('admin')->description('Administrator');
Jetstream::role('editor')->description('Editor');
Jetstream::role('member')->description('Member');
}
These labels populate the role dropdown in the team member management UI. The chosen value is written to the role column when a user joins or is invited to a team.
Verify the Invitation Workflow
From the team dashboard, invite a secondary email address. Jetstream creates a TeamInvitation record and sends a signed acceptance link.
To confirm the membership record is created upon acceptance, open Tinker:
php artisan tinker
Then inspect the pivot table:
DB::table('team_user')->where('role', 'member')->exists();
Replace 'member' with the role you assigned during the invitation. If the invitation was accepted correctly, this returns true.
Prevent Cross-Team Data Leakage
Jetstream stores the active team ID in the session under current_team_id. If your application models do not scope queries to that value, a user who switches teams may still see records from a previous context.
Any model that belongs to a team should filter by the current team. For example, in a Project model:
protected static function booted()
{
static::addGlobalScope('team', function ($query) {
$query->where('team_id', auth()->user()?->current_team_id);
});
}
A global scope automatically constrains every query for that model. Without it—or an equivalent manual where clause—team switching becomes cosmetic rather than functional.
Expected Checks and Recovery Options
| Check | How to Verify |
|---|---|
| Tables exist | In Tinker, run Schema::hasTable('teams') and Schema::hasTable('team_user'); both should return true. |
| Feature active | In Tinker, run Jetstream::hasFeature('teams'); it should return true. |
| Active team set | In Tinker, run auth()->user()->currentTeam; it should return a Team instance, not null. |
| Role persisted | Query team_user and confirm the role column matches the invited role. |
Recovery Options
If you need to disable Teams after enabling it:
- Remove
Features::teams()fromconfig/jetstream.php. - Remove the
HasTeamstrait from theUsermodel. - Roll back the team migrations if the tables are no longer needed:
php artisan migrate:rollback - Clear cached configuration:
php artisan config:clear
Note: Rolling back drops the teams, team_user, and team_invitations tables and their data. Only do this in development or after exporting required records.
Limitations
The built-in team system is intentionally minimal. Roles are simple strings; they do not include granular permissions such as “can delete project” or hierarchical structures like owner > admin > member. If your application requires action-level access control, integrate a dedicated permissions package rather than extending Jetstream’s role strings.
Also, the current_team_id session value is only as safe as the queries that read it. Jetstream does not automatically scope arbitrary models to the active team—you must add that logic yourself.
Final Verification
After completing the steps, log in as a user who belongs to at least two teams. Switch teams in the dashboard and confirm that team-specific records change accordingly. If they do not, revisit the global scope or manual where clauses on your domain models.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.