Documentation for larsasub v0.38.2.
On an older version? Your release notes ship with the package (CHANGELOG.md).
Standalone dashboards
LarsaSub ships twelve ready-made pages — six for the subscriber, six for whoever manages the plans — built as self-contained Vue apps with their own compiled CSS. They work without any host asset pipeline, and you can switch off every one of them if you would rather build your own. Read this page when you install the package, put the pages behind your own authentication, or restyle them.
- What ships
- Publishing the assets
- Switching dashboards on and off
- URLs
- Middleware: the complete stack
- The guard
- Domain
- Routes that are deliberately excluded
- List bounding
- Theming
- Building your own
What ships
Six dashboards serve the subscriber and six serve whoever manages the plans. Each is a Blade page that mounts a compiled Vue app, linked to the others by a shared navigation bar, and each can be switched off on its own.
Subscriber dashboards
| Key | Default URL | What it does |
|---|---|---|
subscription |
billing/dashboard |
The current subscription: subscribe, change plan, cancel, retry or abandon a subscription whose first payment failed, read and accept the terms |
payments |
billing/payments |
Payment history, the manual "pay now" recovery flow for charges and single payments, per-payment invoice download |
payment_methods |
billing/payment-methods |
Add a payment method (mandate setup), remove one, switch a subscription's mandate |
credit |
billing/credit |
Credit balance and ledger, with expiry dates |
profile |
billing/profile |
The invoice profile (personal/business, address, country, VAT number) and, with the VIES check on, the status of that number |
invoices |
billing/invoices |
Invoices and credit notes, with PDF download |
The subscribe card names what the click collects
Each plan on the subscription dashboard carries the quote of its first charge
(SubscriptionCostService::quoteFirstCharge(), as quote in every
availableSubscriptions entry), and the card prints that — not the catalogue
price:
- the amount with its VAT label, as the subscription mails print it;
- an introduction price and what follows it;
- a row per cost line when a
before_first_chargehook adds lines; - the amount again directly above the button ("Due now: …").
On a paid plan the button reads "Order with obligation to pay"; a free plan
keeps "Get started". Both are built into the bundle, like every frontend.json
text — see Multilang.
A card the quote cannot fill shows no button but the reason, and the server refuses the same subscribe:
- prices excluding VAT and no country stored (invoicing on) — a link to
the
profiledashboard ("enter billing details") when that dashboard is on: the amount depends on the country, and the package does not guess. With theprofiledashboard off, your own billing form must storeinvoice_countryon the registration, or a subscriber without one cannot take out a paid plan; subscription.free_plan_mandate = 'refuse'without a payment method — a link topayment_methodswhen that dashboard is on;- a
before_first_chargehook that does not implementContracts\ProspectiveCostLines— "cannot be taken out online"; the boot log andlarsasub:doctorname the hook (Cost lines); - no billable owner for the user (indirect mode with no team yet, explicit mode without a current owner — in simple mode; under Connect the whole dashboard answers 403 first), or a quote that failed — the same "cannot be taken out online"; the log says which.
In Connect mode the subscribe section steps aside while the tenant's Mollie disconnect drains (Disconnecting a tenant).
Management dashboards
These are mode-aware: in Connect mode they are the tenant owner's view of their own tenant; in simple mode they are operator management over the global plans.
| Key | Default URL | What it does |
|---|---|---|
tenant_subscriptions |
billing/admin/subscriptions |
Define, edit and delete plans, toggle them, manage change targets |
tenant_subscribers |
billing/admin/subscribers |
Who is subscribed to what. A checkout abandoned before its first payment (first_payment_abandoned) is not listed or counted |
tenant_charges |
billing/admin/charges |
Charge buckets — planned, in progress, needing attention, owed after chargeback, failed, recently paid — with totals and a lookup by payment reference. The canceled first payment of an abandoned checkout stays out of the buckets; the lookup still finds it |
tenant_invoices |
billing/admin/invoices |
Issued invoices and credit notes |
tenant_connect |
billing/admin/connect |
The Mollie connection, re-authorizing, disconnecting (draining first) and the per-tenant payment methods — Connect mode only |
tenant_fees |
billing/admin/fees |
Application fees charged — Connect mode only |
The invoices and tenant_invoices dashboards additionally require
larsasub.invoicing.enabled; tenant_connect and tenant_fees require
connect.enabled. Their routes are not registered without it.
The Mollie website profile is not on
tenant_connectyet. A tenant whose organization holds several profiles cannot pick one from the bundled page; the package's own choice stands until someone callsMollieConnectService::setProfile(). Build the picker onlistProfiles()/setProfile()if your tenants need it — see Choosing the website profile.
Who can open them
Every page first passes standalone.middleware,
and every action is then authorized through the package's policies against the
signed-in user's billable.
- Subscriber dashboards show the billable the package resolves for the signed-in user (Billable models). In Connect mode they are scoped to that billable's tenant, and answer 403 when no billable or no tenant resolves — never an unscoped page.
- Management dashboards in simple mode open for the users
policies.admin_rolenames as admins (a role name or an e-mail list inLARSASUB_ADMIN_ROLE, or an invokable in a published config). With no role configured nobody is an admin, so the pages answer 403 for everyone;ConfigValidatorwarns at boot and the log says why on each refusal. - Management dashboards in Connect mode open for the tenant owner only: the
user whose key matches the tenant's
connect.tenant_owner_attribute(defaultowner_id). Everyone else gets 403.
Publishing the assets
The dashboards load their CSS and JavaScript from public/vendor/larsasub/.
larsasub:install publishes them, and it does so on every run — run it again
after each upgrade:
php artisan larsasub:install
# or only the assets
php artisan vendor:publish --tag=larsasub-assets --force
The assets are build output that must match the installed package version: a
stale bundle against a newer page payload is a broken page, not a
customization. That is why the install command overwrites them, unlike your
published config. larsasub:doctor warns when a dashboard is enabled and the
assets are missing.
Each page loads the stylesheet, the Vue runtime and its bundle with the
published file's modification time appended (larsasub.css?v=…), so a browser
fetches the new files after you republish instead of running a cached bundle.
Switching dashboards on and off
'standalone' => [
'dashboards' => [
'subscription' => env('LARSASUB_DASHBOARD_SUBSCRIPTION', true),
'payments' => env('LARSASUB_DASHBOARD_PAYMENTS', true),
'payment_methods' => env('LARSASUB_DASHBOARD_PAYMENT_METHODS', true),
'credit' => env('LARSASUB_DASHBOARD_CREDIT', true),
'profile' => env('LARSASUB_DASHBOARD_PROFILE', true),
'invoices' => env('LARSASUB_DASHBOARD_INVOICES', true),
'tenant_connect' => env('LARSASUB_DASHBOARD_TENANT_CONNECT', true),
'tenant_subscriptions' => env('LARSASUB_DASHBOARD_TENANT_SUBSCRIPTIONS', true),
'tenant_subscribers' => env('LARSASUB_DASHBOARD_TENANT_SUBSCRIBERS', true),
'tenant_charges' => env('LARSASUB_DASHBOARD_TENANT_CHARGES', true),
'tenant_fees' => env('LARSASUB_DASHBOARD_TENANT_FEES', true),
'tenant_invoices' => env('LARSASUB_DASHBOARD_TENANT_INVOICES', true),
],
],
All twelve default to true. Switching one off unregisters its routes entirely.
Mails point at dashboards. Four subscriber mails fall back to one when their URL key is empty:
notifications.payment_url(payment failed, chargeback, the dunning reminder) to the payments dashboard, andnotifications.first_payment_urlto the subscription dashboard. Disable those dashboards without configuring a URL and the mails lose their action button —ConfigValidatorwarns for exactly that combination. While a Connect tenant drains its disconnect, the four mails leave the dashboard button out, because the dashboard would refuse the payment or the retry; a URL of your own is still used. In Connect mode the admin button of a tenant admin's company mail falls back to the first of the subscribers, charges, subscriptions and connect dashboards that is registered; with those four off and nonotifications.tenant_admin_url, those mails carry no admin button at all, andConfigValidatorwarns too. See Action-button URLs.
URLs
Each dashboard has its own prefix under standalone.url:
'url' => [
'subscription-dashboard' => env('LARSASUB_STANDALONE_SUBSCRIPTION_URL', 'billing/dashboard'),
'payments-dashboard' => env('LARSASUB_STANDALONE_PAYMENTS_URL', 'billing/payments'),
'payment-methods-dashboard' => env('LARSASUB_STANDALONE_METHODS_URL', 'billing/payment-methods'),
'credit-dashboard' => env('LARSASUB_STANDALONE_CREDIT_URL', 'billing/credit'),
'profile-dashboard' => env('LARSASUB_STANDALONE_PROFILE_URL', 'billing/profile'),
'invoices-dashboard' => env('LARSASUB_STANDALONE_INVOICES_URL', 'billing/invoices'),
'tenant-connect-dashboard' => env('LARSASUB_STANDALONE_TENANT_CONNECT_URL', 'billing/admin/connect'),
'tenant-subscriptions-dashboard' => env('LARSASUB_STANDALONE_TENANT_SUBSCRIPTIONS_URL', 'billing/admin/subscriptions'),
'tenant-subscribers-dashboard' => env('LARSASUB_STANDALONE_TENANT_SUBSCRIBERS_URL', 'billing/admin/subscribers'),
'tenant-charges-dashboard' => env('LARSASUB_STANDALONE_TENANT_CHARGES_URL', 'billing/admin/charges'),
'tenant-fees-dashboard' => env('LARSASUB_STANDALONE_TENANT_FEES_URL', 'billing/admin/fees'),
'tenant-invoices-dashboard' => env('LARSASUB_STANDALONE_TENANT_INVOICES_URL', 'billing/admin/invoices'),
],
The full route table, with names and verbs, is in Routes and webhooks.
Two presentation options apply to all of them:
'show_back' => env('LARSASUB_STANDALONE_SHOW_BACK', true), // render a "back" link
'back' => env('LARSASUB_STANDALONE_BACK_URL', '/'), // where it points
Middleware: the complete stack
standalone.middleware is the middleware every dashboard route gets:
'middleware' => ['web', 'auth', 'verified'], // the default
What you configure is exactly what the routes get. LarsaSub wraps nothing around it — the same way Nova, Horizon and Telescope treat their own middleware key. An entry you leave out is genuinely absent.
Keep
webunless your guard is stateless. It carries the session and CSRF verification. Without it the dashboards still render while every action — cancel, change, pay, save profile — answers 419.
Replace the list for a host with its own auth middleware, without email verification, or with an extra tenant-resolution step:
'middleware' => ['web', 'auth:billing', 'tenant'],
LARSASUB_STANDALONE_MIDDLEWARE takes a comma-separated list. An emptied
value falls back to the default rather than publishing billing pages openly.
The Mollie Connect routes a tenant owner uses — authorize, complete, disconnect, cancel disconnect — run on this same stack, so a host on a non-default guard is authenticated for connecting Mollie exactly as for its billing pages.
The guard
'guard' => env('LARSASUB_STANDALONE_GUARD'), // null = the app's default guard
This names the guard the package reads the current user from. It is always read
through GeneralHelpers::authUser(), never a bare auth()->user(), and
authorization runs through GeneralHelpers::authorize() rather than the
AuthorizesRequests trait — because the Gate resolves its user from the
default guard, so $this->authorize() would deny the very user the page
rendered for on a host stack that never calls shouldUse().
Point this at a non-default guard and you must authenticate against that guard in the middleware too (
auth:billing). Otherwise the route authenticates one user and the package reads another: the pages render and every action denies.ConfigValidatorwarns at boot andlarsasub:doctorreports it, because that mistake looks like a working install right up until the first button.
Domain
'domain' => env('LARSASUB_STANDALONE_DOMAIN'), // e.g. 'billing.example.com'
null (default) leaves the routes unconstrained. The Connect authorize,
complete, disconnect and cancel-disconnect routes follow the same domain
constraint, so a subdomain-scoped billing area keeps its OAuth flow on the same
origin. The OAuth callback does not — see below.
Routes that are deliberately excluded
Three kinds of route are outside standalone.middleware and
standalone.domain, on purpose:
- Webhook routes — unauthenticated by design. The payment provider calls them,
and no session exists. They are throttled instead
(
larsasub.webhooks.rate_limit). RedirectController— the post-payment return URLs. Those URLs are already embedded in payments at the provider and must work without the package's auth stack; a customer returning from their bank in a fresh browser session has to land somewhere. They carry onlyweb.- The Mollie Connect OAuth callback — Mollie redirects to the one URI
registered for your Connect app, built from
config('app.url'), which need not be the billing domain. The callback completes nothing itself: it hands a single-use ticket back to thecompleteroute, which runs on the dashboard stack and re-authenticates the tenant owner.
Changing these through config would be a way to accidentally break payment collection, so none of them is configurable.
List bounding
'page_size' => env('LARSASUB_STANDALONE_PAGE_SIZE', 50),
'list_limit' => env('LARSASUB_STANDALONE_LIST_LIMIT', 500),
page_size paginates the plain lists (invoices and credit notes, on both
invoice dashboards). list_limit caps the client-side-filtered lists
(subscribers, charge buckets) at the most recent N, with a visible truncation
notice in the UI — an unbounded ->get() over a table that only grows
eventually renders megabytes of JSON into every page view. The totals on the
charges dashboard are counted over everything, not over the capped slice.
Theming
The published assets in public/vendor/larsasub/ are self-contained: a full
Tailwind build over the package's own components (larsasub.css), the bundled Vue
runtime (no CDN), and the dashboard bundles with their component styles inlined.
The dashboards render correctly with no host CSS at all.
Host theming is optional and pipeline-agnostic. standalone.theme_view
(LARSASUB_STANDALONE_THEME_VIEW, default null) names a Blade view that is
included in the dashboard <head> after larsasub.css:
'theme_view' => 'larsasub-theme',
Put whatever your asset pipeline uses in it:
{{-- resources/views/larsasub-theme.blade.php --}}
@vite(['resources/css/app.css'])
The view is included with @includeIf, so a name that does not resolve renders
nothing rather than an error — check the page source if your theme does not
apply.
Theme tokens
The dashboards read shadcn-style CSS custom properties, defined on :root and
again on .dark. A stylesheet that defines them re-themes the pages, and
host-defined tokens win because the view is included after the bundled CSS.
larsasub.css defines --background, --foreground, --card, --popover,
--primary, --secondary, --muted, --accent and --destructive (each with
a -foreground pair), plus --border, --input, --ring and --radius. The
bundled pages lean mostly on --background, --foreground, --border,
--card, --muted, --primary, --destructive and --ring:
:root { --primary: #fe9900; --primary-foreground: #0F172A; }
.dark { --primary: #fe9900; --primary-foreground: #0F172A; }
Dark mode is the .dark class on <html>. The layout sets it from the
visitor's own choice (a toggle in the dashboard layout, remembered in the browser)
or, without one, from the system preference.
If you want to restyle package markup with your own Tailwind utilities, a Tailwind v4 host can also scan the bundles:
@source '../../public/vendor/larsasub/*.js';
That is optional — it is not needed for correct rendering.
Emails are themed separately; see Notifications.
Building your own
Switch the dashboards off and drive the services directly — that is the headless path, and everything the dashboards do is available through the public API.
If you want the Vue components but your own build:
php artisan larsasub:install --vue-components
# or
php artisan vendor:publish --tag=larsasub-vue-components
The components land in resources/vendor/larsasub/js/components, next to
the utils, types, composables and frontend translations they import, so
they build as published. The stylesheet whose tokens their classes use comes
along as resources/vendor/larsasub/css/larsasub.css — see
Using the Vue sources in your own build.
Published SFC sources are never overwritten by a later install — once
published they are yours to edit. Restyling through theming
needs none of this.
When you build your own subscriber UI, four contracts are easy to get wrong:
- The "pay now" list is
$owner->payableCharges(), not a hand-rolledwhere('payable', true)query — see Recurring charges. $owner->outstandingCharges()is a show list; never start a payment from it.- Terms enforcement lives in the bundled controller only. A headless flow owns its own consent — see Terms acceptance.
- The amount on your order button comes from
quoteFirstCharge(), never fromprice_amount. ASubscriptionCreator.vuepublished from an earlier release prints the catalogue price: port it to read each plan'squote(andtypes/index.tsif you copied it), or the button names one amount and the bank moves another.