How to Build a Full-Stack Next.js App with Auth, Database, and Deployment in 2026 ⏱️ 10 min read
The modern full-stack JavaScript setup has converged on a clear winner: Next.js for the framework, Supabase for auth and database, and Vercel or Railway for deployment. This stack lets a solo developer ship a production-ready app in an afternoon. Here’s exactly how to do it.
What You’ll Build
A working Next.js 15 application with:
- Email/password authentication (Supabase Auth)
- PostgreSQL database with typed queries
- Protected routes using middleware
- Deployed to production with automatic preview deployments per pull request
Prerequisites: Node.js 20+, a Supabase account (free), a Vercel account (free). Total time: 45-60 minutes.
Step 1: Create Your Next.js Project
Scaffold the project with the App Router (Next.js 13+ default):
npx create-next-app@latest my-app --typescript --tailwind --app --src-dir
cd my-app
Install the Supabase client and helper packages:
npm install @supabase/supabase-js @supabase/ssr
The @supabase/ssr package handles cookie-based auth correctly in the App Router — it’s the officially recommended approach as of 2024 and replaces the older @supabase/auth-helpers-nextjs.
Step 2: Set Up Supabase
Go to supabase.com, create a new project, and grab two values from Settings → API:
NEXT_PUBLIC_SUPABASE_URL— your project URLNEXT_PUBLIC_SUPABASE_ANON_KEY— your public anon key
Create a .env.local file in your project root:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
Now create two Supabase client utilities. First, the browser client at src/utils/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!
)
}
Second, the server client at src/utils/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 }) =>
cookieStore.set(name, value, options))
},
},
}
)
}
Step 3: Add Authentication Middleware
Create src/middleware.ts to protect routes and refresh auth tokens automatically:
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return request.cookies.getAll() },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
supabaseResponse.cookies.set(name, value, options)
})
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
// Redirect unauthenticated users away from protected routes
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return supabaseResponse
}
export const config = {
matcher: ['/dashboard/:path*'],
}
Step 4: Build the Auth Pages
Create a login page at src/app/login/page.tsx with a Server Action to handle sign-in:
'use server'
import { createClient } from '@/utils/supabase/server'
import { redirect } from 'next/navigation'
export async function login(formData: FormData) {
const supabase = await createClient()
const { error } = await supabase.auth.signInWithPassword({
email: formData.get('email') as string,
password: formData.get('password') as string,
})
if (error) redirect('/login?error=Invalid credentials')
redirect('/dashboard')
}
Using Server Actions means no separate API route — the auth logic runs server-side and redirects cleanly. Add a matching signup action using supabase.auth.signUp() with the same pattern.
Step 5: Create a Database Table and Query It
In the Supabase dashboard, go to the SQL Editor and create a sample table:
create table notes (
id uuid default gen_random_uuid() primary key,
user_id uuid references auth.users not null,
content text not null,
created_at timestamptz default now()
);
-- Enable Row Level Security
alter table notes enable row level security;
-- Users can only see their own notes
create policy "Users see own notes" on notes
for select using (auth.uid() = user_id);
Row Level Security (RLS) means even if your app code has a bug, users can never see each other’s data. Always enable it on user-scoped tables.
Query this table in a Server Component (no useEffect, no loading state):
import { createClient } from '@/utils/supabase/server'
export default async function Dashboard() {
const supabase = await createClient()
const { data: notes } = await supabase.from('notes').select()
return (
<ul>
{notes?.map(note => <li key={note.id}>{note.content}</li>)}
</ul>
)
}
Step 6: Deploy to Production
Push your code to GitHub, then connect the repo to Vercel:
- Go to vercel.com → New Project → Import your repo
- Add your environment variables (
NEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_ANON_KEY) - Click Deploy — Vercel auto-detects Next.js and configures everything
From this point, every push to main triggers a production deploy. Every pull request gets a unique preview URL — share it with collaborators or clients for feedback before merging.
Add your production URL to Supabase under Authentication → URL Configuration → Site URL, and add your preview URL pattern (https://*-yourproject.vercel.app) to Redirect URLs to allow auth callbacks on preview deploys.
Final Thoughts
This stack — Next.js + Supabase + Vercel — handles auth, database, and deployment with minimal configuration and zero server management. You get server-side rendering, type-safe database queries, Row Level Security, and automatic preview deployments out of the box.
From here, extend the app by adding Supabase Storage for file uploads, Supabase Realtime for live features, or integrating Stripe (with the Lemon Squeezy or Paddle comparison in mind) for payments. The foundation you’ve built today scales to thousands of users without changing a line of infrastructure code.
Clone the repo, run through these steps, and deploy your first full-stack app today — the whole process takes under an hour.