Introduction
Building a full-stack application used to mean writing backend APIs, managing databases, handling authentication, and configuring file storage — all separate pieces that needed to be glued together. Supabase changes that equation entirely.
Often called the open-source alternative to Firebase, Supabase gives you a complete backend out of the box: a PostgreSQL database, authentication, real-time subscriptions, file storage, and Edge Functions — all accessible through a clean REST and TypeScript SDK.
Paired with Next.js, you get one of the most productive full-stack stacks available today. In this guide, I'll walk you through everything you need to know to get started.
What is Supabase?
Supabase is an open-source platform that wraps PostgreSQL and a suite of backend tools into a developer-friendly experience. Unlike Firebase, which uses a NoSQL (document) database, Supabase is built on PostgreSQL — a battle-tested relational database with powerful features like joins, foreign keys, and full-text search.
Key Features
- Database — Full PostgreSQL with a table editor, SQL editor, and auto-generated REST/GraphQL APIs
- Authentication — Email/password, OAuth (Google, GitHub, Twitter, and more), magic links, and phone auth out of the box
- Real-time — Subscribe to database changes via WebSockets. No polling needed
- Storage — S3-compatible file storage with fine-grained access control via Row Level Security (RLS)
- Edge Functions — Deploy TypeScript/Deno serverless functions at the edge
- Row Level Security (RLS) — PostgreSQL-native access control that locks down your data at the database level
Setting Up Supabase
Step 1: Create a Supabase Project
Head to supabase.com, create an account, and click New Project. You can self-host Supabase via Docker, but the managed cloud version gives you a working backend in under 60 seconds — and the free tier is generous enough for most personal projects.
Step 2: Install the Client SDK
In your Next.js project, install the Supabase JavaScript client:
1npm install @supabase/supabase-jsStep 3: Configure Environment Variables
Create or update your .env.local file:
1NEXT_PUBLIC_SUPABASE_URL=your-project-url
2NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
3SUPABASE_SERVICE_ROLE_KEY=your-service-role-keyYou'll find these values in your Supabase dashboard under Settings → API. The SUPABASE_SERVICE_ROLE_KEY must only be used in server-side code — never expose it to the client.
Connecting Supabase to Next.js
The cleanest approach is to create a single Supabase client module that you import wherever you need it:
1// lib/supabase/client.ts
2import { createClient } from "@supabase/supabase-js"
3
4const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
5const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6
7export const supabase = createClient(supabaseUrl, supabaseAnonKey)For Next.js App Router server-side operations (Server Components, Route Handlers, middleware), you should use the Supabase SSR package which handles cookie-based auth properly:
1npm install @supabase/ssr @supabase/supabase-js1// lib/supabase/server.ts
2import { createServerClient } from "@supabase/ssr"
3import { cookies } from "next/headers"
4
5export async function createClient() {
6 const cookieStore = await cookies()
7
8 return createServerClient(
9 process.env.NEXT_PUBLIC_SUPABASE_URL!,
10 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
11 {
12 cookies: {
13 getAll() {
14 return cookieStore.getAll()
15 },
16 setAll(cookiesToSet) {
17 try {
18 cookiesToSet.forEach(({ name, value, options }) =>
19 cookieStore.set(name, value, options)
20 )
21 } catch {
22 // In Server Components, cookies are read-only
23 }
24 },
25 },
26 }
27 )
28}Building Your First Query
Once connected, querying your database is straightforward. Here's a simple example that fetches posts from a posts table:
1import { supabase } from "@/lib/supabase/client"
2
3async function getPosts() {
4 const { data, error } = await supabase
5 .from("posts")
6 .select("*")
7 .order("created_at", { ascending: false })
8
9 if (error) {
10 console.error("Error fetching posts:", error.message)
11 return []
12 }
13
14 return data
15}Using Row Level Security (RLS)
Supabase's Row Level Security policies are what make it production-ready. Without RLS, anyone with your anon key can read or write all your data. Here's an example policy that only allows authenticated users to read posts:
1-- Enable RLS on the posts table
2ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
3
4-- Policy: Anyone can read public posts
5CREATE POLICY "Public posts are viewable by everyone"
6 ON posts FOR SELECT
7 USING (published = true);
8
9-- Policy: Only authors can update their own posts
10CREATE POLICY "Authors can update their own posts"
11 ON posts FOR UPDATE
12 USING (auth.uid() = author_id);Authentication in Next.js
Supabase Auth handles the entire authentication flow. Here's a minimal email/password sign-in component:
1"use client"
2
3import { useState } from "react"
4import { supabase } from "@/lib/supabase/client"
5
6export default function SignIn() {
7 const [email, setEmail] = useState("")
8 const [password, setPassword] = useState("")
9 const [loading, setLoading] = useState(false)
10
11 async function handleSignIn(e: React.FormEvent) {
12 e.preventDefault()
13 setLoading(true)
14
15 const { error } = await supabase.auth.signInWithPassword({
16 email,
17 password,
18 })
19
20 if (error) {
21 console.error("Sign in error:", error.message)
22 }
23
24 setLoading(false)
25 }
26
27 return (
28 <form onSubmit={handleSignIn}>
29 <input
30 type="email"
31 value={email}
32 onChange={(e) => setEmail(e.target.value)}
33 placeholder="Email"
34 required
35 />
36 <input
37 type="password"
38 value={password}
39 onChange={(e) => setPassword(e.target.value)}
40 placeholder="Password"
41 required
42 />
43 <button type="submit" disabled={loading}>
44 {loading ? "Signing in..." : "Sign In"}
45 </button>
46 </form>
47 )
48}For OAuth providers like GitHub or Google, the flow is even simpler:
1const { data, error } = await supabase.auth.signInWithOAuth({
2 provider: "github",
3 options: {
4 redirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
5 },
6})Real-Time Subscriptions
This is where Supabase really shines. Instead of polling an API, you can subscribe to database changes in real time:
1useEffect(() => {
2 const channel = supabase
3 .channel("posts-changes")
4 .on(
5 "postgres_changes",
6 { event: "*", schema: "public", table: "posts" },
7 (payload) => {
8 console.log("Change received:", payload)
9 // Update your local state here
10 }
11 )
12 .subscribe()
13
14 return () => {
15 supabase.removeChannel(channel)
16 }
17}, [])This listens for INSERT, UPDATE, and DELETE events on the posts table in real time. Perfect for live dashboards, chat apps, or collaborative tools.
Why It Matters
Supabase combined with Next.js gives you a complete full-stack toolkit:
- Next.js handles SSR, SSG, routing, and the frontend layer
- Supabase provides the database, auth, storage, and real-time layer
- Both are open source and can be self-hosted if you need to
- The free tier is generous enough for side projects and portfolios
- TypeScript support is excellent — schemas can be auto-generated with
supabase gen types
Whether you're building a personal blog with authenticated comments, a SaaS dashboard, or a real-time collaboration tool, this stack lets you ship fast without sacrificing flexibility.