PHPackages                             vimatech/laravel-invitation - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. vimatech/laravel-invitation

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

vimatech/laravel-invitation
===========================

Generic email-based invitations for Laravel.

v1.1.0(1mo ago)11MITPHPPHP ^8.3CI passing

Since Jun 23Pushed 1mo agoCompare

[ Source](https://github.com/vimatech-io/laravel-invitations)[ Packagist](https://packagist.org/packages/vimatech/laravel-invitation)[ RSS](/packages/vimatech-laravel-invitation/feed)WikiDiscussions main Synced 2w ago

READMEChangelogDependencies (20)Versions (4)Used By (0)

Laravel Invitation
==================

[](#laravel-invitation)

[![CI](https://github.com/vimatech-io/laravel-invitations/actions/workflows/ci.yml/badge.svg)](https://github.com/vimatech-io/laravel-invitations/actions/workflows/ci.yml)[![Latest Version on Packagist](https://camo.githubusercontent.com/b3734388f7c73c995e1a8b48c3ebe79f710a5726fdeedb8624d2f679c654eded/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f76696d61746563682f6c61726176656c2d696e7669746174696f6e2e737667)](https://packagist.org/packages/vimatech/laravel-invitation)[![Total Downloads](https://camo.githubusercontent.com/da9ec0d2e936d63a573473cccf95ee826a7e5e6e458d383fd439480009740451/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f76696d61746563682f6c61726176656c2d696e7669746174696f6e2e737667)](https://packagist.org/packages/vimatech/laravel-invitation)[![License](https://camo.githubusercontent.com/0c2551ed38fd36e96cd53a664fe7352f65b4cf1b8d996b5d54cddf5991e7b930/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f76696d61746563682f6c61726176656c2d696e7669746174696f6e2e737667)](https://packagist.org/packages/vimatech/laravel-invitation)

Generic email-based invitations for Laravel. Invite anyone to join, access, or accept an action related to any Eloquent model — Organization, Team, Project, Workspace, Document, and more.

Why Laravel Invitation?
-----------------------

[](#why-laravel-invitation)

- Invite users to **any Eloquent model** — not just teams
- Secure token-based workflow (HMAC by default)
- Framework-agnostic — no dependency on Jetstream, Breeze, or any starter kit
- Extensible acceptance handlers and custom notifications
- Production-ready with queued emails, i18n, and rate-limited routes

Quick Start
-----------

[](#quick-start)

```
// 1. Send an invitation
$invitation = Invitations::to('john@example.com')
    ->for($project)
    ->invitedBy(auth()->user())
    ->send();

// 2. Accept the invitation (via token from email)
Invitations::accept($token, auth()->user());
```

```
Invite → Email sent → User clicks link → Accept → Event dispatched

```

> **Subject** — The model being invited to (Project, Team, Organization, Workspace, etc.). Set via `->for($model)`. An invitation without a subject is a "global" invitation.

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 11, 12 or 13

Installation
------------

[](#installation)

```
composer require vimatech/laravel-invitation
```

### Publish the configuration file (optional)

[](#publish-the-configuration-file-optional)

```
php artisan vendor:publish --tag="invitation-config"
```

### Publish and run migrations

[](#publish-and-run-migrations)

```
php artisan vendor:publish --tag="invitation-migrations"
php artisan migrate
```

### Publish views (optional)

[](#publish-views-optional)

```
php artisan vendor:publish --tag="invitation-views"
```

Usage
-----

[](#usage)

### Basic invitation

[](#basic-invitation)

```
use Vimatech\Invitation\Facades\Invitations;

$invitation = Invitations::to('john@example.com')->send();
```

### Invitation to a User model

[](#invitation-to-a-user-model)

If you already have the user model, you can pass it directly — the email will be extracted automatically:

```
$invitation = Invitations::toUser($user)
    ->for($project)
    ->send();

// Or via the HasInvitations trait:
$project->inviteUser($user)->send();
```

### Invitation linked to a model

[](#invitation-linked-to-a-model)

```
$invitation = Invitations::to('john@example.com')
    ->for($project)
    ->invitedBy($currentUser)
    ->expiresInDays(7)
    ->withMeta(['role' => 'admin'])
    ->send();
```

### Using the `HasInvitations` trait

[](#using-the-hasinvitations-trait)

```
use Illuminate\Database\Eloquent\Model;
use Vimatech\Invitation\Concerns\HasInvitations;

class Project extends Model
{
    use HasInvitations;
}

// Then:
$project->invite('john@example.com')
    ->invitedBy($user)
    ->expiresInDays(10)
    ->withMeta(['role' => 'member'])
    ->send();

// List invitations
$project->invitations;
$project->pendingInvitations;
```

### Accepting an invitation

[](#accepting-an-invitation)

```
$invitation = Invitations::accept($token, $user);
```

### Accepting after registration (new user)

[](#accepting-after-registration-new-user)

```
// After user registration (verifies the invitation email matches the user's email):
$invitation = Invitations::acceptForNewUser($token, $newUser);
```

### Cancelling an invitation

[](#cancelling-an-invitation)

```
Invitations::cancel($invitation);
```

### Declining an invitation (by invitee)

[](#declining-an-invitation-by-invitee)

The invitee can actively refuse an invitation:

```
Invitations::decline($token);
```

### Resending an invitation

[](#resending-an-invitation)

Resend generates a new token and resets the expiration. Only pending or expired invitations can be resent — accepted and cancelled invitations will throw an exception.

```
Invitations::resend($invitation);
```

### Querying invitations

[](#querying-invitations)

```
use Vimatech\Invitation\Models\Invitation;

Invitation::pending()->get();
Invitation::accepted()->get();
Invitation::expired()->get();
Invitation::declined()->get();
Invitation::cancelled()->get();
Invitation::forEmail('john@example.com')->get();
Invitation::forSubject($project)->get();
Invitation::invitedBy($user)->get();
```

Metadata
--------

[](#metadata)

Store any custom data with an invitation:

```
$invitation = Invitations::to('john@example.com')
    ->withMeta(['role' => 'editor', 'department' => 'engineering'])
    ->send();

// Access later:
$invitation->meta['role']; // 'editor'
```

Expiration
----------

[](#expiration)

Invitations expire based on the `expires_after_days` config (default: 7 days). You can also set a custom expiration:

```
Invitations::to('john@example.com')
    ->expiresInDays(30)
    ->send();

// Or with a specific date:
Invitations::to('john@example.com')
    ->expiresAt(now()->addWeeks(2))
    ->send();
```

### No expiration

[](#no-expiration)

For use cases like friend requests where invitations should stay active indefinitely:

```
// Per invitation:
Invitations::to('jane@example.com')
    ->for($user)
    ->neverExpires()
    ->send();

// Or globally via config:
// 'expires_after_days' => null,
```

Duplicate Policy
----------------

[](#duplicate-policy)

By default, sending a second invitation to the same email for the same subject throws an `InvitationAlreadyExistsException`:

```
$project->invite('john@example.com')->send(); // ✅
$project->invite('john@example.com')->send(); // ❌ InvitationAlreadyExistsException
```

To allow duplicate pending invitations, set this in your config:

```
'duplicates' => [
    'allow_pending_for_same_email_and_subject' => true,
],
```

Events
------

[](#events)

The following events are dispatched:

EventWhen`InvitationCreated`Invitation record created`InvitationSent`Notification sent`InvitationAccepted`Invitation accepted`InvitationDeclined`Invitation declined by invitee`InvitationExpired`Expired invitation discovered during acceptance`InvitationCancelled`Invitation cancelled`InvitationResent`Invitation resent with new tokenAll events contain the `$invitation` property. `InvitationAccepted` also contains the `$user`.

### Listening to events

[](#listening-to-events)

```
use Vimatech\Invitation\Events\InvitationAccepted;

Event::listen(InvitationAccepted::class, function ($event) {
    $event->invitation->subject->members()->attach($event->user);
});
```

Custom Acceptance Handler
-------------------------

[](#custom-acceptance-handler)

### Via callback

[](#via-callback)

```
use Vimatech\Invitation\InvitationManager;

InvitationManager::acceptedUsing(function ($invitation, $user) {
    $invitation->subject->members()->attach($user, [
        'role' => $invitation->meta['role'] ?? 'member',
    ]);
});
```

### Via config

[](#via-config)

Create a class implementing the `AcceptsInvitations` contract:

```
use Vimatech\Invitation\Contracts\AcceptsInvitations;
use Vimatech\Invitation\Models\Invitation;
use Illuminate\Database\Eloquent\Model;

class MyAcceptanceHandler implements AcceptsInvitations
{
    public function accept(Invitation $invitation, ?Model $user = null): void
    {
        // Your logic here
    }
}
```

Then set it in config:

```
// config/invitation.php
'acceptance_handler' => App\Invitations\MyAcceptanceHandler::class,
```

Custom Notification
-------------------

[](#custom-notification)

You can customize the invitation email in several ways:

### Extend the default notification

[](#extend-the-default-notification)

```
use Vimatech\Invitation\Notifications\InvitationNotification;

class CustomInvitationNotification extends InvitationNotification
{
    protected function getSubjectLine(): string
    {
        return __('Join :team!', ['team' => $this->invitation->subject?->name]);
    }

    protected function getGreetingLine(): string
    {
        return __('You have been invited to collaborate.');
    }

    protected function getActionText(): string
    {
        return __('Accept Invitation');
    }
}
```

### Or create a fully custom notification

[](#or-create-a-fully-custom-notification)

```
// config/invitation.php
'notification' => App\Notifications\CustomInvitationNotification::class,
```

Your notification will receive the `Invitation` model and the plain token in its constructor.

### Translations

[](#translations)

All notification strings use Laravel's `__()` helper. Add translations via JSON files:

```
// lang/fr.json
{
    "You have been invited": "Vous avez été invité",
    "View Invitation": "Voir l'invitation",
    "This invitation will expire on :date.": "Cette invitation expirera le :date.",
    "Invited by: :name": "Invité par : :name"
}
```

Public Routes
-------------

[](#public-routes)

When `routes.enabled` is `true` (default), the package registers:

MethodURINameGET`/invitations/{token}``invitations.preview`POST`/invitations/{token}/accept``invitations.accept`POST`/invitations/{token}/decline``invitations.decline`Configure in `config/invitation.php`:

```
'routes' => [
    'enabled' => true,
    'prefix' => 'invitations',
    'middleware' => ['web'],
    'throttle' => 'throttle:30,1', // Per-IP rate limit. Set to null to disable.
],
```

### Authentication and routes

[](#authentication-and-routes)

The **preview page** (`GET`) is public — anyone with the link can view the invitation details.

The **accept route** (`POST`) does not enforce authentication by default. Two common patterns:

- **Existing user**: Add `auth` middleware, then call `Invitations::accept($token, auth()->user())`
- **New user**: Redirect to registration, then call `Invitations::acceptForNewUser($token, $newUser)` after signup — this verifies the registered email matches the invitation

To require authentication, add `auth` to the route middleware in config:

```
'middleware' => ['web', 'auth'],
```

Database Schema
---------------

[](#database-schema)

```
invitations
├── id
├── uuid
├── email
├── token_hash
├── subject_type / subject_id    (polymorphic, nullable)
├── inviter_type / inviter_id    (polymorphic, nullable)
├── accepted_by_type / accepted_by_id (polymorphic, nullable)
├── status                       (pending, accepted, declined, expired, cancelled)
├── expires_at
├── accepted_at
├── declined_at
├── cancelled_at
├── meta                         (JSON)
└── timestamps

```

Token Security
--------------

[](#token-security)

- Tokens are generated using `Str::random(64)`
- Tokens are hashed before storage using HMAC (default) or bcrypt
- HMAC (recommended): deterministic, allows direct DB lookup (O(1)), relies on `APP_KEY`
- Bcrypt: non-deterministic, requires iterating records (O(n)), resistant to DB leaks
- The plain token is only available at the moment of creation/sending
- Token verification uses constant-time comparison
- Route tokens are validated via regex constraint (`[a-zA-Z0-9]{64}`)

Configuration
-------------

[](#configuration)

Full config options in `config/invitation.php`:

```
return [
    'table' => 'invitations',
    'model' => \Vimatech\Invitation\Models\Invitation::class,
    'expires_after_days' => 7, // Set to null for invitations that never expire
    'notification' => \Vimatech\Invitation\Notifications\InvitationNotification::class,
    'acceptance_handler' => null,
    'routes' => [
        'enabled' => true,
        'prefix' => 'invitations',
        'middleware' => ['web'],
        'throttle' => 'throttle:30,1',
    ],
    'route_name' => 'invitations.preview',
    'url_generator' => null,
    'duplicates' => [
        'allow_pending_for_same_email_and_subject' => false,
    ],
    'token_strategy' => 'hmac', // 'hmac' (recommended) or 'hash'
];
```

Exceptions
----------

[](#exceptions)

All exceptions extend `InvitationException`:

- `InvitationNotFoundException` — Token invalid or no matching invitation
- `InvitationExpiredException` — Invitation has expired
- `InvitationAlreadyAcceptedException` — Already accepted
- `InvitationCancelledException` — Invitation was cancelled
- `InvitationDeclinedException` — Invitation was declined by invitee
- `InvitationAlreadyExistsException` — Duplicate pending invitation

Contributing
------------

[](#contributing)

See [CONTRIBUTING.md](CONTRIBUTING.md).

Changelog
---------

[](#changelog)

Please see [CHANGELOG.md](CHANGELOG.md) for recent changes.

Security
--------

[](#security)

If you discover a security vulnerability, please review our [security policy](SECURITY.md). **Do not** open a public GitHub issue.

Credits
-------

[](#credits)

Built and maintained by [Vimatech](https://vimatech.io). Created by [Adel Zemzemi](https://github.com/adelzemzemi).

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE) for more information.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance91

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 96.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

2

Last Release

42d ago

PHP version history (2 changes)v1.0.0PHP ^8.2

v1.1.0PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/3283664f06a0db1bfdbf282b2691363365c2f73569bcd99d63f5aaa52900ff55?d=identicon)[adelzemzemi](/maintainers/adelzemzemi)

---

Top Contributors

[![adelzemzemi](https://avatars.githubusercontent.com/u/272534830?v=4)](https://github.com/adelzemzemi "adelzemzemi (29 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

laravelemailInviteInvitation

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/vimatech-laravel-invitation/health.svg)

```
[![Health](https://phpackages.com/badges/vimatech-laravel-invitation/health.svg)](https://phpackages.com/packages/vimatech-laravel-invitation)
```

###  Alternatives

[spatie/laravel-health

Monitor the health of a Laravel application

88212.7M182](/packages/spatie-laravel-health)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M353](/packages/psalm-plugin-laravel)[spatie/laravel-permission

Permission handling for Laravel 12 and up

13.0k107.5M1.6k](/packages/spatie-laravel-permission)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M282](/packages/laravel-ai)[propaganistas/laravel-disposable-email

Disposable email validator

6023.2M7](/packages/propaganistas-laravel-disposable-email)[fleetbase/core-api

Core Framework and Resources for Fleetbase API

1239.7k25](/packages/fleetbase-core-api)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
