tomatophp/filament-subscriptions
Filament plugin for managing subscription plans and recurring subscriptions in Laravel admin panels. Define plans and features, assign subscribers, and control access from a clean Filament interface that plugs into your existing billing workflow.
Step 0 — verify the stack before you write a line of code. This package is a passenger on the Filament major version, not a driver. Confirm filament/filament: ^5.0, laravel/framework: ^12.0, php: ^8.2, and laravelcm/laravel-subscriptions: ^1.8 all coexist in composer.json before installing. If you're on Filament v3 or v4, stop — v5.0.0 is not a supported target, and the 1.x / v3 branch is branch-only maintenance, not an LTS line.
Step 1 — install and pin. Require the package, then immediately replace caret ranges with exact versions for all three of Filament, this package, and laravelcm/laravel-subscriptions. Pin them in one commit. The single biggest day-to-day headache in this dependency is a drift between the plugin and the underlying subscriptions library, and exact pins are the cheapest insurance.
Step 2 — publish and migrate. Publish the migrations, config, and translations, then run the package's install command rather than hand-rolling setup. v5.0.0 fixed the ordering bug where the main plan was created before migrations ran, so the install command is now the supported path for throwaway environments, CI seeds, and fresh-tenant bootstrap. Run it end-to-end once in a scratch database to see exactly what it creates before you run it on anything real.
Step 3 — register the plugin on the panel. Add the plugin to your PanelProvider alongside the resources/pages you actually want exposed. This is where you make the first real scoping decision: not every panel needs plan CRUD. An admin panel probably gets everything; a customer-facing panel gets the billing page and the subscription widget only.
Step 4 — look at the domain library first, not the plugin. The plugin ships Filament resources, pages, widgets, and translations. Everything about how subscriptions actually behave — recurring periods, plan swapping, feature lookups, grace periods — lives in laravelcm/laravel-subscriptions. Read that package's trait and model API first; the Filament layer is a thin, well-behaved skin over it. If you find yourself fighting the plugin's UI to change subscription semantics, you're in the wrong layer.
First use case — gate one thing end to end. The fastest way to understand the whole stack is:
exports.csv, with a limit).User model.$user->subscribeTo($plan)).Tables\Actions\Action::make('export')
->visible(fn (): bool => auth()->user()->hasFeature('exports.csv')),
That loop — plan → feature → subscription → gate — is the entire product. Everything else is polish on it.
Treat the plan/feature catalog as your source of truth, or don't adopt this. The package assumes plans and features are the entitlement model. If you already have entitlements in a feature-flag service, a CRM, or a warehouse, adopting this means you now own a reconciliation job forever. Decide explicitly: either migrate entitlements into plans/features, or budget for sync. Don't discover this at launch.
Centralize feature checks behind one method. Do not sprinkle hasFeature('slug') strings through resources, policies, and Blade templates. Wrap it once:
// app/Support/Entitlements.php
public static function allows(string $feature): bool
{
return auth()->user()?->hasFeature($feature) ?? false;
}
Then call Entitlements::allows('exports.csv') everywhere. When the underlying library's API shifts between minor releases, or when you need to layer a kill-switch on top, you change one file instead of forty. This also gives you a single place to add request-level caching if feature lookups become a query hot spot in table row loops.
Gate at the boundary that matches the failure mode. Three distinct gates, three distinct implementations:
->visible(fn () => Entitlements::allows('x')) hides the button. Cosmetic only; never rely on it for security.->authorize() on the action/resource. This is the real gate for anything that mutates data.Feature-limited quantities (seats, exports per month) need a fourth thing the package does not give you: a usage counter. The package stores limits on the feature; enforcing consumption over time is yours to build.
Billing page: verify it behind your real panel path before demoing. v5.0.0 fixed panel URL resolution and redirects, but "fixed on the default path" is not "fixed behind a subdomain-tenant proxy with a path prefix." Test the billing page on your actual deployment shape early. If redirects land on the wrong host, you want to find that in staging, not in a customer call. Where the plugin exposes configurable subscription models, use them — don't accept the library defaults if your domain model differs.
Upgrade/downgrade flows belong to you. The package gives you the screens to create plans, edit features, and manage subscription records. It does not give you a proration engine, a mid-cycle swap policy, or a "downgrade at period end" orchestration. Decide those rules in your own service class and use the plugin as the admin surface for the results. Anything resembling a money movement should never originate from a Filament form action you wrote in five minutes.
Seed plans, don't click them. Because the install command now migrates before creating the main plan, automated provisioning works. Extend that: a seeder that idempotently creates your plan catalog (update-or-create on a stable slug) means every preview environment, CI run, and new tenant starts with the same catalog. Manual plan creation is a smell — it makes staging and production silently diverge on entitlement slugs, which produces bugs that only appear in one environment.
Multi-locale catalog editing is a workflow, not a feature. Plan and feature edits fill every translation. That's excellent for consistency and terrible for anyone who expected to leave a locale blank on purpose. If you plan a multilingual catalog, brief the content owners before the first demo: there is no "partial translation" state. Plan your locale rollout so a new locale is added only when the copy for it exists.
Widgets and dashboards. The package's widgets are a good default for an admin overview (active, canceled, expiring). Drop them into the dashboard early — they double as a smoke test that the package's queries run against your real data volume. Rename or scope them if your panel is customer-facing; a customer should never see aggregate subscription counts.
Test the gate, not the plugin. You don't need to test that Filament renders a resource — upstream does that. Test your entitlement wrapper against the states that matter: no subscription, expired subscription, canceled-but-not-yet-ended, plan without the feature, plan with the feature at limit. Assert the authorization path, not the visibility path.
Pin all three versions and bump them together. Filament, this package, and laravelcm/laravel-subscriptions move as a unit. Bumping the plugin without the library (or vice versa) is the most likely source of a confusing runtime failure. One commit, three version lines, one PR.
cancels_at is gone — hunt down every reader. Renewals no longer write it. Any report, export, dashboard widget, API resource, or downstream ETL that reads cancels_at is now reading nothing (or worse, stale data). Do a repository-wide grep before upgrading, not after. Then check whether the migration drops the column or leaves it orphaned — an orphaned column that silently retains old values is worse than one that errors, because it fails quietly. If you use SELECT * anywhere in exports, audit those too.
Translation auto-fill may be more aggressive than "fill the blanks." Verify with a test: set a non-empty translation for a locale, edit the plan's base name, and see whether the locale-specific value survives. If it's overwritten, every plan edit silently reverts translated copy to the source language. This is a low-severity bug that becomes a high-visibility embarrassment on a multilingual demo. Confirm the behavior empirically on your version, and if it overwrites, route plan edits through a controlled admin process with a review step.
Laravel 13 is permitted by composer.json, which is not the same as tested. Treat it as unproven until you exercise it yourself. On a fresh project this is a cheap experiment; on an existing one, let someone else find the edge cases first.
The install command is the supported path — don't reinvent it. If you hand-write migrations and seed the default plan yourself, you inherit the ordering bug the release fixed. Also be aware that re-running install commands on non-empty databases is the classic way to get duplicate default plans; make your own seeders idempotent so a re-run is safe.
Single maintainer, no LTS. 64 stars and zero dependents is the honest signal: this is buy-acceleration, not outsourced infrastructure. Budget for a fork. Practically, that means: keep your extension points in your code (the entitlement wrapper, a service class for plan transitions), not inside published package files you'd have to patch on every update. Never edit vendor/. If you must override a Filament resource or page, register your own subclass in the panel rather than publishing and editing.
Extension points to prefer, roughly in order of safety:
config/.Version-lockstep is a scheduling constraint, not a technical one. This package's major tracks Filament's major. That means a Filament upgrade is an all-or-nothing program with this plugin on the critical path, and it must never be the reason you can't upgrade. When you evaluate a Filament major, add this package and the subscriptions library to the estimate from day one.
Assign one owner. Knowledge of subscription state semantics (active vs. canceled-but-not-yet-ended vs. expired vs. on grace period) fragments fast across two upstream issue trackers and three models. One engineer owns the domain, writes the entitlement wrapper, and is the person who reads the release notes before a version bump.
Don't let it become your billing system. No payment-provider abstraction, no invoicing, no tax, no dunning, no reconciliation. If the requirement is "take a card and charge it monthly," this package is the admin UI for the result of that, and you need a real billing provider underneath. Recognizing that boundary early prevents a scope conversation that's very hard to reverse.
How can I help you explore Laravel packages today?