DEV Community

kol kol
kol kol

Posted on

My Payment Webhook Fired But Users Got Nothing — Debugging a Silent Revenue Killer

Last week I discovered that people were paying for my product and getting nothing in return.

Not "nothing" as in the payment failed. The payment went through. Money hit my account. But the user who paid got no access, no account, no login — just a success page and a wall of confusion.

This is the story of a silent revenue killer hiding in a 200-line webhook handler.

The Symptom Was Invisible

There were no error logs. No failed transactions. No angry support emails (yet). The problem was structural:

A user subscribes → Paddle processes payment → Webhook fires → userId is null → Account never created.

The money was real. The access was not.

The Root Cause: Three Assumptions, All Wrong

My original flow made three assumptions that seemed perfectly reasonable at 2 AM:

  1. Users exist before they pay. I assumed someone would create an account, log in, then subscribe. But my checkout page let anyone enter an email and pay — no account required.

  2. The webhook will always find the user. My checkout/route.ts didn't pass the user's Supabase ID to Paddle's customData. So when the webhook fired, there was no way to link the payment back to a user.

  3. syncUserPlan(null) is a no-op. It wasn't. It silently did nothing — no error, no warning, no retry queue.

// The bug in one line:
const userId = await findUserByPaddleId(subscription.userId);
// userId = null → syncUserPlan(null) → nothing happens → money collected, access denied
Enter fullscreen mode Exit fullscreen mode

The Fix: Four Changes That Closed the Loop

1. Create Auth Before Payment

// checkout/page.tsx
// Before: redirect straight to Paddle
// After: create Supabase user first, then pay
const { data: user } = await supabase.auth.signUp({
  email,
  password: crypto.randomUUID(), // temporary password
});
// Pass supabase_user_id to Paddle's customData
Enter fullscreen mode Exit fullscreen mode

2. Reverse-Lookup the User in the Webhook

// webhooks/paddle/route.ts
async function findUserIdBySupabaseId(supabaseId: string) {
  const { data } = await supabase
    .from('User')
    .select('id')
    .eq('supabaseId', supabaseId)
    .single();
  return data?.id;
}
Enter fullscreen mode Exit fullscreen mode

3. Magic Link on the Success Page

After payment, users see their email and a "Send Login Link" button. No passwords to remember, no support tickets.

// checkout/success/page.tsx
// Shows: "Check your inbox at user@email.com"
// Button: "Send me a login link" → Supabase magic link
Enter fullscreen mode Exit fullscreen mode

4. Short-Circuit the Conversion Funnel

Old path: Article → Paywall → /pricing → Subscribe → /checkout → Email → Paddle → no account

New path: Article → Paywall → /checkout → Auth created → Paddle → Login link sent

One fewer step. One fewer place for users to drop off.

The Real Lesson: Test the Happy Path Like It's the Edge Case

We obsess over error handling for things that might fail. But the biggest risk is often the assumption that everything that should work actually does.

Questions I now ask myself before any payment flow goes live:

  • What happens if the webhook fires before the user exists?
  • What happens if the user exists but has no email?
  • What happens if the payment succeeds but the database write fails?
  • Can a user lose money without getting the thing they paid for?

If any of those answers are "I'm not sure" or "that can't happen," you probably have a silent revenue killer in production.

The Numbers That Matter

Before this fix:

  • Payments processed: some number > 0
  • Users actually getting access: fewer than they should have
  • Support tickets: zero (because nobody knew to complain)

After this fix:

  • Every payment creates a user
  • Every user gets a login link
  • Every login attempt is tracked

The scariest bugs aren't the ones that crash your app. They're the ones that quietly take your customers' money and give them nothing back.


Have you found a silent failure in your payment flow? I'd love to hear how you caught it — or if you're still looking.

Top comments (0)