← All field notes
App Security

Your App Took the Payment but No Order Showed Up: Where to Look

Stripe shows the charge, your database has no order. Eight ways the record goes missing between checkout and your orders table, and the query that finds every one.

Matthew TurleyNovember 5, 20266 min read

The customer paid. The card was charged. Your app showed a success page, and your orders table has nothing. If your app took the payment but no order showed up, the order record is being created in a place that does not always run, or the write is failing and nobody sees the error.

The fastest way to find out which is to follow one payment through the four hops it takes: Stripe, your server, your database, your screen. Here is the checklist, with the failures I would rule out first.

Start with a query, not a theory

You need to know how big the gap is and whether it has a pattern. Take the payment IDs from Stripe for the last week and compare them with your table.

# Paid payment intents from Stripe (needs the Stripe CLI, live mode)
stripe payment_intents list --limit 100 --live | grep -E '"id"|"status"'
-- In your database: which payment IDs do you have orders for?
select stripe_payment_intent_id, status, created_at
from orders
where created_at > now() - interval '7 days'
order by created_at desc;

Put the two lists side by side. Look at the missing ones: are they all mobile? All guest checkouts? All after a certain deploy? The pattern points at the cause below.

1. The order is created only on the success page

If the order insert runs when the customer lands on /success, then every customer who closes the tab, loses signal, or is sent somewhere else after paying never creates an order. The charge already happened. The browser is not a reliable messenger for a payment event. Stripe's webhook guide is built around this: the server learns about payments from events sent to your endpoint1.

Fix: create or confirm the order in the webhook handler. Let the success page only display the order that the webhook wrote, and show a short "confirming your payment" state until it exists.

2. The insert is blocked by a database access rule, and fails quietly

This one is common in apps built on Supabase. With Row Level Security on and no insert policy, the database rejects the write. The client library returns an error object instead of throwing, so code that only checks for a thrown exception carries on as if it worked.

// Looks fine, silently loses the order
await supabase.from('orders').insert({ stripe_payment_intent_id: pi.id })

// Check the result
const { error } = await supabase.from('orders').insert({ stripe_payment_intent_id: pi.id })
if (error) throw new Error(`order insert failed: ${error.message}`)

A blocked write typically reports "new row violates row-level security policy for table". If you see that message, read the Supabase security checklist before you reach for a quick fix, because the quick fix is usually a policy that lets everyone write.

3. The webhook writes with the public key instead of a server key

A webhook has no logged-in user. If it creates its database client with the public anon key, the access rules treat Stripe's request as an anonymous stranger and block the write. The correct fix is not to open the table to anonymous writes. Create the client in the webhook with a server-only key, kept in an environment variable without a NEXT_PUBLIC_ or VITE_ prefix, and keep that key out of the browser. The API key in the frontend post covers why.

4. The payment can't be matched to a customer or order

Stripe knows the payment. Your database knows the user. Something has to join them. If you never passed your own identifier into Checkout, the webhook has only an email, and the email in Stripe may not match the one on the account.

  • Pass your user or order ID as client_reference_id or in metadata when you create the Checkout Session.
  • Read it back from the event in the webhook and use it to find the order.
  • Store the Stripe payment ID on the order so you can reconcile later.

5. The order is created before payment and never updated

Some flows insert a pending order first, then flip it to paid after the event. If the update matches no row, because the lookup used a field that was never saved, the order stays pending forever and the screen that lists paid orders hides it. Check for rows stuck in pending older than an hour.

6. The order exists, and your screen filters it out

Before you assume the write failed, query the table directly. A status value that no longer matches the filter on your orders page, or a list scoped to a user ID that is empty for guest checkouts, will hide rows that are really there. Check the database, not just the UI.

7. Live payments are landing in the wrong project

AI builders often leave a staging or preview database wired to the same code. A live-mode payment that writes to the staging project looks exactly like a lost order. Confirm which database URL your production environment actually uses, and that the live Stripe keys and the live database belong together.

8. The handler errors after the money has moved

If your handler throws halfway, for example after creating the customer but before creating the order, Stripe sees a 5xx and retries, and you may end up with half-built records. Make the handler idempotent: key the order on the Stripe payment ID with a unique constraint, so a retry updates the same row instead of creating a second one or being skipped.

alter table orders add constraint orders_payment_unique unique (stripe_payment_intent_id);

Then write the order with an upsert on that column.

What the free check and the $47 scan can and cannot tell you

The free Leak Check is an outside view of your live site. Its payment-path check only looks for a checkout or Stripe reference on the page, so it cannot tell you that a paid order failed to save. It does check whether a secret key is sitting in your client bundle, which matters if step 3 tempted anyone to ship a server key to the browser.

The $47 Complete Scan adds anonymous write attempts against the common tables. That is the other half of step 2 and 3: the right state is that a stranger with only the public key cannot write to your orders table, and your server path can. The scan checks the stranger side. It does not run your webhook.

Next step

Run the comparison query above for the last seven days. If the missing payments share a pattern, you now know which step to fix. Then run the free Leak Check on your live URL to see what the outside world sees. If you want the checkout, webhook and database policies read together from the inside, that is the Ship Check.

Related: paid order never reached your CRM and customer paid but is still on the free plan.

Sources

Market figures above are marked with a number that links here. Our own prices are marked as ours and come from our published pricing.

  1. 1. Receive Stripe events in your webhook endpoint, Stripe, checked November 5, 2026. Stripe's webhook guide: verify the signature against the raw request body, return a 2xx quickly, handle duplicate deliveries, and do not depend on event order.

Worried your app has holes like these?

Drop your email and we will run a free teardown of your stack and send back the leaks we find, no pitch.

M
Matthew Turley, Continuum

Fractional CTO and embedded technical partner. 20+ years shipping production software.

Run a free Leak Check →
App Took Payment But No Order Showed: Stripe Checklist