Appearance
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
| Feature | Where |
|---|---|
| A plan catalogue with per-interval prices | config/laraspring-billing.php |
| Hosted checkout and a hosted billing portal | your provider's pages |
| Subscription state kept current by webhooks | subscriptions table |
| A provider-agnostic read API | Billing::subscribed() |
| Paywall for a pay-to-enter product | middleware on the web group |
| Seat limits enforced by laraspring/organizations | MembershipQuota in laraspring/core |
| Pricing screen and a billing settings page | your 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 planEach 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