LA

Laravel Invite Only

Conventions and API reference for the Laravel Invite Only package.

Install

mkdir -p .claude/skills/laravel-invite-only && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/18071" && unzip -o skill.zip -d .claude/skills/laravel-invite-only && rm skill.zip

Installs to .claude/skills/laravel-invite-only

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Conventions and APIs for the offload-project/laravel-invite-only package — polymorphic invitations, token acceptance, bulk invites, scheduled reminders, and event-driven hooks.
176 charsno explicit “when” trigger
Intermediate

Key capabilities

  • Apply `HasInvitations` trait to models that issue invitations
  • Apply `CanBeInvited` trait to the User model
  • Create invitations via `$invitable->invite()` or `InviteOnly` facade
  • Accept invitations using `InviteOnly::accept($token, $user)`
  • Customize notifications by overriding config entries
  • Schedule `invite-only:send-reminders` command daily

How it works

The skill provides conventions and APIs for the `offload-project/laravel-invite-only` package to manage user invitations with polymorphic relationships and a status lifecycle.

Inputs & outputs

You give it
User request for Laravel invitation management
You get back
Invitation model state changes, email notifications, or command-line output

When to use Laravel Invite Only

  • Issue team invitations
  • Manage invitation lifecycle
  • Send automated invite reminders
  • Handle invite events

About this skill

Context

offload-project/laravel-invite-only is a Laravel 11/12/13 package (PHP 8.2+) for managing user invitations against any model via polymorphic relationships. It ships:

  • An Invitation Eloquent model with status lifecycle (pending, accepted, declined, expired, cancelled) backed by an InvitationStatus enum.
  • Two traits: HasInvitations (for models that issue invitations — Team, Organization, Project) and CanBeInvited (for the User model).
  • An InviteOnly facade that wraps token generation, event dispatch, and notification sending.
  • Events for every lifecycle transition: InvitationCreated, InvitationAccepted, InvitationDeclined, InvitationCancelled, InvitationExpired.
  • Structured exceptions: InvalidInvitationException, InvitationAlreadyAcceptedException, InvitationExpiredException.
  • A invite-only:send-reminders Artisan command for scheduled reminder emails and expiration sweeps.

Apply this skill when working in a Laravel app that has offload-project/laravel-invite-only in composer.json, or when the user asks for help with InviteOnly, HasInvitations, CanBeInvited, the Invitation model, or invitation flows in this package.

Rules

Trait usage

  1. Apply HasInvitations to any model that can issue invitations (Team, Organization, Project, Account). Apply CanBeInvited to the User model that receives them.
  2. If a single model both sends and receives invitations (e.g. user-to-user friend invites), use both traits with PHP trait conflict resolution — the two traits each define an acceptedInvitations() method that collides. See the example below.
  3. Prefer getAcceptedInvitations() over the deprecated acceptedInvitations() helper on HasInvitations. The deprecated method will be removed in v3.0.

Status & enum

  1. Use the InvitationStatus enum (InvitationStatus::Pending, Accepted, Declined, Expired, Cancelled). Do not use the deprecated Invitation::STATUS_* string constants; they are kept only for backwards compatibility and will be removed.
  2. Check terminal states via $status->isTerminal() rather than chaining || against individual cases.

Creating invitations

  1. Create invitations through $invitable->invite() / $invitable->inviteMany() (preferred) or the InviteOnly facade. Do not call Invitation::create() directly — the facade handles token generation, expiration defaults, the InvitationCreated event, and the outbound notification.
  2. Pass the invitable model via the trait method ($team->invite(...)), or as the second argument to InviteOnly::invite($email, $invitable, $options). For invitations not tied to any model (e.g. open-platform signup), pass null.
  3. For bulk invites, use inviteMany() and inspect the returned BulkInvitationResult$result->successful (Collection of Invitation) and $result->failed (Collection of ['email' => ..., 'reason' => ...]). It supports partial failure; do not wrap it in a try/catch expecting an exception.
  4. inviteMany() deduplicates against existing pending invitations by default. Pass 'skip_duplicates' => false only when you intentionally want duplicate pending invites.

Accepting / declining / cancelling

  1. Accept by calling InviteOnly::accept($token, $user) (or Invitation::accept via facade). Always pass the authenticated User so accepted_by is recorded.
  2. Catch the typed exceptions individually when handling user-facing flows — InvitationExpiredException, InvitationAlreadyAcceptedException, InvalidInvitationException — to produce specific error messages. Do not catch the bare base InvitationException unless you intentionally want to collapse all failure modes.
  3. Wire the actual "do something on acceptance" logic (attaching a user to a team, granting a role, etc.) in an InvitationAccepted event listener, not inline at every call site. The facade fires the event for you.

Configuration & customization

  1. Customize notifications by overriding the invite-only.notifications.{invitation,reminder,cancelled,accepted} config entries. Setting any of them to null disables that notification. Do not edit the package's notification classes directly.
  2. Adjust expiration window via invite-only.expiration.days (default 7). Set invite-only.expiration.enabled to false for non-expiring invitations.
  3. Configure reminders via invite-only.reminders.after_days (e.g. [3, 5]) and max_reminders. Reminders only fire if reminders.enabled is true.
  4. The default routes are mounted at /invitations with ['web', 'throttle:60,1'] middleware. Keep the throttle (or stricter) — invitation tokens are otherwise susceptible to brute force. Disable the package routes (routes.enabled => false) only if you're providing your own.

Scheduling

  1. Schedule the bundled command to run daily so reminders go out and expired invitations get marked:

    Schedule::command('invite-only:send-reminders --mark-expired')->daily();
    

    Without --mark-expired, pending invitations past expires_at stay in pending status until something else marks them.

Mass assignment / model

  1. The Invitation model is final. To extend behavior, listen to events or wrap calls — do not try to subclass it.
  2. All migration columns are in $fillable. Setting lifecycle fields (accepted_at, accepted_by, declined_at, cancelled_at, last_sent_at, reminder_count) directly via update() is allowed but discouraged — prefer the markAs*() helpers so casts and side effects stay consistent.
  3. Use $invitation->isValid() (pending and not expired) when gating "can this token still be used" checks. isPending() alone is not sufficient.

Examples

Basic setup

// app/Models/Team.php
use OffloadProject\InviteOnly\Traits\HasInvitations;

class Team extends Model
{
    use HasInvitations;
}
// app/Models/User.php
use OffloadProject\InviteOnly\Traits\CanBeInvited;

class User extends Authenticatable
{
    use CanBeInvited;
}

Model that both sends and receives invitations

use OffloadProject\InviteOnly\Traits\CanBeInvited;
use OffloadProject\InviteOnly\Traits\HasInvitations;

class User extends Authenticatable
{
    use HasInvitations, CanBeInvited {
        CanBeInvited::acceptedInvitations insteadof HasInvitations;
        HasInvitations::acceptedInvitations as acceptedInvitationsToModel;
    }
}

In v3.0 HasInvitations::acceptedInvitations() will be removed in favour of getAcceptedInvitations(), eliminating the conflict — at which point the insteadof / as clauses can be dropped.

Sending invitations

$invitation = $team->invite('[email protected]', [
    'role' => 'member',
    'invited_by' => auth()->user(),
    'metadata' => ['source' => 'team-settings'],
]);

Bulk invitations with partial-failure handling

$result = $team->inviteMany(
    ['[email protected]', '[email protected]', 'bad-email'],
    ['role' => 'member', 'invited_by' => auth()->user()],
);

foreach ($result->successful as $invitation) {
    // send to UI, log, etc.
}

foreach ($result->failed as $failure) {
    Log::warning('Skipped invite', $failure); // ['email' => ..., 'reason' => ...]
}

Accepting an invitation

use OffloadProject\InviteOnly\Exceptions\InvalidInvitationException;
use OffloadProject\InviteOnly\Exceptions\InvitationAlreadyAcceptedException;
use OffloadProject\InviteOnly\Exceptions\InvitationExpiredException;
use OffloadProject\InviteOnly\Facades\InviteOnly;

try {
    $invitation = InviteOnly::accept($token, auth()->user());
} catch (InvitationExpiredException) {
    return redirect()->route('login')->withErrors(['invite' => 'This invitation has expired.']);
} catch (InvitationAlreadyAcceptedException) {
    return redirect()->route('dashboard');
} catch (InvalidInvitationException $e) {
    return redirect()->route('login')->withErrors(['invite' => $e->getMessage()]);
}

Wiring side effects via the event

use Illuminate\Support\Facades\Event;
use OffloadProject\InviteOnly\Events\InvitationAccepted;

Event::listen(InvitationAccepted::class, function (InvitationAccepted $event): void {
    $team = $event->invitation->invitable; // Team|Organization|null
    $user = $event->user;
    $role = $event->invitation->role;

    if ($team !== null && $user !== null) {
        $team->users()->attach($user->id, ['role' => $role]);
    }
});

Custom notification

// config/invite-only.php
'notifications' => [
    'invitation' => App\Notifications\TeamInvitationSent::class,
    'reminder'   => App\Notifications\TeamInvitationReminder::class,
    'cancelled'  => null, // disabled
    'accepted'   => App\Notifications\TeamInvitationAccepted::class,
],

Status checks with the enum

use OffloadProject\InviteOnly\Enums\InvitationStatus;

if ($invitation->status === InvitationStatus::Pending) { /* ... */ }

if ($invitation->status->isTerminal()) {
    // accepted | declined | expired | cancelled
}

Scheduled reminders + expiration sweep

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('invite-only:send-reminders --mark-expired')->daily();

Anti-patterns

  • Invitation::create([...]) — bypasses token generation, the InvitationCreated event, and the outbound notification. Always go through the facade or trait method.
  • ❌ Using Invitation::STATUS_PENDING (and the other STATUS_* constants). Deprecated — use InvitationStatus::Pending etc.
  • ❌ Using acceptedInvitations() from HasInvitations. Deprecated — use getAcceptedInvitations().
  • ❌ Subclassing Invitation. The model is final; extend behavior via events or wrapper services.
  • ❌ Catching \Throwable or \Exception around InviteOnly::accept(). Catch the typed exceptions so each failure mode produces a tailored response.
  • ❌ Doing "attach user to team / grant role" work inli

Content truncated.

When not to use it

  • When calling `Invitation::create()` directly
  • When using deprecated `Invitation::STATUS_*` string constants
  • When subclassing the `Invitation` model

Limitations

  • Cannot call `Invitation::create()` directly
  • Cannot use deprecated `Invitation::STATUS_*` string constants
  • Cannot subclass the `Invitation` model

How it compares

This skill offers a structured and event-driven approach to invitation management in Laravel, handling token generation, notifications, and lifecycle transitions automatically.

Compared to similar skills

Laravel Invite Only side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
Laravel Invite Only (this skill)01moNo flagsIntermediate
laravel-specialist122moNo flagsIntermediate
laravel-query-builder01moNo flagsIntermediate
laravel-development02moNo flagsIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

Search skills

Search the agent skills registry