All Docs
FeaturesSaaS FactoryUpdated February 19, 2026

Stripe Billing Integration — Depth Audit Report (v1.0.77)

Stripe Billing Integration — Depth Audit Report

Audit type: DepthAudit | Version: 1.0.77

This page documents the current state of the Stripe billing integration as verified by an automated depth audit. It covers what is fully operational, what is partially implemented, and what must be completed before launch.


Architecture Overview

The billing system is split across three layers:

LayerFileResponsibility
Stripe Clientsrc/lib/stripe.tsLazy singleton; reads STRIPE_SECRET_KEY
Sync Functionssrc/lib/stripe-sync.tsBidirectional sync between internal DB and Stripe
Webhook Handlersrc/app/api/webhooks/stripe/route.tsReceives and verifies Stripe events
Billing Routersrc/lib/routers/billing.tstRPC mutations for billing actions

Production-Ready Components

Stripe Client

  • Instantiated as a lazy singleton from STRIPE_SECRET_KEY.
  • Package: stripe v17.0.0.

Sync Functions (src/lib/stripe-sync.ts)

All of the following make real Stripe API calls:

syncCustomerToStripe(customer)     → Stripe Customer object
syncProductToStripe(product)       → Stripe Product object
createCheckoutSession(params)      → Stripe Checkout Session URL
createPortalSession(customerId)    → Stripe Customer Portal URL
chargeInvoiceViaStripe(invoiceId)  → Stripe PaymentIntent

Webhook Handler (src/app/api/webhooks/stripe/route.ts)

The webhook endpoint:

  1. Reads the raw request body and stripe-signature header.
  2. Calls stripeClient.webhooks.constructEvent(body, sig, secret) to verify authenticity.
  3. Routes verified events to the appropriate handler:
Stripe EventInternal Handler
payment_intent.succeededhandlePaymentIntentSucceeded
payment_intent.payment_failedhandlePaymentIntentFailed
Subscription eventshandleStripeSubscriptionEvent
  1. All handlers write results back to the internal database.

⚠️ Critical Gap — Credit Top-Up Is Not Wired to Stripe

What the UI shows

The "Top Up Credits" button in the billing UI (src/app/.../credit-top-up.tsx) presents itself as a payment action. The current label reads:

"Virtual credits for now — Stripe billing coming soon"

What the code actually does

The topUp tRPC mutation (src/lib/routers/billing.ts, lines 47–70) does not create a Stripe Checkout Session. It directly increments the user's credit balance in the database with no payment collected.

// src/lib/routers/billing.ts (lines 47–70) — CURRENT BEHAVIOUR
// Virtual top-up for now — Stripe billing coming soon
await db.creditBalance.update({
  where: { userId },
  data: { balance: { increment: amount } },
});

Required change

Replace the virtual increment with a createCheckoutSession call and redirect the user to Stripe Checkout. Only credit the balance after the payment_intent.succeeded webhook is received and verified.

// Target behaviour
const session = await createCheckoutSession({
  customerId,
  lineItems: [{ price: creditPriceId, quantity: 1 }],
  successUrl,
  cancelUrl,
});
return { checkoutUrl: session.url };

Risk

SeverityDescription
🔴 CriticalUsers can obtain unlimited free credits by clicking "Top Up"
🔴 CriticalNo revenue is collected for credit purchases
🟡 MediumUsers may believe they have been charged when they have not

This feature must not be enabled in production until the fix is deployed.


Environment Variables Required

VariablePurpose
STRIPE_SECRET_KEYAuthenticates the Stripe client
STRIPE_WEBHOOK_SECRETVerifies incoming webhook signatures

See the environment configuration guide for setup instructions.


Audit Verdict

ComponentStatus
Stripe client singleton✅ Real
Customer / product sync✅ Real
Checkout & portal sessions✅ Real
Invoice charging✅ Real
Webhook verification & routing✅ Real
Credit top-up (topUp mutation)🚨 Virtual — not wired to Stripe