DEV Community

Salam Omarbagaev
Salam Omarbagaev

Posted on

I turned a viral X joke into a working app in one evening (Next.js 15 + Stripe + Supabase)

A post went viral on X: "there should be an app where you upload your bills and bored billionaires swipe right to pay them."

Everyone joked. I wanted to see how fast I could ship a real one. So here's Pay My Mortgage ๐Ÿ , running on Helpeasant, an open-source, one-page "joke-but-real" payoff site.

Yes, it's a joke. Yes, the button works.

๐Ÿ‘‰ Live: helpeasant.com
๐Ÿ‘‰ Repo: github.com/mariache/helpeasant

What it does

  • Live progress counter (% only, 4 decimals, so even $5 visibly moves it)
  • Quick buttons: โ˜• $5 ยท ๐Ÿ• $20 ยท ๐Ÿ  $100, plus any amount
  • A "Pay it all off ๐Ÿคฏ" button that politely explains card limits
  • A Wall of Weirdos for supporters, with emoji tiers instead of amounts
  • Milestones with confetti: "Paid for a doorknob" โ†’ "The garage is basically mine"
  • "Can't chip in? Send it to Elon ๐Ÿš€" share button
  • Installable PWA, dark mode

Stack

  • Next.js 15 (App Router), one page and three API routes
  • Stripe Checkout + a signed webhook
  • Supabase (Postgres), server-side only
  • Vercel for hosting, Cloudflare for DNS

Design decisions I liked

1. The counter only trusts webhooks

The UI never updates the total on a button click. The flow is:

  1. POST /api/checkout creates a Stripe Checkout Session with inline price_data. No products are needed in the dashboard.
  2. Stripe hosts the payment page.
  3. POST /api/webhook verifies the signature and inserts a row only if payment_status === "paid".

Inserts are idempotent on the Stripe session ID, so webhook retries never double-count:

await supabase.from("donations").upsert(
  { stripe_session_id: session.id, amount_cents: session.amount_total, name, message },
  { onConflict: "stripe_session_id", ignoreDuplicates: true }
);
Enter fullscreen mode Exit fullscreen mode

2. Privacy by design

The real balance lives in a server-only env var. The browser only ever gets a rounded percentage, and the wall shows tiers (โ˜• ๐Ÿ• ๐Ÿ›‹๏ธ ๐Ÿ ) instead of amounts. Otherwise total รท % would reveal the balance.

3. A locked-down database

  • Row-level security on, "auto-expose new tables" off
  • Explicit grants to service_role only
  • The client never talks to Supabase directly; everything goes through API routes

4. Config over code

Title, emoji, tagline, owner name and city are all NEXT_PUBLIC_* env vars. A fork is a new campaign with zero code changes: Pay My Car Loan, Pay My Student Debt, you name it.

5. Native share with a fallback

navigator.share on mobile opens WhatsApp, Messages and so on. On desktop it falls back to an X intent with @elonmusk pre-filled.

What actually took the time

The code took maybe an hour. Most of the evening went into setup:

  • Domain + Cloudflare DNS (tip: set records to DNS only, grey cloud, or Vercel complains about the proxy)
  • Stripe verification (it needs a live, reachable site that matches your business name)
  • Test-mode end-to-end run with stripe listen and the 4242 card before switching to live keys

Fork it

git clone https://github.com/mariache/helpeasant
cd helpeasant && npm install
cp .env.example .env.local
npm run dev
Enter fullscreen mode Exit fullscreen mode

The README walks through Supabase, Stripe and Vercel setup in about 15 minutes.


Feedback welcome, especially on abuse and fraud edge cases I haven't covered. And if you can't chip in... you know who to send it to. ๐Ÿš€

Top comments (0)