laravel-team-paddle maintained by labrodev
laravel-team-paddle
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.