labrodev / laravel-team-paddle
Paddle billing adapter for labrodev/laravel-team — Cashier-Paddle gateway, Billable team model, and webhook plan sync.
Requires
- php: ^8.4
- illuminate/support: ^13.0
- labrodev/laravel-team: ^1.0
- laravel/cashier-paddle: ^2.8
- 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
Paddle billing adapter for labrodev/laravel-team — a Cashier-Paddle implementation of the BillingClient seam, a Billable team model, and webhook-driven plan synchronization.
Requirements
- PHP 8.4+
- Laravel 13
- labrodev/laravel-team (installed automatically)
- laravel/cashier-paddle ^2.8 (installed automatically)
Installation
composer require labrodev/laravel-team-paddle
Publish and run the Cashier Paddle migrations (customers, subscriptions, subscription items, transactions):
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate
Publish the package config:
php artisan vendor:publish --tag="team-paddle-config"
Configuration
1. Use the billable team model
Point laravel-team at the shipped Billable team model in config/team.php:
'team_model' => Labrodev\TeamPaddle\Models\BillableTeam::class,
Every team the package resolves then carries Cashier's Billable surface, and the team morph alias (used by Cashier's billable_type columns) maps to it. You may also subclass BillableTeam and configure your own class instead.
2. Map plans to Paddle prices
config/team-paddle.php maps your plan enum backing values to Paddle price ids:
'prices' => [ 'pro' => env('PADDLE_PRICE_PRO'), ],
Plans without an entry (e.g. the free plan) cannot be checked out — StartPlanCheckout downgrades to free by cancelling the subscription instead. Attempting to check out a plan missing from the map throws PaddlePriceNotConfiguredException (an internal misconfiguration error, not a validation concern).
'subscription_type' => 'default',
is the Cashier subscription type used for the single plan subscription.
3. Configure Cashier and the webhook
Follow the Cashier Paddle documentation for the Paddle API key, client-side token, sandbox mode, and — critically — the webhook: Cashier registers a /paddle/webhook route that persists subscription state and fires the Laravel\Paddle\Events\Subscription* events this package listens to. Point your Paddle notification destination at that route and set PADDLE_WEBHOOK_SECRET.
What the package wires up
Labrodev\Team\Contracts\BillingClientis bound toLabrodev\TeamPaddle\PaddleBillingGateway, solaravel-team'sStartPlanCheckout(checkout creation, plan swaps, cancellation, subscription checks) runs against Paddle with no app code.SubscriptionCreated,SubscriptionUpdated,SubscriptionCanceled, andSubscriptionPausedare handled byLabrodev\TeamPaddle\Listeners\SyncTeamPlanFromPaddle, which resolves the effective plan from the subscription's price and applies it viaTeamPlanUpdate(plan + caps snapshot). Canceled, paused, or otherwise invalid subscriptions fall back to the configured free plan.
Reacting to plan changes
When a webhook actually changes a team's effective plan, the package dispatches Labrodev\TeamPaddle\Events\TeamPlanSynced:
final readonly class TeamPlanSynced { public function __construct( public Team $team, public PlanContract $previousPlan, public PlanContract $newPlan, ) {} }
Owner notifications and similar side effects are application infrastructure — listen to this event in your app:
Event::listen(TeamPlanSynced::class, NotifyOwnerOfPlanChange::class);
Testing
composer check # pint --test, phpstan (level 7), rector --dry-run, pest
License
MIT — see LICENSE.md.