Skip to content

Plans

Your catalogue lives in config/laraspring-billing.php. The array key is the plan's logical id, and that id is the only name the rest of your application ever uses: a feature gate says pro, never a Stripe price identifier, so rotating a price in your provider's dashboard is a change to one line here.

php
'plans' => [

    'free' => [
        'name' => 'Free',
        'description' => 'Everything you need to try the product with a small team.',
        'free' => true,
        'seats' => 3,
        'features' => ['Up to 3 members', '1 project', 'Community support'],
    ],

    'pro' => [
        'name' => 'Pro',
        'featured' => true,
        'seats' => 25,
        'trial_days' => 14,
        'features' => ['Up to 25 members', 'Unlimited projects', 'Audit log'],
        'prices' => [
            'monthly' => [
                'amount' => 2900,
                'currency' => 'usd',
                'stripe_price_id' => env('LARASPRING_BILLING_PRO_MONTHLY_PRICE_ID'),
            ],
            'yearly' => [
                'amount' => 29000,
                'currency' => 'usd',
                'stripe_price_id' => env('LARASPRING_BILLING_PRO_YEARLY_PRICE_ID'),
            ],
        ],
    ],

    'enterprise' => [
        'name' => 'Enterprise',
        'contact' => true,
        'contact_url' => 'mailto:sales@example.com',
        'features' => ['Unlimited members', 'SAML single sign-on', 'Custom terms'],
    ],

],

What a plan may declare

KeyMeaning
nameShown on the pricing page.
descriptionOne line under the name. Optional.
featuresStrings for the pricing table, written for a buyer.
pricesOne entry per interval, monthly or yearly.
freeSelectable, nothing to check out. At most one is meaningful.
contactSold by a conversation. Renders a link, never a checkout.
hiddenStill resolves for customers on it, not offered to new ones.
seat_basedCharged per member. See seats.
seatsHow many members the plan allows. Null means unlimited.
trial_daysDays of trial applied at checkout. Null for none.
featuredDraws the "most popular" treatment.

Prices

A price carries an amount in minor units2900 is 29.00 — plus a currency, and one <provider>_price_id per provider that sells it.

The amount is here rather than read back from the provider, and that is a trade worth understanding. A price changed in the Stripe dashboard and not here will render wrong on your pricing page. The alternative is an API call on every load of the most-visited screen in your product, against a service that is allowed to be slow and must not be allowed to take your pricing page down. Only the identifier is ever charged, so the worst case is a display bug rather than a billing one.

Adding a second provider is a key rather than a change to the package:

php
'monthly' => [
    'amount' => 2900,
    'stripe_price_id' => env('LARASPRING_BILLING_PRO_MONTHLY_PRICE_ID'),
    'lemon_squeezy_price_id' => env('LARASPRING_BILLING_PRO_MONTHLY_LEMON_ID'),
],

No identifier reaches the browser

A price identifier in a page prop is billing configuration published to anyone who opens the developer tools. The checkout endpoint takes a plan id and an interval, both validated against this catalogue, and works out what to charge server-side.

The catalogue is validated as it is read

Configuration is a nested array anybody may mistype, and the two places that read it — the pricing screen and the checkout endpoint — are the worst places to find out. So it is parsed into objects at the boundary, once per request, and these are refused loudly:

  • a plan that is both free and contact
  • a free or contact plan that also declares prices
  • an interval that is not monthly or yearly
  • an amount that is not an integer, the usual cause being 29.00 where 2900 was meant
  • a seats or trial_days that is not a positive integer

A price with no identifier for the configured provider fails when somebody tries to buy it, with a message naming the provider and the interval, rather than rendering a button that quietly does nothing.

Reading the catalogue in your own code

php
use Laraspring\Billing\Support\Billing;

Billing::plans()->offered();     // what a pricing page shows, in order
Billing::plans()->get('pro');    // one plan, throws if it is not configured
Billing::plans()->find('pro');   // one plan, or null
Billing::plans()->free();        // your free plan, if you declared one

Billing::plan() returns the plan the active billable is on, falling back to your free plan. That fallback is what makes "everybody is on a plan" true, so a feature table can be read off it for a paying customer and a free one alike.

Seats

seats caps how many members an organization may have. laraspring/organizations enforces it through a contract in laraspring/core, so neither package depends on the other. See the overview for how a pending invitation is counted.

seat_based marks a plan as charged per member. Today it only tells the quota to read the subscription's quantity instead of the fixed seats above; adjusting the quantity as people join and leave lands with seat billing proper.

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