Search by

saucebase / billing

saucebase

Stripe billing for Saucebase: checkout, subscriptions, trials, a pricing page and invoices.

Package info

github.com/saucebase-dev/billing

Type:saucebase-module

pkg:composer/saucebase/billing

Statistics

Installs: 440

Dependents: 0

Suggesters: 1

Stars: 2

Open Issues: 0

v3.1.4 2026-10-23 20:23 UTC

README

Tests Release Saucebase Core License

Stripe billing for Saucebase: checkout, subscriptions, trials, a pricing page and invoices.

A module for Saucebase, the AI-ready Laravel SaaS kit. Modules are Composer packages you add to a Saucebase app; once installed, the code is yours to change.

Features

  • Stripe checkout, straight to Stripe or through your own checkout page
  • Subscriptions that cancel at period end and can be resumed
  • Customer portal for cards, invoices and plan changes
  • Free trials, one per customer, with or without a card up front
  • Grace period after a failed payment, with emails at each step
  • Plan kinds: free, subscription, lifetime and one-off
  • Entitlements: each plan turns features on and sets limits your app checks
  • Pricing page at /pricing, built from your plans
  • Billing settings: current plan, invoices and saved card
  • Catalogue sync with Stripe, both ways
  • Webhooks that are safe to retry and handle events out of order
  • Emails for every subscription and payment change
  • Admin panel for products, subscriptions and customers, with a revenue dashboard
  • Vue and React

Install

New to Saucebase? Start a new app and pick billing when it asks for modules:

curl -fsSL https://install.saucebase.dev | bash

It installs PHP and Composer if they're missing. With the CLI already installed, run saucebase new my-app instead.

Already have a Saucebase app? Add it with:

composer require saucebase/billing
git apply modules/billing/patches/user.patch
php artisan migrate
php artisan modules:seed --module=billing
npm run build

Then create your plans in Stripe and import them with php artisan billing:sync-catalog. They arrive hidden: describe them in the admin panel, under Billing → Products, and turn on Visible.

Manual install

If a patch doesn't apply, because your file has changed, make its change by hand:

user.patch makes users billing owners, which subscriptions need:

// app/Models/User.php
use Modules\Billing\Contracts\BillingOwner;
use Modules\Billing\Traits\Billable;

class User extends Authenticatable implements BillingOwner
{
    use Billable;
}

Configuration

Add your Stripe keys to .env:

STRIPE_SECRET_KEY=
STRIPE_PUBLISHABLE_KEY=
STRIPE_WEBHOOK_SECRET=

In Stripe, under Developers → Webhooks, add https://<your-app>/billing/webhooks/stripe with these events. Its signing secret is STRIPE_WEBHOOK_SECRET.

checkout.session.completed
checkout.session.async_payment_succeeded
customer.subscription.updated
customer.subscription.deleted
customer.subscription.trial_will_end
invoice.paid
invoice.payment_succeeded
invoice.payment_failed
charge.refunded
payment_method.attached
payment_method.detached
customer.updated

Locally, forward them with stripe listen --all-snapshot --forward-to localhost/billing/webhooks/stripe.

The rest is in the admin panel, under Settings → Billing: currency, checkout flow, grace period and trials.

Show plans on the homepage (optional)

The plans live at /pricing. To also list them on the homepage, use core's home::content.after slot. Paths below are relative to the module's resources/js/. A flat install keeps one stack there; a checkout with both keeps them in vue/ and react/, so use the folder of the stack you run.

  1. Share the plans on the homepage only, from shareInertiaData() in src/Providers/BillingServiceProvider.php (core's hook for shared props, called from boot()). This is the same payload BillingPlansController builds:

    use Modules\Billing\Models\Product;
    use Modules\Billing\Services\BillingOwners;
    use Modules\Billing\Services\PlanActions;
    
    protected function shareInertiaData(): void
    {
        Inertia::share('billing.home', function () {
            if (! request()->routeIs('index')) {
                return null;
            }
    
            $products = Product::displayable()->get();
            $user = request()->user();
    
            return [
                'products' => $products,
                ...$this->app->make(PlanActions::class)->for($this->app->make(BillingOwners::class)->for($user), $products, $user),
            ];
        });
    }
  2. Add components/HomePricing, next to ProductSection:

    <!-- Vue: components/HomePricing.vue -->
    <script setup lang="ts">
    import { usePage } from '@inertiajs/vue3';
    import { computed } from 'vue';
    import ProductSection from './ProductSection.vue';
    
    const home = computed(() => usePage().props.billing?.home);
    </script>
    
    <template>
        <ProductSection v-if="home?.products.length" v-bind="home" />
    </template>
    // React: components/HomePricing.tsx
    import { usePage } from '@inertiajs/react';
    import ProductSection from './ProductSection';
    
    export function HomePricing() {
        const home = usePage().props.billing?.home;
    
        return home?.products.length ? <ProductSection {...home} /> : null;
    }
  3. Register it inside setup() in the module's app entry. A lower order renders first. Use your stack's alias, @saucebase/vue or @saucebase/react:

    // registerGlobalComponent is already imported there, from '@saucebase/<stack>/lib/globalComponents'
    import HomePricing from './components/HomePricing.vue'; // React: import { HomePricing } from './components/HomePricing';
    
    // inside setup()
    registerGlobalComponent('home::content.after', HomePricing, { order: 10 });

Run npm run build afterwards.

Demo data

php artisan modules:seed --module=billing --demo

Adds sample plans (pushed to Stripe when your keys are set), sample customers, and a staff account (billing@saucebase.dev) that only sees billing in the admin. All use the DEMO_PASSWORD from .env.

License

MIT. See LICENSE.