renoki-co/jetstream-cashier-billing-portal
A Laravel package that adds a ready-made billing portal to Jetstream apps using Cashier, letting customers manage subscriptions, payment methods, and invoices from a dedicated account area.
This release (4.2.0) is effectively a platform bump disguised as a minor: it pulls in Cashier Stripe 13.x and Cashier Register 6.0, and drops Laravel 7.x. Treat the upgrade as a breaking change even though the package version only moved a patch digit.
Prerequisites before you touch composer:
composer.json for anything else pinning laravel/cashier to 12.x — a transitive constraint will block the update silently.Minimal setup path:
composer require <vendor>/<package>^4.2
php artisan vendor:publish --tag=<package>-config
php artisan vendor:publish --tag=<package>-migrations
php artisan migrate
Environment keys you will need in .env (at minimum):
STRIPE_KEY=
STRIPE_SECRET=
STRIPE_WEBHOOK_SECRET=
Where to look first:
config/<package>.php — plan/feature definitions or the model bindings the package uses.config/cashier.php — currency, webhook tolerance, tax settings. Cashier 13 changed several of these keys; diff against the published stub rather than assuming your old file is still valid.config/cashier-register.php — the Cashier Register 6.0 config. This is where plan/feature model classes and table names are bound.First real use case: define one monthly plan with one feature gate, sync it to Stripe in test mode, then subscribe a seeded user through the package's checkout flow and confirm the feature check flips after the webhook fires. That single loop exercises config, migrations, Stripe sync, webhook handling, and feature resolution — if all five work, the upgrade is sound.
Separate definition from synchronization. Keep plan and feature definitions in config (or a seeder) and treat the Stripe sync command as the only thing that writes to Stripe. Sync should be idempotent — running it twice must not create duplicate products or prices. In Cashier 13 / Cashier Register 6, plans map to Stripe prices under products, so re-running sync after a price change should archive the old price rather than delete it (Stripe prices are immutable).
Gate features, not plan names. The durable pattern is $user->hasFeature('api-access') (or the package's equivalent) rather than $user->subscribedToPlan('pro'). Plan names change during pricing experiments; feature keys don't. Wrap the raw call in a small app-level service or a can() ability so you can swap the implementation later:
Gate::define('use-exports', fn (User $user) => $user->hasFeature('exports'));
Then use @can('use-exports') in Blade and can: middleware on routes — no package API leaking into views.
Webhooks are the source of truth, not the redirect. The checkout success URL fires before the webhook does, so the user often lands back on your app with a subscription that isn't recorded yet. Provision the bare minimum on redirect (show a "processing" state) and let WebhookReceived/WebhookHandled listeners do the entitlement grants. If you must show status immediately, poll or use Cashier::fake()-backed optimistic UI — do not assume the DB is current.
Sync via Stripe CLI in development. Cashier 13's webhook signature verification and event tolerance are strict; the only sane local workflow is:
stripe listen --forward-to localhost:8000/stripe/webhook
Bind the printed whsec_... to STRIPE_WEBHOOK_SECRET in .env.local. Do not disable signature verification to make local testing work — you will forget to re-enable it.
Test with fakes, not the network. Cashier ships Cashier::fake(); Cashier Register has its own plan/feature assertions. Build a factory trait that gives you a subscribed user with a feature set:
$user = User::factory()->subscribedTo('pro')->create();
Keep Stripe calls out of the default test suite entirely — they're slow and flaky under CI.
Upgrade pattern for the Cashier 13 jump. Do the dependency bump on an isolated branch with the full test suite green before touching application code. Then work through the Cashier 13 guide's breaking changes one at a time, committing per change. Common casualties:
Billable trait behaviour around invoice creation and invoiceFor() signatures.cashier.api_version, remove the override and let Cashier manage it.swap() / swapAndInvoice() semantics changed around proration.4.2.0 is a major dependency upgrade wearing a minor version number. Semver on the package is not semver on the ecosystem. Pin exactly (^4.2) and read the upgrade guide before composer update. If you have other packages that depend on Cashier or Cashier Register, resolve them all in one composer run — partial updates produce confusing "class not found" errors from stale autoload maps.
Laravel 7 is gone. If any part of your stack — a Nova version, a first-party package, a PHP constraint — was holding you on Laravel 7, this release is not available to you. Check composer why-not laravel/framework before assuming the update failed for another reason.
Config files don't auto-refresh. vendor:publish won't overwrite an existing config. After upgrading, diff your published config/cashier.php and config/cashier-register.php against the package stubs. Cashier 13 renamed and removed keys, and a leftover key from 12.x usually fails silently — the setting just stops applying.
Migrations need a review, not a blind migrate. Cashier Register 6.0 and Cashier 13 both ship migration changes. Run php artisan migrate --pretend first. On production, back up the subscriptions, subscription_items, and any plan/feature tables before migrating.
Cache the plan/feature map, then invalidate it. Plans and features are read on nearly every request. Cache them; but make sure your sync command busts that cache, or you'll spend an hour debugging why a feature you just added doesn't gate anything.
Model bindings are a common 6.0 break. If you extended the package's Plan or Feature models, verify the parent classes still exist under the same namespace and that your config/cashier-register.php model mappings point at them.
Duplicate Stripe customers. Cashier creates the customer lazily. If you call $user->createAsStripeCustomer() in a registration observer and the package's checkout flow also creates one, you get orphaned customers and mismatched subscriptions. Let one code path own customer creation.
Webhook route and CSRF. The webhook endpoint must be excluded from CSRF and from your auth middleware. Cashier 13's default route registration is fine, but custom route setups frequently break this. Also check your queue: if webhook listeners are queued, a worker outage means subscriptions silently don't activate.
Debugging order when "the user paid but has no access":
stripe listen / the Stripe dashboard show the event delivered and 2xx?webhook_events / stripe_webhook_logs table?Extension points to prefer over forking: the package's events (WebhookReceived, WebhookHandled), the config-driven model bindings, and the sync command's options. Overriding the checkout controller is a last resort — it's the file most likely to change again in 4.3.
How can I help you explore Laravel packages today?