<?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: Vansh Suri</title>
    <description>The latest articles on DEV Community by Vansh Suri (@vansh_suri).</description>
    <link>https://dev.to/vansh_suri</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3956173%2Fd36eae34-fd88-445e-b349-ec564d009808.png</url>
      <title>DEV Community: Vansh Suri</title>
      <link>https://dev.to/vansh_suri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vansh_suri"/>
    <language>en</language>
    <item>
      <title>GolfSub — I Built a Golf Platform That Makes Charitable Giving Automatic, Then Let It Sit for Weeks. Here's the Comeback.</title>
      <dc:creator>Vansh Suri</dc:creator>
      <pubDate>Thu, 28 May 2026 09:21:02 +0000</pubDate>
      <link>https://dev.to/vansh_suri/golfsub-i-built-a-golf-platform-that-makes-charitable-giving-automatic-then-let-it-sit-for-40l7</link>
      <guid>https://dev.to/vansh_suri/golfsub-i-built-a-golf-platform-that-makes-charitable-giving-automatic-then-let-it-sit-for-40l7</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhnhvg4offi5g7xt7dfwq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhnhvg4offi5g7xt7dfwq.png" alt="Home Page" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
A few weeks ago I had a half-finished side project on my hard drive and a guilty conscience every time I opened GitHub.&lt;/p&gt;

&lt;p&gt;The idea was simple: golfers already pay for apps to track their handicap. They already donate to charities. They already enter competitions. What if one subscription did all three — and did them in a way that felt trustworthy and a little exciting?&lt;/p&gt;

&lt;p&gt;That's GolfSub. Subscribers log their rounds through a clean score ledger, get a live handicap calculated from their last 5 rounds, choose a charity that receives 10% of their subscription automatically, and enter a weekly draw where matching 3, 4, or 5 numbers unlocks escalating cash prizes — executed by a tamper-proof PostgreSQL function so every result is fully auditable.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live app&lt;/strong&gt;: &lt;a href="https://golflers.vercel.app/" rel="noopener noreferrer"&gt;https://golflers.vercel.app/&lt;/a&gt;&lt;br&gt;
🔗 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/vanshsuri07/Golf-Subscription" rel="noopener noreferrer"&gt;https://github.com/vanshsuri07/Golf-Subscription&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack&lt;/strong&gt;: Next.js 15, React 19, Tailwind CSS 4, Supabase (PostgreSQL + RLS), NextAuth.js v5, Stripe, Framer Motion, Shadcn UI, Vercel.&lt;/p&gt;


&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foeaq9iw8zrniahnuzgk2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foeaq9iw8zrniahnuzgk2.png" alt="Admin Page" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffmeuzei07tq3xz6gel7j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffmeuzei07tq3xz6gel7j.png" alt="Past Results / draw history" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzeyd2s75je69nvr5m2l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzeyd2s75je69nvr5m2l.png" alt="Direct Your Impact — charity selection" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
The project had been sitting untouched for a few weeks. Not because I lost interest — because I hit a wall on the part I was most nervous about: money moving automatically to charities.&lt;/p&gt;

&lt;p&gt;When I left it, the charity percentage calculation was running on the client. That's a problem. Anyone with devtools open could manipulate the split before it hit the server. Stripe webhooks weren't being verified either, so a bad actor could technically spoof a payment event and trigger a charity credit without paying.&lt;/p&gt;

&lt;p&gt;This was the piece I had to get right before I could call the project done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's what the fix actually looked like:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stripe fires a &lt;code&gt;checkout.session.completed&lt;/code&gt; event when a subscription goes through. I moved all the math to a server-side webhook handler that first verifies the request signature using &lt;code&gt;stripe.webhooks.constructEvent()&lt;/code&gt; — if the signature doesn't match, the handler exits immediately. No verified signature, no charity credit. Simple.&lt;/p&gt;

&lt;p&gt;Once verified, the handler reads &lt;code&gt;CHARITY_PERCENTAGE&lt;/code&gt; from environment config (never hardcoded, never touchable by the client), calculates the split, and writes a record to Supabase. The whole thing runs in one atomic operation on the server. The client never sees the math.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&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="nx"&gt;sig&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="o"&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&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;amount&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;amount_total&lt;/span&gt;&lt;span class="o"&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;charityAmount&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;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&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;CHARITY_PERCENTAGE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&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="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;charity_splits&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;user_id&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;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;charity_id&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;charityId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount_pence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;charityAmount&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;Fixing this one thing unlocked everything else — because once the money flow was trustworthy, I could confidently build the charity impact dashboard that shows subscribers their running total. That dashboard is now my favourite part of the whole app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The other things I finished:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NextAuth.js v5 session persistence was broken across protected routes — fixed by correcting the adapter config and adding proper Supabase RLS policies so users only ever read their own data&lt;/li&gt;
&lt;li&gt;The weekly draw function was a rough SQL sketch; I rewrote it as a proper &lt;code&gt;SECURITY DEFINER&lt;/code&gt; PostgreSQL function that picks numbers, computes prize tiers, and logs results atomically — no client-side randomness, every draw is reproducible and auditable&lt;/li&gt;
&lt;li&gt;The handicap logic didn't handle fewer than 5 rounds — added a guard that scales the window down gracefully&lt;/li&gt;
&lt;li&gt;Wrote &lt;code&gt;npm run db:push&lt;/code&gt; and &lt;code&gt;npm run db:seed&lt;/code&gt; scripts so any contributor can get a working local environment without manually seeding data&lt;/li&gt;
&lt;li&gt;Unified the component layer around Shadcn UI + Radix UI to remove the patchwork of ad-hoc styles that made some pages look like a different app&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;The Stripe webhook handler is where Copilot earned its keep most visibly.&lt;/p&gt;

&lt;p&gt;I'd written webhook handlers before but always had to look up the exact &lt;code&gt;constructEvent&lt;/code&gt; signature verification pattern. This time I typed a comment — &lt;code&gt;// verify stripe signature and extract event&lt;/code&gt; — and Copilot completed the try/catch block almost perfectly, including the correct error response shape Stripe expects when verification fails. That saved me maybe 20 minutes of docs-diving, which sounds small, but when you're trying to finish a project that's been sitting for weeks, small frictions are what kill momentum.&lt;/p&gt;

&lt;p&gt;The PostgreSQL draw function was the other highlight. I described what I needed in a comment — atomic number selection, prize tier mapping, audit log insert — and Copilot drafted a PL/pgSQL skeleton that got me 70% of the way there. The remaining 30% (the &lt;code&gt;SECURITY DEFINER&lt;/code&gt; clause, the specific prize tier logic) required me to actually think, which felt like the right division of labour.&lt;/p&gt;

&lt;p&gt;The place it was least helpful: Supabase RLS policy syntax. Copilot's suggestions were plausible-looking but subtly wrong in a way that only showed up at runtime. I ended up writing those by hand after the third bad suggestion. Worth knowing if you're building on Supabase.&lt;/p&gt;

&lt;p&gt;Overall: Copilot was best at eliminating the boilerplate tax on things I already understood. It made finishing feel achievable instead of exhausting.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
