Implementing Granular Team Permissions in Laravel Jetstream
Learn how to move beyond basic roles in Laravel Jetstream by mapping pivot table roles to Laravel Gates for granular, maintainable team permissions.
04 Jul 2025, 19:16 UTC

The Problem: Default Roles Aren't Enough
Laravel Jetstream's team feature provides a robust foundation for multi-tenancy, but it ships with a basic role implementation. By default, Jetstream gives you a roles column on the team_user pivot table, but it doesn't define what those roles actually do. Many developers find themselves stuck between the simplicity of Jetstream's defaults and the complexity of a full-blown RBAC (Role-Based Access Control) package.
The takeaway is this: You don't need an external package for mid-level complexity. By leveraging Laravel Gates and the existing pivot table, you can map Jetstream roles to specific application permissions without adding overhead to your vendor folder.
Mapping Roles to Permissions
Jetstream manages the relationship between users and teams via a many-to-many pivot. To make this functional, you must define a mapping—usually in a configuration file or a service provider—that translates a role name (like 'administrator' or 'editor') into a set of capabilities.
Instead of checking if ($user->hasRole('admin')) throughout your controllers, which creates rigid code, you should check for a specific permission. This allows you to change which roles have that permission later without touching your business logic.
Worked Example: Permission-Based Gates
To implement this, first define your roles in app/Providers/JetstreamServiceProvider.php. This example assumes you have installed Jetstream with the --teams flag.
// app/Providers/AuthServiceProvider.php
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot()
{
Gate::define('update-project', function (User $user) {
$team = $user->currentTeam;
// Retrieve the user's role within the current team from the pivot table
$role = $user->teams()->where('team_id', $team->id)
->first()->pivot->role;
return in_array($role, ['admin', 'editor']);
});
}
Applying the Gate in a Controller
Run this logic in your controllers to protect specific actions. Ensure you are using the auth:sanctum or web middleware so the user session is available.
public function update(Request $request, Project $project)
{
if (Gate::denies('update-project')) {
abort(403, 'Your team role does not allow this action.');
}
// Proceed with update logic
}
Verification Steps
- Database Check: Verify the
team_usertable contains therolecolumn. - Session Check: Ensure the user has a
current_team_idset in the session; otherwise,$user->currentTeamwill return null. - Permission Test: Attempt to access the route with a user assigned the 'member' role to confirm the 403 response.
Trade-offs and Limitations
This approach is lightweight and utilizes first-party tools, but it has specific limitations:
- Flat Hierarchy: This method handles flat roles well. If you need hierarchical roles (e.g., a 'Manager' who inherits all 'Editor' permissions plus more), you will end up with large, repetitive arrays in your Gate definitions.
- Session Dependency: Jetstream relies on the session to track the
currentTeam. In multi-tab environments, if a user switches teams in one tab, the other tab's requests will suddenly be processed under the new team context, which can lead to unexpected data mutations. - Update Friction: If you need to customize the invitation email or the role assignment UI, you must publish and override the Jetstream controllers. This adds a maintenance burden during framework updates.
Actionable Closing
For most B2B applications, the built-in pivot role system is sufficient. Start by defining your permission map in the AuthServiceProvider. If you find your permission logic exceeding 50 lines or requiring complex inheritance, that is the signal to migrate to a dedicated package like Spatie Permission. Until then, keep your authorization logic decoupled from the role names to ensure your application remains flexible.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.