Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Teams Laravel Package

jurager/teams

jurager/teams is a Laravel package for adding team management to your app. It supports team creation, member invitations, roles and permissions, and integrates with Laravel's authorization and Eloquent models, making multi-user workspaces easy to build.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish first. This package is small enough that you should read its src/ before wiring it in — the README is the primary contract, and at 235 stars there's no Stack Overflow safety net.

composer require jurager/teams
php artisan vendor:publish --provider="Jurager\Teams\TeamServiceProvider"
php artisan migrate

Publish both the config and the migrations before running anything. The config holds the model map (your User, Team, and often a Membership/pivot model) and the role/permission definitions; almost every customization decision lives there rather than in code.

Wire the trait onto your User model. That's the single step that unlocks the API:

use Jurager\Teams\Traits\HasTeams;

class User extends Authenticatable
{
    use HasTeams;
}

If your pivot needs extra columns (invited_by, joined_at), publish and edit the migration before the first migrate — retrofitting pivot columns on a live teams table is annoying because every membership row needs backfilling.

First use case: a per-team ability check. This is what you'll actually ship on day one — a controller action that only lets a team member with the right role perform an action:

Route::put('/teams/{team}/posts/{post}', function (Team $team, Post $post) {
    abort_unless(
        auth()->user()->belongsToTeam($team)
        && auth()->user()->hasTeamPermission($team, 'post:update'),
        403
    );

    $post->update(request()->validate([...]));
})->middleware(['auth', 'team.context']);

Verify the exact method names against the README before committing — belongsToTeam, hasTeamPermission, hasTeamRole are the Jetstream-shaped names this package mirrors, but the fluent API has shifted across minors.

Where to look first, in order: published config/teams.php → the HasTeams trait → the membership model's role lookup → the middleware. Those four files cover 90% of the behavior.

Implementation Patterns

Model the team as the tenancy boundary early. The package gives you teams, memberships, roles, and permission checks — it does not give you query scoping. Decide up front whether Post, Invoice, etc. get a team_id column, and add a global scope or a ->forTeam($team) scope in the same PR that introduces teams. Retrofitting tenancy after data exists is the expensive mistake.

Resolve the current team in middleware, once. Don't thread $team through every controller by hand. A small middleware that reads the route parameter (or a current_team_id column on users) and stores it in the container keeps authorization checks terse:

public function handle($request, Closure $next)
{
    $team = $request->route('team') ?? $request->user()?->currentTeam;

    abort_if(! $team || ! $request->user()?->belongsToTeam($team), 403);

    app()->instance('current_team', $team);
    return $next($request);
}

Keep roles coarse, permissions fine. The sustainable split: three or four roles (owner, admin, member) defined in config, and a permission string per action (post:update, billing:manage). Check permissions in controllers, check roles only where the role is the business rule (e.g. "only the owner may delete the team"). Checking roles everywhere collapses the moment a customer asks for a custom role.

Policies as the glue. Team permissions are a data concern; Laravel policies are the authorization surface. Thin policies that delegate to the package read well and keep Blade/Gate::allows calls uniform:

public function update(User $user, Post $post): bool
{
    return $user->hasTeamPermission($post->team, 'post:update');
}

Ownership transfer is a transaction, not a role update. Whichever way the package exposes it, wrap "promote B to owner, demote A to admin" in DB::transaction() and re-read the team row inside the transaction. Two concurrent transfers otherwise leave a team with zero or two owners.

Invitations deserve their own state machine. Most teams packages ship an invitation table with a token and an accepted_at. Resist the urge to model invitations as users with a null password — you'll fight unique-email constraints and end up with half-registered accounts in your analytics.

Test with factories, not fixtures. Team::factory()->hasAttached($user, ['role' => 'admin']) (or the package's equivalent) makes authorization tests one-liners. Write one test per permission string asserting both the allowed and the denied path — this is the highest-value test suite you'll have, because permission regressions are silent.

Gotchas and Tips

Config is the model map, and it's cached. If you swap in your own Team model, every cached config in production must be rebuilt — php artisan config:cache after deploy, or the old class name resolves and you get inexplicable Class not found in queue workers while web requests work fine.

Permission caching is the usual silent bug. If the package memoizes a member's abilities, changing a role mid-request (or in a queued job that ran before the change) leaves stale results. Read the trait's cache-invalidation code, and if it's absent, call $user->unsetRelation('teams') or flush explicitly after any role mutation.

Don't collide with Jetstream or Breeze Teams. If you've ever installed Jetstream with teams, you already have teams and team_user tables and a current_team_id column. Publish the migrations and read them side-by-side with your schema before running — a failed migration on a shared table is a bad afternoon. Drop or rename the Jetstream ones first on greenfield-ish projects.

Soft-deleted teams leave live memberships. If Team uses SoftDeletes, every membership query must join through non-trashed teams or you'll grant access to a deleted team's resources. Prefer a deleted_at check in your middleware over relying on the trait.

Route-model binding bypasses tenancy. Route::get('/teams/{team}/posts/{post}') with implicit binding happily hands you a Post from another team. Either scope the binding explicitly (->scopeBindings()) or assert $post->team_id === $team->id in the policy. This is the number-one IDOR path in team-based apps.

N+1 on the team switcher. A dropdown listing every team with member counts and the user's role is death by a thousand queries unless you eager load: $user->teams()->withCount('members')->withPivot('role'). Cache that list per user if the switcher sits in a global layout.

Escape hatches worth knowing. Because models are config-mapped, you can subclass Team and add relationships (->withCount, ->hasMany(Post::class)) without forking. Extend the membership model too if you need audit columns — but keep the package's interface intact, since the trait type-hints against the configured class.

Version pinning. The last release shown is dated in the future relative to most dependency policies, and the project is pre-1.0-ish in maturity. Pin a caret range and read the CHANGELOG on every bump; a small maintainer set means breaking changes can land with a minor. Keep your authorization logic behind policies and traits you own so a package swap is a one-file change rather than a grep across the codebase.

Debugging checklist when a check unexpectedly returns false: (1) is the user actually attached — $user->teams()->where('teams.id', $team->id)->exists(); (2) did the role persist, or did an earlier update silently no-op; (3) is config cached in this environment; (4) is $team the configured model class or a stale one; (5) does the permission string exactly match config (typos here fail open or closed depending on implementation — test both).

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect