Step-by-step guide to building a subscription web app: plans, checkout, recurring billing, invoicing, taxes, retries, analytics, and security best practices.

Before you pick a payments provider or design your database, get clear on what you’re actually selling and how customers will change over time. Most billing problems are requirement problems in disguise.
A helpful way to reduce risk early is to treat billing as a product surface, not just a backend feature: it touches checkout, permissions, emails, analytics, and support workflows.
Start by choosing the commercial shape of your product:
Write down examples: “A company with 12 members downgrades to 8 mid-month” or “A consumer pauses for a month, then returns.” If you can’t describe it clearly, you can’t build it reliably.
At minimum, document the exact steps and outcomes for:
Also decide what should happen to access when payment fails: instant lock, limited mode, or a grace window.
Self-service reduces support load but requires a customer portal, clear confirmation screens, and guardrails (e.g., preventing downgrades that break limits). Admin-managed changes are simpler early on, but you’ll need internal tooling and audit logs.
Choose a few measurable targets to steer product decisions:
These metrics help you prioritize what to automate first—and what can wait.
Before you write any billing code, decide what you’re actually selling. A clean plan structure reduces support tickets, failed upgrades, and “why was I charged?” emails.
Common models work well, but they behave differently in billing:
If you mix models (e.g., base plan + per-seat + usage overages), document the logic now—this becomes your billing rules.
Offer monthly and annual if it fits your business. Annual plans usually need:
For trials, decide:
Add-ons should be priced and billed like mini-products: one-time vs recurring, quantity-based or fixed, and whether they’re compatible with every plan.
Coupons need simple guardrails: duration (one-time vs repeating), eligibility, and whether they apply to add-ons.
For grandfathered plans, decide if users can keep old pricing forever, until they change plans, or until a sunset date.
Use plan names that signal outcomes (“Starter”, “Team”) rather than internal labels.
For each plan, define feature limits in plain language (e.g., “Up to 3 projects”, “10,000 emails/month”) and ensure the UI shows:
A subscription app feels simple on the surface (“charge monthly”), but billing gets messy unless your data model is clear. Start by naming your core objects and making their relationships explicit, so reporting, support, and edge cases don’t turn into one-off hacks.
At minimum, plan for these:
A useful rule: Plans describe value; Prices describe money.
Subscriptions and invoices both need statuses. Keep them explicit and time-based.
For Subscription, common statuses are: trialing, active, past_due, , . For : , , , , .
Store the current status and the timestamps/reasons that explain it (e.g., canceled_at, cancel_reason, past_due_since). This makes support tickets much easier.
Billing needs an append-only audit log. Record who did what and when:
Draw a clear line:
This separation keeps self-service safe while giving operations the tools they need.
Choosing your payments setup is one of the highest-leverage decisions you’ll make. It affects development time, support load, compliance risk, and how quickly you can iterate on pricing.
For most teams, an all-in-one provider (for example, Stripe Billing) is the fastest path to recurring payments, invoices, tax settings, customer portals, and dunning tools. You trade some flexibility for speed and proven edge-case handling.
A custom billing engine can make sense if you have unusual contract logic, multiple payment processors, or strict requirements around invoicing and revenue recognition. The cost is ongoing: you’ll be building and maintaining proration, upgrades/downgrades, refunds, retry schedules, and a lot of bookkeeping.
Hosted checkout pages reduce your PCI compliance scope because sensitive card details never touch your servers. They’re also easier to localize and keep up to date (3DS, wallet payments, etc.).
Embedded forms can offer tighter UI control, but they typically increase your security responsibilities and testing burden. If you’re early-stage, hosted checkout is usually the pragmatic default.
Assume payments happen outside your app. Use provider webhooks (events) as the source of truth for subscription state changes—payment succeeded/failed, subscription updated, charge refunded—and update your database accordingly. Make webhook handlers idempotent and retry-safe.
Write down what happens for card declines, expired cards, insufficient funds, bank errors, and chargebacks. Define what the user sees, what emails go out, when access is paused, and what support can do. This reduces surprises when the first failed renewal hits.
This is the point where your pricing strategy turns into a working product: users pick a plan, pay (or start a trial), and immediately get the right level of access.
If you’re trying to ship an end-to-end subscription web app quickly, a vibe-coding workflow can help you move faster without skipping the details above. For example, in Koder.ai you can describe your plan tiers, seat limits, and billing flows in chat, then iterate on the generated React UI and Go/PostgreSQL backend while keeping your requirements and data model aligned.
Your pricing page should make it easy to choose without second-guessing. Show each tier’s key limits (seats, usage, features), what’s included, and the billing interval toggle (monthly/annual).
Keep the flow predictable:
If you support add-ons (extra seats, priority support), let users select them before checkout so the final price is consistent.
Checkout isn’t just taking a card number. It’s where edge cases show up, so decide what you’ll require up front:
After payment, verify the provider’s result (and any webhook confirmation) before unlocking features. Store the subscription status and entitlements, then provision access (e.g., enable premium features, set seat limits, start usage counters).
Send the essentials automatically:
Make these emails match what users see in-app: plan name, renewal date, and how to cancel or update payment details.
A customer billing portal is where support tickets go to die—in a good way. If users can fix billing issues themselves, you’ll reduce churn, chargebacks, and “please update my invoice” emails.
Start with the essentials and make them hard to miss:
If you’re integrating a provider like Stripe, you can either redirect to their hosted portal or build your own UI and call their APIs. Hosted portals are faster and safer; custom portals give more control over branding and edge cases.
Plan changes are where confusion happens. Your portal should clearly show:
Define proration rules upfront (e.g., “upgrades effective immediately with prorated charge; downgrades apply at next renewal”). Then make the UI mirror that policy, including an explicit confirmation step.
Offer both:
Always show what happens to access and billing, and send a confirmation email.
Add a “Billing history” area with download links for invoices and receipts, plus payment status (paid, open, failed). This is also a good place to link to /support for edge cases like VAT ID corrections or invoice re-issues.
Invoicing is more than “send a PDF.” It’s a record of what you charged, when you charged it, and what happened afterward. If you model the invoice lifecycle clearly, support and finance tasks become much easier.
Treat invoices as stateful objects with rules for how they transition. A simple lifecycle might include:
Keep transitions explicit (e.g., you can’t edit an Open invoice; you must void and reissue), and record timestamps for auditability.
Generate invoice numbers that are unique and human-friendly (often sequential with a prefix, like INV-2026-000123). If your payment provider generates numbers, store that value too.
For PDFs, avoid storing raw files in your app database. Instead, store:
Refund handling should reflect your accounting needs. For simple SaaS, a refund record tied to a payment may be enough. If you need formal adjustments, support credit notes and link them to the original invoice.
Partial refunds require line-item clarity: store the refunded amount, currency, reason, and which invoice/payment it relates to.
Customers expect self-service. In your billing area (e.g., /billing), show invoice history with status, amount, and download links. Also email finalized invoices and receipts automatically, and resend them on demand from the same screen.
Taxes are one of the easiest ways for subscription billing to go wrong—because what you charge depends on where your customer is, what you sell (software vs. “digital services”), and whether the buyer is a consumer or a business.
Start by listing where you will sell and what tax regimes are relevant:
If you’re unsure, treat this as a business decision, not a coding task—get advice early so you don’t need to redo invoices later.
Your checkout and billing settings should capture the minimum data required to calculate tax correctly:
For B2B VAT, you may need to apply a reverse-charge or exemption rule when a valid VAT ID is provided—your billing flow should make this predictable and visible to the customer.
Many payment providers offer built-in tax calculation (e.g., Stripe Tax). This can reduce errors and keep rules up to date. If you sell in many jurisdictions, have high volume, or need advanced exemptions, consider a dedicated tax service instead of hard-coding rules.
For every invoice/charge, save a clear tax record:
This makes it far easier to answer “why was I charged tax?”, handle refunds correctly, and produce clean finance reports later.
Failed payments are normal in subscription businesses: cards expire, limits change, banks block charges, or customers simply forget to update details. Your job is to recover revenue without surprising users or creating support tickets.
Start with a clear schedule and keep it consistent. A common approach is 3–5 automatic retries over 7–14 days, paired with email reminders that explain what happened and what to do next.
Keep reminders focused:
If you use a provider like Stripe, lean on built-in retry rules and webhooks so your app reacts to real payment events rather than guessing.
Define (and document) what “past-due” means. Many apps allow a short grace period where access continues, especially for annual plans or business accounts.
A practical policy:
Whatever you choose, make it predictable and visible in the UI.
Your checkout and billing portal should make updating a card fast. After an update, immediately attempt to pay the latest open invoice (or trigger the provider’s “retry now” action) so customers see an instant resolution.
Avoid “Payment failed” with no context. Show a friendly message, the date/time, and next steps: try another card, contact the bank, or update billing details. If you have a /billing page, link users there directly and keep the button wording consistent across emails and the app.
Your subscription billing flow won’t stay “set and forget.” Once real customers are paying, your team will need safe, repeatable ways to help them without editing production data by hand.
Start with a small admin area that covers the most common support requests:
Add lightweight tools that let support resolve issues in one interaction:
Not every staff member should be able to change billing. Define roles such as Support (read + notes), Billing Specialist (refunds/credits), and Admin (plan changes). Enforce permissions on the server, not only in the UI.
Log every sensitive admin action: who did it, when, what changed, and the related customer/subscription IDs. Make logs searchable and exportable for audits and incident review, and link entries to the affected customer profile.
Analytics is where your billing system turns into a decision-making tool. You’re not just collecting payments—you’re learning which plans work, where customers struggle, and what revenue you can rely on.
Start with a small set of subscription metrics you can trust end-to-end:
Point-in-time totals can hide problems. Add subscription cohort views so you can compare retention for customers who started in the same week/month.
A simple retention chart answers questions like: “Do annual plans retain better?” or “Did last month’s pricing change reduce week-4 retention?”
Instrument key actions as events and attach context (plan, price, coupon, channel, account age):
Keep a consistent event schema so reporting doesn’t turn into a manual cleanup project.
Set up automated alerts for:
Deliver alerts to the tools your team actually watches (email, Slack), and link to an internal dashboard route like /admin/analytics so support can investigate quickly.
Subscriptions fail in small, expensive ways: a webhook delivered twice, a retry that charges again, or a leaked API key that lets someone create refunds. Use the checklist below to keep billing safe and predictable.
Store payment provider keys in a secrets manager (or encrypted environment variables), rotate them regularly, and never commit them to git.
For webhooks, treat every request as untrusted input:
If you’re using Stripe (or a similar provider), use their hosted Checkout, Elements, or payment tokens so raw card numbers never touch your servers. Don’t store PAN, CVV, or magnetic stripe data—ever.
Even if you save a “payment method,” store only the provider’s reference ID (e.g., pm_...) plus last4/brand/expiry for display.
Network timeouts happen. If your server retries “create subscription” or “create invoice,” you can accidentally double-charge.
Use a sandbox environment and automate tests that cover:
Before shipping schema changes, run a migration rehearsal on production-like data and replay a sample of historical webhook events to confirm nothing breaks.
If your team is iterating rapidly, consider adding a lightweight “planning mode” step before implementation—whether that’s an internal RFC or a tool-assisted workflow. In Koder.ai, for instance, you can outline billing states, webhook behaviors, and role permissions first, then generate and refine the app with snapshots and rollback available as you test edge cases.
canceledpauseddraftopenpaidvoiduncollectible