---
title: "Discussion on \"Why Stripe Test Mode Lies: A Solo Dev's 36-Hour Payment Infrastructure Trench Warfare\" | Hashnode"
description: "Discussion on \"Why Stripe Test Mode Lies: A Solo Dev's 36-Hour Payment Infrastructure Trench Warfare\". A solo dev's 36-hour battle debugging Stripe and RevenueCat webhooks for a Kotlin Multiplatform app — and the 3 fixes that saved it."
url: "https://hashnode.com/posts/why-stripe-test-mode-lies-a-solo-dev-s-36-hour-payment-infrastructure-trench-warfare/6accab3a2cf2ed754311386a"
---

[Feed](https://hashnode.com/feed) /

Discussion

# [Why Stripe Test Mode Lies: A Solo Dev's 36-Hour Payment Infrastructure Trench Warfare](https://epicdevlog.hashnode.dev/why-stripe-test-mode-lies-a-solo-dev-s-36-hour-payment-infrastructure-trench-warfare?utm_source=hashnode&utm_medium=feed)

A solo dev's 36-hour battle debugging Stripe and RevenueCat webhooks for a Kotlin Multiplatform app — and the 3 fixes that saved it.

Oct 12 · 3 min read · epicdevlog.hashnode.dev  
Read the article

-   [#kotlin](https://hashnode.com/tag/kotlin)
-   [#productivity](https://hashnode.com/tag/productivity)
-   [#notes](https://hashnode.com/tag/notes)
-   [#buildinpublic](https://hashnode.com/tag/buildinpublic)
-   [#indiehacker](https://hashnode.com/tag/indiehacker)
-   [#stripe](https://hashnode.com/tag/stripe)

## Written by

[Epic Devs](https://hashnode.com/@epicdevs)

@epicdevs · London, UK

  
[View profile](https://hashnode.com/@epicdevs)

Building dual-platform SaaS on nights and weekends. No fluff, just code, Stripe nightmares, and the reality of shipping

I'm Usama, a PM and solo dev building EpicNotes (a native KMP app for the Android+Mac combo). I write about the unglamorous side of shipping: wiring webhooks at 2 AM, fighting app store compliance, and the code that actually survives.

## Responses 8

Markdown supported · ⌘↵ to post

[Paul Hartness](https://hashnode.com/@retryfix)

[Oct 21](https://hashnode.com/posts/why-stripe-test-mode-lies-a-solo-dev-s-36-hour-payment-infrastructure-trench-warfare/6accab3a2cf2ed754311386a/comment/6ad89c7aa7508b2879f9f9bb)

The one that catches most people: test mode never shows you a renewal failing a month later. In production, a chunk of failed renewals are temporary declines that a later retry recovers, and the rest need the customer to update their card. Neither shows up until you've had real subscribers for a few billing cycles.

[Epic Devs](https://hashnode.com/@epicdevs)

[Oct 21](https://hashnode.com/posts/why-stripe-test-mode-lies-a-solo-dev-s-36-hour-payment-infrastructure-trench-warfare/6accab3a2cf2ed754311386a/comment/6ad89c7aa7508b2879f9f9bb#reply-6ad8a9ff10e63767dd0e8890)

So true — billing cycle two is the real launch. Test mode teaches you how subscriptions start; production teaches you how they fail. The past\_due vs canceled distinction bit me too. At first I was revoking access on the very first invoice.payment\_failed, which nuked users whose bank would've approved Stripe's retry two days later. Now I keep entitlements alive through the retry window (Smart Retries + dunning emails enabled) and only flip to canceled when Stripe says the subscription is genuinely dead. And the card-update half is why I eventually added a "payment failed — update your card" screen that deep-links to the billing portal. Tiny feature, outsized churn saver.

Did you bake a grace period into your own entitlement logic, or do you let the payment provider's retry schedule handle it entirely?

[Paul Hartness](https://hashnode.com/@retryfix)

[Oct 21](https://hashnode.com/posts/why-stripe-test-mode-lies-a-solo-dev-s-36-hour-payment-infrastructure-trench-warfare/6accab3a2cf2ed754311386a/comment/6ad89c7aa7508b2879f9f9bb#reply-6ad8cf1ae66fc4a503526ce8)

Ha, "cycle two is the real launch" is going on a sticky note. past\_due vs canceled is the one that bites everyone: treat past\_due as "still a customer, still recoverable" and you avoid locking people out who'd have paid on the next retry. Good luck with cycle two.

[StackShortlist](https://hashnode.com/@veristria)

[Oct 17](https://hashnode.com/posts/why-stripe-test-mode-lies-a-solo-dev-s-36-hour-payment-infrastructure-trench-warfare/6accab3a2cf2ed754311386a/comment/6ad2fc6e02a2aad0092ee6a2)

The zombie Pro user case is a great example of why out-of-order webhooks bite. Fetching the live subscription state by subscription\_id instead of trusting the payload is a clean fix. The $2 real purchase and refund test is cheap insurance too. Congrats on launching EpicNotes.

[Mohan](https://hashnode.com/@resumeorbitz)

[Oct 13](https://hashnode.com/posts/why-stripe-test-mode-lies-a-solo-dev-s-36-hour-payment-infrastructure-trench-warfare/6accab3a2cf2ed754311386a/comment/6ace1152f4ebcd481f9cbcb9)

The zombie-user bug is a great catch. Trusting webhook order feels reasonable right up until a retry lands late, and re-fetching the live subscription by ID is the kind of fix that’s only obvious after you’ve been burned. I’m a solo dev on Supabase too and haven’t wired payments into my product yet, so this goes straight into my notes. Did you also store processed event IDs to skip duplicates, or was the re-fetch enough on its own?

[Epic Devs](https://hashnode.com/@epicdevs)

[Oct 13](https://hashnode.com/posts/why-stripe-test-mode-lies-a-solo-dev-s-36-hour-payment-infrastructure-trench-warfare/6accab3a2cf2ed754311386a/comment/6ace1152f4ebcd481f9cbcb9#reply-6ace11ef342d15a038821ba4)

Thank you! And honestly — re-fetch alone WAS enough for _state_ correctness, and that's the part I'd emphasize to anyone pre-payments: if every handler re-fetches live state by subscription ID instead of trusting the payload, processing becomes convergent. A duplicate event just re-fetches the same truth and writes the same row. Duplicates become harmless no-ops against the DB.

Where it still bites is side effects. The DB converges, but nothing stops a duplicate from re-sending the "Welcome to Pro" email, double-logging an upgrade analytics event, or burning a redundant Stripe API call. So no — I didn't originally store event IDs, and I learned the hard way that state idempotency ≠ side-effect idempotency.

What's on the backlog now: a small processed\_events table (event\_id PK, type, created\_at, processed\_at). Handler checks it first and returns 200 early on duplicates, plus an ordering guard — only apply if event.created is newer than the last applied timestamp for that subscription, so a stale retry can't overwrite fresher state under concurrent delivery.

Since you're pre-payments on Supabase, the combo I'd wire from day one: (1) verify signatures, (2) re-fetch live state, (3) event-ID store for side effects. It's maybe 30 lines of Edge Function and it deletes an entire class of 2 AM bugs. Happy to share my handler skeleton if it's useful — good luck with the wiring, it's about to get interesting!

[Mohan](https://hashnode.com/@resumeorbitz)

[Oct 14](https://hashnode.com/posts/why-stripe-test-mode-lies-a-solo-dev-s-36-hour-payment-infrastructure-trench-warfare/6accab3a2cf2ed754311386a/comment/6ace1152f4ebcd481f9cbcb9#reply-6acf8cf354e3a146575d578f)

"State idempotency != side-effect idempotency" is the line I needed. I'd have assumed the re-fetch covered everything and then wondered why someone got two welcome emails. The ordering guard on event.created is a nice touch too. And yes, I'd love the handler skeleton. Signatures, re-fetch, event-ID store from day one is going straight into my Supabase Edge Function plan. Thanks for writing it all out!

[Mohan](https://hashnode.com/@resumeorbitz)

[Oct 22](https://hashnode.com/posts/why-stripe-test-mode-lies-a-solo-dev-s-36-hour-payment-infrastructure-trench-warfare/6accab3a2cf2ed754311386a/comment/6ace1152f4ebcd481f9cbcb9#reply-6ada4fb1106819373ed5278e)

Hey Usama, just circling back on the handler skeleton. Still keen to see it whenever you get a minute, no rush. And congrats on getting EpicNotes live!
