Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

upnorthal's avatar

Spatie/laravel-permissions - Teams Functionality

I see that teams functionality has recently been committed to Spatie’s package. (https://github.com/spatie/laravel-permission/pull/1804)

Has anyone tried this yet ? I have included my progress in case its useful for anyone else along with a question on middleware.

What I have below is working and actually really flexible ie. a user can belong to many branches and each branch can have its own role / set of permissions etc.

To get it working:-

  • I have installed a fresh Laravel App and installed Jetstream ontop (with no Jetstream Teams)
  • Installed Spatie permissions with Teams enabled.
  • I have created a pivot table between Users table and Branches table. (Branch is my “Team”)
  • I have extended the standard Users table to have a new column called current_branch_id

Quick note: using the Spatie. Config\Permissions.php file, you are able to custom define what your “teams” foreign key should be. In my case, I have relabelled “team_id” as “branch_id”

We can see from the documentation that a little more needs doing to make this work in the way of setting up a Middleware. (https://spatie.be/docs/laravel-permission/v5/basic-usage/teams-permissions)

Actually, its a little more than setting up a middle ware to ‘prime’ Spatie with the current team_id (branch_id in my case). The suggestion is for the current team_id to be held in session. (why does Jetstream teams not store this in session? I see it hits the DB each time it wants the current team)

What I have done is created a new Middleware to store the current branch ID a user is associated with in session,

SetBranchID.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SetBranchID
{

    public function handle(Request $request, Closure $next)
    {
        if ( ! empty(auth()->user())) {
            Session()->put(['current_branch_id' => Auth()->user()->current_branch_id]);
        }

        return $next($request);
    }
}


Also, a middleware for Spatie based on the documentation.

SetBranchSpatie.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SetBranchSpatie
{

    public function handle(Request $request, Closure $next)
    {
        if(!empty(auth()->user())){
            // session value set on login
            app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId(session('current_branch_id'));
        }


        return $next($request);
    }
}


Question

Where should the above middleware’s be called in the Kernel.php definitions ?

For now, I’ve just added them to the bottom of ‘Web’

Kernel.php


    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Laravel\Jetstream\Http\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\SetBranchID::class,  //    <<——— Set the current branch_id
            \App\Http\Middleware\SetBranchSpatie::class,  //  <<—   prime Spatie with current branch_id

        ],

        'api' => [
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,

        ],
    ];

The Spatie teams documentation clearly states, “NOTE: You must add your custom Middleware to $middlewarePriority on app/Http/Kernel.php.”

The Laravel docs shows us this $middlewarePriority is an array (https://laravel.com/docs/8.x/middleware#sorting-middleware).


/**
 * The priority-sorted list of middleware.
 *
 * This forces non-global middleware to always be in the given order.
 *
 * @var array
 */
protected $middlewarePriority = [
    \Illuminate\Cookie\Middleware\EncryptCookies::class,
    \Illuminate\Session\Middleware\StartSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
    \Illuminate\Routing\Middleware\ThrottleRequests::class,
    \Illuminate\Session\Middleware\AuthenticateSession::class,
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
    \Illuminate\Auth\Middleware\Authorize::class,
];

Where abouts in this array should we place the Spatie middleware ?

0 likes
7 replies
Olixr's avatar

I found myself in a similar situation. I have my own teams implementation and I was hoping to enable the teams feature in permissions. With the help of your post, the documentation and some testing I have a working setup as well. I thought I would share my experiences too. (Side note: Also I am glad I found this thread, it was found through an obscured search and doesn't actually show forum search results. )

I have following all the details from the docs and the steps you listed as well. My middleware and controllers all look very similar to yours with the exception my team is named "team" vs "branch".

My steps to setup:

  • Installed Laravel App
  • Setup a Teams and Team Users Models for teams and assignents
  • Added a current_team_id to User model for team switching
  • Installed Spatie Permissions with Teams Enabled per docs

I created middleware as well for both setting current team into session and priming spatie permissions with this team id. The middleware are as follows:

SetCurrentTeam.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SetCurrentTeam
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        if ( ! empty(auth()->user())) {
            session()->put(['current_team_id' => auth()->user()->current_team_id]);
        }

        return $next($request);
    }
}

SetCurrentTeamPermission.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SetCurrentTeamPermission
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        if(!empty(auth()->user())){
            // session value set on login
            app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId(session('current_team_id'));
        }

        return $next($request);
    }
}

I am using the permissions in a role based approach. I have seeded my system with global permissions and then the team based roles have assess to these permissions. As suggested by the package documentation this approach allows for us to make use of the can() authorization helpers to then check for these permissions.

To clean up and organize logic I also made the suggested Policy to contain this logic and then just point authentication to the appropriate policy using laravels built in methods.

Here is a copy of one of my policies for a Claim model that a user has access too:

<?php

namespace App\Policies;

use App\Models\Claim;
use App\Models\User;
use \Spatie\Permission\Models\Role;
use Illuminate\Auth\Access\HandlesAuthorization;

class ClaimPolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can view any models.
     *
     * @param  \App\Models\User  $user
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function viewAny(User $user)
    {
        if($user->can('view-all-claims')) {
            return true;
        }

        if($user->can('view-own-claims')) {
            return true;
        }

        if($user->can('view-assigned-claims')) {
            return true;
        }
        
        dd($user->roles);

    }

    /**
     * Determine whether the user can view the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Claim  $claim
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function view(User $user, Claim $claim)
    {
        if($user->can('view-own-claims')) {
            return $user->id == $claim->user_id;
        }

        if($user->can('view-assigned-claims')) {
            return $claim->adjusters()->where('claim_users.user_id', $user->id)->count() > 0;
        }

        if($user->can('view-claims')) {
            return true;
        }

    }

    /**
     * Determine whether the user can create models.
     *
     * @param  \App\Models\User  $user
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function create(User $user)
    {
        if($user->can('create-claims')) {
            return true;
        }
    }

    /**
     * Determine whether the user can update the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Claim  $claim
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function update(User $user, Claim $claim)
    {
        if($user->can('edit-own-claims')) {
            return $user->id == $claim->user_id;
        }

        if($user->can('edit-assigned-claims')) {
            return $claim->adjusters()->where('claim_users.user_id', $user->id)->count() > 0;
        }

        if($user->can('edit-claims')) {
            return true;
        }
    }

    /**
     * Determine whether the user can delete the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Claim  $claim
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function delete(User $user, Claim $claim)
    {
        if($user->can('delete-own-claims')) {
            return $user->id == $claim->user_id;
        }

        if($user->can('delete-assigned-claims')) {
            return $claim->adjusters()->where('claim_users.user_id', $user->id)->count() > 0;
        }

        if($user->can('delete-claims')) {
            return true;
        }
    }

Then inside my web.php routes file you can make use of the policy through middleware:

	Route::get('/claims', 'App\Http\Controllers\ClaimController@index')->middleware('can:viewAny,'.App\Models\Claim::class)->name('claim');
	Route::post('/claims/{claim}', 'App\Http\Controllers\ClaimController@update')->middleware('can:update,claim')->name('claim.update');

Something important to note here. @upnorthal I had a route middleware priority set similar to yours wondering where they should be. I later discovered my answer. I have been trying to figure out a bug for some time now as to why the policy was working for everything except the viewAll method. After dumping the user many times I discovered the roles were not being pulled for the user which then means none of the permissions passing. So no matter what my viewAll method was failing. It took me a while to think about the issue.

Then I discovered it. I was wondering why the roles were not being pulled in and then I remember that they are associated with the teams. If for some reason the team was not set then it didn't know what roles to grab and defaults to none. I recalled that my middleware was supposed to be setting this team id and so I thought to look there. Everything checked out but then I realized the issue. The team id WAS being set but AFTER the authorization middleware. This meant that any middleware making use of can() was not seeing the set team id.

So this leads us to the question of the middleware priority. I found that my middleware must come BEFORE the authorization middleware in order for the team id to be set into session and primed into permissions before the authorization check take place. This results in the following route Kernel for my route priority:

Kernel.php

/**
     * The priority-sorted list of middleware.
     *
     * This forces non-global middleware to always be in the given order.
     *
     * @var string[]
     */
    protected $middlewarePriority = [
        \Illuminate\Cookie\Middleware\EncryptCookies::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
        \Illuminate\Routing\Middleware\ThrottleRequests::class,
        \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \App\Http\Middleware\SetCurrentTeam::class, 
        \App\Http\Middleware\SetCurrentTeamPermission::class,
        \Illuminate\Auth\Middleware\Authorize::class, // <-- Place permissions setup before this
    ];

I certainly hope this helps anyone else in similar situations and avoid some long debugging. @upnorthal thanks for your initial discussion on the matter!

Perhaps we can submit a PR to the package docs and add some additional information to the teams setup.

Happy coding all!

6 likes
secondman's avatar

@Olixr

Or anyone else ... $middlewarePriority has been removed from the Kernal, how do we order these now to ensure that the teams based middlewares are loaded before Authorize middleware?

I can't seem to find any documentation on how this is done now.

Nesster's avatar

@secondman Kinda wondering about this too. Is $middlewarePriority effectively removed ? Can't find much info about this either.

1 like
Dalbo's avatar

@Nesster @secondman

Was also just looking at this. From the docs, $middlewarePriority isn't included in the Kernel.php by default, but can be added back in. The docs include the default value to use. I can't include a link on my first day, but if you look up the Laravel middleware docs for version 10.x and find the Sorting Middleware section.

1 like
Richee's avatar

Reporting my experience for others trying to use Spatie Permissions with teams.

@upnorthal and @olixr thankyou for this post and I followed your instructions and managed to get the "middleware' => ['can:Create user']" (for example) working. I spent a day trying to get the Spatie RoleMiddleware, PermissionMiddleware and RoleOrPermissionMiddleware working but failed. It could be to do with my cache as I could not consistently access permissiosn in my cache. There is another post out there about difficulties dealing with the cache. Not really sure what the issue was as I could not work it out.

This leads me to believe that it is best to follow @upnorthal approach as I think this could be a very viable approach. As it also uses native laravel it could be more robust should cache be problematic.

The routes/controllers can be protected via permissions with "can" then grouped into roles. This provides flexibility to create a variety of roles based on permissions (i.e role based permissions :)

You also know there is a consistent implementation of the middleware with permissions. This is helpful especially if you create views for teams that show "Team>Role>Permissions". It is very visible and manageable.

As roles can only belong to one team this allows for another useful layer on top for the right use case.

Not sure I am adding anything new here, but hope it helps. Thanks again to the posters.

phrfpeixoto's avatar

Hello. I'm coming back from Laravel since the old days of Laravel 5, and I'm a bit lost. I wanna refactor an old project that has its own roles and permissions system, and stumbled upon Spatie.

I think the Teams feature is what I need, but I'm not sure, as the documentation seems a little sparse and lacking...

In my application, I want some users to have a set of permissions within a given team/group. IE: A manager user can request a password reset process for any user in their own group. They should also be allowed access only to view/write projects for their own group.

ie:

  • Group 1 has Projects A, B, and C
  • Group 2 has Projects A, D, and F

While I think this is all simple enough, and the Teams feature can model that, I have doubts about how I'm going to define roles and permissions.

ie: $user()->can('request-password-reset'). But to what group does that apply?

Please or to participate in this conversation.