Billing & Stripe Integration Gap — What We Found and How We're Fixing It
Billing & Stripe Integration Gap — What We Found and How We're Fixing It
Release: v1.0.46 · Category: Enterprise Infrastructure · Severity: High
The Short Version
The SaaS Factory platform has a fully implemented internal billing engine. It creates invoices, tracks subscriptions, manages dunning, and applies credits. What it does not do — and should — is talk to Stripe. Real money movement for the platform's own subscriptions is not wired up. This post explains what exists, what is missing, and the path to fixing it.
What Was Built
The billing infrastructure is not a stub. Three production-grade modules handle the full lifecycle of an invoice:
billing-engine.ts
The core orchestration layer. It generates invoices on billing cycle boundaries, applies any available credits, and records the result. From the engine's perspective, a charge either succeeds (credits cover it) or fails (credits are insufficient) — but this determination never leaves the database. No Stripe API call is made at any point.
billing-schema.ts
The data model underpinning the billing system. Subscription plans, invoice states (pending, paid, overdue, written_off), payment records, and credit ledger entries are all modelled here. The schema is well-structured and will map cleanly onto Stripe's object model once integration is implemented.
dunning-crons.ts
Scheduled cron jobs that run the dunning sequence: retry reminders, escalation notices, and ultimately subscription suspension for unresolved overdue invoices. These crons fire on schedule and correctly identify overdue invoices — but the retry attempts have no mechanism to actually collect payment from a customer.
What Is Missing
Stripe is the designated payments layer in ARCHITECTURE.md. The expectation is that the factory's own billing system uses Stripe for real money movement, just as every product the factory builds uses Stripe for its customers' money movement.
The gap breaks down into three areas:
1. Payment Collection
When an invoice is generated, billing-engine.ts should create a Stripe Payment Intent and attempt to confirm it against the customer's stored payment method. Currently, no such call is made. The invoice is recorded as pending indefinitely, or resolved via credits only.
2. Customer & Subscription Sync
Stripe maintains its own customer and subscription objects. The platform's internal records need to stay in sync with Stripe — particularly for plan upgrades, downgrades, trial conversions, and cancellations. None of this sync logic currently exists.
3. Webhook Ingestion
Stripe communicates payment outcomes asynchronously via webhooks. The platform's internal state (invoice.paid, invoice.payment_failed, subscription.cancelled) should be updated in response to Stripe webhook events, making Stripe the source of truth for payment status. The template app the factory generates for new SaaS products already has this webhook handler — it simply has not been applied to the factory's own billing layer.
Why This Matters
The dunning system is particularly affected. It is sending retry sequences — reminder emails, escalation notices — against invoices that can never be collected because there is no payment instrument connected. Customers will receive dunning communications for a system that is not actually trying to charge them, which will create confusion and erode trust.
More fundamentally: the platform cannot generate revenue for itself until this is resolved. Every subscription and every invoice that has been recorded in the database represents money that has not moved.
The Fix
The resolution follows the same pattern already used in the factory's template app. The work items are:
-
Stripe customer provisioning — On account creation, create a corresponding
Customerobject in Stripe and store thestripe_customer_idagainst the internal account record. -
Payment Intent creation on invoice generation — When
billing-engine.tsgenerates an invoice, create a Stripe Payment Intent for the invoice amount and attempt confirmation. Update the internal invoice state based on the Payment Intent outcome. -
Subscription sync — Mirror plan creation, upgrades, downgrades, trials, and cancellations to Stripe Subscriptions so that Stripe's billing cycle can optionally take over scheduled charge creation in future.
-
Webhook handler for the factory itself — Implement a
/api/webhooks/striperoute (or equivalent Inngest-backed handler) that processespayment_intent.succeeded,payment_intent.payment_failed,invoice.paid,invoice.payment_failed, andcustomer.subscription.deletedevents to keep internal state authoritative. -
Dunning alignment — Once Stripe is the payment source of truth,
dunning-crons.tsshould read payment status from Stripe-backed invoice state rather than the internal charge records, ensuring retry signals are grounded in real payment attempts.
Current State Summary
| Capability | Status |
|---|---|
| Invoice generation | ✅ Working (internal only) |
| Credit application | ✅ Working |
| Dunning / retry scheduling | ✅ Working (but ineffective without Stripe) |
| Stripe customer creation | ❌ Not implemented |
| Stripe Payment Intent creation | ❌ Not implemented |
| Stripe subscription sync | ❌ Not implemented |
| Stripe webhook processing | ❌ Not implemented |
| Real payment collection | ❌ Not functional |
Environment Variables Required
Once the Stripe integration is implemented, the following environment variables must be configured:
| Variable | Purpose |
|---|---|
STRIPE_SECRET_KEY | Server-side API key for creating customers, payment intents, and subscriptions |
STRIPE_WEBHOOK_SECRET | Used to verify the signature of incoming Stripe webhook events |
STRIPE_PUBLISHABLE_KEY | Client-side key for rendering Stripe Elements (payment forms) |
This post tracks a high-severity infrastructure gap identified in v1.0.46. It will be updated as the Stripe integration is implemented and shipped.