laravel-billing maintained by cleaniquecoders
Laravel Billing
A gateway-agnostic subscription & invoicing engine for Laravel — with an optional Livewire + Flux billing UI. One package to maintain — payment gateways are an extension point (a contract), not separate packages. Ships a built-in local gateway so any app has working billing on day one, plus optional, publishable customer-facing pages (plans, subscription management, invoices, receipts) — ideal for demo, development, UAT, and testing.
Why this design
- One package, one repo. No per-gateway sub-packages. BayarCash, ToyyibPay, Chip, senangPay, Stripe… each app writes a small driver class implementing the
PaymentGatewaycontract. The package never references a real gateway by name. - Batteries included. A first-class
LocalGatewaydriver (no real money) is the default, so a fresh app can run the full subscribe → activate → invoice flow immediately. - Headless core, optional UI. Models, services, contract, events, manager — fully usable with no UI at all. Need billing pages fast? Enable the bundled Livewire + Flux UI (browse plans, subscribe, manage the subscription, list/inspect invoices, download invoice & receipt PDFs) behind your own
authmiddleware. Either way your app keeps control of routes and access. - Tenancy optional. The bill target is polymorphic: attach
BillabletoUser(single-tenant) orTeam/Workspace/Organization(multi-tenant). The package does not care. - Malaysia-friendly invoicing. MYR default, SST/SSM-aware invoice template, atomic sequential numbering — all configurable and neutral by default.
Installation
composer require cleaniquecoders/laravel-billing
Publish the config and (if using the database plan store) the migrations:
php artisan vendor:publish --tag="laravel-billing-config"
php artisan vendor:publish --tag="laravel-billing-migrations"
php artisan migrate
Optionally publish the invoice/checkout views and the plan seeder:
php artisan vendor:publish --tag="laravel-billing-views"
php artisan vendor:publish --tag="billing-seeders"
A fresh install defaults to
BILLING_GATEWAY=local, so demo/UAT works before any merchant account exists.
To use the optional billing UI, install Livewire and Flux in your app (the package guards on their presence, so this is only needed if you enable the UI):
composer require livewire/livewire livewire/flux
Make a model billable
Any model becomes billable by implementing the Billable contract and using the HasSubscriptions trait.
use CleaniqueCoders\LaravelBilling\Contracts\Billable;
use CleaniqueCoders\LaravelBilling\Concerns\HasSubscriptions;
class User extends Authenticatable implements Billable
{
use HasSubscriptions;
public function billingAddress(): array
{
return ['No. 1, Jalan Contoh', '50000 Kuala Lumpur'];
}
}
HasSubscriptions provides billingEmail(), billingName() and billingAddress() defaults (override as needed) plus:
$user->subscription(); // current access-granting subscription, or null
$user->subscribedTo('pro'); // bool
$user->onTrial(); // bool
$user->onGracePeriod(); // bool (access until current_period_end)
$user->plan(); // active Plan, or the configured default/free
$user->invoices(); // MorphMany
$user->canConsume('seats', 1); // gated by plan limits
$user->recordUsage('seats', 1);
Plans — config and/or database
config('billing.plans') is always the canonical source. The store setting decides where reads come from:
store = 'config'—Planinstances are built on the fly from the array. Noplanstable needed.store = 'database'— plans are read from theplanstable; the publishablePlanSeederhydrates it idempotently from config (updateOrCreateontier).
use CleaniqueCoders\LaravelBilling\Services\PlanRepository;
$plans = app(PlanRepository::class)->all(); // Collection<Plan>
$plan = app(PlanRepository::class)->find('pro'); // ?Plan
$free = app(PlanRepository::class)->default(); // Plan
Limits are an open map (seats, messages_per_month, api_calls, …) — your app declares which meters exist. A null limit means unlimited.
Subscribe flow
use CleaniqueCoders\LaravelBilling\Facades\Billing;
use CleaniqueCoders\LaravelBilling\Enums\PlanInterval;
$plan = app(PlanRepository::class)->find('pro');
$intent = Billing::checkout($user, $plan, PlanInterval::Monthly, route('billing.done'));
return redirect($intent->redirectUrl);
checkout() creates a pending (Incomplete) subscription, delegates to the active driver, and correlates the eventual webhook by externalId. With the local gateway:
- Approve on the dev-checkout page → a
SubscriptionActivatedevent flows through the same code path a real gateway uses → subscription activates and an invoice is issued. - Decline → the subscription stays
Incomplete. - Set
BILLING_LOCAL_AUTO=trueto auto-approve (great for CI/tests).
Webhooks (your app wires the route, the package does the work)
use CleaniqueCoders\LaravelBilling\Facades\Billing;
Route::post('/webhooks/{gateway}', function (Request $request, string $gateway) {
$event = Billing::gateway($gateway)->parseWebhook($request);
abort_if($event === null, 401);
Billing::handle($event); // dedups on providerEventId, transitions state, issues invoices, fires events
return response()->noContent();
});
Billing::handle() replay-guards on providerEventId, locates the subscription by gateway_subscription_id/externalId, transitions status, calls IssueInvoice on activate/renew, and fires the matching event.
Write your own gateway
Implement the single extension point and register the driver class in config. That's the entire surface.
namespace App\Billing;
use CleaniqueCoders\LaravelBilling\Contracts\{Billable, PaymentGateway};
use CleaniqueCoders\LaravelBilling\DataTransferObjects\{CheckoutIntent, WebhookEvent};
use CleaniqueCoders\LaravelBilling\Enums\{PlanInterval, WebhookEventType};
use CleaniqueCoders\LaravelBilling\Models\{Plan, Subscription};
use Illuminate\Http\Request;
class BayarCashGateway implements PaymentGateway
{
public function createCheckout(Billable $billable, Plan $plan, PlanInterval $interval, string $returnUrl): CheckoutIntent
{
// call your SDK / HTTP here
return new CheckoutIntent(redirectUrl: $url, externalId: $reference);
}
public function cancel(Subscription $subscription): void { /* … */ }
public function parseWebhook(Request $request): ?WebhookEvent
{
// verify signature; return null if invalid
return new WebhookEvent(
type: WebhookEventType::SubscriptionActivated,
externalId: $request->input('reference'),
amountCents: (int) $request->input('amount') * 100,
providerEventId: $request->input('event_id'),
rawPayload: $request->all(),
);
}
}
// config/billing.php
'default' => env('BILLING_GATEWAY', 'bayarcash'),
'gateways' => [
'local' => ['driver' => 'local', 'enabled' => env('BILLING_LOCAL_ENABLED', true)],
'bayarcash' => ['driver' => App\Billing\BayarCashGateway::class],
],
The manager resolves the class through the container, so constructor injection works. You can also register a driver at runtime:
Billing::extend('toyyibpay', fn ($app) => new ToyyibPayGateway($app['config']['services.toyyibpay']));
Events
Listen to drive your own side effects (provision access, dunning, Slack notifications). The package itself only updates subscription/invoice state and issues invoices.
SubscriptionActivated · SubscriptionRenewed · SubscriptionCanceled · PaymentSucceeded · PaymentFailed · InvoiceIssued
Invoicing
Invoices use atomic, row-locked sequential numbering (INV-2026-000001) and are rendered to PDF from a brandless, SST/SSM-aware Blade template, stored via Laravel's Filesystem (disk configurable). Set seller details under config('billing.company'). Publish and override resources/views/vendor/billing/invoice-pdf.blade.php to brand it.
Billing UI (optional)
The package ships a Livewire + Flux UI that closes the full subscribe → pay → invoice → receipt loop. It is opt-in and scoped to the authenticated billable.
| Plans | Billing portal |
|---|---|
![]() |
![]() |
More screenshots and the full walkthrough: Billing UI docs.
Requires livewire/livewire and livewire/flux in your app. Enable and scope it in config('billing.routes'):
// config/billing.php
'routes' => [
'enabled' => true,
'prefix' => 'billing',
'middleware' => ['web', 'auth'],
],
That registers:
| Route | Name | What it shows |
|---|---|---|
GET /billing/plans |
billing.plans |
Plan cards with monthly/annual toggle; Subscribe runs Billing::checkout() |
GET /billing |
billing.portal |
Overview (current subscription, cancel/resume) + Invoices tab with a detail modal |
GET /billing/success |
billing.success |
Payment-success receipt card (amount, number, Download invoice / receipt) |
GET /billing/invoices/{invoice:uuid}/download |
billing.invoices.download |
Streams the stored invoice PDF |
GET /billing/invoices/{invoice:uuid}/receipt |
billing.invoices.receipt |
Streams a receipt PDF derived from the paid invoice |
The billable is resolved via config('billing.billable_resolver') (defaults to request()->user()); set it to scope billing to a Team/Workspace. Every query is constrained to that billable, and the download/receipt routes 403 on a foreign invoice. Publish laravel-billing-views to override the Flux markup.
Component ownership — package vs your app
| Lives in the package | Lives in your app |
|---|---|
Contracts\PaymentGateway, Contracts\Billable |
Concrete gateway drivers (BayarCashGateway, …) |
Gateways\LocalGateway (bundled default) |
The real gateway SDK dependency |
| Models, migrations, enums, DTOs, events | The Billable model + use HasSubscriptions |
IssueInvoice, PlanRepository, BillingManager, facade |
Custom routes/UI beyond the bundled pages, access-control middleware |
Webhook dispatcher Billing::handle() |
The webhook route + any environment/tenancy gating |
Brandless invoice PDF template + InvoiceIssuedMail |
Company/tax config values, branded template override |
| Optional Livewire + Flux billing UI (publishable, overridable) | Enabling/scoping the routes, livewire/flux install, branding |
UI: the package ships an optional Livewire + Flux billing UI (plans, subscription, invoices, receipts) plus the dev-only local checkout page. It is opt-in — disable
billing.routes.enabled(or skip installinglivewire/flux) and the package stays fully headless. Publish and override the views to brand them, or build your own pages against the same models and facade.
Documentation
Full documentation lives in docs/:
- Getting Started — install, make a model billable, define plans
- Architecture — domain models, gateways, webhooks, events
- Billing UI — pages, routes, invoices & receipts (with screenshots)
- Configuration — every
config/billing.phpkey - Development — workbench preview & testing
- Examples — full cycle & custom gateway
Testing
composer test # Pest (Livewire components tested headlessly via Livewire::test)
composer analyse # PHPStan / Larastan
vendor/bin/pint # code style
Preview the UI locally (Testbench workbench)
The package ships a Testbench workbench so you can click through the full subscribe → pay → invoice → receipt flow with the real Flux UI:
npm install
npm run dev # Vite + Tailwind (compiles Flux styles) — keep running
php vendor/bin/testbench workbench:build # once: create sqlite db + run migrations
php vendor/bin/testbench serve # http://127.0.0.1:8000
Open the root URL — it auto-logs in a demo billable and lands on /billing/plans.
The workbench (workbench/, testbench.yaml) configures a demo plan matrix
(config store), the local gateway with the dev "Approve" checkout page, and
SST tax — mirroring what a host app sets in config/billing.php.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Credits
License
The MIT License (MIT). Please see License File for more information.

