Next.js + Supabase: The Complete Full-Stack Setup Guide
A practical, end-to-end guide to building a production-ready full-stack app with Next.js and Supabase - auth, database, Row Level Security and deployment.
Next.js and Supabase are one of the most productive combinations for building full-stack applications today. Next.js gives you the App Router, React Server Components and a polished developer experience. Supabase gives you a real Postgres database, authentication, storage and realtime — all behind a clean SDK.
Together, they let a small team ship production software fast, without babysitting infrastructure. This guide walks through a complete, production-ready setup: authentication, protected data, Row Level Security and deployment.
Why Next.js and Supabase work so well together
- Postgres, not a proprietary database. You get a real SQL database with joins, transactions and full-text search. There is no lock-in.
- Server Components reduce client JavaScript. You can query the database on the server, render HTML, and ship almost nothing to the browser.
- Authentication is built in. Email/password, magic links, OAuth and Row Level Security all come out of the box.
- Instant APIs. Supabase auto-generates a REST and realtime API for every table.
Prerequisites
- Node.js 18 or newer
- A Supabase account (the free tier is generous)
- Basic familiarity with React and TypeScript
1. Create the Supabase project
Create a project in the Supabase dashboard and copy three values from Project Settings → API: the project URL, the anon key, and the service role key. The service role key bypasses Row Level Security, so it must never reach the browser.
2. Install the Supabase client
npm install @supabase/supabase-js @supabase/ssr
In the App Router you typically need two clients: a browser client for client components, and a server client that reads cookies for server components and route handlers.
Browser client
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
Server client
// lib/supabase/server.ts
import { cookies } from 'next/headers'
import { createServerClient } from '@supabase/ssr'
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 }) =>
cookieStore.set(name, value, options)
)
},
},
}
)
}
3. Environment variables
Store your keys in .env.local and never commit them. Only the anon key is safe for the browser; prefix it with NEXT_PUBLIC_.
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
4. Authentication with email and password
Supabase Auth handles sessions for you. On the server you read the user with getUser(), which validates the token with Supabase rather than trusting a cookie blindly.
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
Always use getUser() for authorization decisions. getSession() only reads the cookie and can be spoofed, so treat it as a convenience for UI, not a security boundary.
5. Query data from a Server Component
export default async function Page() {
const supabase = await createClient()
const { data: posts } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
6. Lock it down with Row Level Security
Every table in the public schema should have Row Level Security enabled. RLS is the single most important security control in Supabase: it decides which rows a given role can read or write, directly in the database.
A safe default is to allow public reads and restrict writes to authenticated users:
alter table posts enable row level security;
create policy "Public can read posts"
on posts for select
using (true);
create policy "Users can insert their own posts"
on posts for insert
with check (auth.uid() = user_id);
For a deeper walkthrough, see Supabase Row Level Security: A Practical Guide.
7. Deploy to Vercel
Push the repository to GitHub and import it into Vercel. Add the same environment variables in the Vercel dashboard. Server Components and route handlers run on the server, so the service role key stays server-side. If you use next/image with remote avatars, add the Supabase storage domain to remotePatterns.
Common mistakes to avoid
- Shipping the service role key to the client. It bypasses RLS entirely. Keep it server-only.
- Forgetting RLS. A table without RLS is readable by anyone with the anon key.
- Using
getSession()for authorization. UsegetUser()instead. - Over-fetching. Select only the columns you need and paginate with
range().
Final thoughts
Next.js and Supabase remove most of the boilerplate that used to come with full-stack development. The combination gives you a real database, secure auth and a deployment story that scales — without a dedicated backend team. Start small, enable RLS on every table, and iterate. You can see examples of systems I have built this way on the works page.