<?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: Uudg</title>
    <description>The latest articles on DEV Community by Uudg (@uudg).</description>
    <link>https://dev.to/uudg</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%2F333383%2F0954a6f1-a5ca-4c8e-a1b0-a848055ce6a0.png</url>
      <title>DEV Community: Uudg</title>
      <link>https://dev.to/uudg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/uudg"/>
    <language>en</language>
    <item>
      <title>Cancelled customers who keep access: the Stripe webhook bug nobody reports</title>
      <dc:creator>Uudg</dc:creator>
      <pubDate>Tue, 29 Sep 2026 16:06:44 +0000</pubDate>
      <link>https://dev.to/uudg/cancelled-customers-who-keep-access-the-stripe-webhook-bug-nobody-reports-5gbi</link>
      <guid>https://dev.to/uudg/cancelled-customers-who-keep-access-the-stripe-webhook-bug-nobody-reports-5gbi</guid>
      <description>&lt;p&gt;If you take subscriptions through Stripe, you keep two copies of "who is paying": Stripe's, and a column in your own database (&lt;code&gt;subscribed&lt;/code&gt;, &lt;code&gt;is_pro&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;...) that your app actually checks. A webhook keeps them in sync.&lt;/p&gt;

&lt;p&gt;When the webhook misses an event, the two copies disagree. It fails in two directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Paid, no access.&lt;/strong&gt; Someone pays and stays locked out. They email you within the hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancelled, still has access.&lt;/strong&gt; Someone cancels, refunds or stops paying, and keeps everything. Nobody emails you about free access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post is about the second one, because it's the one you only find by looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The webhook only handles the purchase
&lt;/h3&gt;

&lt;p&gt;Generated and tutorial webhooks often subscribe to exactly one event, &lt;code&gt;checkout.session.completed&lt;/code&gt;, which grants access. Nothing ever takes it away. You need, at minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;customer.subscription.updated&lt;/code&gt;: status changes (&lt;code&gt;past_due&lt;/code&gt;, &lt;code&gt;unpaid&lt;/code&gt;, &lt;code&gt;canceled&lt;/code&gt;) and plan changes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;customer.subscription.deleted&lt;/code&gt;: the subscription has actually ended&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check in the Stripe dashboard: open your webhook endpoint and look at which events it's subscribed to. If &lt;code&gt;customer.subscription.deleted&lt;/code&gt; isn't there, Stripe never sends it and there's nothing to retry.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Reacting to &lt;code&gt;cancel_at_period_end&lt;/code&gt; instead of the actual end
&lt;/h3&gt;

&lt;p&gt;When a customer cancels "at period end", Stripe sends &lt;code&gt;customer.subscription.updated&lt;/code&gt; with &lt;code&gt;cancel_at_period_end: true&lt;/code&gt;. They've paid until the period ends, so &lt;strong&gt;don't&lt;/strong&gt; revoke access then. Revoke on &lt;code&gt;customer.subscription.deleted&lt;/code&gt;, which fires when the period actually ends. Handlers that only look at the flag either cut paying customers off early or never cut anyone off.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Access granted by the success page
&lt;/h3&gt;

&lt;p&gt;If the checkout success page sets &lt;code&gt;subscribed = true&lt;/code&gt;, anyone who opens that URL gets access, paid or not, and a cancellation webhook can't fix what the webhook never granted. Access should only ever be granted by the webhook, after Stripe confirms payment.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The webhook is failing entirely
&lt;/h3&gt;

&lt;p&gt;Look at recent deliveries on the endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;404:&lt;/strong&gt; the route moved or was renamed in a deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;400:&lt;/strong&gt; signature verification failed. Stripe signs the raw body: read it as text before parsing (&lt;code&gt;await req.text()&lt;/code&gt; in Next.js route handlers and Deno), and check the signing secret belongs to this endpoint and this mode (test and live are different).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;500:&lt;/strong&gt; the handler threw; your function logs say why.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once fixed, replay the failed events from the dashboard. Don't wait long: in live mode Stripe retries a failed delivery for about three days and then gives up, so an endpoint that was down over a long weekend can lose events for good. This also happens with correct code, for example when a deploy takes the endpoint down at the wrong moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Refunds and disputes
&lt;/h3&gt;

&lt;p&gt;A refund or a chargeback doesn't cancel the subscription by itself. If they should end access, handle &lt;code&gt;charge.refunded&lt;/code&gt; and &lt;code&gt;charge.dispute.created&lt;/code&gt; (the dispute event carries a charge id; fetch the charge to find the customer). Decide the policy first: a goodwill refund might keep access; a chargeback usually shouldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to find the customers already affected
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In Stripe, go to &lt;strong&gt;Billing → Subscriptions&lt;/strong&gt; and filter by &lt;strong&gt;Canceled&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Look each customer up in your access table. Anyone who still has access is affected.&lt;/li&gt;
&lt;li&gt;Do the reverse for &lt;strong&gt;Active&lt;/strong&gt;: every active subscriber should have access.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Match on the Stripe customer id if you store it (store it, if you don't); email matching breaks when people pay with a different address than they signed up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the check
&lt;/h2&gt;

&lt;p&gt;Doing this by hand once is fine. Doing it every week is where it slips. I built &lt;a href="https://entitled.dev/?utm_source=devto" rel="noopener noreferrer"&gt;Entitled&lt;/a&gt; to do this comparison on a schedule: it reads Stripe with a restricted read-only key and your app through a read-only Postgres role or a small read-only endpoint, and lists every customer the two sides disagree about, with a suggested fix. The first scan is free.&lt;/p&gt;

&lt;p&gt;Whether or not you use it, check your webhook's event list today. It takes two minutes.&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>webdev</category>
      <category>saas</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Lightweight GitLab pipeline tracker in your Mac’s notch</title>
      <dc:creator>Uudg</dc:creator>
      <pubDate>Tue, 25 Aug 2026 08:57:08 +0000</pubDate>
      <link>https://dev.to/uudg/lightweight-gitlab-pipeline-tracker-in-your-macs-notch-37lb</link>
      <guid>https://dev.to/uudg/lightweight-gitlab-pipeline-tracker-in-your-macs-notch-37lb</guid>
      <description>&lt;p&gt;Hey everyone!&lt;/p&gt;

&lt;p&gt;I juggle quite a few GitLab repos across my team, and honestly, constantly alt-tabbing to the browser just to see if a pipeline passed or failed was driving me crazy.&lt;/p&gt;

&lt;p&gt;So I put together a small weekend project called Pipeline Island.&lt;/p&gt;

&lt;p&gt;It basically hugs your MacBook notch and quietly lets you know when a pipeline is running, finishes, or breaks.&lt;/p&gt;

&lt;p&gt;Quick details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local &amp;amp; private: Uses your own GitLab token locally. No third-party servers, no telemetry.&lt;/li&gt;
&lt;li&gt;Mac only: Built native for macOS.&lt;/li&gt;
&lt;li&gt;Free &amp;amp; MIT licensed: Code is fully open.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/Uudg/pipeline-island" rel="noopener noreferrer"&gt;https://github.com/Uudg/pipeline-island&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love to know what you think or if there are features you’d like to see added!&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>swift</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
