DEV Community

Cover image for 3 Stripe/Express bugs that don't show up until production
Chethan S
Chethan S

Posted on

3 Stripe/Express bugs that don't show up until production

I rebuild the same SaaS backend basics for every side project — auth, multi-tenancy, RBAC, Stripe billing — and always make at least one subtle mistake. So I built it once, properly: Node.js/TypeScript/Express/PostgreSQL/Prisma, boilerplate, tested against real requests. Here are three bugs from the process that a tutorial won't warn you about.

  1. Webhook signature verification fails silently — because of middleware order

Stripe signs the raw request body. If express.json() runs before your webhook route, req.body is already a parsed object by the time you try to verify it — signature check fails every time, with an error that gives no hint it's an ordering problem.

// Must come before app.use(express.json())
app.post('/api/billing/webhook', express.raw({ type: 'application/json' }), handleStripeWebhook);
app.use(express.json());

  1. Stripe will redeliver events — without idempotency you'll double-process a signup

Stripe explicitly documents at-least-once delivery. Guard it with an event-id table:

const seen = await prisma.processedWebhookEvent.findUnique({ where: { id: event.id } });
if (seen) return res.status(200).json({ received: true, duplicate: true });

// ...handle the event...

// Record success AFTER processing — so a mid-handler crash gets safely retried
await prisma.processedWebhookEvent.create({ data: { id: event.id, type: event.type } });

  1. Stripe's status enum outgrows your database enum

Map active/trialing/canceled etc. 1:1 to a DB enum and it works — until Stripe adds a status you didn't model (e.g. paused), and the write throws. Use an allowlist with a logged fallback instead of a hard crash:

const KNOWN = new Set(['trialing', 'active', 'past_due', 'canceled', 'incomplete', 'incomplete_expired', 'unpaid']);
function toPrismaStatus(status: Stripe.Subscription.Status): SubscriptionStatus {
if (KNOWN.has(status)) return status as SubscriptionStatus;
console.warn(Unrecognized Stripe status "${status}" — falling back to 'past_due'.);
return 'past_due' as SubscriptionStatus;
}

Not a full fix — extend the enum when you adopt the feature — but it turns a silent break into a visible warning.

What's actually verified

Auth, multi-tenancy, and RBAC are live-tested against a running server (signup, tenant isolation, token rotation, role checks — all real HTTP requests). Stripe billing follows the practices above and is code-reviewed, but not live-tested end-to-end — India's Stripe onboarding is invite-only right now. Flagging that here, same as on the listing.

If you want it

Full source, MIT-style license, setup + troubleshooting README: https://schethan.gumroad.com/l/saas-backend-starter — $49, or $29 with code LAUNCH20 for the first 20.

Top comments (0)