<?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: Salam Omarbagaev</title>
    <description>The latest articles on DEV Community by Salam Omarbagaev (@mariache).</description>
    <link>https://dev.to/mariache</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%2F461257%2F38e310eb-f0ec-4e28-abaa-583dfdc0a132.jpeg</url>
      <title>DEV Community: Salam Omarbagaev</title>
      <link>https://dev.to/mariache</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mariache"/>
    <language>en</language>
    <item>
      <title>I turned a viral X joke into a working app in one evening (Next.js 15 + Stripe + Supabase)</title>
      <dc:creator>Salam Omarbagaev</dc:creator>
      <pubDate>Fri, 30 Oct 2026 03:30:11 +0000</pubDate>
      <link>https://dev.to/mariache/i-turned-a-viral-x-joke-into-a-working-app-in-one-evening-nextjs-15-stripe-supabase-l0p</link>
      <guid>https://dev.to/mariache/i-turned-a-viral-x-joke-into-a-working-app-in-one-evening-nextjs-15-stripe-supabase-l0p</guid>
      <description>&lt;p&gt;A post went viral on X: &lt;em&gt;"there should be an app where you upload your bills and bored billionaires swipe right to pay them."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Everyone joked. I wanted to see how fast I could ship a real one. So here's &lt;strong&gt;Pay My Mortgage&lt;/strong&gt; 🏠, running on &lt;strong&gt;Helpeasant&lt;/strong&gt;, an open-source, one-page "joke-but-real" payoff site.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Yes, it's a joke. Yes, the button works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;👉 &lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://helpeasant.com" rel="noopener noreferrer"&gt;helpeasant.com&lt;/a&gt;&lt;br&gt;
👉 &lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/mariache/helpeasant" rel="noopener noreferrer"&gt;github.com/mariache/helpeasant&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffh956qg889ib6mskqsfz.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffh956qg889ib6mskqsfz.png" alt=" " width="800" height="1100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live progress counter (% only, 4 decimals, so even $5 visibly moves it)&lt;/li&gt;
&lt;li&gt;Quick buttons: ☕ $5 · 🍕 $20 · 🏠 $100, plus any amount&lt;/li&gt;
&lt;li&gt;A "Pay it all off 🤯" button that politely explains card limits&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Wall of Weirdos&lt;/strong&gt; for supporters, with emoji tiers instead of amounts&lt;/li&gt;
&lt;li&gt;Milestones with confetti: &lt;em&gt;"Paid for a doorknob"&lt;/em&gt; → &lt;em&gt;"The garage is basically mine"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;"Can't chip in? &lt;strong&gt;Send it to Elon&lt;/strong&gt; 🚀" share button&lt;/li&gt;
&lt;li&gt;Installable PWA, dark mode&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 15&lt;/strong&gt; (App Router), one page and three API routes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe Checkout&lt;/strong&gt; + a signed webhook&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; (Postgres), server-side only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; for hosting, &lt;strong&gt;Cloudflare&lt;/strong&gt; for DNS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Design decisions I liked
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The counter only trusts webhooks
&lt;/h3&gt;

&lt;p&gt;The UI never updates the total on a button click. The flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;POST /api/checkout&lt;/code&gt; creates a Stripe Checkout Session with inline &lt;code&gt;price_data&lt;/code&gt;. No products are needed in the dashboard.&lt;/li&gt;
&lt;li&gt;Stripe hosts the payment page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/webhook&lt;/code&gt; verifies the signature and inserts a row only if &lt;code&gt;payment_status === "paid"&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Inserts are &lt;strong&gt;idempotent&lt;/strong&gt; on the Stripe session ID, so webhook retries never double-count:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await supabase.from("donations").upsert(
  { stripe_session_id: session.id, amount_cents: session.amount_total, name, message },
  { onConflict: "stripe_session_id", ignoreDuplicates: true }
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  2. Privacy by design
&lt;/h3&gt;

&lt;p&gt;The real balance lives in a server-only env var. The browser only ever gets a &lt;strong&gt;rounded percentage&lt;/strong&gt;, and the wall shows tiers (☕ 🍕 🛋️ 🏠) instead of amounts. Otherwise &lt;code&gt;total ÷ %&lt;/code&gt; would reveal the balance.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. A locked-down database
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Row-level security on, "auto-expose new tables" off&lt;/li&gt;
&lt;li&gt;Explicit grants to &lt;code&gt;service_role&lt;/code&gt; only&lt;/li&gt;
&lt;li&gt;The client never talks to Supabase directly; everything goes through API routes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  4. Config over code
&lt;/h3&gt;

&lt;p&gt;Title, emoji, tagline, owner name and city are all &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; env vars. A fork is a new campaign with zero code changes: &lt;em&gt;Pay My Car Loan&lt;/em&gt;, &lt;em&gt;Pay My Student Debt&lt;/em&gt;, you name it.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Native share with a fallback
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;navigator.share&lt;/code&gt; on mobile opens WhatsApp, Messages and so on. On desktop it falls back to an X intent with &lt;code&gt;@elonmusk&lt;/code&gt; pre-filled.&lt;/p&gt;
&lt;h2&gt;
  
  
  What actually took the time
&lt;/h2&gt;

&lt;p&gt;The code took maybe an hour. Most of the evening went into &lt;strong&gt;setup&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain + Cloudflare DNS (tip: set records to &lt;strong&gt;DNS only&lt;/strong&gt;, grey cloud, or Vercel complains about the proxy)&lt;/li&gt;
&lt;li&gt;Stripe verification (it needs a live, reachable site that matches your business name)&lt;/li&gt;
&lt;li&gt;Test-mode end-to-end run with &lt;code&gt;stripe listen&lt;/code&gt; and the &lt;code&gt;4242&lt;/code&gt; card before switching to live keys&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Fork it
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/mariache/helpeasant
cd helpeasant &amp;amp;&amp;amp; npm install
cp .env.example .env.local
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The README walks through Supabase, Stripe and Vercel setup in about 15 minutes.&lt;/p&gt;




&lt;p&gt;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. 🚀&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>nextjs</category>
      <category>opensource</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
