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:
| Layer | File | Responsibility |
|---|---|---|
| Stripe Client | src/lib/stripe.ts | Lazy singleton; reads STRIPE_SECRET_KEY |
| Sync Functions | src/lib/stripe-sync.ts | Bidirectional sync between internal DB and Stripe |
| Webhook Handler | src/app/api/webhooks/stripe/route.ts | Receives and verifies Stripe events |
| Billing Router | src/lib/routers/billing.ts | tRPC mutations for billing actions |
Production-Ready Components
Stripe Client
- Instantiated as a lazy singleton from
STRIPE_SECRET_KEY. - Package:
stripev17.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:
- Reads the raw request body and
stripe-signatureheader. - Calls
stripeClient.webhooks.constructEvent(body, sig, secret)to verify authenticity. - Routes verified events to the appropriate handler:
| Stripe Event | Internal Handler |
|---|---|
payment_intent.succeeded | handlePaymentIntentSucceeded |
payment_intent.payment_failed | handlePaymentIntentFailed |
| Subscription events | handleStripeSubscriptionEvent |
- 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
| Severity | Description |
|---|---|
| 🔴 Critical | Users can obtain unlimited free credits by clicking "Top Up" |
| 🔴 Critical | No revenue is collected for credit purchases |
| 🟡 Medium | Users 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
| Variable | Purpose |
|---|---|
STRIPE_SECRET_KEY | Authenticates the Stripe client |
STRIPE_WEBHOOK_SECRET | Verifies incoming webhook signatures |
See the environment configuration guide for setup instructions.
Audit Verdict
| Component | Status |
|---|---|
| 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 |