NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Teams for Laravel

Nested teams

For hierarchies such as company → department → team. Nesting is off by default — set TEAMS_NESTING=true after the parent_id migration ran (see Installation); while it is off nothing reads the column. Membership stays per team:

use RoundlyConsulting\Teams\Facades\Teams;

// .env → TEAMS_NESTING=true (after the parent_id migration ran)
Teams::for($department)->moveUnder($company);   // Team — place under a parent
Teams::for($department)->moveToRoot();          // Team — detach

$department->parent;         // ?Team
$company->children;          // Collection<Team>
$team->ancestors();          // nearest first; stops at a soft-deleted ancestor
$company->descendants();     // level by level
$team->root();               // the topmost ancestor, or the team itself
$team->depth();              // 0 for a root team

moveUnder() and moveToRoot() run MoveTeamAction, which locks both rows for the transaction, saves and fires TeamMoved with the team, the new parent and the previous parent key; Teams::fake() records it. Every parent change is validated on save — through the action or a direct $team->update(['parent_id' => …]) — and throws TeamHierarchyException when nesting is off, when the new parent is the team itself or one of its descendants (soft-deleted ones included), or when any team of the moved subtree would sit deeper than nesting.max_depth (default 4, 1–100; the root is depth 0). Detaching is always allowed.

Force-deleting a team that still has child teams fails — move or delete them first. Soft-deleting a parent keeps the link but stops ancestors() and role inheritance, since a trashed team grants nothing. Every walk is bounded by max_depth and remembers visited rows, so even data corrupted into a loop by a raw query cannot spin forever.

Roles inherit down

With TEAMS_INHERIT_ROLES_DOWN=true — effective only with nesting on — a role held on a team also applies on every team below it, never above:

  • Inherits — memberHasPermission() and hasTeamPermission(), resolved against the role set of the team the role is held on; memberHasRole(), hasTeamRole() and @teamRole; the teams.<permission> Gate abilities, @teamPermission and team policies; isAccessibleBy() and so the teams.context middleware, switchTo(), setCurrentMany() and the queued-job re-check; with the permissions bridge, grants in a team above.
  • Does not inherit — hasActiveMember(), hasMember(), findMember(), isActiveMemberOf(), teamRole(), belongsToTeam() and isMemberOf(): membership stays per team. Ownership — isOwnedBy(), teams.owner, @teamOwner and the policy before() — is per team too, though a parent owner’s owner role permissions still inherit.
  • Expired memberships, and ones that have not started yet, never count on any level.

Team-scoped data usually points at the root team:

// Team-scoped data usually points at the root team:
Teams::switchTo($team->root());

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.