agentsclimarketplace

Nextjs supabase auth

Skill Inteligentsensingsolutions/tdd-dev-workflow/skills/nextjs-supabase-auth

This skill should be used when the user asks about "Supabase Auth with Next.js", "authentication Next.js", "login Supabase", "auth middleware Next.js", "protected routes", "auth callback", "Supabase SSR auth", "session management Next.js", "@supabase/ssr", or needs to integrate Supabase authentication in a Next.js App Router application.From its SKILL.md

Install
npx -y skills add Inteligentsensingsolutions/tdd-dev-workflow --skill nextjs-supabase-auth

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

12.8 KB, as published. Nobody here has run it

Next.js + Supabase Auth Integration

Core Principle

Use @supabase/ssr for all Supabase Auth in Next.js. The deprecated @supabase/auth-helpers-nextjs package must not be used. Handle token refresh in middleware, not in client components. Validate sessions server-side with getUser(), never getSession().


Client Setup

Three distinct Supabase clients are required. Each serves a different runtime context.

1. Browser Client

Create lib/supabase/client.ts for Client Components:

import { createBrowserClient } from '@supabase/ssr'

export const createClient = () =>
  createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )

Use this client in 'use client' components only. It reads and writes cookies automatically via the browser.

2. Server Client

Create lib/supabase/server.ts for Server Components, Server Actions, and Route Handlers:

import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export const createClient = async () => {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) => {
            cookieStore.set(name, value, options)
          })
        },
      },
    }
  )
}

3. Middleware Client

Create lib/supabase/middleware.ts for the Next.js middleware layer:

import { createServerClient } from '@supabase/ssr'
import { type NextRequest, NextResponse } from 'next/server'

export const updateSession = async (request: NextRequest) => {
  const response = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => request.cookies.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) => {
            request.cookies.set(name, value)
            response.cookies.set(name, value, options)
          })
        },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()

  // Redirect unauthenticated users to login
  if (!user && !request.nextUrl.pathname.startsWith('/login') &&
      !request.nextUrl.pathname.startsWith('/auth')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  // Redirect authenticated users away from login
  if (user && request.nextUrl.pathname.startsWith('/login')) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return response
}

This client sets cookies on both the request and response objects. Setting on the request ensures downstream Server Components read the refreshed token. Setting on the response sends the updated cookie to the browser.


Auth Middleware

Create middleware.ts at the project root:

import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/middleware'

export async function middleware(request: NextRequest) {
  return await updateSession(request)
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'],
}

Middleware Responsibilities

  • Run on every request except static assets.
  • Refresh session tokens by calling getUser(), which triggers cookie renewal.
  • Redirect unauthenticated users to /login.
  • Redirect authenticated users away from /login to /dashboard.

Auth Callback Route

Create app/auth/callback/route.ts to handle OAuth and Magic Link redirects:

import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  const next = searchParams.get('next') ?? '/dashboard'

  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      return NextResponse.redirect(`${origin}${next}`)
    }
  }

  return NextResponse.redirect(`${origin}/login?error=auth_callback_failed`)
}

Sign In / Sign Up Patterns

Server Actions (Preferred)

Create app/login/actions.ts:

'use server'

import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export async function signIn(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')
}

export async function signUp(formData: FormData) {
  const supabase = await createClient()

  const { error } = await supabase.auth.signUp({
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  })

  if (error) redirect('/login?error=signup_failed')
  redirect('/login?message=check_email')
}

Always call redirect() after any auth state change in Server Actions. The redirect terminates the action and prevents stale state.

Client-Side OAuth

For OAuth providers, use the browser client:

'use client'

import { createClient } from '@/lib/supabase/client'

export function OAuthButton() {
  const handleLogin = async () => {
    const supabase = createClient()
    await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: { redirectTo: `${window.location.origin}/auth/callback` },
    })
  }

  return <button onClick={handleLogin}>Sign in with Google</button>
}

Protected Routes Pattern

Server Components

Protect pages by checking the user in Server Components:

import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export default async function DashboardPage() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) redirect('/login')

  return <div>Welcome, {user.email}</div>
}

Use getUser() which validates the JWT with the Supabase Auth server. Never use getSession() in Server Components -- it only reads the JWT locally without validation and can be spoofed.

Layout-Level Protection

Apply auth checks in a layout to protect an entire route group:

// app/(protected)/layout.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export default async function ProtectedLayout({ children }: { children: React.ReactNode }) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) redirect('/login')

  return <>{children}</>
}

Auth State in Client Components

Create an auth provider for client-side auth state:

'use client'

import { createContext, useContext, useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import type { User } from '@supabase/supabase-js'

const AuthContext = createContext<{ user: User | null }>({ user: null })

export function AuthProvider({ children, initialUser }: {
  children: React.ReactNode
  initialUser: User | null
}) {
  const [user, setUser] = useState<User | null>(initialUser)

  useEffect(() => {
    const supabase = createClient()
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => setUser(session?.user ?? null)
    )
    return () => subscription.unsubscribe()
  }, [])

  return <AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
}

export const useAuth = () => useContext(AuthContext)

Pass initialUser from a Server Component to avoid a loading flash. Wrap the app in root layout.


Anti-Patterns

Anti-PatternWhy It FailsCorrect Approach
getSession() for security checksReads JWT locally without validation; can be tamperedUse getUser() which validates with Supabase
Storing auth state in localStorageBypasses cookie-based session; breaks SSRLet @supabase/ssr manage cookies
Skipping token refresh in middlewareSessions expire silently; users get 401sAlways call getUser() in middleware
Using @supabase/auth-helpers-nextjsDeprecated; incompatible with App Router patternsUse @supabase/ssr
Auth in API Routes instead of Server ActionsAdds unnecessary API layer; loses RSC benefitsUse Server Actions for auth mutations
Client-side redirect after Server ActionCreates flash of unauthorized contentUse redirect() inside the Server Action

Role-Based Access Control

Combine Supabase Auth with RLS and user metadata to enforce role-based access:

// Server Action to check admin role
export async function requireAdmin() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) redirect('/login')
  if (user.app_metadata?.role !== 'admin') redirect('/unauthorized')

  return user
}

Set roles via Supabase Admin API or database triggers -- never from client-side code. Store roles in app_metadata (server-writable only), not user_metadata (client-writable).

RLS Policies with Auth

RLS policies reference the authenticated user automatically:

-- Allow users to read only their own data
CREATE POLICY "Users read own data" ON profiles
  FOR SELECT USING (auth.uid() = id);

-- Allow admins to read all data
CREATE POLICY "Admins read all" ON profiles
  FOR SELECT USING (
    (SELECT raw_app_meta_data->>'role' FROM auth.users WHERE id = auth.uid()) = 'admin'
  );

Password Reset Flow

Initiate Reset

'use server'

export async function resetPassword(formData: FormData) {
  const supabase = await createClient()

  const { error } = await supabase.auth.resetPasswordForEmail(
    formData.get('email') as string,
    { redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback?next=/reset-password` }
  )

  if (error) redirect('/login?error=reset_failed')
  redirect('/login?message=check_email_for_reset')
}

Handle Reset Callback

After the user clicks the email link, they arrive at the callback route with an auth code. The callback exchanges the code for a session, then redirects to the password update form:

// app/reset-password/page.tsx
export default async function ResetPasswordPage() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) redirect('/login')

  return <UpdatePasswordForm />
}

Environment Variables

Required in .env.local:

NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

Both are prefixed with NEXT_PUBLIC_ because the browser client needs them. The anon key is safe to expose -- RLS policies enforce security, not the key.


Quick Reference

ConcernLocationClient
Token refreshmiddleware.tsMiddleware client
OAuth/Magic Link callbackapp/auth/callback/route.tsServer client
Sign in/up mutationsServer ActionsServer client
OAuth initiationClient ComponentBrowser client
Route protection (SSR)Server Component or LayoutServer client
Real-time auth stateClient Component + ProviderBrowser client
Security validationAnywhere server-sidegetUser() only

Dependencies

SkillReason
nextjs-patternsApp Router architecture, Server Components, middleware, Server Actions, route groups, and layouts
supabase-patternsSupabase client initialization, Auth API surface, Row Level Security integration, and database access patterns
react-best-practices (transitive)Component architecture, hooks, context, and rendering patterns (via nextjs-patterns)
typescript-pro (transitive)Type safety, generics, and strict configuration (via nextjs-patterns, supabase-patterns)
postgres-best-practices (transitive)Database schema design and query patterns underlying RLS (via supabase-patterns)
tailwind-patterns (transitive)Styling patterns for auth UI components (via nextjs-patterns)

Used By

None -- this is a top-layer integration skill.

Keep looking

Skills are one crate of 628,971. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.