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.zipInstalls 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.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
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
InvitationEloquent model with status lifecycle (pending,accepted,declined,expired,cancelled) backed by anInvitationStatusenum. - Two traits:
HasInvitations(for models that issue invitations — Team, Organization, Project) andCanBeInvited(for the User model). - An
InviteOnlyfacade 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-remindersArtisan 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
- Apply
HasInvitationsto any model that can issue invitations (Team, Organization, Project, Account). ApplyCanBeInvitedto the User model that receives them. - 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. - Prefer
getAcceptedInvitations()over the deprecatedacceptedInvitations()helper onHasInvitations. The deprecated method will be removed in v3.0.
Status & enum
- Use the
InvitationStatusenum (InvitationStatus::Pending,Accepted,Declined,Expired,Cancelled). Do not use the deprecatedInvitation::STATUS_*string constants; they are kept only for backwards compatibility and will be removed. - Check terminal states via
$status->isTerminal()rather than chaining||against individual cases.
Creating invitations
- Create invitations through
$invitable->invite()/$invitable->inviteMany()(preferred) or theInviteOnlyfacade. Do not callInvitation::create()directly — the facade handles token generation, expiration defaults, theInvitationCreatedevent, and the outbound notification. - Pass the invitable model via the trait method (
$team->invite(...)), or as the second argument toInviteOnly::invite($email, $invitable, $options). For invitations not tied to any model (e.g. open-platform signup), passnull. - For bulk invites, use
inviteMany()and inspect the returnedBulkInvitationResult—$result->successful(Collection ofInvitation) and$result->failed(Collection of['email' => ..., 'reason' => ...]). It supports partial failure; do not wrap it in a try/catch expecting an exception. inviteMany()deduplicates against existing pending invitations by default. Pass'skip_duplicates' => falseonly when you intentionally want duplicate pending invites.
Accepting / declining / cancelling
- Accept by calling
InviteOnly::accept($token, $user)(orInvitation::acceptvia facade). Always pass the authenticatedUsersoaccepted_byis recorded. - Catch the typed exceptions individually when handling user-facing flows —
InvitationExpiredException,InvitationAlreadyAcceptedException,InvalidInvitationException— to produce specific error messages. Do not catch the bare baseInvitationExceptionunless you intentionally want to collapse all failure modes. - Wire the actual "do something on acceptance" logic (attaching a user to a team, granting a role, etc.) in an
InvitationAcceptedevent listener, not inline at every call site. The facade fires the event for you.
Configuration & customization
- Customize notifications by overriding the
invite-only.notifications.{invitation,reminder,cancelled,accepted}config entries. Setting any of them tonulldisables that notification. Do not edit the package's notification classes directly. - Adjust expiration window via
invite-only.expiration.days(default 7). Setinvite-only.expiration.enabledtofalsefor non-expiring invitations. - Configure reminders via
invite-only.reminders.after_days(e.g.[3, 5]) andmax_reminders. Reminders only fire ifreminders.enabledis true. - The default routes are mounted at
/invitationswith['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
-
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 pastexpires_atstay inpendingstatus until something else marks them.
Mass assignment / model
- The
Invitationmodel isfinal. To extend behavior, listen to events or wrap calls — do not try to subclass it. - All migration columns are in
$fillable. Setting lifecycle fields (accepted_at,accepted_by,declined_at,cancelled_at,last_sent_at,reminder_count) directly viaupdate()is allowed but discouraged — prefer themarkAs*()helpers so casts and side effects stay consistent. - 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, theInvitationCreatedevent, and the outbound notification. Always go through the facade or trait method. - ❌ Using
Invitation::STATUS_PENDING(and the otherSTATUS_*constants). Deprecated — useInvitationStatus::Pendingetc. - ❌ Using
acceptedInvitations()fromHasInvitations. Deprecated — usegetAcceptedInvitations(). - ❌ Subclassing
Invitation. The model isfinal; extend behavior via events or wrapper services. - ❌ Catching
\Throwableor\ExceptionaroundInviteOnly::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.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| Laravel Invite Only (this skill) | 0 | 1mo | No flags | Intermediate |
| laravel-specialist | 12 | 2mo | No flags | Intermediate |
| laravel-query-builder | 0 | 1mo | No flags | Intermediate |
| laravel-development | 0 | 2mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
laravel-specialist
Jeffallan
Use when building Laravel 10+ applications requiring Eloquent ORM, API resources, or queue systems. Invoke for Laravel models, Livewire components, Sanctum authentication, Horizon queues.
laravel-query-builder
relaticle
Build filtered, sorted, and included API endpoints using spatie/laravel-query-builder. Activates when working with QueryBuilder, AllowedFilter, AllowedSort, AllowedInclude, or when the user mentions query parameters, API filtering, sorting, includes, or spatie/laravel-query-builder.
laravel-development
monicahq
Expert guidance for Laravel PHP development following best practices, SOLID principles, and Laravel conventions
laravel-eloquent
HoangNguyen0403
Write performant Eloquent queries with eager loading, reusable scopes, and strict lazy-loading prevention in Laravel. Use when defining model relationships, creating query scopes, or processing large datasets with chunk/cursor.
developing-with-turbo-streams
hotwired-laravel
Basics of developing with Turbo Streams in web applications. Activate when working on projects that utilize Turbo Streams for enhancing user experience through real-time updates, dynamic content changes, and partial page updates without full reloads.
payment-integration
mrgoonie
Integrate payments with SePay (VietQR), Polar, Stripe, Paddle (MoR subscriptions), Creem.io (licensing). Checkout, webhooks, subscriptions, QR codes, multi-provider orders.