
MongoDB Schema Design for SaaS Billing — A Practical Guide
SaaS billing is one of those things that seems simple until you actually build it. A user signs up, pays monthly, gets access. But behind that flow lies a data model that needs to handle plan changes, proration, trial periods, coupon codes, failed payments, invoice generation, and usage metering — all without corrupting financial records.
After building billing systems for multiple SaaS products at Meteoric, I've landed on a MongoDB schema design that handles most subscription scenarios without over-engineering. Here's the approach that's worked across production projects.
The Core Collections
We use four main collections for billing: organizations, subscriptions, invoices, and usage_events. Each serves a specific purpose and references the others through organization_id.
The organizations collection stores the tenant — company name, billing email, default currency, and tax info. Subscriptions track the current plan, status (active/trialing/past_due/canceled), current period dates, and the Stripe subscription ID for synchronization.
Invoices are generated per billing cycle and store line items, amounts, currency, status (pending/paid/overdue/refunded), and a PDF URL. Usage events track metered billing — API calls, storage used, seats occupied — with timestamps for accurate billing.
Why Embedded Documents Beat References for Subscriptions
A common mistake is storing the subscription as a separate collection with a reference to the organization. This works, but every page load that needs billing status requires a JOIN-like lookup. In MongoDB, embedding the active subscription inside the organization document eliminates this.
The tradeoff is document size. If a single organization has hundreds of subscription history records, embedding everything becomes unwieldy. Our rule: embed the active subscription and last 3 invoices, reference the rest in a separate invoices collection with organization_id.
Handling Plan Changes and Proration
When a user upgrades or downgrades their plan, we create a proration record rather than modifying the existing subscription document. The proration calculates the credit for unused days on the old plan and the charge for remaining days on the new plan. This keeps an audit trail that accounting teams can verify.
The proration calculation follows a simple formula: (daily_rate_old × days_unused) - (daily_rate_new × days_remaining). If the result is positive, the user gets a credit. If negative, they're charged the difference on the next invoice.
Usage Metering at Scale
For metered billing (e.g., pay-per-API-call), storing individual events in MongoDB works up to about 100,000 events per month. Beyond that, we aggregate hourly using MongoDB's $bucket aggregator and store rollups in a usage_summary collection. The raw events get archived to cold storage after 90 days.
The aggregation pipeline groups events by organization_id and hour, then calculates the count. This gives us a hourly usage record that's fast to query for invoice generation and requires minimal storage.
Invoice Generation Strategy
We generate invoices reactively — when a subscription renews, we calculate line items and create the invoice document. Each invoice stores a snapshot of the pricing at that moment, not a reference to the current plan. This is critical: if you change a plan price later, past invoices should not change.
The invoice document includes line_items as an array of embedded objects, each with description, quantity, unit_price, and total. This denormalization is intentional — it makes invoice rendering a single database read with no joins needed.
What About Stripe?
We use Stripe as the payment processor, not the source of truth for billing logic. Stripe handles payment collection, webhooks, and dispute management. But our MongoDB schema is the authoritative record of what was billed, when, and why. This dual approach means we're never locked into Stripe and can switch processors if needed.
The key insight: Stripe webhooks update our local documents, but our local documents drive the billing UI and reporting. This gives users fast page loads (no Stripe API calls on every request) and a consistent billing history even if Stripe experiences downtime. For a broader look at how we build full-stack SaaS products, see our tech stack philosophy and the MVP case study that uses these patterns.

