<?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: Mihir kanzariya</title>
    <description>The latest articles on DEV Community by Mihir kanzariya (@mihirkanzariya).</description>
    <link>https://dev.to/mihirkanzariya</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%2F271724%2F04df8c1f-7f8f-49c7-8800-4fd47418c12d.png</url>
      <title>DEV Community: Mihir kanzariya</title>
      <link>https://dev.to/mihirkanzariya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mihirkanzariya"/>
    <language>en</language>
    <item>
      <title>Five affiliate fraud patterns I caught on Stripe (and the queries that found them)</title>
      <dc:creator>Mihir kanzariya</dc:creator>
      <pubDate>Fri, 19 Jun 2026 06:48:24 +0000</pubDate>
      <link>https://dev.to/mihirkanzariya/five-affiliate-fraud-patterns-i-caught-on-stripe-and-the-queries-that-found-them-29a1</link>
      <guid>https://dev.to/mihirkanzariya/five-affiliate-fraud-patterns-i-caught-on-stripe-and-the-queries-that-found-them-29a1</guid>
      <description>&lt;p&gt;Your affiliate program will attract fraud before it attracts revenue. That sounds pessimistic, but the math checks out: a 30% recurring commission on a $49/mo plan pays $14.70/mo per referred customer. Fraudsters do the arithmetic faster than you do.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://referralful.com/?utm_source=dev.to&amp;amp;utm_medium=content&amp;amp;utm_campaign=article"&gt;Referralful&lt;/a&gt;, affiliate software for SaaS on Stripe. These are five patterns I've seen in production, with the Stripe data points and queries that catch them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Self-referrals
&lt;/h2&gt;

&lt;p&gt;The simplest one. An affiliate signs up, generates their own referral link, opens an incognito window, and subscribes through it. They collect commission on their own purchase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection signal:&lt;/strong&gt; The affiliate's email domain, billing name, or Stripe customer metadata overlaps with the referred customer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;affiliate_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stripe_customer_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;affiliates&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;referrals&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;affiliate_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;affiliate_email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;billing_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;billing_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the Stripe payment method fingerprint too. Stripe assigns a unique &lt;code&gt;fingerprint&lt;/code&gt; to each card. Same card on the affiliate account and the referred account? Self-referral.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Compare card fingerprints via Stripe API
&lt;/span&gt;&lt;span class="n"&gt;affiliate_methods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;affiliate_stripe_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;customer_methods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;referred_stripe_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;affiliate_fps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;affiliate_methods&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;customer_fps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;customer_methods&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;overlap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;affiliate_fps&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;customer_fps&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;flag_referral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;referral_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared_card_fingerprint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Churn-and-re-sign
&lt;/h2&gt;

&lt;p&gt;An affiliate refers a real customer. The customer churns. The affiliate convinces them to cancel and re-subscribe through a fresh referral link, resetting the commission window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection signal:&lt;/strong&gt; The same Stripe card fingerprint or email appears in multiple referral records, attributed to the same affiliate, with short gaps between subscriptions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;affiliate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;referral_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_referral&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;latest_referral&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;referrals&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;affiliate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_email&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DAY&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is simple: deduplicate on the customer, not the referral event. First-touch attribution, permanent. If a customer was ever referred by affiliate A, every future subscription from that customer still belongs to affiliate A's original referral, and you only pay the commission window once.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Cookie stuffing via redirect chains
&lt;/h2&gt;

&lt;p&gt;An affiliate embeds your referral link in an iframe, image pixel, or redirect chain on a high-traffic page. Visitors never see the link or click it intentionally. The affiliate's cookie lands in their browser, and if they happen to sign up later, the affiliate gets credit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection signal:&lt;/strong&gt; Referral click volume with abnormally low conversion rates, or referral cookies set without a matching &lt;code&gt;Referer&lt;/code&gt; header from a legitimate page.&lt;/p&gt;

&lt;p&gt;Track these columns on every referral click:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;click&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;timestamp&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;affiliate&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;id&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;ip&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;address&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;agent&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;referer&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;header&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;converted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;affiliate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;clicks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;converted&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;conversions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;converted&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;conv_rate&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;referral_clicks&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;click_timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;affiliate_id&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;conv_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An affiliate with 5,000 clicks and 2 conversions isn't driving real traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Coupon code leaking
&lt;/h2&gt;

&lt;p&gt;You give an affiliate a discount coupon to share with their audience. They post it on coupon aggregator sites (RetailMeNot, Honey, browser extensions). Customers who would have paid full price find the coupon, use it, and the affiliate collects commission on a discounted sale they never influenced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection signal:&lt;/strong&gt; High coupon redemption volume with referral source = coupon site, not the affiliate's actual content.&lt;/p&gt;

&lt;p&gt;On the Stripe side, listen to &lt;code&gt;customer.subscription.created&lt;/code&gt; and check &lt;code&gt;subscription.discount.coupon.id&lt;/code&gt; against your affiliate coupon map. Then compare the &lt;code&gt;customer.created&lt;/code&gt; timestamp with the referral click timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# If the customer existed BEFORE clicking the affiliate link,
# they probably found the coupon independently
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;referral_click&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;flag_referral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;referral_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_predates_click&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structural fix: stop tying coupons to affiliates. Use the referral link for attribution and a separate, non-affiliate discount for promotions. Or accept that coupon leaking is the cost of running discounts and price it into your commission structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Disposable card churn
&lt;/h2&gt;

&lt;p&gt;Affiliates (or bots they run) create accounts using virtual/disposable cards, subscribe at the lowest tier, collect the first commission payout, then let the subscription lapse. Stripe processes the initial charge successfully, your system records a valid referral, and the commission enters your payout queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection signal:&lt;/strong&gt; Multiple referred customers with the same IP, the same card BIN range, or subscriptions that cancel within the first billing cycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;affiliate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_referrals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canceled_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
                &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canceled_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_period_end&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;early_cancels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canceled_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
                &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canceled_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_period_end&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cancel_rate&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;referrals&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscription_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'60 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;affiliate_id&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;cancel_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An affiliate with a 70% first-cycle cancellation rate across 20 referrals isn't sending real customers.&lt;/p&gt;

&lt;p&gt;The structural defense: hold commissions for a pending period (30, 60, or 90 days) before they become payable. If the referred customer cancels or the charge gets refunded during that window, the commission never pays out. I wrote about the ledger design for this in my &lt;a href="https://dev.to/mihirkanzariya/clawing-back-affiliate-commissions-when-a-customer-refunds-design-the-ledger-first-1fk9"&gt;previous article on clawbacks&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the checks before the first payout
&lt;/h2&gt;

&lt;p&gt;The worst time to discover fraud is after you've paid the commission. Wire these queries into a pre-payout review step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before any payout batch, run the self-referral and shared-fingerprint checks.&lt;/li&gt;
&lt;li&gt;Flag any affiliate whose referrals have a first-cycle cancel rate above 50%.&lt;/li&gt;
&lt;li&gt;Flag conversion rates below 0.1% on high-click affiliates.&lt;/li&gt;
&lt;li&gt;Deduplicate referrals by customer email + card fingerprint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You don't need ML or a fraud vendor for this. A few SQL queries and a manual review queue handle it until you're processing hundreds of affiliates. Start there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building Referralful, affiliate software for SaaS startups on Stripe. Free until your first affiliate signs up.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>saas</category>
      <category>security</category>
      <category>payments</category>
    </item>
    <item>
      <title>Clawing back affiliate commissions when a customer refunds: design the ledger first</title>
      <dc:creator>Mihir kanzariya</dc:creator>
      <pubDate>Thu, 18 Jun 2026 08:42:12 +0000</pubDate>
      <link>https://dev.to/mihirkanzariya/clawing-back-affiliate-commissions-when-a-customer-refunds-design-the-ledger-first-1fk9</link>
      <guid>https://dev.to/mihirkanzariya/clawing-back-affiliate-commissions-when-a-customer-refunds-design-the-ledger-first-1fk9</guid>
      <description>&lt;p&gt;You pay an affiliate 30% for referring a customer. Three weeks later that customer refunds. Now the affiliate is holding a commission on a sale that no longer exists, and you have to decide what happens to that money. If you only thought about this after it happened, you have already lost the clean version of the fix.&lt;/p&gt;

&lt;p&gt;I build affiliate software on Stripe, and the clawback case is the one teams skip until it bites. Here is how to design for it before it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Earned is not the same as payable
&lt;/h2&gt;

&lt;p&gt;The first mistake is treating a commission as money the moment the sale clears. It isn't. A commission is a claim against a sale that might still reverse. Stripe makes that gap concrete: a customer can refund whenever they want, and a card dispute can land up to 120 days after the charge. Your payout schedule (say, monthly) runs on a completely different clock.&lt;/p&gt;

&lt;p&gt;So you have two timelines that don't line up: how long a sale can still reverse, and how soon you pay out. Pay before the first window closes and you are paying real money on revenue you may have to give back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hold commissions pending before they become payable
&lt;/h2&gt;

&lt;p&gt;The fix is a holdback period. A commission lands in a "pending" state when the sale clears, and it only becomes "available" for payout after N days with no refund or dispute. Set N from your own refund data, not a guess. If most refunds happen in the first 30 days, a 30 to 45 day hold covers the bulk of them without making affiliates wait forever.&lt;/p&gt;

&lt;p&gt;This one decision removes most clawbacks entirely, because the commission never reaches an affiliate's wallet until the risky window has passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store a ledger, not a balance
&lt;/h2&gt;

&lt;p&gt;Here is the part that saves you later: never store an affiliate's earnings as a single number you edit. Store immutable ledger entries and compute the balance from them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;commission_entries
  id
  affiliate_id
  charge_id            -- the Stripe charge this relates to
  type                 -- earned | reversed | paid_out
  amount_cents         -- signed: + for earned, - for reversed/paid
  status               -- pending | available
  created_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A refund does not edit the original row. It writes a new &lt;code&gt;reversed&lt;/code&gt; entry that points at the same charge. The affiliate's balance is just &lt;code&gt;SUM(amount_cents)&lt;/code&gt; over their entries. You get an audit trail for free, every reversal is explainable, and you never lose the history of why a number changed. When an affiliate emails asking why their balance dropped, you can show them the exact charge and the exact reversal.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the refund lands after you already paid
&lt;/h2&gt;

&lt;p&gt;Sometimes a refund or a dispute arrives after the commission went out the door, usually because a chargeback showed up 60 days later. Now the affiliate's balance goes negative. You have three honest options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Carry the negative forward against future earnings. This is the common one and the fairest: the next commissions they earn pay down the deficit before anything becomes payable again.&lt;/li&gt;
&lt;li&gt;Reverse the transfer. If you pay through Stripe Connect, you can reverse a past transfer, but it is aggressive and it will surprise people. Reserve it for fraud or for affiliates who have gone silent with a large negative.&lt;/li&gt;
&lt;li&gt;Write it off below a threshold. Chasing back a 4 dollar commission costs more in goodwill than it returns. Pick a floor and absorb anything under it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Whatever you choose, decide it once and apply it the same way every time. Inconsistent clawbacks are how you lose good affiliates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stripe wiring
&lt;/h2&gt;

&lt;p&gt;Two webhooks drive this: &lt;code&gt;charge.refunded&lt;/code&gt; and &lt;code&gt;charge.dispute.created&lt;/code&gt;. Each one looks up the commission entries tied to that charge and writes a &lt;code&gt;reversed&lt;/code&gt; entry for the matching amount. For a partial refund, reverse the same proportion of the commission, not the whole thing.&lt;/p&gt;

&lt;p&gt;A detail worth stating: process these as triggers to re-read state, not as the final word. A refund webhook can arrive while the original commission is still settling on your side. Look up the current state of the charge and the existing entries, then write the reversal against what you actually find. (Idempotency matters here too: key the reversal on the refund ID so a redelivered webhook doesn't reverse twice.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Show pending and available separately
&lt;/h2&gt;

&lt;p&gt;This is a product decision as much as an accounting one. Affiliates hate surprise clawbacks more than they like fast payouts. If your dashboard shows one big "earnings" number that quietly drops when a refund lands, every reversal feels like you took their money. Show pending and available as two separate numbers, with the date each pending commission becomes available. Then nobody counts money that isn't theirs yet, and a reversal during the pending window is a non-event instead of a support ticket.&lt;/p&gt;

&lt;p&gt;We keep payout fees at zero and we run this exact pending-then-available model, because the trust cost of getting it wrong is higher than any fee. (Disclosure: I work on Referralful, &lt;a href="https://referralful.com/?utm_source=dev.to&amp;amp;utm_medium=content&amp;amp;utm_campaign=article"&gt;https://referralful.com/?utm_source=dev.to&amp;amp;utm_medium=content&amp;amp;utm_campaign=article&lt;/a&gt; , affiliate software built on Stripe.)&lt;/p&gt;

&lt;p&gt;Design the ledger and the holdback before you write your first commission. Retrofitting an immutable ledger onto a system that stored a single balance is a migration you do not want to run while affiliates are watching their numbers.&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>saas</category>
      <category>payments</category>
    </item>
    <item>
      <title>Paying SaaS affiliates on Stripe: the part teams forget to build</title>
      <dc:creator>Mihir kanzariya</dc:creator>
      <pubDate>Tue, 16 Jun 2026 07:32:09 +0000</pubDate>
      <link>https://dev.to/mihirkanzariya/paying-saas-affiliates-on-stripe-the-part-teams-forget-to-build-4a5d</link>
      <guid>https://dev.to/mihirkanzariya/paying-saas-affiliates-on-stripe-the-part-teams-forget-to-build-4a5d</guid>
      <description>&lt;p&gt;I build affiliate software for SaaS, so I spend most of my time in the part of an affiliate program people forget about: paying the affiliates. Attribution gets the attention. Payouts are where programs break.&lt;/p&gt;

&lt;p&gt;A few things bite once the first commissions come due. How I handle each on Stripe.&lt;/p&gt;

&lt;h2&gt;
  
  
  A commission is not a payout
&lt;/h2&gt;

&lt;p&gt;The first mistake is treating "this referral converted" as "we owe this person money now." You don't, yet. A Stripe payment can be refunded, disputed, or fail on the next renewal. Pay the affiliate the moment the charge succeeds and you end up clawing money back, which is awkward and sometimes impossible.&lt;/p&gt;

&lt;p&gt;So I keep two states. A commission is earned when the payment clears, and payable only after a hold period. I use 30 days, to cover the refund window. During the hold I listen to &lt;code&gt;charge.refunded&lt;/code&gt; and &lt;code&gt;charge.dispute.created&lt;/code&gt; and reverse the commission before it becomes payable. The affiliate sees "pending" until then. That is honest, and it saves you the clawback email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recurring commissions need a stop date
&lt;/h2&gt;

&lt;p&gt;If you pay 30% for 12 months on a subscription, you count months, not payments. A customer who pauses and resumes, or downgrades mid-term, throws off &lt;code&gt;invoice.paid&lt;/code&gt; events that no longer map to "month 4 of 12."&lt;/p&gt;

&lt;p&gt;Anchor the schedule to the subscription's start. On each &lt;code&gt;invoice.paid&lt;/code&gt;, compute how many months have passed since the referral converted. Past month 12 the commission is zero, even though the invoice still fires. Storing the referral date and doing the math each time beats a running counter that drifts every time a subscription changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payout rails: Connect or manual
&lt;/h2&gt;

&lt;p&gt;You have two honest ways to get money to affiliates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stripe Connect&lt;/strong&gt; with Express accounts. The affiliate onboards once, Stripe handles KYC and bank details, and you send transfers. Good at volume. It adds onboarding friction and puts you inside Stripe's payout timing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual payouts&lt;/strong&gt; through PayPal, Wise, or bank transfer, tracked in your own ledger. Less friction to start, more bookkeeping for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ledger matters more than the rail. Every commission needs a row with its state (earned, payable, paid, reversed) and a payout reference. If your "who do we owe" number lives only in a Stripe dashboard view, you cannot answer an affiliate's "where's my money" email.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring failures that cost trust
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimum thresholds.&lt;/strong&gt; Pay out at $50 instead of $4.20 and you cut transfer fees and noise. Show the threshold so nobody is surprised.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed payouts.&lt;/strong&gt; Bank details go stale. Catch &lt;code&gt;transfer.failed&lt;/code&gt; and &lt;code&gt;payout.failed&lt;/code&gt;, mark the commission payable again, and email the affiliate. A silent failure is how you lose a good promoter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Currency.&lt;/strong&gt; Your affiliate in another country thinks in their currency. Decide up front whether you pay in your settlement currency or theirs, and put it in the terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tax forms.&lt;/strong&gt; US payouts over the reporting threshold mean a 1099. Collect tax info at onboarding, not in December.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I keep payout fees at zero
&lt;/h2&gt;

&lt;p&gt;Some affiliate platforms take a cut of every payout on top of their monthly fee. That charges you twice: once to run the program, again to pay the people growing it. When I built &lt;a href="https://referralful.com/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=launch-listings"&gt;Referralful&lt;/a&gt; (disclosure: my product, affiliate software for SaaS built on Stripe), I kept payout fees at 0%. The commission belongs to the affiliate.&lt;/p&gt;

&lt;p&gt;Attribution tells you who earned the money. The payout pipeline tells you whether they trust you enough to keep selling for you. The second one is where I would spend the time.&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>saas</category>
      <category>payments</category>
    </item>
    <item>
      <title>Recurring affiliate commissions on Stripe: the edge cases nobody warns you about</title>
      <dc:creator>Mihir kanzariya</dc:creator>
      <pubDate>Sun, 14 Jun 2026 06:55:35 +0000</pubDate>
      <link>https://dev.to/mihirkanzariya/recurring-affiliate-commissions-on-stripe-the-edge-cases-nobody-warns-you-about-4ec6</link>
      <guid>https://dev.to/mihirkanzariya/recurring-affiliate-commissions-on-stripe-the-edge-cases-nobody-warns-you-about-4ec6</guid>
      <description>&lt;p&gt;Every tutorial on building affiliate tracking with Stripe covers the happy path: user clicks a referral link, signs up, pays, affiliate gets a commission. Done.&lt;/p&gt;

&lt;p&gt;But if you are building recurring commissions for a SaaS product, the happy path is maybe 60% of what actually happens. The other 40% is edge cases that will either cost you money, break affiliate trust, or both.&lt;/p&gt;

&lt;p&gt;Here is what I ran into building commission logic on top of Stripe's billing API, and how I handled each case.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Plan changes mid-cycle
&lt;/h2&gt;

&lt;p&gt;A customer upgrades from $20/mo to $50/mo halfway through their billing cycle. The affiliate was earning 30% on the $20 plan. Do they now earn 30% on $50? On the prorated amount for this cycle? On the full $50 starting next cycle?&lt;/p&gt;

&lt;p&gt;The answer depends on how you listen to Stripe events. If you use &lt;code&gt;invoice.paid&lt;/code&gt; (which you should for recurring), you will get the prorated amount for this cycle. The affiliate's commission naturally adjusts because it is always a percentage of what was actually charged.&lt;/p&gt;

&lt;p&gt;The mistake I see: using &lt;code&gt;customer.subscription.updated&lt;/code&gt; to recalculate commissions. That event fires before payment, so you end up crediting commissions on amounts that might fail to charge.&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;// Good: commission based on what was actually paid&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;webhookEndpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;enabled_events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice.paid&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="c1"&gt;// Inside your webhook handler&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amountPaid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount_paid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// actual charged amount&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commission&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amountPaid&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;commissionRate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Failed payments and dunning
&lt;/h2&gt;

&lt;p&gt;Stripe retries failed payments over a configurable window (Smart Retries or your own schedule). During that window, the subscription is &lt;code&gt;past_due&lt;/code&gt; but not cancelled.&lt;/p&gt;

&lt;p&gt;Should the affiliate earn commission on a payment that fails three times and then succeeds on the fourth retry? Yes, because the customer paid. Should they earn commission if the payment never succeeds and the subscription cancels? Obviously not.&lt;/p&gt;

&lt;p&gt;The key insight: if you only listen to &lt;code&gt;invoice.paid&lt;/code&gt;, this handles itself. You never credit a commission until money actually moves. The problem comes when you try to get clever with &lt;code&gt;invoice.payment_failed&lt;/code&gt; and preemptively clawback. Do not do that. Wait for the final outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Refunds inside vs outside the attribution window
&lt;/h2&gt;

&lt;p&gt;You offer a 14-day refund policy. A customer gets a refund on day 10. The affiliate already earned commission on that payment. You need to clawback.&lt;/p&gt;

&lt;p&gt;But what about a refund on day 45? Or day 90? At some point, the refund is a customer service decision that should not penalize the affiliate who brought a real, paying customer.&lt;/p&gt;

&lt;p&gt;I use a simple rule: clawback commissions on refunds within the first 30 days of the specific payment. After that, the commission stands. This is fair to affiliates and prevents the situation where a 6-month customer gets a partial refund and the affiliate loses their commission retroactively.&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;// On charge.refunded event&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;charge&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;created&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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;refundDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;daysSincePayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refundDate&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;paymentDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysSincePayment&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Clawback the commission&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;clawbackCommission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;charge&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;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Commission stands, log for transparency&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;logRefundNoClawback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;charge&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;span class="nx"&gt;daysSincePayment&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;h2&gt;
  
  
  4. Annual vs monthly: same rate or different?
&lt;/h2&gt;

&lt;p&gt;A customer pays $200/year instead of $20/month. The annual plan is a discount ($240 annualized vs $200 paid). If the affiliate rate is 30%, do they earn $60 upfront or $6/month?&lt;/p&gt;

&lt;p&gt;Most programs pay the commission on what was actually charged. So the affiliate gets 30% of $200 = $60 as a single commission when the annual invoice pays. This is simpler to implement and aligns incentives: the affiliate helped close a higher-commitment customer.&lt;/p&gt;

&lt;p&gt;The trap: if you pay $60 upfront on annual and the customer refunds at month 3, you need to clawback $60, not $15. Annual commissions carry more clawback risk, so your refund window matters more.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The free trial attribution gap
&lt;/h2&gt;

&lt;p&gt;Customer clicks an affiliate link, starts a 14-day free trial, and converts to paid on day 15. If your attribution only fires on &lt;code&gt;checkout.session.completed&lt;/code&gt;, you captured the affiliate at trial start. Good.&lt;/p&gt;

&lt;p&gt;But what if the trial does not require a payment method? Then &lt;code&gt;checkout.session.completed&lt;/code&gt; might not fire at all during the trial. The customer adds their card later through the billing portal. If you are not careful, that second step has no affiliate context attached.&lt;/p&gt;

&lt;p&gt;The fix: store the affiliate attribution in your own database at signup time (not in Stripe metadata alone). When the subscription transitions from trialing to active, look up the affiliate from your records, not from the Stripe event.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Multi-currency edge cases
&lt;/h2&gt;

&lt;p&gt;Your product charges in USD, EUR, and GBP. An affiliate in the US refers a customer who pays in EUR. The commission is calculated as 30% of the EUR amount. But when you pay the affiliate, you pay in USD.&lt;/p&gt;

&lt;p&gt;Do you convert at the exchange rate when the customer paid, or when you run payouts? If you batch payouts monthly, the exchange rate can shift 2-3% in either direction.&lt;/p&gt;

&lt;p&gt;Pick one: convert at payment time and store the USD-equivalent commission immediately, or convert at payout time and accept the variance. I convert at payment time because it makes the affiliate's dashboard accurate in real-time. They see exactly what they earned without waiting for payout-day exchange rates.&lt;/p&gt;




&lt;p&gt;None of these problems are hard individually. But if you do not think about them upfront, you end up with affiliate disputes, incorrect payouts, and patching logic months after launch.&lt;/p&gt;

&lt;p&gt;The general principle: always base commissions on &lt;code&gt;invoice.paid&lt;/code&gt; (real money moved), store attribution in your own database (not just Stripe metadata), and define your clawback rules before your first affiliate asks about them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I build affiliate tracking for SaaS products on Stripe. If you are working on something similar, I wrote about &lt;a href="https://referralful.com/?utm_source=dev.to&amp;amp;utm_medium=content&amp;amp;utm_campaign=article"&gt;cookie-less affiliate attribution on Stripe&lt;/a&gt; previously.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>saas</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Tracking affiliate referrals on Stripe without third-party cookies</title>
      <dc:creator>Mihir kanzariya</dc:creator>
      <pubDate>Fri, 12 Jun 2026 10:13:52 +0000</pubDate>
      <link>https://dev.to/mihirkanzariya/tracking-affiliate-referrals-on-stripe-without-third-party-cookies-5fh9</link>
      <guid>https://dev.to/mihirkanzariya/tracking-affiliate-referrals-on-stripe-without-third-party-cookies-5fh9</guid>
      <description>&lt;p&gt;Third-party cookies are mostly gone. Safari blocks them, Firefox blocks them, and Chrome is finishing the job. If your affiliate attribution leans on a third-party cookie set on the advertiser's domain, a large share of your referrals vanish before checkout.&lt;/p&gt;

&lt;p&gt;I build Referralful, a Stripe-native affiliate tool, so I spend most of my week on this exact problem. Here is the approach that survives cookie blocking, with the Stripe-specific parts that trip people up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attribution chain
&lt;/h2&gt;

&lt;p&gt;A referral has to survive four hops:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Someone clicks an affiliate link.&lt;/li&gt;
&lt;li&gt;They land on your site, browse, maybe leave and come back days later.&lt;/li&gt;
&lt;li&gt;They start a Stripe Checkout session.&lt;/li&gt;
&lt;li&gt;They pay, sometimes on a different device.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each hop is a place to lose the referral. Cookies handle hop 2 badly across browsers and not at all across devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: capture the referral first-party
&lt;/h2&gt;

&lt;p&gt;When the affiliate link hits your domain (&lt;code&gt;?ref=jane&lt;/code&gt;), read the code and store it in two places, not one: a first-party cookie on &lt;strong&gt;your&lt;/strong&gt; domain, plus &lt;code&gt;localStorage&lt;/code&gt; as a backup. First-party cookies on your own domain are not the ones browsers are killing. The 60-day window most programs advertise is just a cookie &lt;code&gt;max-age&lt;/code&gt;.&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;// on landing, if ?ref= is present&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ref&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`rf_ref=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; Max-Age=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; Path=/; SameSite=Lax`&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rf_ref&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&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;h2&gt;
  
  
  Step 2: hand the referral to Stripe at checkout
&lt;/h2&gt;

&lt;p&gt;This is the step people miss. Do not rely on the cookie still being readable when the webhook fires. Pass the referral &lt;strong&gt;into&lt;/strong&gt; the Checkout Session as &lt;code&gt;client_reference_id&lt;/code&gt; (or &lt;code&gt;metadata&lt;/code&gt;) when you create it:&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;priceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;client_reference_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;readRef&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;        &lt;span class="c1"&gt;// 'jane'&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;affiliate_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;readRef&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;success_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cancel_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the referral lives inside the Stripe object. It no longer depends on a cookie, a device, or how many days pass before payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: attribute on the webhook, not the browser
&lt;/h2&gt;

&lt;p&gt;Listen for &lt;code&gt;checkout.session.completed&lt;/code&gt; and read the reference back. That is your attribution. Server-side, deterministic, and device-independent.&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;// webhook&lt;/span&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;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout.session.completed&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client_reference_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&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;affiliate_ref&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;creditAffiliate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount_total&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;h2&gt;
  
  
  Step 4: recurring commissions follow renewals
&lt;/h2&gt;

&lt;p&gt;The hard part with SaaS is that the first payment is not the only one. Store the affiliate against the Stripe &lt;code&gt;customer&lt;/code&gt; at first attribution, then listen to &lt;code&gt;invoice.paid&lt;/code&gt; for that customer and credit the commission again on each renewal, for as long as your terms allow (we use the first 12 months of payments).&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;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice.paid&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inv&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;affiliateForCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inv&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;withinCommissionWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inv&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;creditAffiliate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inv&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;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount_paid&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;h2&gt;
  
  
  Three things we learned the hard way
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refunds claw back commissions.&lt;/strong&gt; Listen to &lt;code&gt;charge.refunded&lt;/code&gt; and reverse the credit, or your payouts slowly drift above your revenue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-referrals are most of your fraud.&lt;/strong&gt; Block a conversion when the affiliate's email or card fingerprint matches the buyer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coupon codes are a second attribution path.&lt;/strong&gt; If an affiliate shares a code instead of a link, attribute on the &lt;code&gt;discount&lt;/code&gt; in the session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this needs a third-party cookie. The trick is to stop treating the browser as the source of truth and let Stripe carry the referral through to the webhook.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I build &lt;a href="https://referralful.com/?utm_source=dev.to&amp;amp;utm_medium=content&amp;amp;utm_campaign=article"&gt;Referralful&lt;/a&gt;, which does the above for Stripe SaaS. The pattern is the same whether you build it yourself or buy it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>stripe</category>
      <category>saas</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I replaced 4 SaaS tools with one workspace. Here's what actually happened.</title>
      <dc:creator>Mihir kanzariya</dc:creator>
      <pubDate>Fri, 13 Mar 2026 21:38:38 +0000</pubDate>
      <link>https://dev.to/mihirkanzariya/i-replaced-4-saas-tools-with-one-workspace-heres-what-actually-happened-39gp</link>
      <guid>https://dev.to/mihirkanzariya/i-replaced-4-saas-tools-with-one-workspace-heres-what-actually-happened-39gp</guid>
      <description>&lt;p&gt;So about 8 months ago I got fed up.&lt;/p&gt;

&lt;p&gt;Our team of 6 was paying for Jira, Notion, Slack (premium), and some random bug tracker I can't even remember the name of. Total monthly bill was somewhere around $380. Not insane money, but the real cost wasn't the subscriptions.&lt;/p&gt;

&lt;p&gt;The real cost was context switching.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tab hell problem
&lt;/h2&gt;

&lt;p&gt;Here's what a typical morning looked like: open Jira to check sprint status, switch to Notion to read the spec, hop to Slack because someone pinged about a blocker, back to Jira to update the ticket, then realize the spec in Notion is outdated because someone updated it in a Google Doc instead.&lt;/p&gt;

&lt;p&gt;Sound familiar? Yeah.&lt;/p&gt;

&lt;p&gt;I counted one day and I had 23 tabs open across 4 different tools, all for the same project. My brain was spending more energy remembering where stuff lived than actually doing the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually wanted
&lt;/h2&gt;

&lt;p&gt;I wrote down what our team actually needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kanban boards (not the overly complex Jira kind, just cards in columns)&lt;/li&gt;
&lt;li&gt;A wiki/docs space that lives next to the tasks&lt;/li&gt;
&lt;li&gt;Real-time editing so we stop stepping on each other's work
&lt;/li&gt;
&lt;li&gt;Some kind of notifications that aren't as noisy as Slack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. We didn't need 400 Jira fields. We didn't need Notion's infinite nesting rabbit hole. We just needed stuff to be in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The switch
&lt;/h2&gt;

&lt;p&gt;We moved everything into a single workspace tool. Tasks, docs, discussions, all in one app. The first week was rough honestly, people kept going back to old habits. But by week 3 something clicked.&lt;/p&gt;

&lt;p&gt;Meetings got shorter because everyone could see the same board and the same docs without sharing links. Standup went from 25 minutes to 12 because nobody was digging through three apps to find their update.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually improved
&lt;/h2&gt;

&lt;p&gt;The biggest win was fewer "where is this?" questions. When your tasks and docs live in the same space, you don't lose context. You click from a task directly into the related doc. No more copying Notion links into Jira tickets.&lt;/p&gt;

&lt;p&gt;Onboarding got way faster too. New devs had one app to learn instead of four. One login, one search bar, one place to look.&lt;/p&gt;

&lt;p&gt;And the notification noise dropped dramatically. Instead of Slack pings for every little thing, we use inline comments on the actual work. Way less distracting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I miss (being honest)
&lt;/h2&gt;

&lt;p&gt;Jira's reporting was solid if you actually used it. The burndown charts and velocity tracking were nice for sprint retros. Most unified tools don't go that deep on analytics yet.&lt;/p&gt;

&lt;p&gt;And Notion's database views were genuinely powerful for non-dev use cases. Our marketing team loved those.&lt;/p&gt;

&lt;p&gt;But for our dev team? The tradeoff was worth it. Less context switching &amp;gt; fancier features we barely used.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Before: ~$380/month across 4 tools, 23+ tabs open, 25 min standups&lt;br&gt;
After: way less per month, usually 4-6 tabs total, 12 min standups&lt;/p&gt;

&lt;p&gt;The time savings compound. When you're not hunting for information across apps, you just... build stuff faster. Novel concept I know.&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;If your team is small-to-mid size and you're juggling Jira + Notion + Slack + whatever, seriously consider consolidating. The fancy features you think you need? You probably don't. What you need is less friction between thinking about what to build and actually building it.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>saas</category>
      <category>startup</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Why your bug reports are useless (and how to fix them)</title>
      <dc:creator>Mihir kanzariya</dc:creator>
      <pubDate>Fri, 13 Mar 2026 21:38:05 +0000</pubDate>
      <link>https://dev.to/mihirkanzariya/why-your-bug-reports-are-useless-and-how-to-fix-them-749</link>
      <guid>https://dev.to/mihirkanzariya/why-your-bug-reports-are-useless-and-how-to-fix-them-749</guid>
      <description>&lt;p&gt;I've spent the last year building dev tools and honestly, the thing that frustrates me most isn't the code. It's the bug reports.&lt;/p&gt;

&lt;p&gt;You know the ones. "It's broken." "The page doesn't work." "Something is wrong with the login." Cool. Super helpful. Let me just fix "something" real quick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem isn't laziness
&lt;/h2&gt;

&lt;p&gt;Most people genuinely want to help when they report a bug. They just don't know what information matters. And honestly, why would they? They're not developers. They don't know that the difference between Chrome 120 and Safari 17 might be the entire reason things are broken.&lt;/p&gt;

&lt;p&gt;So they do their best. They write "the button doesn't work" and move on with their day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually makes a bug report useful
&lt;/h2&gt;

&lt;p&gt;After triaging hundreds of reports across multiple projects, here's what I actually need to reproduce a bug fast:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. What were you trying to do?&lt;/strong&gt;&lt;br&gt;
Not "I was on the website." Tell me the specific action. "I clicked the save button after editing the project name." That's gold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. What happened vs what you expected?&lt;/strong&gt;&lt;br&gt;
"I clicked save and nothing happened. I expected it to save and show a success message." Now I know exactly what to look for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The boring technical stuff&lt;/strong&gt;&lt;br&gt;
Browser, OS, screen size, whether you're on mobile or desktop. I know, nobody wants to write this down. But it matters so much. Half the bugs I've seen in the last 6 months were viewport or browser specific.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Steps to reproduce&lt;/strong&gt;&lt;br&gt;
If you can tell me 1, 2, 3 what you did before things broke, I can probably fix it in 20 minutes instead of 2 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real solution: don't make users think about it
&lt;/h2&gt;

&lt;p&gt;Here's the thing I've realized. Asking users to provide all this context is fighting human nature. People won't do it consistently no matter how many times you ask.&lt;/p&gt;

&lt;p&gt;The better approach is capturing this stuff automatically. There are tools now that grab the browser info, the page URL, even let users click on the exact element that's broken. The technical context gets attached without the user doing anything extra.&lt;/p&gt;

&lt;p&gt;I've been experimenting with element-level bug reporting where users literally just click on what's broken, add a quick note, and the tool captures everything else. CSS selectors, viewport size, browser version, the URL, a screenshot. All automatic.&lt;/p&gt;

&lt;p&gt;The difference in triage time is honestly wild. What used to take 30 minutes of back-and-forth DMs now takes like 2 minutes to understand and start fixing.&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;Bad bug reports aren't a people problem, they're a tooling problem. Stop asking users to be better reporters and start giving them tools that capture context automatically. Your future self debugging at 2am will thank you.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>beginners</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Why I stopped juggling Notion and Jira (and what I use now)</title>
      <dc:creator>Mihir kanzariya</dc:creator>
      <pubDate>Sat, 07 Mar 2026 14:44:32 +0000</pubDate>
      <link>https://dev.to/mihirkanzariya/why-i-stopped-juggling-notion-and-jira-and-what-i-use-now-3hl8</link>
      <guid>https://dev.to/mihirkanzariya/why-i-stopped-juggling-notion-and-jira-and-what-i-use-now-3hl8</guid>
      <description>&lt;p&gt;I've been building software for about 4 years now. And for most of that time, my workflow looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jira&lt;/strong&gt; for task tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notion&lt;/strong&gt; for docs, wikis, meeting notes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack&lt;/strong&gt; for everything else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three tabs minimum, always open. Context switching all day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem nobody talks about
&lt;/h2&gt;

&lt;p&gt;Everyone debates Jira vs Linear or Notion vs Obsidian. But the real productivity killer isn't which tool you pick — it's having too many of them.&lt;/p&gt;

&lt;p&gt;I'd create a task in Jira, write the spec in Notion, then paste the link back into the Jira ticket. When someone asked "where's the doc for X?" the answer was always "let me find it." Half my day was just navigating between tabs.&lt;/p&gt;

&lt;p&gt;And syncing? Forget about it. Notion docs would get stale because nobody remembered to update them after the sprint changed. Jira tickets referenced Notion pages that no longer existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tried first
&lt;/h2&gt;

&lt;p&gt;I went through the usual suspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linear&lt;/strong&gt; — great for tasks, but still needed something for docs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ClickUp&lt;/strong&gt; — tried to do everything, felt bloated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monday.com&lt;/strong&gt; — too much drag-and-drop, not enough keyboard shortcuts for my taste&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of them solved the core problem: I still needed multiple tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually worked
&lt;/h2&gt;

&lt;p&gt;About 8 months ago I started using &lt;a href="https://blocpad.com" rel="noopener noreferrer"&gt;Blocpad&lt;/a&gt; — basically a workspace that combines kanban boards, a slash-command editor (like Notion), and wiki pages all in one app. Everything syncs in real time.&lt;/p&gt;

&lt;p&gt;The thing that sold me honestly wasn't some killer feature. It was just... not having to switch tabs anymore. I create a task, write the spec right there in the same place, and my team can see both without asking "where's the link?"&lt;/p&gt;

&lt;p&gt;Some things I actually like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tasks and docs live together. No more pasting links between tools&lt;/li&gt;
&lt;li&gt;Real-time presence — I can see who's looking at what, which sounds small but actually reduces a lot of "hey are you working on this?" messages&lt;/li&gt;
&lt;li&gt;The editor feels snappy. Not Notion-level polish yet but honestly I care more about speed than animations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things that are still rough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No mobile app yet (they're working on it apparently)&lt;/li&gt;
&lt;li&gt;The onboarding could be better — took me a bit to figure out the slash commands&lt;/li&gt;
&lt;li&gt;Missing some integrations I want (GitHub is there, but I want better Slack integration)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;Look, I'm not saying everyone should use what I use. The point is: if you're spending 20+ minutes a day just navigating between your tools, that's a workflow problem worth solving.&lt;/p&gt;

&lt;p&gt;Before you add another tool to your stack, ask yourself if you can remove one first. The best productivity hack I found this year wasn't a new app — it was having fewer apps.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Currently building a SaaS and sharing the journey. If you're into #buildinpublic stuff, I post updates on &lt;a href="https://x.com/kanzariyamihir1" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; too.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>webdev</category>
      <category>startup</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Why I stopped juggling Notion and Jira (and what I use now)</title>
      <dc:creator>Mihir kanzariya</dc:creator>
      <pubDate>Fri, 06 Mar 2026 17:42:34 +0000</pubDate>
      <link>https://dev.to/mihirkanzariya/why-i-stopped-juggling-notion-and-jira-and-what-i-use-now-44mj</link>
      <guid>https://dev.to/mihirkanzariya/why-i-stopped-juggling-notion-and-jira-and-what-i-use-now-44mj</guid>
      <description>&lt;p&gt;I've been building SaaS tools for about two years now and honestly the biggest productivity killer wasn't bad code or missing features. It was switching between apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup that drove me crazy
&lt;/h2&gt;

&lt;p&gt;For the longest time my workflow looked like this: Jira for tickets, Notion for docs and wikis, Slack for... well everything else. Three tabs minimum open at all times, three different search bars, three sets of notifications.&lt;/p&gt;

&lt;p&gt;And look, these are great tools individually. I'm not here to trash them. But the constant context switching was killing my focus. I'd be writing a spec in Notion, need to reference a ticket in Jira, then someone pings me on Slack about that same ticket, and suddenly I've lost 15 minutes just navigating between windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The breaking point
&lt;/h2&gt;

&lt;p&gt;The real moment was when I realized my team was duplicating info everywhere. We had the same feature described in a Notion doc AND a Jira epic AND a Slack thread. When something changed, maybe one of those got updated. Maybe.&lt;/p&gt;

&lt;p&gt;I spent a whole Friday afternoon just syncing information across tools. That's when I knew something had to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually wanted
&lt;/h2&gt;

&lt;p&gt;Pretty simple honestly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tasks and docs in the same place so I don't copy-paste between them&lt;/li&gt;
&lt;li&gt;Real-time collab so I'm not waiting for someone to "finish editing"&lt;/li&gt;
&lt;li&gt;Something that doesn't take 3 months to set up (looking at you, Jira)&lt;/li&gt;
&lt;li&gt;A slash command editor because I've been spoiled by Notion's UX&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I ended up building
&lt;/h2&gt;

&lt;p&gt;So I ended up building &lt;a href="https://blocpad.com" rel="noopener noreferrer"&gt;Blocpad&lt;/a&gt; because nothing quite fit. It's basically kanban boards + a Notion-style editor + real-time presence all in one app. Built it with Next.js and Supabase.&lt;/p&gt;

&lt;p&gt;The biggest win? When someone creates a task, the context is RIGHT THERE. The doc, the discussion, the status — same page. No more "let me find that Notion link" or "check the Jira ticket for details."&lt;/p&gt;

&lt;h2&gt;
  
  
  Stuff I learned along the way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You don't need every feature on day one.&lt;/strong&gt; We started with just tasks and docs. Added the wiki later. Added integrations even later. Shipped fast, iterated based on what people actually asked for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time is harder than you think.&lt;/strong&gt; Supabase Realtime handles a lot but conflict resolution in collaborative editing is a whole rabbit hole. Worth it though — seeing cursors move in real time makes remote work feel less lonely lol.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All-in-one doesn't mean bloated.&lt;/strong&gt; The temptation is to add everything. We actively resist adding features that don't serve the core workflow. If you need advanced Gantt charts, use a dedicated tool. We're not trying to replace everything, just the daily stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it for everyone?
&lt;/h2&gt;

&lt;p&gt;Nah probably not. Big enterprises with 500+ person teams and complex ITSM needs should stick with Jira tbh. But if you're a startup, a small dev team, or a solo founder who's tired of paying for 4 different tools that don't talk to each other... might be worth a look.&lt;/p&gt;

&lt;p&gt;We're still early and building in public. Would love to hear how other people handle the multi-tool chaos — or if you've just accepted it as part of life at this point lol.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your current stack for project management + docs? Genuinely curious if anyone's found a setup that doesn't involve 5 browser tabs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>webdev</category>
      <category>startup</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Simple Search componentReactJS</title>
      <dc:creator>Mihir kanzariya</dc:creator>
      <pubDate>Thu, 14 Nov 2019 09:23:20 +0000</pubDate>
      <link>https://dev.to/mihirkanzariya/simple-search-componentreactjs-hoe</link>
      <guid>https://dev.to/mihirkanzariya/simple-search-componentreactjs-hoe</guid>
      <description>&lt;p&gt;&lt;a href="https://gist.github.com/mihir-kanzariya/f11377a28badb4471cfc033572d7c862" rel="noopener noreferrer"&gt;https://gist.github.com/mihir-kanzariya/f11377a28badb4471cfc033572d7c862&lt;/a&gt;&lt;/p&gt;

</description>
      <category>simplesearchcomponentreactjs</category>
    </item>
  </channel>
</rss>
