Skip to content

Billing

Billing ships as laraspring/billing, a Composer package your application installs. It owns the plan catalogue, the subscription state behind it and the adapters that talk to payment providers; your edition owns every screen those flows put in front of a customer.

The first adapter is Stripe, built on Cashier. It is an adapter rather than the design: nothing above it knows what Stripe is, which is what lets a second provider be an addition rather than a rewrite.

What you get out of the box

FeatureWhere
A plan catalogue with per-interval pricesconfig/laraspring-billing.php
Hosted checkout and a hosted billing portalyour provider's pages
Subscription state kept current by webhookssubscriptions table
A provider-agnostic read APIBilling::subscribed()
Paywall for a pay-to-enter productmiddleware on the web group
Seat limits enforced by laraspring/organizationsMembershipQuota in laraspring/core
Pricing screen and a billing settings pageyour edition renders them

Who is billed

One decision shapes everything else, and it is a config key rather than a code change:

php
// config/laraspring-billing.php
'billable' => env('LARASPRING_BILLING_BILLABLE', 'organization'),

organization (the default) charges the active tenant. One card, one invoice, and everyone in the organization covered by it. The tenant is resolved through TenantResolver, so this package never mentions an organization and does not depend on laraspring/organizations.

user charges the signed-in individual, for a product where a seat is a person rather than a company.

The package never touches your models

Cashier's usual advice is to put its Billable trait on User and add four columns to the users table. A package that shipped billing cannot do that: it would be reaching into a model your application owns.

So laraspring/billing keeps its own Customer record instead, attached to whatever is paying through a polymorphic relation, with Cashier pointed at it via Cashier::useCustomerModel(). Your User model is untouched, your users table is untouched, and switching from billing organizations to billing users is a config key rather than a migration.

If you want your own name and address on the invoice rather than the billable's name and email, implement Laraspring\Billing\Contracts\ProvidesBillingDetails on the model being billed.

Asking what somebody is paying for

Four questions cover the whole surface, and none of them mentions a provider:

php
use Laraspring\Billing\Support\Billing;

Billing::subscribed();               // is the active billable paying for anything?
Billing::subscribed(null, 'pro');    // …specifically for the pro plan?
Billing::onTrial();                  // are they inside a trial?
Billing::plan();                     // the plan they are on, falling back to your free plan

Each takes the billable as an optional first argument and falls back to whatever is active, because the overwhelmingly common call is about the current request.

Billing::subscribed() is true during a trial and stays true after a cancellation until the period they paid for runs out. That is deliberate: cancelling is not an eviction, and a gate that read the raw status would lock somebody out of time they had already bought.

Everything is read from local state, never from the provider's API. This is called on every gated page load, and a paywall that made an HTTP request per request would make your provider's availability your application's availability. Webhooks are what keep that state current, so configuring them is not optional.

Who may spend the money

Checkout and the portal are behind an ability, Laraspring\Billing\Support\Billing::MANAGE. The package registers a permissive default, "anybody signed in may manage their own billing", which is right when the billable is the user and wrong the moment it is an organization.

The reference edition narrows it, and this is the pattern to copy: the edition is the only thing that knows both packages are installed, so it defines the ability and the package's default loses to it.

php
// app/Providers/AppServiceProvider.php
Gate::define(Billing::MANAGE, function (User $user, mixed $billable): bool {
    if (! $billable instanceof Organization) {
        return $user->is($billable);
    }

    return Roles::administers($billable->roleFor($user));
});

Seats

A plan can cap how many people an organization may have:

php
'pro' => [
    'seats' => 25,
],

laraspring/organizations enforces it, and does so without ever learning that a billing package exists. The number travels through Laraspring\Core\Contracts\MembershipQuota, a contract declared in laraspring/core, answered "no limit" by default, and implemented by laraspring/billing off the plan the tenant is paying for.

A pending invitation holds a seat, so an administrator cannot send four links for three places; the person an invitation was sent to is checked against members alone, or the last seat on a plan could never be filled.

Where things live

packages/billing/
  config/laraspring-billing.php     the plan catalogue and every switch
  src/Contracts/              BillingProvider, HandlesWebhooks, the screen renderer
  src/Plans/                  the catalogue, parsed and validated
  src/Subscriptions/          the normalised subscription every screen sees
  src/Stripe/                 the Stripe adapter, and the only file that knows Cashier
  src/Models/Customer.php     the record that keeps Cashier off your user model

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