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
- Node.js 18+
- npm or yarn (latest)
- Git
- Supabase account
- Vercel account
- Code editor (VS Code, Cursor, etc.)
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