Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Cashier Billing Provider Laravel Package

maartenpaauw/filament-cashier-billing-provider

Integrate Laravel Cashier Stripe subscriptions into Filament multi-tenant panels. Configure plans in cashier.php and make your Billable model the tenant model; supports recurring subscriptions, trials, promo codes, tax IDs, and metered prices.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install

composer require maartenpaauw/filament-cashier-billing-provider

Check the version compatibility matrix before committing — the package majors track Filament majors (v3 of the package targets Filament 4/5, Laravel 12/13, PHP 8.5, Cashier 16). Pin accordingly in composer.json; a stray composer update that bumps Filament without the matching provider version is the most likely first-day breakage.

2. Make your Filament tenant the Billable model

This is the single most important constraint: the class implementing Billable must be the same class used as your panel's tenant model (typically Team, Company, Organization). If you currently bill the User, you have a migration ahead of you — subscriptions, stripe_id, and payment methods all move to the tenant table before the provider is useful.

3. Define plans in config/cashier.php

'plans' => [
    'default' => [
        'product_id' => env('CASHIER_STRIPE_SUBSCRIPTION_DEFAULT_PRODUCT_ID'),
        'price_id'   => env('CASHIER_STRIPE_SUBSCRIPTION_DEFAULT_PRICE_ID'),
        'trial_days' => 14,
        'allow_promotion_codes' => true,
        'collect_tax_ids' => true,
        'metered_price' => true,
    ],
],

The array key becomes the plan type unless you set type explicitly. You can also use a backed enum as the key. Keep the Stripe IDs in .env per environment (test mode vs live mode) and always php artisan config:clear after editing.

4. Wire it into the panel provider

use Maartenpaauw\Filament\Cashier\Stripe\BillingProvider;

public function panel(Panel $panel): Panel
{
    return $panel
        ->tenantBillingProvider(new BillingProvider('default'))
        ->requiresTenantSubscription();
}

First use case to prove out: a tenant that registers, lands on the panel, is redirected to the billing page, subscribes against the default plan, and gains access. If that loop works with Stripe test keys and the CLI forwarding webhooks, everything else is configuration.

Where to look first: config/cashier.php for plan shape, your AdminPanelProvider for enablement, and vendor/maartenpaauw/filament-cashier-billing-provider/src — the README is thin, and the BillingProvider constructor, the tenant repository, and the RedirectIfUserNotSubscribed middleware are the three files that explain actual behaviour.

Implementation Patterns

Start without enforcement, then gate. Add ->tenantBillingProvider(...) alone on a panel first so tenants can self-serve billing while you verify webhooks and the Stripe dashboard line up. Add ->requiresTenantSubscription() once subscription state is trustworthy — the middleware redirects unsubscribed tenants, and a half-configured Stripe account will lock everyone out of the panel.

Tier plans via multiple entries. Since v2.1.0 you can register several plans; give each a stable, semantic key ('starter', 'pro') and let the key double as the plan type. The panel renders the plan names/options from this array, so this config is effectively your pricing UI — keep the labels user-facing.

Keep types immutable. type defaults to the array key and is what subscription checks resolve against. Renaming a key or changing type after tenants have subscribed silently breaks both the billing page and the middleware, because existing Stripe subscriptions still point at the old product. Treat key names as database values: change the display name, never the key.

Choose one trial strategy. trial_days (a fixed number of days at checkout) and has_generic_trial (Cashier's generic trial, evaluated on the Billable model) are mutually exclusive — pick one, and if you use has_generic_trial remember to implement trialEndsAt() on the tenant model, otherwise the middleware's view of "subscribed" won't match Cashier's.

Layered on purpose. v3.0.0 introduced a layered architecture and a tenant repository (v1.6.1) to remove duplication. That means the BillingProvider is a thin Filament adapter over small, injectable pieces. When you need different behaviour — custom plan resolution, a filtered plan list, extra checkout options — read the layer below the adapter and bind your own implementation in a service provider rather than subclassing the Filament provider and copy-pasting it.

Metered plans need reporting. metered_price => true configures the checkout side, but you still own usage reporting via $tenant->reportUsage(). Wire that into whatever produces the billable event (API calls, seats, jobs) and test it against a Stripe test clock so you can actually see an invoice generate.

Gate beyond the panel. requiresTenantSubscription() only covers the panel. For routes outside Filament, add the shipped RedirectIfUserNotSubscribed middleware to the route group — since v3.0.1 it correctly passes through ordinary Response instances, so it composes with other middleware.

Local development loop:

stripe listen --forward-to localhost:8000/stripe/webhook
php artisan cashier:webhook --event=customer.subscription.updated

Use a Stripe test clock to fast-forward trials and renewal failures instead of waiting 14 days, and reset the tenant's subscription between manual test runs. In tests, fake at the Cashier level ($tenant->subscriptions) rather than hitting Stripe — the provider's own tests use Orchestra Testbench and are worth copying as a pattern.

Multiple panels, multiple postures. A common split: an internal/admin panel with no billing requirements and a customer-facing panel with ->requiresTenantSubscription(). The billing provider is per-panel, so this is one line each — don't force a single panel to be both.

Gotchas and Tips

  • Recurring subscriptions only. The README calls this out explicitly. No one-time payments, no invoices-as-products, no Checkout mode=payment. If your product needs credit top-ups, that's a separate Cashier integration, not a plan entry.
  • Plan config is authoritative, Stripe config is real. Mismatched price_id values between .env files are the #1 cause of "clicked subscribe, nothing happened." Cross-check the price ID and that it belongs to the product ID in the same entry.
  • v2.0.0 changed subscription detection from price ID to product ID. If you're upgrading from 1.x, read UPGRADING.md before deploying: multiple prices under one product now count as the same subscription. Plans that intentionally share a product will collide.
  • env() inside config/cashier.php is config-cached. After changing .env in staging or production you must php artisan config:cache again. Locally, config:clear is usually the fix for "the plan list is stale."
  • Enum plans are supported but the enum must be backed (string or int) and round-trip cleanly against the config key. ->value, not the case name, is what ends up in type.
  • Promotion codes and tax IDs are Checkout-session flags. allow_promotion_codes and collect_tax_ids do nothing if your Stripe account or the price doesn't permit them; verify in the dashboard rather than debugging the PHP.
  • Webhooks are non-negotiable. The middleware reads local Cashier tables, which are only current if Stripe webhooks are reaching /stripe/webhook and the signature secret matches. A tenant who cancels in the Stripe portal but retains panel access almost always has a webhook problem, not a provider problem.
  • Package major must match Filament major. v2.x is Filament 3, v3.x is Filament 4/5. Installing the wrong one produces confusing container/interface errors during panel boot rather than a clean composer conflict.
  • The README is the whole documentation. Budget time to read src/ for anything beyond the happy path — plan resolution order, the default plan fallback (added v1.3.0), and how the tenant repository resolves the current tenant. Grepping for config('cashier.plans is the fastest entry point.
  • Debugging redirect loops: put a breakpoint or logger() in RedirectIfUserNotSubscribed and dump $tenant->subscribed(), the resolved plan type, and the product ID it's checking. Nine times out of ten the subscription exists in Stripe but the local subscriptions row is missing its stripe_product value because webhooks never ran.
  • BillingProvider('default') is the fallback plan, not a hard requirement — v1.3.0 made the default key the implicit default. If you name your plans differently, either keep a default entry or pass the intended key explicitly.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels