Skip to content

Stripe

Stripe is the first provider adapter, built on Laravel Cashier. Cashier is entirely contained in one class, Laraspring\Billing\Stripe\StripeBillingProvider; nothing above it mentions a price, a proration or an incomplete payment.

Setting it up

dotenv
LARASPRING_BILLING_PROVIDER=stripe

STRIPE_KEY=pk_live_...
STRIPE_SECRET=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

LARASPRING_BILLING_PRO_MONTHLY_PRICE_ID=price_...
LARASPRING_BILLING_PRO_YEARLY_PRICE_ID=price_...

Create one Stripe product per plan and one price per interval, then put each price id in your catalogue. Run the migrations and you are done: the tables are shipped by the package.

What the adapter does, and does not

Checkout and the portal are Stripe's own hosted pages. Card details never reach your application, and 3-D Secure, wallets, tax, proration, dunning and receipts are Stripe's problem rather than a second billing UI in your codebase.

Reads never leave your machine. Asking what somebody is subscribed to reads the subscriptions table that Cashier's webhook handler keeps current. Stripe being slow makes checkout slow; it must never make your dashboard slow, and it must never make your dashboard unavailable.

That is why the provider contract has three methods — checkout, portal, read state — and no cancel(), swap() or resume(). Every one of those already exists in the portal, done properly and localised.

Webhooks

Configure them. They are what keeps local state current, which is what every gate in your application reads. Point Stripe at:

https://your-app.test/billing/webhook

The route is named billing.webhook whichever adapter you use, so changing provider does not change the URL an operator already configured.

It is mounted outside the web middleware group on purpose. The request comes from a server rather than a browser: it has no session and no CSRF token to present, and asking it for one would reject every webhook. What authenticates it is the Stripe signature, verified by Cashier's controller when STRIPE_WEBHOOK_SECRET is set.

Because laraspring/billing moved the customer off your user model, the sync lands on the package's Customer record — matched on stripe_id — rather than on a User row that has no such column.

Subscription status

Stripe's eight statuses are normalised onto a small set the rest of the kit understands. Two mappings are worth knowing:

  • past_due still entitles. A card that failed once is being retried; the customer keeps their access while Stripe dunning runs.
  • canceled entitles until ends_at. Cancelling at period end leaves paid time on the subscription, and a gate that read the status alone would take it away.

A status Stripe adds after this was written falls back to Cashier's own judgement rather than being guessed at, so a new status cannot silently evict paying customers.

Subscriptions started outside your application

One created by hand in the Stripe dashboard, or one on a price that has since been dropped from your catalogue, is real and keeps entitling its customer; it simply has no logical plan to name. Billing::subscribed() is true, and Billing::subscribed(null, 'pro') is false, which is the right answer for both questions.

Testing without Stripe

The package's own suite never reaches the Stripe API, and neither should yours. Every screen, the paywall and the seat quota depend only on Laraspring\Billing\Contracts\BillingProvider, so bind a stand-in:

php
$this->app->singleton(BillingProvider::class, StubBillingProvider::class);

tests/Fixtures/StubBillingProvider.php is a copyable example. What is left to test against the real adapter — that a checkout session is created with the arguments you meant — is worth doing once by hand against Stripe's test mode, and is not worth a test double asserting that a fake was called the way you told it to expect.

The tables

TableWhat it holds
billing_customersOne row per billable, with its Stripe customer id
subscriptionsCashier's schema, keyed to customer_id
subscription_itemsCashier's schema

Do not publish Cashier's migrations

laraspring/billing ships its own copies. Cashier's create_customer_columns migration adds billing columns to your users table, which this package deliberately never does, and its subscriptions migration would collide with the copy shipped here.

Laraspring is a commercial starter kit. Buying it gets you the source.