Time-boxed memberships
Pass expiresAt to grant temporary access — contractors, trials. Once it passes, the member resolves no role and therefore no permissions, so every Gate and Blade permission check fails closed:
use RoundlyConsulting\Teams\Facades\Teams;
use RoundlyConsulting\Teams\Models\Member;
Teams::for($team)->members()->add($contractor, 'editor', expiresAt: now()->addDays(30));
$member = Teams::for($team)->members()->find($contractor);
$member->isExpired(); // true once expires_at has passed
$member->role(); // null once expired — no role, no permissions
Member::query()->active(); // expires_at null or in the future
Member::query()->expired(); // expires_at set and in the past
Member::query()->expiringWithin(7); // active, lapsing within 7 daysOnce expired, can('teams.…'), @teamPermission, @teamRole, memberHasRole(), hasTeamRole(), teamsWithRole() and teamRole() all fail. The row itself stays for audits until pruned, so hasMember() and belongsToTeam() still match it — check a role or permission, not bare membership, when gating access.
Renewing
// While the membership is active, a new expiresAt extends it in place.
Teams::for($team)->members()->add($contractor, 'editor', expiresAt: now()->addDays(30));
// Re-adding an active member without expiresAt keeps the current expiry.
Teams::for($team)->members()->add($contractor, 'editor');
// Once it has expired, re-adding revives it as a fresh membership: role, meta and
// expiry come from this call (no expiresAt = permanent), and TeamMemberAdded fires.
Teams::for($team)->members()->add($contractor, 'editor', expiresAt: now()->addDays(30));Who lapses soon
teams:members:expiring lists active memberships lapsing within a window, so you can send renewal reminders before access ends. It’s read-only by default — MembershipExpiringSoon fires only with --notify. Already-expired and never-expiring memberships are excluded:
php artisan teams:members:expiring # read-only table, window from config
php artisan teams:members:expiring --days=3 # narrow the window
php artisan teams:members:expiring --days=3 --notify # fire MembershipExpiringSoon per memberThe same from your own scheduled job — no Artisan needed — or query directly; then turn the event into a notification:
use RoundlyConsulting\Teams\Facades\Teams;
use RoundlyConsulting\Teams\Models\Member;
Teams::members()->expiring(days: 7); // Collection<Member> — read-only
Teams::members()->notifyExpiring(days: 7); // fires MembershipExpiringSoon per member
// or query directly:
Member::query()->expiringWithin(7)->get();use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Teams\Events\MembershipExpiringSoon;
Event::listen(function (MembershipExpiringSoon $event): void {
// $event->member->member — whose access lapses
// $event->member->expires_at — when
});Pruning lapsed memberships
teams:members:prune (or Teams::members()->prune()) force-deletes memberships whose expiry passed more than members.prune_after (30 days) ago and fires MembershipExpired per row. Member is also Prunable with the same window, so php artisan model:prune takes the same rows down the same path — MembershipExpired fires either way:
use Illuminate\Support\Facades\Schedule;
// routes/console.php
Schedule::command('teams:members:expiring --notify')->daily(); // renewal reminders
Schedule::command('teams:members:prune')->daily(); // force-delete + MembershipExpired
// or from your own job:
Teams::members()->prune(); // int — the same force-delete + MembershipExpired
// php artisan model:prune collects the same rows and fires MembershipExpired too.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 cryptoBy 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.