Supabase Integration Patterns for Next.js
Overview
Standards for integrating Supabase (auth, database, storage, realtime) with Next.js 15 App Router. Covers client setup, authentication flows, RLS policies, and data access patterns.
Client Setup
Server Client (for Server Components, API Routes, Server Actions)
File: src/lib/supabase/server.ts
typescript
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 {
// Server Component'te cookie set edilemez, ignore
}
},
},
}
)
}
Browser Client (for Client Components)
File: src/lib/supabase/client.ts
typescript
import { createBrowserClient } from "@supabase/ssr"
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
Middleware (for Auth Session Refresh)
File: src/middleware.ts
typescript
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 }) => {
request.cookies.set(name, value)
supabaseResponse.cookies.set(name, value, options)
})
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
// Protected routes
if (!user && request.nextUrl.pathname.startsWith("/dashboard")) {
const url = request.nextUrl.clone()
url.pathname = "/login"
return NextResponse.redirect(url)
}
return supabaseResponse
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|api/).*)"],
}
Authentication Patterns
Email/Password Login
typescript
"use server"
import { createClient } from "@/lib/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) return { error: error.message }
redirect("/dashboard")
}
OAuth (Google, GitHub)
typescript
"use server"
import { createClient } from "@/lib/supabase/server"
import { redirect } from "next/navigation"
import { headers } from "next/headers"
export async function loginWithGoogle() {
const supabase = await createClient()
const origin = (await headers()).get("origin")
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${origin}/auth/callback`,
},
})
if (data.url) redirect(data.url)
}
Auth Callback Route
File: src/app/auth/callback/route.ts
typescript
import { createClient } from "@/lib/supabase/server"
import { NextResponse } from "next/server"
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get("code")
if (code) {
const supabase = await createClient()
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(`${origin}/dashboard`)
}
Data Access Patterns
Server Component (Direct Query)
typescript
import { createClient } from "@/lib/supabase/server"
export default async function PostsPage() {
const supabase = await createClient()
const { data: posts } = await supabase
.from("posts")
.select("*, author:users(name, avatar_url)")
.order("created_at", { ascending: false })
return <PostList posts={posts ?? []} />
}
Client Component (React Query + Supabase)
typescript
"use client"
import { useQuery } from "@tanstack/react-query"
import { createClient } from "@/lib/supabase/client"
export const usePosts = () => {
const supabase = createClient()
return useQuery({
queryKey: ["posts"],
queryFn: async () => {
const { data, error } = await supabase
.from("posts")
.select("*")
.order("created_at", { ascending: false })
if (error) throw error
return data
},
})
}
Row Level Security (RLS) Patterns
Always enable RLS on all tables. Common policies:
sql
-- Users can read all public posts CREATE POLICY "Public posts readable by all" ON posts FOR SELECT USING (published = true); -- Users can only edit their own posts CREATE POLICY "Users can update own posts" ON posts FOR UPDATE USING (auth.uid() = user_id); -- Users can only insert as themselves CREATE POLICY "Users can insert own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id); -- Users can only delete their own posts CREATE POLICY "Users can delete own posts" ON posts FOR DELETE USING (auth.uid() = user_id);
Environment Variables
Required in .env.local:
code
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
Anti-Patterns
- •Never use service_role key on the client side
- •Never skip RLS - enable it on every table
- •Never store Supabase URL/key in code, use env variables
- •Never use
supabase.auth.getSession()on the server - usegetUser()instead (security) - •Never create Supabase client outside of functions (breaks cookie handling)