labrodev / laravel-team
Teams, memberships, invitations, roles and plan/billing mechanics for Laravel SaaS applications — the Labrodev multi-tenancy core.
Requires
- php: ^8.4
- illuminate/database: ^13.0
- illuminate/support: ^13.0
- spatie/laravel-data: ^4.23
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- driftingly/rector-laravel: ^2.5
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
README
Teams, memberships, invitations, roles and plan/billing mechanics for Laravel SaaS applications — the Labrodev multi-tenancy core.
The package owns the mechanism: teams with owner/admin/member roles, invitation
flow, a per-team subscription-plan caps snapshot (-1 = unlimited), plan checkout /
swap / cancel orchestration behind a gateway contract, and a team morph alias so
polymorphic rows (e.g. a billing provider's billable_type) never depend on class FQCNs.
The application owns the policy: its plan enum (cases, pricing, cap keys), the rules
that consume caps, and all UI. Payment providers are adapter packages implementing the
BillingClient contract — see labrodev/laravel-team-paddle.
Installation
composer require labrodev/laravel-team php artisan vendor:publish --tag=team-config php artisan vendor:publish --tag=team-migrations php artisan migrate
Wiring the application
1. User model
use Labrodev\Team\Concerns\HasTeams; use Labrodev\Team\Contracts\TeamMember; final class User extends Authenticatable implements TeamMember { use HasTeams; }
The users table needs a nullable current_team_id column.
2. Plan enum
Define a string-backed enum implementing Labrodev\Team\Contracts\PlanContract and point
config/team.php at it:
enum Plan: string implements PlanContract { case Free = 'free'; case Pro = 'pro'; public function label(): string { /* ... */ } public function isPaid(): bool { /* ... */ } /** @return array<string, mixed> */ public function caps(): array { return match ($this) { self::Free => ['monitors' => 3, 'seats' => 1], self::Pro => ['monitors' => -1, 'seats' => -1], }; } }
// config/team.php 'user_model' => App\Models\User::class, 'plan_enum' => App\Enums\Plan::class, 'free_plan' => 'free',
TeamCreate snapshots the free plan's caps() onto the team's subscription_plans row,
so later plan-definition changes never retro-affect existing teams. Read caps via
$team->subscriptionPlan->cap('monitors').
Applications that prefer typed cap columns can add them in their own migration on top of
the published subscription_plans table and bind their own snapshot logic.
3. Exception rendering
Package exceptions never construct a ValidationException. They implement one of two
contracts; register render mappings once (e.g. bootstrap/app.php):
use Labrodev\Team\Contracts\ProvidesFieldError; use Labrodev\Team\Contracts\ProvidesFlashError; $exceptions->render(fn (ProvidesFieldError $e, Request $r) => $r->expectsJson() ? response()->json(['message' => $e->userMessage(), 'errors' => [$e->field() => [$e->userMessage()]]], 422) : redirect()->back()->withErrors([$e->field() => $e->userMessage()])); $exceptions->render(fn (ProvidesFlashError $e, Request $r) => $r->expectsJson() ? response()->json(['message' => $e->userMessage()], 422) : redirect()->back()); // + your flash-toast mechanism
4. Billing gateway
Bind an adapter implementing Labrodev\Team\Contracts\BillingClient (or install
labrodev/laravel-team-paddle). With team.sandbox enabled, StartPlanCheckout
bypasses the gateway and applies the target plan directly — for local development
without a configured payment provider.
Usage
// Create a team (owner membership + plan snapshot + current-team switch, in one transaction) $team = app(TeamCreate::class)(user: $user, teamCreateData: new TeamCreateData(name: 'Acme')); // Invite / accept app(TeamInvitationCreate::class)(team: $team, user: $inviter, teamInvitationCreateData: $data); app(TeamInvitationAcceptedStatusUpdate::class)(teamInvitation: $invitation, user: $user); // Plans app(StartPlanCheckout::class)(team: $team, plan: Plan::Pro, customerName: ..., customerEmail: ..., returnUrl: ...);
Authorization: TeamPolicy ships with view/update/delete/member-management/manageBilling
abilities driven by TeamRole/TeamPermission.
Testing
composer check # pint --test, phpstan (level 7), rector --dry-run, pest
License
MIT — see LICENSE.md.