Hosting + Billing

How to set up hosting, payments, and user accounts without overcomplicating things. We’ll show you simple setups that scale from zero to real customers. You don’t need enterprise infrastructure on day one.

When you’re just getting started with your SaaS product, there’s a natural temptation to overcomplicate your infrastructure. But modern tools let you go from “idea” to “running app with customers” using very little code, and with infrastructure that scales with you. This page outlines a solid, scalable, and simple setup we recommend for most solo or small-team SaaS products, especially if you’re bootstrapping.

Frontend Hosting

If you’re using a static site or a Single Page App (SPA) - like one built with Vite, React, Svelte, or similar - then your hosting needs are pretty minimal. These apps can be compiled down to static assets (HTML, CSS, JS) and served through a CDN.

We recommend Cloudflare Pages. It’s free, fast, and deploys from GitHub with every push. Since your app gets deployed across their global edge network, it can handle thousands of users a day - or a sudden spike from a Hacker News post - without you having to do anything.

Cloudflare Logo

Easy Win

It’s worth emphasizing: by having a static or SPA frontend, your app can scale globally, instantly, and cheaply. There’s nothing to “configure” for scale. That’s a huge win.

There are trade-offs with having a “public client” (vs a “confidential client” like a server-rendered app), but for most SaaS products, this is the right choice. It’s fast, simple, and you can focus on building features instead of managing servers.

Backend Services

For your backend, Supabase is the most complete, developer-friendly option we’ve used.

Supabase Logo

It handles almost everything you’d need to write a proper SaaS:

  • Database: Postgres database (with Row Level Security built in)
  • AuthN and AuthZ: (including OAuth with Google, GitHub, etc.)
  • REST and GraphQL APIs: that respect your security rules
  • Client SDKs for JS, Python, Dart, and more
  • Serverless Functions: (equivalent to AWS Lambda, but easier)
  • Edge functions: for ultra-fast response times
  • File storage and image resizing
  • Realtime subscriptions
  • Centralized logging and monitoring

We strongly recommend using the hosted Supabase platform unless you absolutely must self-host. Their infrastructure scales, the pricing is fair, and it’s fully managed - meaning less time on ops, more time building your product. Check it out, they have a very generous free tier: Supabase Pricing.

The Big Picture

Here’s what this setup looks like in practice:

  • Frontend: Cloudflare Pages (infinitely scalable static/SPAs)
  • Backend: Supabase (hosted, scalable Postgres, Auth, APIs, etc.)

You get end-to-end scalability and high availability out of the box. No Kubernetes. No load balancers. No EC2 instance to babysit. No disk space issues. That means less friction, faster iteration, and a smoother path to revenue.

Payment Providers

Billing is where you start handling real money - and where things get more serious. You’ll need to pick a payment processor, and thankfully, there are a few mature, well-documented options.

Here are the most common providers in order of popularity:

  • Stripe - Best-in-class documentation, flexible billing models, great ecosystem.
  • Paddle - Focused on SaaS, handles tax compliance globally.
  • Square - More common in retail but works online too.
  • Braintree - Owned by PayPal, good for complex use cases.
  • Lemon Squeezy - Newer, easy to integrate, built for indie devs.

Stripe is the default choice for most devs.

Stripe Logo

It has deep language SDKs, great testing tools, and supports everything from one-time purchases to usage-based billing.

Billing Architecture

Under the hood, payments should be treated like a financial ledger. Don’t just call stripe.charge() and move on. Here are some real-world architecture tips:

  • Audit Log: Keep a local audit log of every transaction.
  • Track Payment Lifecycle: Use Stripe’s webhook system to track subscription lifecycle events.
  • Queue Payment Processing: Use message queues (like Supabase Functions + edge queues or a hosted service like Pipedream).
  • Redundant Subscription State: Store subscription state redundantly in your database with high-precision timestamps so your app can enforce access control even when Stripe is down.
PCI-DSS Scope Consideration

Generally, if you handle credit card data directly, you need to be PCI-DSS compliant. This is a set of security standards designed to ensure that all companies that accept, process, store or transmit credit card information maintain a secure environment. See: Wikipedia

If you don’t touch raw card data - and let Stripe or another PCI-compliant processor handle that entirely - your compliance burden drops significantly. PCI Logo
Use Stripe Checkout or their Elements UI components. Do not roll your own credit card form unless you really know what you’re doing.

Handling Disputes and Cancellations

You will get chargebacks. You will have people cancel. Plan for it upfront.

  • Set up a clear refund and cancellation policy. Make it visible.
  • Handle disputes through your payment provider’s dashboard.
  • Use soft-deletion or grace periods to give users time to reactivate.
  • Automate email notifications when payments fail or a subscription is about to expire.
  • Store logs and notes for any manual changes to account status.

For long-term success, it’s better to be generous with refunds and cancellations. A hit to your MRR is recoverable - a bad reputation isn’t.

The 7-to-1 Ratio

Research in psychology and marketing suggests that negative experiences have a more significant impact on customers than positive ones. The often-cited ratio is that it takes about seven positive experiences to counteract one negative experience. This highlights the importance of maintaining high standards in customer service and product quality.

Put another way, your happy customer will barely tell 1-2 people about their good experience with you. But your unhappy customer will tell 7-10 people about their bad experience. So, it’s worth investing in a good customer experience, especially when it comes to billing and disputes.

Payment Processing Flow

Let’s walk through what actually happens when someone clicks “Purchase” in your app. We’ll cover the happy path, error handling, and recovery strategies.

The key principle here is idempotency. Every payment request should have a unique identifier so you can safely retry operations without double-charging customers.

sequenceDiagram
    participant 👤 User
    participant 💻 Frontend
    participant 🗄️ Database
    participant 📬 Queue
    participant 🧠 Backend
    participant 💳 Stripe

    👤 User->>💻 Frontend: Clicks "Purchase Pro Plan"
    💻 Frontend->>🗄️ Database: Create payment_request (status: pending)
    🗄️ Database-->>💻 Frontend: Returns request_id
    💻 Frontend->>📬 Queue: Enqueue payment job
    📬 Queue-->>💻 Frontend: Job Queued
    💻 Frontend-->>👤 User: Show "Processing..."

    📬 Queue->>🧠 Backend: Process payment job
    🧠 Backend->>🗄️ Database: Update status: processing
    🧠 Backend->>💳 Stripe: Create payment intent
    
    alt Payment Success
        rect rgb(0, 255, 0, 0.05)
            💳 Stripe-->>🧠 Backend: Payment succeeded
            🧠 Backend->>🗄️ Database: Update status: completed
            🧠 Backend->>🗄️ Database: Upgrade user subscription
            🧠 Backend->>📬 Queue: Enqueue welcome email
            🧠 Backend-->>💻 Frontend: Webhook/polling update
            💻 Frontend-->>👤 User: Show success + redirect
        end
    else Payment Declined
        rect rgb(255, 0, 0, 0.05)
            💳 Stripe-->>🧠 Backend: Payment failed (declined)
            🧠 Backend->>🗄️ Database: Update status: failed, reason: declined
            🧠 Backend-->>💻 Frontend: Webhook/polling update
            💻 Frontend-->>👤 User: Show error + retry option
        end
    else Backend Down
        rect rgb(255, 127, 0, 0.2)
            📬 Queue->>📬 Queue: Retry after exponential backoff
            Note over 📬 Queue: Max 3 retries over 24 hours
        end
    end

Database State Management

Your database should be the single source of truth for payment state. Here’s a minimal but robust schema approach:

-- Payment requests table
CREATE TABLE payment_requests (
  id UUID PRIMARY KEY,
  user_id UUID NOT NULL,
  plan_id VARCHAR(50) NOT NULL,
  amount_cents INTEGER NOT NULL,
  currency VARCHAR(3) NOT NULL,
  status VARCHAR(20) NOT NULL, -- pending, processing, completed, failed, expired
  failure_reason VARCHAR(100),
  stripe_payment_intent_id VARCHAR(100),
  idempotency_key VARCHAR(100) UNIQUE NOT NULL,
  created_at TIMESTAMP NOT NULL,
  updated_at TIMESTAMP NOT NULL
);

-- Subscription state table
CREATE TABLE subscriptions (
  id UUID PRIMARY KEY,
  user_id UUID NOT NULL,
  plan_id VARCHAR(50) NOT NULL,
  status VARCHAR(20) NOT NULL, -- active, past_due, canceled, expired
  current_period_start TIMESTAMP NOT NULL,
  current_period_end TIMESTAMP NOT NULL,
  stripe_subscription_id VARCHAR(100),
  created_at TIMESTAMP NOT NULL,
  updated_at TIMESTAMP NOT NULL
);

Message Queue vs Database Polling

You have two main patterns for handling async payment processing:

  • Use a queue like Redis, AWS SQS, or Supabase Edge Functions
  • Immediate processing with built-in retry logic
  • Better separation of concerns
  • Easier to monitor and debug

Option 2: Database Polling

  • Background job checks for pending payments every few minutes
  • Simpler to implement initially
  • Can miss time-sensitive operations
  • Harder to handle complex retry logic

We recommend starting with a message queue. It’s more resilient and scales better.

Error Handling Strategies

For Declined Cards

  • Don’t retry automatically - the card was actively declined
  • Show a clear error message to the user
  • Offer alternative payment methods
  • Log the decline reason for analytics

For Network/API Errors

  • Use exponential backoff: retry after 1min, 5min, 30min
  • Maximum of 3-5 retries over 24 hours
  • After max retries, mark as expired and notify user
  • Always use the same idempotency key for retries

For Backend Downtime

  • Queue systems should handle this automatically with retries
  • Database transactions should be atomic (all-or-nothing)
  • Use database-level constraints to prevent inconsistent states
  • Monitor failed job rates and set up alerts

Webhook Reliability

Stripe sends webhooks for subscription events, but networks fail. Here’s how to handle that:

flowchart TD
    A[Stripe Webhook Received] --> B{Validate Signature}
    B -->|Invalid| C[Return 400, Log Error]
    B -->|Valid| D[Parse Event Data]
    D --> E{Event Already Processed?}
    E -->|Yes| F[Return 200, Skip Processing]
    E -->|No| G[Process Event + Update DB]
    G --> H{Processing Successful?}
    H -->|No| I[Return 500, Stripe Will Retry]
    H -->|Yes| J[Mark Event as Processed]
    J --> K[Return 200]

Best Practices

  • Always return 200 OK for successfully processed webhooks
  • Return 500 for temporary failures (Stripe will retry)
  • Store webhook events in your database to prevent duplicates
  • Validate webhook signatures to prevent spoofing
  • Process webhooks idempotently

Recovery and Reconciliation

Things will go wrong. Plan for these scenarios:

  • Daily reconciliation job: Compare your database against Stripe’s records
  • Manual recovery tools: Admin interface to retry failed payments
  • Customer support hooks: Easy way to refund, upgrade, or fix account issues
  • Monitoring dashboards: Track payment success rates, failure reasons, and processing times
Testing Your Payment Flow

Test every failure scenario in your staging environment:

  • Declined credit cards (use Stripe’s test card numbers)
  • Network timeouts during payment processing
  • Webhook delivery failures
  • Backend downtime during peak traffic

Your payment system is only as strong as its weakest failure mode.

Summary

You don’t need a PhD in DevOps to launch a SaaS product. Between static hosting (Cloudflare Pages) and a powerful backend-as-a-service (Supabase), you can ship faster and sleep easier.

Payments can get complex, but with Stripe or Paddle, you can offload the hardest parts and stay within PCI compliance.

Set up your billing to be robust, auditable, and testable. Plan for edge cases like disputes, downgrades, and delinquent users.

You don’t need to reinvent this stuff. You just need a setup that scales with you and gets out of your way.