Subscription Billing — Blazor Blueprint
Login Register

💡 Two money flows, opposite directions — keep them apart. The payments layer lets each tenant charge its customers into its own merchant account. This page is the other direction: you charging tenant organisations for access to your product, into your account. Different credentials, different tables, different admin pages.

Turning It On

Billing is off by default and needs two switches, both on:

  1. Features:PlatformSubscriptionBilling in configuration — makes the surfaces exist at all.
  2. A runtime "billing is live" flag on the platform payment settings — lets you pause charging without a redeploy.

Both are read through a single gate, deliberately. An earlier arrangement had three different readers that could disagree: pausing billing stopped premium-app gating while seat limits kept refusing invitations, and a config-flag-off deployment still had a live checkout reachable by URL. If you extend this area, go through the same gate — do not add a fourth reader.

The runtime flag can only be switched on while the config flag is on, but it survives the config flag being turned off later — which is why neither one alone is the answer.

The Three Surfaces

  • /Platform/Plans — the plan catalogue. Tiers, monthly and yearly prices, trial length, feature entitlements, numeric limits, and which premium apps the plan includes.
  • /Platform/Subscriptions — oversight across every organisation: current plan, status, MRR. Read-only — there are no operator actions on it, so comping a plan or cancelling on a customer's behalf is not something you can do from here yet. The MRR figure also takes its currency from the first active subscription, so treat it as indicative if you sell in more than one.
  • /Admin/Billing — what an organisation sees: plan picker with a monthly/yearly toggle, subscribe, cancel, resume.

💡 Three money pages, and the names matter. /Admin/PaymentSettings is "Payment Providers" — how an organisation charges its customers. /Platform/PaymentSettings is "Payment Accounts" — how you charge organisations. /Admin/Billing is what an organisation pays you. They were nearly given overlapping names once; don't let it happen again.

Example plans seed on first boot when billing is on and the catalogue is empty. They are ordinary rows — edit or delete them.

What You Can Charge

  • Monthly and yearly subscriptions.
  • Pay for the year — a one-off charge with auto-renew off.
  • Trials, per plan, in days.
  • Mid-period plan switches are prorated — unused time is credited against the first charge on the new plan. Credit beyond that charge is not carried forward today.

Charges run through the same three payment providers as the tenant-facing layer — Stripe, GoCardless, PayPal — but against your platform credentials, configured at /Platform/PaymentSettings. No tenant payment rows are written.

⚠️ Unattended auto-renewal needs a stored payment method. A GoCardless mandate provides one; card-on-file for Stripe and PayPal is a follow-up. Without a stored method a renewal moves the subscription to past due and prompts the customer to pay, rather than charging silently. So in practice both "subscription" and "pay for the year" behave as pay-per-period with a renewal prompt — the auto-renew seam is in place, but verify it against your own provider account before promising customers otherwise.

The Lifecycle Sweep

A background sweep activates paid-and-confirmed subscriptions, ends trials, flags renewals and period-ends, retries failed charges, and expires subscriptions past the dunning grace window (BackgroundServices:Subscriptions:GraceDays, default 7). It runs in the background worker, or in-process in Lite mode.

⚠️ Set GraceDays in the web host's configuration as well as the worker's. Entitlement expiry reads it in the web host; the sweep reads it in the worker. Set it in one place only and the two silently drift apart by the difference — access outliving the subscription, or ending before it.

Past due retries — it doesn't just wait

Inside the grace window the sweep re-attempts the stored payment method at most once per day, then expires. Each retry uses a day-scoped idempotency key. That detail is load-bearing: reusing the per-period key would make the provider replay the original failed charge, and no retry could ever succeed. Every transition into or out of dunning emails the organisation's active owners and admins.

Activation is polling, with a webhook fast-path

A subscription is confirmed by polling the provider when the buyer returns from checkout, and again in the sweep. A signed webhook endpoint activates faster when it arrives; polling remains the safety net, so a missed webhook delays activation rather than losing it.

⚠️ Exactly one checkout reference is polled. Any path that stops tracking a checkout must expire it at the provider first — a superseded plan switch, a cancellation taken with a checkout open, an abandoned session. Skip that and the buyer returns to the old tab (a hosted checkout session can live around 24 hours), pays, and the money is captured with nothing on your side to activate against and no ledger row. Abandoned checkouts are themselves expired after AbandonedCheckoutDays (default 3), which must stay above your provider's hosted checkout lifetime so the link is dead before polling stops.

Entitlements

A plan carries arbitrary string feature keys and numeric limits, resolved per organisation. An organisation with no entitled subscription resolves to an empty set — no access, rather than default access.

Three things are wired up already:

  • Premium apps — a plugin marked as requiring a subscription is gated behind the plans that list it. The gate is soft in navigation (hidden from the nav, shown in the launcher as a locked "Upgrade" tile) and hard at the route (a redirect to billing). Non-premium apps, the platform organisation, and platform admins are always allowed, and the whole thing is a no-op when billing is off.
  • Seat limits — a member cap enforced at invite and add.
  • Branding removal — hides the "Powered by" footer credit.

Add your own by checking a feature key or limit wherever it matters. State lives in the platform database, on purpose: entitlement has to resolve before tenant context does, since it decides whether the request may reach the tenant at all.

💡 The Domains-style case. Not every platform charge is a subscription charge. A charge explicitly marked as a subscription answers to the billing pause; other platform charges do not — so a deployment that sells no subscriptions can still take one-off platform payments. Keep that flag correct on anything new you add.

Ready to Build?

Start charging for your product when you are ready. The complete source is free to use — including commercially — while your business earns under £100k/year. Past that, a one-time commercial licence (from £499, excl. VAT) applies. Every tier ships the same product; nothing is feature-gated.

🔓 Source-available • Free under £100k/yr revenue • Commercial licence (from £499) above that • Full source code included

Welcome! How can we help you today?
An unhandled error has occurred. Reload