Supabase Auth + Next.js: Row Level Security, Magic Links, and OAuth in 30 Minutes
Supabase is PostgreSQL + auth + storage + realtime, hosted.
Here's a complete auth setup with RLS in under an hour.
Setup
npm install @supabase/supabase-js @supabase/ssr
// 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!
)
}
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createServerSupabaseClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get: (name) => cookieStore.get(name)?.value,
set: (name, value, options) => cookieStore.set({ name, value, ...options }),
remove: (name, options) => cookieStore.set({ name, value: '', ...options }),
},
}
)
}
Authentication Methods
const supabase = createClient()
// Magic link (passwordless)
await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: { emailRedirectTo: `${origin}/auth/callback` },
})
// OAuth (Google, GitHub, etc.)
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: `${origin}/auth/callback` },
})
// Email + password
await supabase.auth.signInWithPassword({ email, password })
// Sign out
await supabase.auth.signOut()
Auth Callback Route
// app/auth/callback/route.ts
import { createServerSupabaseClient } 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 = createServerSupabaseClient()
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(`${origin}/dashboard`)
}
Middleware for Protected Routes
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'
export async function middleware(request: NextRequest) {
let response = NextResponse.next()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { /* ... */ } }
)
const { data: { user } } = await supabase.auth.getUser()
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*'],
}
Row Level Security
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Users can only read published posts or their own
CREATE POLICY "posts_select" ON posts
FOR SELECT USING (
published = true
OR auth.uid() = author_id
);
-- Users can only insert their own posts
CREATE POLICY "posts_insert" ON posts
FOR INSERT WITH CHECK (auth.uid() = author_id);
-- Users can only update/delete their own posts
CREATE POLICY "posts_update" ON posts
FOR UPDATE USING (auth.uid() = author_id);
RLS runs at the database level. Even if your app code is wrong, the DB won't leak data.
Getting the Current User (Server Component)
// app/dashboard/page.tsx
import { createServerSupabaseClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function Dashboard() {
const supabase = createServerSupabaseClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/login')
// Direct DB query — RLS automatically filters to this user's data
const { data: posts } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
return <PostList posts={posts ?? []} user={user} />
}
The AI SaaS Starter Kit ships with NextAuth (not Supabase Auth) but the same RLS and multi-provider OAuth patterns apply. All pre-configured. $99 one-time.
Top comments (0)