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
- pestphp/pest-plugin-laravel: ^3.0
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 | ❌ |
| 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)
License
The MIT License (MIT). Please see License File for more information.
Credits
Built and maintained by Vimatech. Created by Adel Zemzemi.