<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Webappers</title>
    <description>The latest articles on DEV Community by Webappers (@webppers).</description>
    <link>https://dev.to/webppers</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4084781%2F3cf2b323-3a0f-42af-a289-babb60eb498e.png</url>
      <title>DEV Community: Webappers</title>
      <link>https://dev.to/webppers</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/webppers"/>
    <language>en</language>
    <item>
      <title>Testing Stripe webhooks locally without ngrok</title>
      <dc:creator>Webappers</dc:creator>
      <pubDate>Mon, 31 Aug 2026 09:58:16 +0000</pubDate>
      <link>https://dev.to/webppers/testing-stripe-webhooks-locally-without-ngrok-5864</link>
      <guid>https://dev.to/webppers/testing-stripe-webhooks-locally-without-ngrok-5864</guid>
      <description>&lt;p&gt;The standard advice for testing webhooks locally is "use ngrok". It works, but it's the wrong first answer for Stripe specifically, because Stripe ships a free tool that does the job better and doesn't expose your laptop to the internet.&lt;/p&gt;

&lt;p&gt;Here's the whole thing, plus the two situations where you do need something else.&lt;/p&gt;

&lt;p&gt;**&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stripe CLI does this for free
&lt;/h2&gt;

&lt;p&gt;**&lt;br&gt;
Install it, log in, and forward events straight to your local server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe login
stripe listen &lt;span class="nt"&gt;--forward-to&lt;/span&gt; localhost:3000/webhooks/stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prints a signing secret on startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Ready! Your webhook signing secret is whsec_1234abcd... &lt;span class="o"&gt;(&lt;/span&gt;^C to quit&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This secret is different from the one in your Dashboard. It's specific to this listen session. Put it in your local env, not your production one — using the Dashboard's secret while running stripe listen is one of the most common causes of signature verification failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;whsec_1234abcd...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in a second terminal, fire an event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe trigger payment_intent.succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No tunnel, no public URL, no firewall exposure. It also survives restarts of your dev server, which ngrok URLs on the free tier famously do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filter to the events you care about
&lt;/h2&gt;

&lt;p&gt;stripe listen forwards everything by default, which gets noisy fast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe listen &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--events&lt;/span&gt; payment_intent.succeeded,customer.subscription.updated &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--forward-to&lt;/span&gt; localhost:3000/webhooks/stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The raw body trap, since you'll hit it anyway
&lt;/h2&gt;

&lt;p&gt;Signature verification hashes the exact bytes Stripe sent. If a body parser turned them into an object first, verification cannot succeed. In Express, order matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This route must be registered BEFORE express.json()&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhooks/stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                  &lt;span class="c1"&gt;// Buffer, untouched&lt;/span&gt;
      &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;                          &lt;span class="c1"&gt;// everything else after&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;n the Next.js App Router, use await req.text() — never await req.json().&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the CLI runs out
&lt;/h2&gt;

&lt;p&gt;Two cases, and they're both about the same thing: stripe trigger sends a synthetic event. It's generated by Stripe to be well-formed, not drawn from your account.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your real events don't look like the synthetic ones
Synthetic events have empty metadata, no connected-account fields, no expansions, and none of the quirks your actual integration produces. If your handler reads metadata.orderId that you set at checkout, stripe trigger will never populate it, and the handler will look broken locally while working in production — or, worse, the reverse.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To see what your account actually sends, you need to capture a real event. Add a second webhook endpoint in the Dashboard pointing at a request-capturing URL. Stripe delivers to every configured endpoint, so this sits alongside your real one and breaks nothing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You need the same event more than once
Some events are genuinely hard to produce on demand — a subscription renewal, a dispute, a failed payment retry. stripe trigger can fake the shape but not your specific instance of it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Dashboard has a Resend button on each event under Developers → Events, which re-delivers that exact event to your endpoints. That's free and it's often all you need. Its limits: it fires at your registered endpoint, so it can't target localhost unless stripe listen is running, and you can't modify the payload before resending — which you often want to do while narrowing down which field breaks your handler.&lt;/p&gt;

&lt;p&gt;For that loop — capture once, edit, fire repeatedly at your own endpoint — you need a dedicated tool. Hookdeck has a free tier and does it well. webhook.site captures free with no signup but is oriented at inspection rather than replay. I build Eventfy, which is a paid product built around exactly that edit-and-refire loop — mentioned for completeness, and the free options above genuinely cover most of what's in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Local development: stripe listen --forward-to. Free, no tunnel, use the secret it prints.&lt;br&gt;
Seeing what your account really sends: a second endpoint pointing at a capture URL.&lt;br&gt;
Replaying one event repeatedly: Dashboard Resend first; a replay tool if you need to modify the payload between attempts.&lt;br&gt;
ngrok is a good tool that you mostly don't need here.&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>webhooks</category>
      <category>testing</category>
      <category>node</category>
    </item>
    <item>
      <title>Your AI wrote that webhook handler from the docs. The docs are abridged.</title>
      <dc:creator>Webappers</dc:creator>
      <pubDate>Mon, 31 Aug 2026 09:52:26 +0000</pubDate>
      <link>https://dev.to/webppers/your-ai-wrote-that-webhook-handler-from-the-docs-the-docs-are-abridged-3oob</link>
      <guid>https://dev.to/webppers/your-ai-wrote-that-webhook-handler-from-the-docs-the-docs-are-abridged-3oob</guid>
      <description>&lt;p&gt;There's a specific bug I've hit enough times to recognise on sight now, and I haven't seen it written up. It comes from a habit that is otherwise a good one: asking an AI assistant to write your webhook handler.&lt;/p&gt;

&lt;p&gt;The handler it produces looks right. It passes review. It matches the provider's documentation exactly. And then the first real event hits it in production and it throws on a field that isn't there.&lt;/p&gt;

&lt;p&gt;Why this happens specifically with webhooks&lt;br&gt;
An AI assistant writes your handler from what it learned: the provider's documentation, blog posts, and Stack Overflow answers. For most code that's fine — the docs for a REST client describe the REST client accurately.&lt;/p&gt;

&lt;p&gt;Webhook documentation is different, because documentation payloads are abridged on purpose. A real Stripe payment_intent.succeeded event is several hundred lines. Nobody would put that in a getting-started guide, so the docs show a trimmed version with the interesting fields and an ellipsis. That trimmed version is entirely honest as documentation and entirely misleading as a schema.&lt;/p&gt;

&lt;p&gt;So your assistant writes a handler for the illustration, not for the payload. And it has never seen your actual account's events, because nobody publishes those — they're full of real customer data.&lt;/p&gt;

&lt;p&gt;The result is a handler that is confidently wrong in a small number of predictable ways.&lt;/p&gt;

&lt;p&gt;The four shapes this takes&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Nesting that the example flattened
Clerk's user.created is the one that gets everybody. There is no email on the user object. There's an array of email addresses, plus a pointer to which one is primary:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What an assistant tends to write&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// undefined&lt;/span&gt;

&lt;span class="c1"&gt;// What Clerk actually sends&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email_addresses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;primary_email_address_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;email_addresses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;primary_email_address_id&lt;/span&gt;
&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;email_address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The first version doesn't throw. It writes undefined into your database and you find out days later.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Absent versus null
Docs examples usually show every field populated, because a field with a value is more instructive than one without. So the assistant writes:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;metadata is {} on most real events, which is fine. But on some providers optional objects are omitted entirely rather than sent empty — and then metadata is undefined and this line throws Cannot read properties of undefined. Optional chaining costs nothing and is the whole fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Expandable fields that arrive as strings
This one is Stripe-specific and genuinely nasty. Several fields are either an ID string or a full object depending on how the event was created and what was expanded. The docs example shows the expanded object. Reality often sends the string:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;// Works in the docs example, throws on half your real events&lt;br&gt;
const customerEmail = event.data.object.customer.email;&lt;/p&gt;

&lt;p&gt;// Actually safe&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The envelope, not the row
Supabase database webhooks don't send you the row. They send an envelope describing what happened to it:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INSERT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"record"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paid"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"old_record"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assistants routinely write const { id, status } = payload and get nothing, because the row is under record. On UPDATE you also need old_record to know what changed, and on DELETE the row is in old_record and record is null.&lt;/p&gt;

&lt;p&gt;Why testing doesn't catch it&lt;br&gt;
Because the test fixture has the same ancestry as the bug. You ask for tests, and the assistant writes a fixture — from the same documentation example that produced the handler. The handler passes against a payload shaped exactly like the one it was written for. Both are wrong in the same direction, so they agree.&lt;/p&gt;

&lt;p&gt;Provider test tools help less than you'd hope, too. stripe trigger payment_intent.succeeded sends a synthetic event, and synthetic events are also idealised. They're generated, not drawn from your account. The mismatch you care about is between the docs and your real traffic.&lt;/p&gt;

&lt;p&gt;The fix is boring and it works&lt;br&gt;
Stop writing handlers against documentation. Write them against one real payload.&lt;/p&gt;

&lt;p&gt;Capture one real event. Add a second webhook endpoint in your provider's dashboard pointing at any URL that records raw requests. Providers fan out to every configured endpoint, so this runs alongside your real handler and changes nothing.&lt;br&gt;
Trigger it for real. Not the test button — make an actual test-mode purchase, sign up an actual user. You want your account's real output.&lt;br&gt;
Diff it against what your handler assumes. This takes about two minutes and is where you find all four bugs above at once.&lt;br&gt;
Save that payload as your test fixture. Now your tests are anchored to reality rather than to the same illustration that produced the bug.&lt;br&gt;
Replay it while you fix. Being able to fire the identical request at your handler repeatedly is the difference between a ten-minute fix and an afternoon of making test purchases.&lt;br&gt;
Step 5 is the one people skip, and it's the one that hurts. Without replay, each iteration means generating a fresh real event, which for something like a subscription renewal can be genuinely difficult to produce on demand.&lt;/p&gt;

&lt;p&gt;Tools for the capture step&lt;br&gt;
webhook.site is free, needs no signup, and is perfect for steps 1–3. Open it, copy the URL, done. For a one-off diff it's all you need and I'd start there.&lt;/p&gt;

&lt;p&gt;For step 5 you want something that stores events and re-fires them at your own endpoint. Hookdeck does this and has a free tier. I built Eventfy for the same loop, aimed specifically at people shipping AI-written code — it's a paid product with a trial, so weigh it accordingly.&lt;/p&gt;

&lt;p&gt;The tool matters much less than the habit, though. One real payload, captured before you write the handler, prevents every bug in this post.&lt;/p&gt;

&lt;p&gt;If you've hit a fifth shape of this I haven't listed, I'd genuinely like to hear it — I suspect there are more.&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>debugging</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The hardest part of vibe coding isn't building the app</title>
      <dc:creator>Webappers</dc:creator>
      <pubDate>Wed, 19 Aug 2026 10:36:49 +0000</pubDate>
      <link>https://dev.to/webppers/the-hardest-part-of-vibe-coding-isnt-building-the-app-1l7b</link>
      <guid>https://dev.to/webppers/the-hardest-part-of-vibe-coding-isnt-building-the-app-1l7b</guid>
      <description>&lt;p&gt;I've been thinking about this a lot lately.&lt;/p&gt;

&lt;p&gt;With &lt;em&gt;Claude, Codex, Cursor, etc.&lt;/em&gt;, I can get an app from an idea to something working &lt;em&gt;ridiculously quickly&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And honestly, that's great.&lt;/p&gt;

&lt;p&gt;But then comes the part that isn't as fun.&lt;/p&gt;

&lt;p&gt;You put it in front of actual users.&lt;/p&gt;

&lt;p&gt;Someone does something you didn't think of.&lt;/p&gt;

&lt;p&gt;An API returns something weird.&lt;/p&gt;

&lt;p&gt;A background job fails.&lt;/p&gt;

&lt;p&gt;A user creates an account and the account doesn't get added.&lt;/p&gt;

&lt;p&gt;An AI-generated function technically works, but under one specific condition it does the completely wrong thing.&lt;/p&gt;

&lt;p&gt;And suddenly you're spending more time figuring out what actually happened than &lt;em&gt;fixing the problem&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That's the bit I think we're going to underestimate with vibe coding.&lt;/p&gt;

&lt;p&gt;We're getting much better at creating software.&lt;/p&gt;

&lt;p&gt;We're not necessarily getting better at knowing when the software is quietly going wrong.&lt;/p&gt;

&lt;p&gt;That's why I've been building &lt;a href="https://shipsure.space/" rel="noopener noreferrer"&gt;Shipsure&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The idea is to give AI-built apps another layer of visibility around the stuff that tends to go wrong once they're actually being used.&lt;/p&gt;

&lt;p&gt;Not another AI coding agent.&lt;/p&gt;

&lt;p&gt;Not another "paste your error into AI" tool.&lt;/p&gt;

&lt;p&gt;More like:&lt;/p&gt;

&lt;p&gt;Your app broke. Here's exactly what happened.&lt;/p&gt;

&lt;p&gt;I'm still figuring out what the product should become, so I'm genuinely curious about this:&lt;/p&gt;

&lt;p&gt;What's the most annoying bug you've had in an AI-built app that took way longer to find than it should have?&lt;/p&gt;

&lt;p&gt;Not the biggest bug.&lt;br&gt;
The stupid one.&lt;/p&gt;

&lt;p&gt;The one where you eventually found it and thought "there is absolutely no reason this took me 3 hours."&lt;/p&gt;

&lt;p&gt;Those are the problems I'm interested in.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vibecoding</category>
      <category>beginners</category>
      <category>security</category>
    </item>
    <item>
      <title>I kept wondering why my AI-built apps kept breaking after they worked</title>
      <dc:creator>Webappers</dc:creator>
      <pubDate>Wed, 19 Aug 2026 10:24:44 +0000</pubDate>
      <link>https://dev.to/webppers/i-kept-wondering-why-my-ai-built-apps-kept-breaking-after-they-worked-154b</link>
      <guid>https://dev.to/webppers/i-kept-wondering-why-my-ai-built-apps-kept-breaking-after-they-worked-154b</guid>
      <description>&lt;p&gt;AI coding tools have made building an app ridiculously fast.&lt;/p&gt;

&lt;p&gt;You can describe an idea to Claude, Cursor or Codex and have a working application in a matter of days or minutes depending on the project.&lt;/p&gt;

&lt;p&gt;The part I'm finding more interesting now is what happens after you've shipped it.&lt;/p&gt;

&lt;p&gt;Because most modern apps aren't really one application anymore.&lt;/p&gt;

&lt;p&gt;You've got Stripe talking to your backend.&lt;/p&gt;

&lt;p&gt;Shopify sending webhooks.&lt;/p&gt;

&lt;p&gt;Supabase handling your database.&lt;/p&gt;

&lt;p&gt;Resend sending emails.&lt;/p&gt;

&lt;p&gt;Slack receiving notifications.&lt;/p&gt;

&lt;p&gt;Some random API you needed for one feature.&lt;/p&gt;

&lt;p&gt;And then you've got AI agents sitting somewhere in the middle doing things too.&lt;/p&gt;

&lt;p&gt;Everything works great...&lt;br&gt;
Right?&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;&lt;em&gt;The webhook problem&lt;/em&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One of the easiest examples is a webhook.&lt;/p&gt;

&lt;p&gt;Say someone pays through Stripe.&lt;/p&gt;

&lt;p&gt;Stripe sends your application an event.&lt;/p&gt;

&lt;p&gt;Your backend processes it.&lt;/p&gt;

&lt;p&gt;You update the database.&lt;/p&gt;

&lt;p&gt;Then maybe you send a Slack notification.&lt;/p&gt;

&lt;p&gt;Pretty simple.&lt;/p&gt;

&lt;p&gt;Until your endpoint returns a 500.&lt;/p&gt;

&lt;p&gt;Or the server times out.&lt;/p&gt;

&lt;p&gt;Or your code throws an error because the payload wasn't what you expected.&lt;/p&gt;

&lt;p&gt;Or the third-party API is temporarily down.&lt;/p&gt;

&lt;p&gt;Now you've got an event that didn't get processed.&lt;/p&gt;

&lt;p&gt;And unless you've built the monitoring yourself, you might not even know it happened.&lt;/p&gt;

&lt;p&gt;I've ended up writing the same kind of stuff over and over:&lt;/p&gt;

&lt;p&gt;receive webhook&lt;br&gt;
      ↓&lt;br&gt;
process it&lt;br&gt;
      ↓&lt;br&gt;
something fails&lt;br&gt;
      ↓&lt;br&gt;
log error&lt;br&gt;
      ↓&lt;br&gt;
try to figure out what happened&lt;br&gt;
      ↓&lt;br&gt;
manually reproduce it&lt;br&gt;
      ↓&lt;br&gt;
fix it&lt;br&gt;
      ↓&lt;br&gt;
try again&lt;/p&gt;

&lt;p&gt;And that's before you even start thinking about retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;&lt;strong&gt;This gets worse with AI-built applications&lt;/strong&gt;&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;I think this is one of the less-discussed problems with vibe coding.&lt;/p&gt;

&lt;p&gt;AI has made the building part dramatically easier.&lt;/p&gt;

&lt;p&gt;But it also makes it incredibly easy to end up with an application that has 15 different integrations held together by code you generated three weeks ago.&lt;/p&gt;

&lt;p&gt;You might know roughly what the system does.&lt;/p&gt;

&lt;p&gt;But when something fails at 2am, you don't necessarily know:&lt;/p&gt;

&lt;p&gt;Which event failed&lt;br&gt;
What payload was received&lt;br&gt;
Which endpoint processed it&lt;br&gt;
What response came back&lt;br&gt;
How long the request took&lt;br&gt;
Whether it was retried&lt;br&gt;
Whether the failure came from your app or the third-party service&lt;/p&gt;

&lt;p&gt;That's the part that becomes painful.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;&lt;strong&gt;So I built Eventfy&lt;/strong&gt;&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://eventfy.space/" rel="noopener noreferrer"&gt;Eventfy&lt;/a&gt; is basically a place to see what's happening with your webhooks and integrations.&lt;/p&gt;

&lt;p&gt;You can inspect events, see their delivery history, retry failed requests and replay events when you need to.&lt;/p&gt;

&lt;p&gt;Instead of having:&lt;/p&gt;

&lt;p&gt;Stripe → ??? → your application&lt;/p&gt;

&lt;p&gt;you actually get somewhere to see what happened in between.&lt;/p&gt;

&lt;p&gt;The goal isn't to add another giant infrastructure platform to your stack.&lt;/p&gt;

&lt;p&gt;It's to make those annoying integration problems much easier to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  I think this becomes more important as AI coding gets better
&lt;/h2&gt;

&lt;p&gt;There's an interesting irony here.&lt;/p&gt;

&lt;p&gt;The better AI gets at writing software, the more software people are going to ship.&lt;/p&gt;

&lt;p&gt;And the more software people ship, the more integrations they're going to have.&lt;/p&gt;

&lt;p&gt;Which means the bottleneck might move from:&lt;/p&gt;

&lt;p&gt;"Can I build this?"&lt;/p&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;p&gt;"Can I actually keep this thing running?"&lt;/p&gt;

&lt;p&gt;AI can write the webhook handler.&lt;/p&gt;

&lt;p&gt;It can write the API integration.&lt;/p&gt;

&lt;p&gt;It can write the retry logic.&lt;/p&gt;

&lt;p&gt;But when something inevitably goes wrong, you still need to know what happened.&lt;/p&gt;

&lt;p&gt;That's the problem I'm interested in solving with Eventfy.&lt;/p&gt;

&lt;p&gt;Building the app is getting easier. Keeping &lt;em&gt;everything&lt;/em&gt; connected is still a &lt;em&gt;pain&lt;/em&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
