DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Supabase Auth + Next.js: Row Level Security, Magic Links, and OAuth in 30 Minutes

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
Enter fullscreen mode Exit fullscreen mode
// 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 }),
      },
    }
  )
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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`)
}
Enter fullscreen mode Exit fullscreen mode

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*'],
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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)