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

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 days

Once 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 member

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