ownlife-web-logo
beginnerDevelopmentJuly 8, 2026

Build A Production-Ready Blog with Supabase and Next.js Server Actions

Learn how to integrate authentication, real-time features, and secure database operations in a single deployment that scales to millions of users.

Sponsor

Build A Production-Ready Blog with Supabase and Next.js Server Actions

Build Production-Ready APIs with Supabase and Next.js Server Actions

Build a complete blog API with authentication, real-time comments, and automated deployments using Next.js Server Actions and Supabase. This production-ready application handles content management, user interactions, and database operations through a single Next.js deployment.

Prerequisites

Step 1 — Create Next.js Project

npx create-next-app@latest blog-api --typescript --tailwind --eslint --app
cd blog-api
npm install @supabase/supabase-js @supabase/ssr

Initialize a new Next.js project with TypeScript and install Supabase dependencies. The @supabase/ssr package provides server-side rendering support for authentication.

Expected result: New Next.js project with Supabase packages installed.

Step 2 — Configure Supabase Database

-- Execute in Supabase SQL Editor
CREATE TABLE posts (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  title text NOT NULL,
  content text NOT NULL,
  author_id uuid REFERENCES auth.users(id) NOT NULL,
  created_at timestamp DEFAULT now(),
  updated_at timestamp DEFAULT now()
);

CREATE TABLE comments (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  post_id uuid REFERENCES posts(id) ON DELETE CASCADE,
  author_id uuid REFERENCES auth.users(id) NOT NULL,
  content text NOT NULL,
  created_at timestamp DEFAULT now()
);

-- Enable Row Level Security
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;

-- RLS Policies
CREATE POLICY "Posts are viewable by everyone" ON posts FOR SELECT USING (true);
CREATE POLICY "Users can create their own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = author_id);
CREATE POLICY "Users can update their own posts" ON posts FOR UPDATE USING (auth.uid() = author_id);

CREATE POLICY "Comments are viewable by everyone" ON comments FOR SELECT USING (true);
CREATE POLICY "Users can create comments" ON comments FOR INSERT WITH CHECK (auth.uid() = author_id);

Create the database schema with posts and comments tables. Row Level Security (RLS) ensures users can only modify their own content while allowing public read access.

Expected result: Database tables created with proper security policies in Supabase dashboard.

Step 3 — Set Up Environment Variables

# .env.local
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
// lib/supabase.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export const createClient = () => {
  const cookieStore = cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
      },
    }
  )
}

Configure Supabase environment variables and create a server client that works with Next.js cookies. According to the Supabase documentation, this pattern ensures secure server-side authentication.

Expected result: Supabase client configured for server-side rendering.

Step 4 — Create Server Actions

// app/actions/posts.ts
'use server'

import { createClient } from '@/lib/supabase'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  const supabase = createClient()
  
  const title = formData.get('title') as string
  const content = formData.get('content') as string
  
  const { data: { user } } = await supabase.auth.getUser()
  
  if (!user) {
    redirect('/login')
  }

  const { error } = await supabase
    .from('posts')
    .insert({
      title,
      content,
      author_id: user.id
    })

  if (error) {
    throw new Error(error.message)
  }

  revalidatePath('/posts')
  redirect('/posts')
}

export async function getPosts() {
  const supabase = createClient()
  
  const { data: posts, error } = await supabase
    .from('posts')
    .select('*, author:auth.users(email)')
    .order('created_at', { ascending: false })

  if (error) {
    throw new Error(error.message)
  }

  return posts
}

Build Server Actions for CRUD operations. The 'use server' directive at the top ensures these functions run server-side only. As outlined in the Next.js documentation, Server Actions provide type-safe, progressive enhancement for forms.

Expected result: Server Actions that handle database operations with authentication.

Step 5 — Build Authentication Components

// app/login/page.tsx
import { createClient } from '@/lib/supabase'
import { redirect } from 'next/navigation'

export default function Login() {
  const signIn = async (formData: FormData) => {
    'use server'

    const email = formData.get('email') as string
    const password = formData.get('password') as string
    const supabase = createClient()

    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    })

    if (error) {
      redirect('/login?message=Could not authenticate user')
    }

    redirect('/posts')
  }

  return (
    <form action={signIn} className="max-w-md mx-auto mt-8 space-y-4">
      <input
        name="email"
        type="email"
        placeholder="Email"
        required
        className="w-full p-2 border rounded"
      />
      <input
        name="password"
        type="password"
        placeholder="Password"
        required
        className="w-full p-2 border rounded"
      />
      <button type="submit" className="w-full bg-blue-500 text-white p-2 rounded">
        Sign In
      </button>
    </form>
  )
}

Create authentication forms using Server Actions. The form automatically handles progressive enhancement - it works without JavaScript but enhances with it.

Expected result: Working login form that authenticates users through Supabase.

Step 6 — Create Dynamic Blog Pages

// app/posts/page.tsx
import { getPosts, createPost } from '@/app/actions/posts'
import { createClient } from '@/lib/supabase'

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

  return (
    <div className="max-w-4xl mx-auto p-4">
      {user && (
        <form action={createPost} className="mb-8 space-y-4">
          <input
            name="title"
            placeholder="Post title"
            className="w-full p-2 border rounded"
            required
          />
          <textarea
            name="content"
            placeholder="Write your post..."
            className="w-full p-2 border rounded h-32"
            required
          />
          <button type="submit" className="bg-green-500 text-white px-4 py-2 rounded">
            Create Post
          </button>
        </form>
      )}

      <div className="space-y-6">
        {posts.map((post) => (
          <article key={post.id} className="border p-4 rounded">
            <h2 className="text-xl font-bold">{post.title}</h2>
            <p className="text-gray-600 text-sm">By {post.author?.email}</p>
            <p className="mt-2">{post.content}</p>
          </article>
        ))}
      </div>
    </div>
  )
}

Build the main posts page with server-side rendering and integrated forms. According to the TypeScript Handbook, this approach provides full type safety from database to UI.

Expected result: Dynamic blog page displaying posts with create functionality for authenticated users.

Step 7 — Add Real-time Comments

// app/posts/[id]/comments.tsx
'use client'

import { useEffect, useState } from 'react'
import { createClient } from '@supabase/supabase-js'

export default function Comments({ postId }: { postId: string }) {
  const [comments, setComments] = useState<any[]>([])
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )

  useEffect(() => {
    fetchComments()

    const channel = supabase
      .channel('comments')
      .on('postgres_changes', 
        { event: 'INSERT', schema: 'public', table: 'comments' },
        (payload) => {
          setComments(prev => [...prev, payload.new])
        }
      )
      .subscribe()

    return () => {
      supabase.removeChannel(channel)
    }
  }, [postId])

  const fetchComments = async () => {
    const { data } = await supabase
      .from('comments')
      .select('*, author:auth.users(email)')
      .eq('post_id', postId)

    setComments(data || [])
  }

  return (
    <div className="mt-8">
      <h3 className="text-lg font-bold mb-4">Comments</h3>
      {comments.map((comment) => (
        <div key={comment.id} className="border-l-4 border-blue-200 pl-4 mb-4">
          <p>{comment.content}</p>
          <span className="text-sm text-gray-500">— {comment.author?.email}</span>
        </div>
      ))}
    </div>
  )
}

Implement real-time comments using Supabase's real-time subscriptions. This client component updates automatically when new comments are added from any browser session.

Expected result: Live-updating comment system that shows new comments instantly.

Step 8 — Deploy to Vercel

# Install Vercel CLI
npm i -g vercel

# Deploy
vercel

# Add environment variables in Vercel dashboard
# NEXT_PUBLIC_SUPABASE_URL
# NEXT_PUBLIC_SUPABASE_ANON_KEY

Deploy your application to Vercel with proper environment variable configuration. The Vercel documentation shows that Next.js applications deploy automatically with zero configuration.

Expected result: Live production application accessible via Vercel URL.

What You've Built

You've created a production-ready blog API that combines Next.js Server Actions with Supabase's database and real-time capabilities. The application handles user authentication, CRUD operations for posts and comments, real-time updates, and proper security through Row Level Security policies. Your API runs entirely within a single Next.js application, eliminating the need for separate backend services while maintaining type safety and performance through server-side rendering.

Next Steps

  • Add image upload functionality using Supabase Storage
  • Implement post categories and tags with additional

Build details

Difficulty:Beginner
Time to Complete:
Tag:Development

Built this project?

Share your experience, challenges, and wins. Help others learn from your journey and inspire them to build their own version.

Sponsor