vimatech / laravel-membership
Polymorphic memberships for Laravel.
Requires
- php: ^8.3
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- pestphp/pest: ^3.0 || ^4.0
- pestphp/pest-plugin-laravel: ^3.0 || ^4.0
This package is auto-updated.
Last update: 2026-06-26 19:43:04 UTC
README
Polymorphic memberships for Laravel.
Laravel Membership lets you attach members and roles to any Eloquent model — organizations, teams, projects, workspaces, communities, or anything else.
It answers who belongs to what and with which role — nothing more.
Why Laravel Membership?
Most Laravel apps eventually need to answer:
- Who belongs to this organization?
- What role does this user have in this project?
- Can we prevent removing the last owner?
- Can the same membership logic work for teams, projects and workspaces?
Laravel Membership provides a small backend-only layer for that.
Feature Matrix
| Feature | Supported |
|---|---|
| Polymorphic memberships | ✅ |
| Enum roles | ✅ |
| Role hierarchy | ✅ |
| Guards (last owner, etc.) | ✅ |
| Events | ✅ |
| Scopes | ✅ |
| Soft deletes | ✅ |
| Policy helpers | ✅ |
| Invitations | ➕ (use Laravel Invitation) |
| Permissions | ❌ (use Spatie) |
| Billing | ❌ |
| UI | ❌ |
Laravel Membership vs Permissions
Laravel Membership manages:
- who belongs to what
- which role they have
Permission packages (like Spatie) manage:
- what users can do
They are complementary, not competing.
Use Cases
- SaaS organizations
- Teams
- Projects
- Workspaces
- Communities
- Collaborative apps
- Multi-tenant applications
- Agency client portals
- Internal company tools
Installation
Requirements
- PHP 8.3+
- Laravel 11, 12 or 13
composer require vimatech/laravel-membership
Publish config
php artisan vendor:publish --tag=membership-config
Publish migrations
php artisan vendor:publish --tag=membership-migrations php artisan migrate
Usage
Make a model have members
use Vimatech\Membership\Concerns\HasMembers; class Organization extends Model { use HasMembers; }
Make a model act as a member
use Vimatech\Membership\Concerns\HasMemberships; class User extends Authenticatable { use HasMemberships; }
Create a roles enum (optional)
use Vimatech\Membership\Contracts\MembershipRole; enum OrganizationRole: string implements MembershipRole { case Owner = 'owner'; case Admin = 'admin'; case Member = 'member'; public function level(): int { return match ($this) { self::Owner => 100, self::Admin => 50, self::Member => 10, }; } public function label(): string { return match ($this) { self::Owner => 'Owner', self::Admin => 'Admin', self::Member => 'Member', }; } }
You can also use plain strings — enums are optional.
Add a member
$organization->addMember($user, OrganizationRole::Owner); // With metadata $organization->addMember($user, OrganizationRole::Member, metadata: ['source' => 'invitation']); // With invited_by $organization->addMember($user, OrganizationRole::Member, invitedBy: $currentUser);
Remove a member
$organization->removeMember($user);
Update a role
$organization->updateMemberRole($user, OrganizationRole::Admin);
Check membership
// From entity side $organization->hasMember($user); $organization->isAdmin($user); $organization->hasMemberWithRole($user, OrganizationRole::Owner); // From member side $user->isMemberOf($organization); $user->hasRole($organization, OrganizationRole::Admin); $user->hasAnyRole($organization, [OrganizationRole::Admin, OrganizationRole::Owner]); $user->hasRoleAtLeast($organization, OrganizationRole::Member);
Query members
$organization->members(); $organization->membersWithRole(OrganizationRole::Admin); $organization->admins(); $organization->owners();
Query memberships from member side
$user->memberships()->get(); $user->membershipFor($organization); $user->ownedMemberships()->get(); $user->adminMemberships()->get(); $user->membershipables();
Scopes
use Vimatech\Membership\Models\Membership; Membership::forMember($user)->get(); Membership::forMembershipable($organization)->get(); Membership::withRole(OrganizationRole::Admin)->get(); Membership::withAnyRole([OrganizationRole::Admin, OrganizationRole::Owner])->get(); Membership::owners()->get(); Membership::admins()->get(); Membership::joined()->get(); Membership::recent()->get();
Policy helpers
use Vimatech\Membership\Support\MembershipGate; // In a Policy public function update(User $user, Project $project): bool { return MembershipGate::for($user)->isAdmin($project); } public function delete(User $user, Organization $organization): bool { return MembershipGate::for($user)->isOwner($organization); } public function view(User $user, Project $project): bool { return MembershipGate::for($user)->isMemberOf($project); } // Check a specific role MembershipGate::for($user)->hasRole($project, 'editor');
Facade (optional)
use Vimatech\Membership\Facades\Membership; Membership::add($user, $organization, 'admin'); Membership::remove($user, $organization); Membership::has($user, $organization); Membership::role($user, $organization); // returns 'admin'
Complete Example
use Vimatech\Membership\Concerns\HasMembers; use Vimatech\Membership\Concerns\HasMemberships; use Vimatech\Membership\Contracts\MembershipRole; // 1. Define your models class Organization extends Model { use HasMembers; } class User extends Authenticatable { use HasMemberships; } // 2. Define your roles enum OrganizationRole: string implements MembershipRole { case Owner = 'owner'; case Admin = 'admin'; case Member = 'member'; public function level(): int { return match ($this) { self::Owner => 100, self::Admin => 50, self::Member => 10, }; } public function label(): string { return $this->name; } } // 3. Use it $organization = Organization::create(['name' => 'Vimatech']); $user = User::create(['name' => 'Adel']); $organization->addMember($user, OrganizationRole::Owner); $user->isMemberOf($organization); // true $user->hasRole($organization, OrganizationRole::Owner); // true $user->hasRoleAtLeast($organization, OrganizationRole::Admin); // true $organization->isAdmin($user); // true (owner is admin) $organization->owners(); // Collection with $user
Events
The following events are dispatched:
| Event | When |
|---|---|
MemberAdded |
After a member is added |
MemberRemoved |
After a member is removed |
MemberRoleUpdated |
After a member's role is changed |
Each event contains the Membership instance and an optional $actor.
Guards
Guards are configurable protections in config/membership.php:
'guards' => [ 'prevent_removing_last_owner' => true, // Enabled by default 'prevent_removing_last_admin' => false, 'prevent_self_demotion' => false, 'prevent_role_escalation' => false, ],
When prevent_self_demotion or prevent_role_escalation are enabled, pass an $actor:
$organization->updateMemberRole( member: $user, role: OrganizationRole::Member, actor: $currentUser, );
Configuration
// config/membership.php return [ 'models' => [ 'membership' => \Vimatech\Membership\Models\Membership::class, ], 'tables' => [ 'memberships' => 'memberships', ], // Fallback role levels (used when roles are plain strings) 'roles' => [ 'owner' => 100, 'admin' => 50, 'member' => 10, ], 'owner_roles' => ['owner'], 'admin_roles' => ['owner', 'admin'], 'guards' => [ 'prevent_removing_last_owner' => true, 'prevent_removing_last_admin' => false, 'prevent_self_demotion' => false, 'prevent_role_escalation' => false, ], 'soft_deletes' => false, ];
Soft Deletes
By default, removing a member permanently deletes the row. To keep membership history instead, enable soft deletes:
-
Set
'soft_deletes' => trueinconfig/membership.php -
If your app is already in production, create a migration:
php artisan make:migration add_soft_deletes_to_memberships_table
Schema::table('memberships', function (Blueprint $table) { $table->softDeletes(); });
If you enable
soft_deletesbefore running your initial migration, the column is added automatically.
Once enabled:
$organization->removeMember($user); // soft deletes (sets deleted_at) $membership = Membership::withoutGlobalScopes()->forMember($user)->first(); $membership->restore(); // restores the membership $membership->forceDelete(); // permanently deletes $membership->trashed(); // true if soft deleted
Philosophy
Laravel Membership is intentionally minimal.
The package focuses on:
- Memberships
- Roles
- Role hierarchy
- Membership guards
Design principles:
- Backend-only, UI agnostic
- No auth assumptions
- No User model assumptions
- No permissions system
- No billing assumptions
- Enum-friendly roles
- Polymorphic by default
- Laravel-native API
- Clean and testable actions
It does not aim to become a permissions framework, a billing system, a UI framework, or a complete SaaS platform.
Possible Future Extensions
- Invitation bridge
- Audit logs
- Membership expiration
- Filament integrations
- Livewire components
Future extensions may be released as separate packages to keep the core package small and focused.
Testing
composer test
Contributing
Contributions are welcome.
Please ensure:
- Tests pass (
composer test) - PHPStan passes (
composer analyse) - Code style is formatted with Pint (
composer format)
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our Security Policy for reporting vulnerabilities.
License
The MIT License (MIT). Please see License File for more information.
Credits
Built and maintained by Vimatech. Created by Adel Zemzemi.