DEV Community

Cover image for GolfSub — I Built a Golf Platform That Makes Charitable Giving Automatic, Then Let It Sit for Weeks. Here's the Comeback.
Vansh Suri
Vansh Suri

Posted on

GolfSub — I Built a Golf Platform That Makes Charitable Giving Automatic, Then Let It Sit for Weeks. Here's the Comeback.

GitHub “Finish-Up-A-Thon” Challenge Submission

This is a submission for the GitHub Finish-Up-A-Thon Challenge


What I Built

Home Page
A few weeks ago I had a half-finished side project on my hard drive and a guilty conscience every time I opened GitHub.

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?

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.

🔗 Live app: https://golflers.vercel.app/
🔗 GitHub: https://github.com/vanshsuri07/Golf-Subscription

Stack: Next.js 15, React 19, Tailwind CSS 4, Supabase (PostgreSQL + RLS), NextAuth.js v5, Stripe, Framer Motion, Shadcn UI, Vercel.


Demo

Admin Page

Past Results / draw history


The Comeback Story

Direct Your Impact — charity selection
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.

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.

This was the piece I had to get right before I could call the project done.

Here's what the fix actually looked like:

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

Once verified, the handler reads CHARITY_PERCENTAGE 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.

const event = stripe.webhooks.constructEvent(
  req.body,
  sig,
  process.env.STRIPE_WEBHOOK_SECRET!
);

if (event.type === 'checkout.session.completed') {
  const amount = event.data.object.amount_total!;
  const charityAmount = Math.round(
    amount * (Number(process.env.CHARITY_PERCENTAGE) / 100)
  );
  await supabase.from('charity_splits').insert({
    user_id: metadata.userId,
    charity_id: metadata.charityId,
    amount_pence: charityAmount,
  });
}
Enter fullscreen mode Exit fullscreen mode

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.

The other things I finished:

  • 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
  • The weekly draw function was a rough SQL sketch; I rewrote it as a proper SECURITY DEFINER PostgreSQL function that picks numbers, computes prize tiers, and logs results atomically — no client-side randomness, every draw is reproducible and auditable
  • The handicap logic didn't handle fewer than 5 rounds — added a guard that scales the window down gracefully
  • Wrote npm run db:push and npm run db:seed scripts so any contributor can get a working local environment without manually seeding data
  • 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

My Experience with GitHub Copilot

The Stripe webhook handler is where Copilot earned its keep most visibly.

I'd written webhook handlers before but always had to look up the exact constructEvent signature verification pattern. This time I typed a comment — // verify stripe signature and extract event — 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.

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 SECURITY DEFINER clause, the specific prize tier logic) required me to actually think, which felt like the right division of labour.

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.

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

Top comments (0)