---
title: "The Teams facade — Teams for Laravel | Roundly"
description: "One entry point: Teams::for($team) handles for members, invites, join requests and roles, plus global roles, permissions and housekeeping."
url: https://roundly-consulting.com/open-source/docs/teams-for-laravel/facade
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Teams for Laravel](https://roundly-consulting.com/open-source/docs/teams-for-laravel.md)

# The Teams facade

Teams is the entry point for everything the package does. Team-scoped work hangs off Teams::for($team), which returns a handle with one sub-accessor per area; cross-team work — accepting an invite by code, housekeeping, the role and permission vocabulary — sits on the facade itself:

```php
use RoundlyConsulting\Teams\DataTransferObjects\CreateTeamData;
use RoundlyConsulting\Teams\Facades\Teams;

// Create a team; the owner joins with the configured owner role.
$team = Teams::create(new CreateTeamData(name: 'Acme', owner: $owner));

// Members
Teams::for($team)->members()->add($alice, 'admin', expiresAt: now()->addYear());
Teams::for($team)->members()->changeRole($alice, 'editor');   // throws MemberNotFoundException for a non-member
Teams::for($team)->members()->remove($bob);                   // bool
Teams::for($team)->members()->all();                          // Collection<Member>
Teams::for($team)->members()->has($alice);                    // bool
Teams::for($team)->members()->find($alice);                   // ?Member

// Invites
$invite = Teams::for($team)->invites()->create(role: 'member', email: 'jane@acme.test', maxUses: 1);
Teams::for($team)->invites()->resend($invite);   // rotate code, extend expiry
Teams::for($team)->invites()->revoke($invite);   // bool
Teams::for($team)->invites()->pending();         // Collection<Invite>
Teams::invites()->accept($invite, $user, email: $user->email); // email must match an email-targeted invite
Teams::invites()->accept('the-code', $user);     // a link invite (no email) by its code

// Join requests
$request = Teams::for($team)->joinRequests()->open($user, requestedRole: 'member', message: 'Hi');
Teams::for($team)->joinRequests()->approve($request, by: $admin, role: 'member');
Teams::for($team)->joinRequests()->deny($request, by: $admin);
Teams::for($team)->joinRequests()->pending();    // Collection<JoinRequest>

// Per-team roles, ownership, settings and the companion packages
Teams::for($team)->roles()->define('editor', 'Editor', ['posts.edit']);
Teams::for($team)->roles()->all();               // effective role map
Teams::for($team)->transferOwnershipTo($alice);  // Team
Teams::for($team)->settings();                   // TeamSettings (options package)
Teams::for($team)->contacts();                   // ContactBook (contacts package)
Teams::for($team)->addresses();                  // AddressBook (addresses package)
Teams::for($team)->connections();                // PendingConnection (connections package)
Teams::for($team)->team();                       // the scoped Team model

// Global vocabulary and housekeeping
Teams::roles()->register('admin', 'Admin', ['*']);
Teams::permissions()->register('posts.publish', 'Publish posts', group: 'Content');
Teams::invites()->prune();                       // int
Teams::members()->expiring(days: 7);             // Collection<Member>, read-only
Teams::members()->notifyExpiring(days: 7);       // fires MembershipExpiringSoon
Teams::members()->prune();                       // int
Teams::joinRequests()->expire();                 // int — auto-decline expired pending requests
```

## Facade methods

| Method | Returns | Notes |
| --- | --- | --- |
| `create(CreateTeamData $data)` | Team | Seats the owner with roles.owner. |
| `for(Team $team)` | TeamHandle | Team-scoped handle — see the next table. |
| `invites()` | Invites | `accept(), find(), prune()` |
| `members()` | Members | `expiring(), notifyExpiring(), prune()` |
| `joinRequests()` | JoinRequests | `expire()` |
| `roles()` | RoleProvider | The configured role driver: register(), find(), all(). |
| `permissions()` | PermissionRegistry | `register(), group(), find(), all(), fromRoles()` |
| `fake()` | TeamsFake | Recording fake — see Testing. |

## The team handle

Teams::for($team) returns a TeamHandle. Every write returns its result — a Member, an Invite, a bool — not the handle, so there is no chaining:

| Teams::for($team)->… | Returns | Notes |
| --- | --- | --- |
| `members()->add($member, $role, $meta = [], $expiresAt = null)` | Member | Idempotent; revives a removed or expired row; respects the seat cap. |
| `members()->remove($member)` | bool | Soft-deletes; false for a non-member. |
| `members()->changeRole($member, $role)` | Member | Throws MemberNotFoundException for a non-member. |
| `members()->all() / has($m) / find($m)` | Collection / bool / ?Member | all() includes expired memberships. |
| `invites()->create($role, $expiresAt, $email, $invitedBy, $meta, $maxUses = 1)` | Invite | See Invitations. |
| `invites()->resend($invite) / revoke($invite)` | Invite / bool | Refuses another team’s invite. |
| `invites()->pending()` | Collection<Invite> | The team’s unexpired invites. |
| `joinRequests()->open($requester, $requestedRole, $message, $meta, $expiresAt)` | JoinRequest | Honours the join policy; refuses active members; idempotent while pending. |
| `joinRequests()->approve($request, by:, role:) / deny($request, by:)` | Member / JoinRequest | Refuses another team’s request and one already resolved. |
| `joinRequests()->requireApprovalFrom() / rule() / quorum()` | new handle | Stage multi-admin sign-off for the next open(). |
| `joinRequests()->pending()` | Collection<JoinRequest> | |
| `roles()->define($key, $name, $permissions = [], $description = '')` | TeamRole | Per-team override, upserted on (team, key). |
| `roles()->all() / find($key)` | array / ?Role | Global roles merged with this team’s overrides. |
| `transferOwnershipTo($owner)` | Team | All-or-nothing; demotes the previous owner to roles.admin. |
| `settings()` | TeamSettings | See Team settings. |
| `contacts() / addresses() / connections()` | ContactBook / AddressBook / PendingConnection | See Integrations. |
| `team()` | Team | The scoped model. |

## Scoping is a security boundary

A handle from Teams::for($teamA) refuses rows that belong to another team, before anything runs. So a controller can take the team from the route and the invite or request from user input without an extra ownership check:

| Call with another team’s row | Throws |
| --- | --- |
| `invites()->resend($invite) / revoke($invite)` | `InviteNotFoundException` |
| `joinRequests()->approve($request, …) / deny($request, …)` | `JoinRequestNotFoundException` |
| `members()->changeRole($nonMember, …)` | `MemberNotFoundException` |

```php
use RoundlyConsulting\Teams\Exceptions\InviteNotFoundException;
use RoundlyConsulting\Teams\Facades\Teams;

// The team comes from the route, the invite from user input — no extra ownership check.
try {
    Teams::for($team)->invites()->revoke($invite);
} catch (InviteNotFoundException) {
    // "Invite #42 does not belong to this team." — nothing ran, nothing was recorded
}
```

## Model shorthand

The model convenience methods delegate to the same manager, so they behave — and are recorded by Teams::fake() — exactly like the handle calls:

| Model method | Same as |
| --- | --- |
| `$team->addMember($m, $role, $meta, $expiresAt)` | `Teams::for($team)->members()->add(…)` |
| `$team->removeMember($m)` | `Teams::for($team)->members()->remove($m)` |
| `$team->invite($role, $expiresAt, $email, $invitedBy, $meta, $maxUses)` | `Teams::for($team)->invites()->create(…)` |
| `$team->defineRole($key, $name, $permissions, $description)` | `Teams::for($team)->roles()->define(…)` |
| `$team->roles()` | `Teams::for($team)->roles()->all()` |
| `$invite->acceptBy($member, $email)` | `Teams::invites()->accept($invite, $member, $email)` |
| `$invite->resend() / revoke()` | `Teams::for($invite->team)->invites()->resend/revoke($invite)` |
| `$membership->removeFromTeam()` | `Teams::for($membership->team)->members()->remove(…)` |

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/teams-for-laravel.md)
