<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<rss version="2.0" 
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Portfolio Blog</title>
    <link>https://mfeti.dev</link>
    <description>Technical articles and insights</description>
    <language>en-us</language>
    <lastBuildDate>Tue, 02 Jun 2026 07:00:21 GMT</lastBuildDate>
    <atom:link href="https://mfeti.dev/api/rss/feed.xml" rel="self" type="application/rss+xml"/>
    <generator>Portfolio Blog RSS Generator</generator>
    
    <item>
      <title><![CDATA[Building Full-Stack Apps with Next.js 15 and Supabase]]></title>
      <link>https://mfeti.dev/blog/building-full-stack-apps-with-nextjs-15-and-supabase</link>
      <guid isPermaLink="true">https://mfeti.dev/blog/building-full-stack-apps-with-nextjs-15-and-supabase</guid>
      <description><![CDATA[Learn how to build a production-ready full-stack app using Next.js 15 App Router and Supabase which covering auth, RLS, real-time subscriptions, file storage, and deployment to Vercel.]]></description>
      <content:encoded><![CDATA[<h2>Introduction</h2><p>The combination of <strong>Next.js 15</strong> and <strong>Supabase</strong> has rapidly become one of the most productive stacks for building full-stack web applications. You get React Server Components, streaming SSR, and the App Router on the frontend paired with a fully managed PostgreSQL database, built-in auth, real-time subscriptions, and file storage on the backend.</p><p>In this guide we'll build a complete application from scratch, covering every layer of the stack. By the end, you'll have a production-ready app deployed to Vercel.</p><h2>Prerequisites</h2><ul><li><p>Node.js 18+</p></li><li><p>A <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2" href="https://supabase.com">Supabase</a> account (free tier works)</p></li><li><p>A <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2" href="https://vercel.com">Vercel</a> account for deployment</p></li><li><p>Familiarity with React and TypeScript basics</p></li></ul><h2>1. Project Setup</h2><p>Start by scaffolding a new Next.js project with the App Router and TypeScript:</p><pre class="rounded-lg bg-surface-container-low p-4 font-mono text-sm overflow-x-auto my-4"><code>npx create-next-app@latest my-app \
  --typescript \
  --tailwind \
  --eslint \
  --app \
  --src-dir=false
cd my-app</code></pre><p>Then install the Supabase client libraries:</p><pre class="rounded-lg bg-surface-container-low p-4 font-mono text-sm overflow-x-auto my-4"><code>npm install @supabase/supabase-js @supabase/ssr</code></pre><h2>2. Supabase Project Configuration</h2><p>Create a new project in the <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2" href="https://supabase.com/dashboard">Supabase Dashboard</a>. Once created, grab your <strong>Project URL</strong> and <strong>anon key</strong> from <em>Settings → API</em> and add them to <code>.env.local</code>:</p><pre class="rounded-lg bg-surface-container-low p-4 font-mono text-sm overflow-x-auto my-4"><code>NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key</code></pre><h2>3. Database Schema Design</h2><p>Good schema design is the foundation of any robust app. Here's a simple schema for a task management app run this SQL in the Supabase SQL Editor:</p><pre class="rounded-lg bg-surface-container-low p-4 font-mono text-sm overflow-x-auto my-4"><code>-- Profiles table (extends auth.users)
create table profiles (
  id uuid references auth.users on delete cascade primary key,
  username text unique not null,
  full_name text,
  avatar_url text,
  updated_at timestamptz default now()
);

-- Tasks table
create table tasks (
  id uuid default gen_random_uuid() primary key,
  user_id uuid references profiles(id) on delete cascade not null,
  title text not null,
  description text,
  completed boolean default false,
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- Indexes
create index tasks_user_id_idx on tasks(user_id);
create index tasks_created_at_idx on tasks(created_at desc);</code></pre><h2>4. Row-Level Security (RLS)</h2><p>RLS is one of Supabase's killer features. It lets you enforce authorization rules directly in the database, so even if someone bypasses your API, they can't access data they shouldn't see.</p><pre class="rounded-lg bg-surface-container-low p-4 font-mono text-sm overflow-x-auto my-4"><code>-- Enable RLS
alter table profiles enable row level security;
alter table tasks enable row level security;

-- Profiles: users can only read/update their own profile
create policy "Users can view own profile"
  on profiles for select using (auth.uid() = id);

create policy "Users can update own profile"
  on profiles for update using (auth.uid() = id);

-- Tasks: users can only CRUD their own tasks
create policy "Users can view own tasks"
  on tasks for select using (auth.uid() = user_id);

create policy "Users can insert own tasks"
  on tasks for insert with check (auth.uid() = user_id);

create policy "Users can update own tasks"
  on tasks for update using (auth.uid() = user_id);

create policy "Users can delete own tasks"
  on tasks for delete using (auth.uid() = user_id);</code></pre><h2>5. Supabase Client Setup</h2><p>With <code>@supabase/ssr</code>, you get separate client helpers for Server Components, Client Components, and Route Handlers:</p><pre class="rounded-lg bg-surface-container-low p-4 font-mono text-sm overflow-x-auto my-4"><code>// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() { return cookieStore.getAll() },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =&gt;
            cookieStore.set(name, value, options)
          )
        },
      },
    }
  )
}</code></pre><h2>6. Authentication with Server Components</h2><p>Next.js Server Components let you check auth state server-side before rendering no flash of unauthenticated content.</p><pre class="rounded-lg bg-surface-container-low p-4 font-mono text-sm overflow-x-auto my-4"><code>// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export default async function DashboardPage() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) redirect('/login')

  const { data: tasks } = await supabase
    .from('tasks')
    .select('*')
    .order('created_at', { ascending: false })

  return (
    &lt;main&gt;
      &lt;h1&gt;Welcome back, {user.email}&lt;/h1&gt;
      &lt;TaskList tasks={tasks ?? []} /&gt;
    &lt;/main&gt;
  )
}</code></pre><h2>7. Real-Time Subscriptions</h2><p>Supabase lets you subscribe to database changes in real time using <strong>Postgres replication</strong>. Here's a client component that listens for new tasks:</p><pre class="rounded-lg bg-surface-container-low p-4 font-mono text-sm overflow-x-auto my-4"><code>'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import type { Task } from '@/lib/types'

export function RealtimeTaskList({ initialTasks }: { initialTasks: Task[] }) {
  const [tasks, setTasks] = useState(initialTasks)
  const supabase = createClient()

  useEffect(() =&gt; {
    const channel = supabase
      .channel('tasks-channel')
      .on('postgres_changes', {
        event: '*',
        schema: 'public',
        table: 'tasks',
      }, (payload) =&gt; {
        if (payload.eventType === 'INSERT') {
          setTasks(prev =&gt; [payload.new as Task, ...prev])
        } else if (payload.eventType === 'DELETE') {
          setTasks(prev =&gt; prev.filter(t =&gt; t.id !== payload.old.id))
        } else if (payload.eventType === 'UPDATE') {
          setTasks(prev =&gt; prev.map(t =&gt;
            t.id === payload.new.id ? payload.new as Task : t
          ))
        }
      })
      .subscribe()

    return () =&gt; { supabase.removeChannel(channel) }
  }, [])

  return &lt;ul&gt;{tasks.map(t =&gt; &lt;li key={t.id}&gt;{t.title}&lt;/li&gt;)}&lt;/ul&gt;
}</code></pre><h2>8. File Storage</h2><p>Supabase Storage makes handling file uploads simple. Here's how to upload an avatar:</p><pre class="rounded-lg bg-surface-container-low p-4 font-mono text-sm overflow-x-auto my-4"><code>async function uploadAvatar(file: File, userId: string) {
  const supabase = createClient()
  const ext = file.name.split('.').pop()
  const path = `${userId}/avatar.${ext}`

  const { error } = await supabase.storage
    .from('avatars')
    .upload(path, file, { upsert: true })

  if (error) throw error

  const { data } = supabase.storage.from('avatars').getPublicUrl(path)
  return data.publicUrl
}</code></pre><h2>9. Deployment to Vercel</h2><p>Deploying a Next.js + Supabase app to Vercel takes under 5 minutes:</p><ol><li><p>Push your code to GitHub.</p></li><li><p>Import the repo in the <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2" href="https://vercel.com/new">Vercel dashboard</a>.</p></li><li><p>Add your environment variables (<code>NEXT_PUBLIC_SUPABASE_URL</code> and <code>NEXT_PUBLIC_SUPABASE_ANON_KEY</code>).</p></li><li><p>Click <strong>Deploy</strong>.</p></li></ol><p>Vercel automatically detects Next.js and configures the build correctly. Your app will be live in ~2 minutes.</p><h2>Performance Comparison</h2><table class="border-collapse table-auto w-full my-4" style="min-width: 100px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th class="border border-outline-variant px-4 py-2 bg-surface-container font-semibold" colspan="1" rowspan="1" style="text-align: left;"><p><strong>Feature</strong></p></th><th class="border border-outline-variant px-4 py-2 bg-surface-container font-semibold" colspan="1" rowspan="1" style="text-align: left;"><p><strong>Next.js + Supabase</strong></p></th><th class="border border-outline-variant px-4 py-2 bg-surface-container font-semibold" colspan="1" rowspan="1" style="text-align: left;"><p><strong>Traditional REST API</strong></p></th><th class="border border-outline-variant px-4 py-2 bg-surface-container font-semibold" colspan="1" rowspan="1" style="text-align: left;"><p><strong>Firebase</strong></p></th></tr><tr><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>Cold start</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>~50ms</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>~200ms</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>~80ms</p></td></tr><tr><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>Auth setup time</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>15 min</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>2–4 hrs</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>30 min</p></td></tr><tr><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>Real-time</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>✅ Built-in</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>❌ Custom</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>✅ Built-in</p></td></tr><tr><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>SQL support</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>✅ Full PostgreSQL</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><ul><li><p>✅ Depends</p></li></ul></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>❌ NoSQL only</p></td></tr><tr><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>Free tier DB size</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>500 MB</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>N/A</p></td><td class="border border-outline-variant px-4 py-2" colspan="1" rowspan="1" style="text-align: left;"><p>1 GB</p></td></tr></tbody></table><h2>Conclusion</h2><p>Next.js 15 and Supabase together form one of the most powerful and developer-friendly stacks available today. You get the performance of React Server Components, the simplicity of a managed Postgres backend, and the flexibility to scale from side project to production app without changing your architecture.</p><p>The patterns covered in this guide RLS, server-side auth, real-time subscriptions, and file storage are the foundation of almost every full-stack app. Start with this as your base and build from there.</p><p>Happy shipping! 🚀</p>]]></content:encoded>
      <pubDate>Tue, 02 Jun 2026 07:00:21 GMT</pubDate>
      <author>mohammed@mfeti.dev (Mohammed Ahmed)</author>
      <category>nextjs</category>
      <category>supabase</category>
      <category>typescript</category>
      <category>react</category>
      <category>full-stack</category>
      <category>postgresql</category>
      <category>vercel</category>
    </item>
  </channel>
</rss>