A billing portal your app adds —
a catalog of plans, subscriptions, invoices, payment
methods, and transactions, plus a ready-made web UI
under /billing. It bills a tenant or an
individual user, and lets you plug in any payment
provider.
ForgeBilling gives your app the day-to-day machinery of selling something: you define billable plans, hand one to whoever you bill, and the component tracks the resulting subscription, generates invoices, records payment methods, and files transactions against each charge. Out of the box it ships a full portal your customers use to pick a plan, enter payment details, and view their invoices.
/billingPrerequisites: ForgeBilling requires the routing layer, the database and ORM layers, and the view and component layers (for its portal UI). Payment itself is a provider you plug in — a manual provider ships so the whole flow works immediately.
Before anything else, the component works out who the current billing entity is. A resolver looks for, in order: the active tenant (provided by ForgeMultiTenant), then the signed-in user (provided by the auth layer). This is what makes the component work across either shape of app:
tenant_id.
When neither a tenant nor a user is present, the component proceeds with an empty view rather than failing — so the portal stays browsable.
php forge.php package:install-module --module=ForgeBilling
Installing migrates the module's tables and mounts
the billing routes and middleware. Six tables back
the component, all held in the app's shared
database (each carrying a tenant_id
column where the billing entity is a tenant):
billing_plans — the plan catalog.billing_subscriptions — one per billing entity, linked to a plan.invoices & invoice_items — bills and their line items.payment_methods — saved payment details per entity.transactions — a record of every charge attempt.
A plan is a priced tier: a name, a slug, an amount
and currency, a billing interval, and an optional
feature list (useful for displaying what a plan
includes). Intervals are free-form strings such as
monthly, yearly,
weekly, or one_time.
# Create a monthly plan for $15 USD
php forge.php modules:billing:plan:create --name=Pro --slug=pro \
--amount=15 --currency=USD --interval=monthly \
--features=api_access,custom_domain
Create, list, disable, or delete plans from the same command surface or from your code. Disabling a plan retires it from the portal; existing subscriptions are left untouched.
A subscription links a billing entity to a plan and
carries a status — active,
trial, past_due,
canceled, or expired —
plus optional trial, period-end, and cancel
timestamps. Assign a tenant to a plan from the
command line:
# Give the Pro plan to a tenant
php forge.php modules:billing:tenant:assign --tenant=upper --plan=plan-pro
From the portal, subscribing requires a saved
payment method first; once one exists the flow
activates the subscription for the current billing
entity and rolls its period forward. Users can
cancel from the subscription page, which marks it
canceled.
In a request, the subscription is resolved once up front and then read consistently by the rest of the component — the page you render sees a stable, current subscription throughout.
Each chargeable period earns an invoice with a
human-friendly number, an amount and currency, and
a status — pending,
paid, overdue,
canceled, or refunded.
Line items are stored alongside it so an invoice
shows exactly what was billed.
For recurring billing, the component fires a
GenerateInvoiceEvent per due
subscription: the listener creates the invoice,
records its line item, and rolls the period end
forward by the plan's interval. You can review
what's due and dispatch it from the command line
(see below).
Charging money goes through a payment provider.
The component defines a small
PaymentProvider contract —
charge, refund,
tokenize, and a name —
and keeps a registry of providers. A
manual provider ships by default so the
flow works end to end, and you register your own
(Stripe, a gateway, or an in-house ledger) with the
same interface:
use Modules\ForgeBilling\Services\PaymentProviderRegistry;
use Modules\ForgeBilling\Contracts\PaymentProviderInterface;
// Resolve the registry and add your provider
$registry = $container->get(PaymentProviderRegistry::class);
$registry->register(new MyProvider());
A provider's charge returns a result
with a transaction id and whether it succeeded; the
component records that attempt in
transactions and, on success, marks
the invoice paid. Payment methods are saved per
billing entity and the registry holds whichever
provider your app has configured.
Bring the actual card processor: the bundled manual provider always approves charges, which is perfect for development or for apps that collect payment outside the portal. For real card processing, register a provider backed by your processor.
The component ships a complete, styled portal
mounted under /billing. It resolves
the current billing entity automatically and
renders a dashboard, the priced plans, invoice
history and detail, saved payment methods, and a
subscription page:
/billing)
— current plan, subscription status, latest
invoice, and recent invoices.
/billing/plans)
— the active plan grid with a subscribe
action and a "current plan" marker.
/billing/invoices and
/billing/invoices/{id}) —
invoice history and a line-item detail view.
/billing/payment-methods) —
add and remove saved payment methods.
/billing/subscription) —
current plan details and a cancel action.
The portal is ordinary views and components, so you style and regroup it to match your app rather than forking a black box.
Small helpers answer billing questions anywhere in your app — badges, dashboards, or gating UI:
billing_subscription(); // ?BillingSubscription — the current entity's plan
billing_is_active(); // bool — subscription is active or on trial
billing_on_trial(); // bool — currently on a trial
Underneath, each concern is its own service you can inject — plans, subscriptions, invoices, payment methods, and payments:
use Modules\ForgeBilling\Services\BillingPlanService;
use Modules\ForgeBilling\Services\BillingSubscriptionService;
use Modules\ForgeBilling\Services\InvoiceService;
use Modules\ForgeBilling\Services\PaymentService;
$plans = $container->get(BillingPlanService::class);
$subs = $container->get(BillingSubscriptionService::class);
$invoices = $container->get(InvoiceService::class);
$pay = $container->get(PaymentService::class);
$plan = $plans->create('Pro', 'pro', 15.0, 'USD', 'monthly');
$sub = $subs->assign('tenant-id', $plan->id);
$invoice = $invoices->create('tenant-id', $sub->id, $plan->amount, 'USD');
$result = $pay->charge('tenant-id', $invoice->id, $plan->amount, 'USD');
The services keep the branch logic out of your code so building your own checkout, admin, or reporting screen is a matter of calling them.
Manage plans, assignments, and recurring billing from the command line:
# Plans
php forge.php modules:billing:plan:list
php forge.php modules:billing:plan:create --name=Pro --slug=pro \
--amount=15 --currency=USD --interval=monthly
php forge.php modules:billing:plan:disable --id=plan-pro
# Assign a plan to a billing entity
php forge.php modules:billing:tenant:assign --tenant=upper --plan=plan-pro
# Recurring billing — preview, or dispatch and process due invoices
php forge.php modules:billing:generate-invoices --dry-run
php forge.php modules:billing:generate-invoices --process
Plan creation and assignment accept interactive prompts when you omit values. The invoice generator finds active subscriptions whose period is due, prints a summary in dry-run, and dispatches the invoice events when told to process.
ForgeBilling and ForgeSaas sit at different layers and work well together:
RequiresFeature,
RequiresPlan,
WithinLimit) that gate your
routes.
They don't call each other directly, but they're natural companions: ForgeBilling records the plan and the charge, ForgeSaas enforces what that plan unlocks. You can use either alone — bill customers without gating features, or gate features without charging.
Running both in a multi-tenant app, the same
tenant id links a
billing_subscriptions row (the
charge) to a
tenant_subscriptions row (the
entitlement), so keep the id in sync across the
two when you create a tenant.