How to Connect Supabase to a Next.js App (and Deploy on Vercel)
buildinpublicsupabasenextjsvercel
Prerequisites
- A Supabase project
- A Next.js App Router project
- Node.js 18+
Step 1: Install dependencies
npm install @supabase/supabase-js @supabase/ssr
Step 2: Create the lib/supabase trio
A Next.js App Router project needs three different Supabase clients: one for the browser, one for the server, and one for middleware.
lib/supabase/client.ts — for the browser
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
lib/supabase/server.ts — for the server
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) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Called from a Server Component — safe to ignore
}
},
},
}
)
}
lib/supabase/middleware.ts — session refresh
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let response = 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 }) =>
request.cookies.set(name, value)
)
response = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options)
)
},
},
}
)
await supabase.auth.getUser()
return response
}
Step 3: Configure .env.local
Create a .env.local file at the root of your project and add your Supabase URL and publishable key:
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-publishable-key
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
Notes:
.env.localmust be in.gitignore— never commit it- The publishable key is safe to expose to the browser (it is designed to be public)
- The secret key (service_role) must never be prefixed with
NEXT_PUBLIC_. It is server-side only.
Step 4: Read data in a Server Component
After setting up the clients, I replaced hard-coded data in my vocal practice app with real Supabase data.
Result: the web version now shows the exact same data as the existing mobile app — verified 1:1.
Data fetch helper
import { createClient } from '@/lib/supabase/server'
export async function getUnits(lang = 'cn') {
const supabase = await createClient()
const { data, error } = await supabase
.from('unit_lang')
.select('*')
.eq('lang', lang)
.order('unit_num')
if (error) return []
return data ?? []
}
Page usage
// app/page.tsx
import { getUnits } from '@/lib/data/units'
import DashboardContent from '@/components/DashboardContent'
export default async function HomePage() {
const units = await getUnits('cn')
return <DashboardContent units={units} />
}
Client-side filtering by day
The days field is a comma-separated string (e.g. "1,2,3"), so filtering happens on the client:
const currentUnits = units.filter((unit) => {
const dayArray = unit.days.split(',').map((d) => Number(d.trim()))
return dayArray.includes(activeDay)
})
Key takeaways
daysis a comma-separated string ("1,2,3") — usesplit + includesto filterneed_pay = 0means free,need_pay = 1means VIP- Duration is
0for now (real durations live in theparttable — next migration)
Commit and push
