<?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: Frank Mendez</title>
    <description>The latest articles on DEV Community by Frank Mendez (@frankmendez).</description>
    <link>https://dev.to/frankmendez</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%2F851284%2F66782c4d-188c-4e01-8161-93796f702126.png</url>
      <title>DEV Community: Frank Mendez</title>
      <link>https://dev.to/frankmendez</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/frankmendez"/>
    <language>en</language>
    <item>
      <title>🚀 Building a Newsletter Subscription Feature That Actually Works (No BS Edition)</title>
      <dc:creator>Frank Mendez</dc:creator>
      <pubDate>Tue, 21 Apr 2026 09:14:33 +0000</pubDate>
      <link>https://dev.to/frankmendez/building-a-newsletter-subscription-feature-that-actually-works-no-bs-edition-3h7h</link>
      <guid>https://dev.to/frankmendez/building-a-newsletter-subscription-feature-that-actually-works-no-bs-edition-3h7h</guid>
      <description>&lt;h2&gt;
  
  
  The Problem With Most “Newsletter Features”
&lt;/h2&gt;

&lt;p&gt;Let’s be honest—most newsletter setups are either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overkill (hello 20-step Mailchimp workflows)&lt;/li&gt;
&lt;li&gt;Manual (someone forgets to hit “send”)&lt;/li&gt;
&lt;li&gt;Or duct-taped together like a weekend hackathon project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What we actually want is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;User subscribes → You publish → Email goes out automatically&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s it. No ceremony.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 The Approach: Keep It Lean
&lt;/h2&gt;

&lt;p&gt;This feature was built into a Blog CMS with a few strict rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No new vendors (use existing stack)&lt;/li&gt;
&lt;li&gt;No double opt-in friction&lt;/li&gt;
&lt;li&gt;Fully automated sending&lt;/li&gt;
&lt;li&gt;One-click unsubscribe (because lawsuits are expensive 😅)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Full design spec: &lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Core Architecture
&lt;/h2&gt;

&lt;p&gt;Instead of relying on external newsletter platforms, this setup runs fully in-house:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; Supabase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email:&lt;/strong&gt; Resend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduler:&lt;/strong&gt; Vercel Cron&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Flow (a.k.a. “what actually happens”)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;User enters email → stored in DB&lt;/li&gt;
&lt;li&gt;Admin publishes a post&lt;/li&gt;
&lt;li&gt;System schedules a send (with delay)&lt;/li&gt;
&lt;li&gt;Cron job runs every minute&lt;/li&gt;
&lt;li&gt;Emails are sent automatically&lt;/li&gt;
&lt;li&gt;User can unsubscribe with one click&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No dashboards. No manual triggers. No “oops we forgot to send.”&lt;/p&gt;




&lt;h2&gt;
  
  
  🗄️ Data Model (Simple but Powerful)
&lt;/h2&gt;

&lt;p&gt;Two tables. That’s it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;newsletter_subscriptions&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Tracks subscribers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email (unique)&lt;/li&gt;
&lt;li&gt;subscribed_at&lt;/li&gt;
&lt;li&gt;unsubscribed_at (null = active)&lt;/li&gt;
&lt;li&gt;unsubscribe_token (for one-click unsubscribe)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;newsletter_sends&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Acts like a queue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;post_id (1 send per post)&lt;/li&gt;
&lt;li&gt;scheduled_at&lt;/li&gt;
&lt;li&gt;status (pending → sending → sent/failed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation is key. It lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schedule emails&lt;/li&gt;
&lt;li&gt;Retry failures&lt;/li&gt;
&lt;li&gt;Track delivery&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⏱️ The Secret Sauce: Delayed Sending
&lt;/h2&gt;

&lt;p&gt;Instead of blasting emails immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEWSLETTER_DELAY_MINUTES=60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gives you time to fix mistakes after publishing&lt;/li&gt;
&lt;li&gt;Avoids “oops typo in production” emails&lt;/li&gt;
&lt;li&gt;Feels more intentional&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔁 Automation via Cron
&lt;/h2&gt;

&lt;p&gt;A Vercel cron job runs every minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; → /api/newsletter/cron
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any &lt;code&gt;pending&lt;/code&gt; sends?&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;scheduled_at &amp;lt;= now()&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If yes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mark as &lt;code&gt;sending&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fetch active subscribers&lt;/li&gt;
&lt;li&gt;Send emails&lt;/li&gt;
&lt;li&gt;Update status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something crashes midway?&lt;/p&gt;

&lt;p&gt;👉 Anything stuck in &lt;code&gt;sending&lt;/code&gt; for 10+ minutes is marked as &lt;code&gt;failed&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;No silent failures. No ghost jobs.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✉️ Subscription Experience (UX Matters)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Subscribe Form
&lt;/h3&gt;

&lt;p&gt;Placed at the bottom of every blog post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email input&lt;/li&gt;
&lt;li&gt;Subscribe button&lt;/li&gt;
&lt;li&gt;Instant feedback (success or error)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No confirmation email. No friction.&lt;/p&gt;

&lt;p&gt;Because let’s be real—every extra step kills conversions.&lt;/p&gt;




&lt;h2&gt;
  
  
  ❌ Unsubscribe (Don’t Mess This Up)
&lt;/h2&gt;

&lt;p&gt;Every email includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/newsletter/unsubscribe?token=xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click → done.&lt;/p&gt;

&lt;p&gt;No login. No “are you sure?” guilt trips.&lt;/p&gt;

&lt;p&gt;Just clean, respectful UX.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛡️ Edge Cases You’ll Actually Hit
&lt;/h2&gt;

&lt;p&gt;Handled upfront:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate email&lt;/td&gt;
&lt;td&gt;“Already subscribed”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Re-subscribe&lt;/td&gt;
&lt;td&gt;Reactivates user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid token&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No subscribers&lt;/td&gt;
&lt;td&gt;Send marked as complete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cron crash&lt;/td&gt;
&lt;td&gt;Auto-mark failed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is where most systems break. Don’t skip it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Testing Strategy
&lt;/h2&gt;

&lt;p&gt;Because “it works on my machine” isn’t a strategy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests:&lt;/strong&gt; subscribe/unsubscribe logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration tests:&lt;/strong&gt; cron + email dispatch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E2E tests:&lt;/strong&gt; full user flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yes, even for a “simple” feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Why This Approach Wins
&lt;/h2&gt;

&lt;p&gt;Let’s compare:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mailchimp&lt;/td&gt;
&lt;td&gt;Expensive + overkill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual send&lt;/td&gt;
&lt;td&gt;Someone forgets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zapier hacks&lt;/td&gt;
&lt;td&gt;Fragile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;This system&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clean, automatic, predictable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You control everything. No black boxes.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This isn’t just a newsletter feature—it’s an &lt;strong&gt;event-driven system&lt;/strong&gt; disguised as one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Publish event → triggers queue&lt;/li&gt;
&lt;li&gt;Queue → processed by cron&lt;/li&gt;
&lt;li&gt;State machine → tracks delivery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s simple on the surface, but solid under the hood.&lt;/p&gt;

&lt;p&gt;And that’s the sweet spot.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔥 If You’re Building Something Similar…
&lt;/h2&gt;

&lt;p&gt;Start here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep your data model clean&lt;/li&gt;
&lt;li&gt;Automate everything&lt;/li&gt;
&lt;li&gt;Design for failure (because it &lt;em&gt;will&lt;/em&gt; happen)&lt;/li&gt;
&lt;li&gt;Avoid adding tools just because they exist&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏁 TL;DR
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;If your newsletter requires manual effort, it’s broken.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Automate it. Keep it lean. Ship it.&lt;/p&gt;

</description>
      <category>newsletter</category>
      <category>supabase</category>
      <category>vercel</category>
      <category>cronjob</category>
    </item>
    <item>
      <title>Notifying Admins When Users Confirm Their Email — The Right Way with Supabase and Next.js"</title>
      <dc:creator>Frank Mendez</dc:creator>
      <pubDate>Thu, 16 Apr 2026 10:12:24 +0000</pubDate>
      <link>https://dev.to/frankmendez/notifying-admins-when-users-confirm-their-email-the-right-way-with-supabase-and-nextjs-23o6</link>
      <guid>https://dev.to/frankmendez/notifying-admins-when-users-confirm-their-email-the-right-way-with-supabase-and-nextjs-23o6</guid>
      <description>&lt;p&gt;Full blog =&amp;gt; &lt;a href="https://blog.frankmendez.site/blog/event-driven-user-notifications-with-supabase-webhooks-and-nextjs-no-edge-functions-needed" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;br&gt;
Steps (Article Sections)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Hook / Problem Statement — Why polling or signup-time notifications are wrong; email confirmation is async and outside your app's request cycle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Architecture Overview — The 6-step event chain diagram (confirmation link → auth.users.confirmed_at → Postgres trigger → public.profiles.confirmed_at → Supabase Database Webhook → Next.js API route). Why bridging via a public-schema table is necessary (Supabase webhooks can't observe auth.*).&lt;br&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%2Fvvv64br7sg807wrawktk.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%2Fvvv64br7sg807wrawktk.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Data Layer — The add_confirmed_at_to_profiles migration: adding the nullable confirmed_at column and the Postgres AFTER UPDATE trigger function handle_user_confirmed() that syncs it. Key insight: OLD.confirmed_at IS NULL AND NEW.confirmed_at IS NOT NULL guard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Webhook API Route — app/api/webhooks/user-confirmed/route.ts. Cover: shared-secret verification via x-webhook-secret header (security), parsing Supabase's { type, table, record } payload, independent try/catch per notification channel, and why always returning 200 OK prevents Supabase retry storms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Notification Services — user-confirmed.ts. Resend for email (HTML body with name/email/ID/timestamp), Slack Incoming Webhook via fetch. Emphasize isolation — one failure never blocks the other.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Supabase Dashboard Config — The one-time manual step: create the DB webhook pointing to the deployed route, set the header, add a confirmed_at IS NOT NULL filter to reduce noise.&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://supabase.com/images/features/database-webhooks.png" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;supabase.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment Variables — Table of the 4 new vars (RESEND_API_KEY, ADMIN_EMAIL, SLACK_WEBHOOK_URL, WEBHOOK_SECRET) with descriptions.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2F8rvrsjln4hjsai34bkms.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%2F8rvrsjln4hjsai34bkms.png" alt=" "&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%2Fggnb3ezlm71m64b03g3m.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%2Fggnb3ezlm71m64b03g3m.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;What's Out of Scope (and Why) — No retry logic (intentional), no multi-admin, no notification prefs UI — acknowledge these and link to potential follow-ups.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Closing — Summary of the pattern: Postgres trigger → public table → Supabase webhook → app route. Reusable for other auth events (password reset, MFA enrollment, etc.).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;
  
  
  Supabase Database Webhook Setup
&lt;/h1&gt;
&lt;h2&gt;
  
  
  User Confirmed Notification Webhook
&lt;/h2&gt;

&lt;p&gt;This webhook fires when a user confirms their email and triggers admin notifications (email + Slack).&lt;/p&gt;
&lt;h3&gt;
  
  
  One-time setup in Supabase Dashboard
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Database → Webhooks&lt;/strong&gt; in the Supabase Dashboard&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create a new hook&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Fill in the following fields:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;user-confirmed-notification&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Table&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public.profiles&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Events&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTTP Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Method&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;URL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://your-domain.com/api/webhooks/user-confirmed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Under &lt;strong&gt;HTTP Headers&lt;/strong&gt;, add:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-webhook-secret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(value of your &lt;code&gt;WEBHOOK_SECRET&lt;/code&gt; env var)&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Confirm&lt;/strong&gt; to save.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Required: add confirmed_at filter
&lt;/h3&gt;

&lt;p&gt;Without this filter, the webhook fires on every profile update (role changes, name updates, etc.), causing duplicate notifications. Add this filter condition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Column:&lt;/strong&gt; &lt;code&gt;confirmed_at&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operator:&lt;/strong&gt; &lt;code&gt;is not&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value:&lt;/strong&gt; &lt;code&gt;null&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures the webhook only fires when &lt;code&gt;confirmed_at&lt;/code&gt; is set.&lt;/p&gt;
&lt;h3&gt;
  
  
  Required environment variables
&lt;/h3&gt;

&lt;p&gt;Make sure these are set in &lt;code&gt;.env.local&lt;/code&gt; (dev) and in your Vercel / hosting environment (prod):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;RESEND_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;RESEND_FROM_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;ADMIN_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;SLACK_WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>architecture</category>
      <category>nextjs</category>
      <category>postgres</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>🚀 I Built a Full-Stack Blog CMS with Next.js + Supabase (And You Can Use It Today)</title>
      <dc:creator>Frank Mendez</dc:creator>
      <pubDate>Wed, 15 Apr 2026 09:20:09 +0000</pubDate>
      <link>https://dev.to/frankmendez/i-built-a-full-stack-blog-cms-with-nextjs-supabase-and-you-can-use-it-today-23bk</link>
      <guid>https://dev.to/frankmendez/i-built-a-full-stack-blog-cms-with-nextjs-supabase-and-you-can-use-it-today-23bk</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;br&gt;
I built a production-ready blog CMS using modern tools like Next.js (App Router), Supabase, and Tailwind. It has auth, roles, drafts, an AI assistant, and a headless API. It’s open-source — and yes, you can try it right now 👇&lt;br&gt;
👉 Live: &lt;a href="https://blog.frankmendez.site/blog" rel="noopener noreferrer"&gt;https://blog.frankmendez.site/blog&lt;/a&gt;&lt;br&gt;
👉 Repo: &lt;a href="https://github.com/frank-mendez/nextjs-blog-cms" rel="noopener noreferrer"&gt;https://github.com/frank-mendez/nextjs-blog-cms&lt;/a&gt;&lt;br&gt;
👉 Register: &lt;a href="https://blog.frankmendez.site/register" rel="noopener noreferrer"&gt;https://blog.frankmendez.site/register&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 Why I Built This
&lt;/h2&gt;

&lt;p&gt;Let’s be honest — most CMS platforms fall into two categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Too simple&lt;/strong&gt; → great for beginners, painful for devs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too complex&lt;/strong&gt; → enterprise-level headache just to publish a blog post&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted something in between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developer-friendly&lt;/li&gt;
&lt;li&gt;Production-ready&lt;/li&gt;
&lt;li&gt;Actually enjoyable to use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also… I got tired of wiring the same auth + roles + editor setup over and over again. So I decided to build it once — properly.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 What This CMS Can Do
&lt;/h2&gt;

&lt;p&gt;This isn’t just a “Hello World blog.” It’s a &lt;strong&gt;serious CMS&lt;/strong&gt; disguised as a clean UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔐 Authentication &amp;amp; Roles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Supabase Auth out of the box&lt;/li&gt;
&lt;li&gt;Role-based access control (Admin / Author)&lt;/li&gt;
&lt;li&gt;Enforced using Row Level Security (RLS) — not just frontend checks (we don’t do fake security here)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✍️ Writing Experience
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;WYSIWYG editor (because Markdown wars are over… mostly)&lt;/li&gt;
&lt;li&gt;Draft &amp;amp; publish workflow&lt;/li&gt;
&lt;li&gt;Clean writing interface without distractions&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🤖 AI Writing Assistant
&lt;/h3&gt;

&lt;p&gt;Yes, there’s AI. Because of course there is.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate content ideas&lt;/li&gt;
&lt;li&gt;Assist in writing posts&lt;/li&gt;
&lt;li&gt;Speed up your workflow when your brain says “nope”&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🔌 Headless API
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;REST API ready to plug into anything&lt;/li&gt;
&lt;li&gt;Use it for mobile apps, dashboards, or your next side project&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ⚡ Developer Experience
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Built with &lt;strong&gt;Next.js App Router&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Styled with TailwindCSS + shadcn/ui&lt;/li&gt;
&lt;li&gt;Clean structure, easy to extend&lt;/li&gt;
&lt;li&gt;MCP-powered workflows (because we like things fast)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠️ Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js (App Router)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Supabase (DB + Auth + RLS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI:&lt;/strong&gt; TailwindCSS + shadcn/ui&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI:&lt;/strong&gt; OpenAI integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment:&lt;/strong&gt; Vercel-ready&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basically, if you’re a modern frontend dev — this stack will feel like home.&lt;/p&gt;




&lt;h2&gt;
  
  
  👀 Live Demo
&lt;/h2&gt;

&lt;p&gt;Don’t take my word for it — try it yourself:&lt;/p&gt;

&lt;p&gt;👉 Browse posts:&lt;br&gt;
&lt;a href="https://blog.frankmendez.site/blog" rel="noopener noreferrer"&gt;https://blog.frankmendez.site/blog&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 Create an account:&lt;br&gt;
&lt;a href="https://blog.frankmendez.site/register" rel="noopener noreferrer"&gt;https://blog.frankmendez.site/register&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes, you can actually use it. No “coming soon” nonsense.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Why You Might Find This Useful
&lt;/h2&gt;

&lt;p&gt;This project is perfect if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Want a &lt;strong&gt;starter CMS&lt;/strong&gt; without reinventing auth + roles&lt;/li&gt;
&lt;li&gt;Are learning &lt;strong&gt;Next.js + Supabase in a real-world setup&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Need a &lt;strong&gt;headless blog backend&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Want to study &lt;strong&gt;RLS done right&lt;/strong&gt; (seriously, this part matters)&lt;/li&gt;
&lt;li&gt;Just like exploring clean, modern full-stack apps&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📦 Open Source (And Why I Need Your Help)
&lt;/h2&gt;

&lt;p&gt;Here’s the thing — open source only works if people actually… use it 😅&lt;/p&gt;

&lt;p&gt;If you find this useful:&lt;/p&gt;

&lt;p&gt;⭐ Star the repo → &lt;a href="https://github.com/frank-mendez/nextjs-blog-cms" rel="noopener noreferrer"&gt;https://github.com/frank-mendez/nextjs-blog-cms&lt;/a&gt;&lt;br&gt;
🍴 Fork it and build your own version&lt;br&gt;
🐛 Open issues / suggest improvements&lt;br&gt;
🚀 Share it with other devs&lt;/p&gt;

&lt;p&gt;Every star helps push this project further (and tells me I’m not coding into the void).&lt;/p&gt;




&lt;h2&gt;
  
  
  🧭 What’s Next
&lt;/h2&gt;

&lt;p&gt;I’m planning to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Comments system&lt;/li&gt;
&lt;li&gt;Better SEO tooling&lt;/li&gt;
&lt;li&gt;More AI features (without turning it into Skynet)&lt;/li&gt;
&lt;li&gt;Plugin system (maybe 👀)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧵 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This project started as “I just need a blog CMS…”&lt;/p&gt;

&lt;p&gt;…and turned into a full-blown platform.&lt;/p&gt;

&lt;p&gt;If you’ve ever thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“There has to be a better way to build this stack…”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yeah — same. This is my attempt at that.&lt;/p&gt;




&lt;p&gt;If you try it out, I’d love to hear your feedback.&lt;br&gt;
If it breaks… well, congratulations, you found a feature.&lt;/p&gt;

&lt;p&gt;Happy building 👨‍💻&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
