Case StudyBilling & Revenue Operations PlatformAPI Integration & Data PipelineBackend-Heavy Full-Stack

Subscription & Billing Platform for US SaaS Company

Built a custom billing and subscription management platform for a mid-stage US SaaS company that had outgrown Stripe's native dashboard. The system handles plan management, usage-based metering, invoice generation, dunning workflows, and revenue reporting — all integrated with Stripe as the payment processor.

SaaS / B2B·5 months·
ReactNode.jsPostgreSQLStripe APIRedisDockerAWSTypeScript
Problem Defined
Architecture
Implementation
Shipped
System Overview
React
Node.js
PostgreSQL
Stripe API
Redis
Docker

Primary stack components — see Architecture Decisions section for detail

Problem

The company had approximately 1,200 paying accounts across three pricing tiers, with a growing segment on usage-based plans. Their billing workflow was a patchwork of Stripe dashboard operations, manual spreadsheet adjustments for usage overages, and a custom script that ran monthly to reconcile subscription changes. Finance spent roughly two full days each month closing the books because the source of truth for revenue was split between Stripe, their internal database, and a shared Google Sheet. Proration errors on mid-cycle plan changes were a recurring support issue — roughly 15 tickets per month were billing disputes.

Context

The product is a B2B analytics platform sold to mid-market companies. Pricing includes a base subscription fee plus usage-based charges calculated from API call volume and data storage. Customers can upgrade, downgrade, add seats, or switch between annual and monthly billing at any point in their cycle. The existing setup used Stripe Billing for subscription management, but all usage-based charges were calculated externally and pushed to Stripe as manual invoice line items. There was no self-service portal — all plan changes went through the support team.

Constraints

Stripe had to remain the payment processor.

Stripe had to remain the payment processor — the company's finance team had built their revenue recognition workflow around Stripe's reporting, and switching processors was out of scope. Usage data needed to be metered in near-real-time (within 15 minutes of the API call) to support customers who monitor their usage dashboards throughout the day. The system had to handle proration correctly for every combination of plan change, billing cycle, and payment method. PCI compliance requirements meant we could not store or transmit card data — everything had to go through Stripe's tokenisation layer.

Approach

We built a billing service layer that sits between the product and Stripe. All subscription lifecycle events (creation, upgrades, downgrades, cancellations, renewals) are managed through our service, which translates business logic into Stripe API calls and maintains a local copy of subscription state. Usage metering is event-driven: the product emits usage events to a Redis-backed queue, which a worker process aggregates into 15-minute windows and writes to PostgreSQL. Invoice generation pulls from both the subscription and usage tables to produce a unified invoice.

Architecture Decisions

Chose to maintain a local subscription state mirror rather than relying solely on Stripe's API for reads. This avoids rate-limiting issues during bulk operations and gives us sub-millisecond read latency for the customer dashboard. Used Stripe webhooks as the source of truth for payment outcomes — our system processes webhook events idempotently and updates local state accordingly. The usage metering pipeline uses Redis Streams for event ingestion because it handles backpressure well and gives us exactly-once processing semantics with consumer groups. PostgreSQL stores aggregated usage records, not raw events — we accepted losing raw event granularity to keep the database manageable.

Tradeoffs

Maintaining a local state mirror of Stripe subscriptions introduces the risk of state drift. We mitigate this with a nightly reconciliation job that compares our local records against the Stripe API and flags discrepancies. In practice, drift has been rare (two incidents in five months, both caused by webhook delivery delays during Stripe maintenance windows). We also chose not to build a full self-service portal in the initial release — customers can view their usage and invoices but still contact support for plan changes. This was a scope decision: the plan change logic (proration, credit application, mid-cycle adjustments) needed more validation with real scenarios before we exposed it to self-service.

Implementation Notes

Result

Monthly close time for the finance team went from two full days to approximately four hours. Billing-related support tickets dropped from around 15 per month to 2–3 per month. Usage data is now visible to customers within 15 minutes of the API call, compared to the previous next-day batch update. The company's finance team now uses the platform's revenue dashboard as their primary reporting tool rather than exporting data from Stripe and reconciling it manually.

Lessons Learned

The biggest lesson was that billing logic is domain-specific in ways that are hard to anticipate.

Stripe handles the common cases well, but every company has edge cases that don't fit neatly into Stripe's model — things like custom contract terms, negotiated discounts that apply only to the base fee but not usage charges, or mid-cycle plan changes that coincide with a failed payment retry. We spent roughly 30% of the project timeline on proration and edge case handling alone. The reconciliation job was originally an afterthought but turned out to be essential for the finance team's confidence in the system. If we were starting over, we'd design the reconciliation layer from day one rather than bolting it on later.

Have a similar challenge?

Let's talk about your project.

Start a conversation