Authentication

Last updated on 2026-08-30

The kit uses Supabase Auth for all authentication, with three client types for different contexts and middleware for route protection.

Auth Flow Overview

User signs up → Supabase creates auth.users row
             → Trigger creates profiles row
             → User is redirected to /account

User logs in → Cookie-based session created via @supabase/ssr
            → Middleware validates session on protected routes
            → Admin routes check app_metadata.role

Supabase Client Types

The kit uses three Supabase clients, each for a different context:

Browser Client (lib/supabase/client.ts)

Used in client components for real-time subscriptions and client-side auth:

import { createBrowserClient } from "@supabase/ssr"

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

Server Client (lib/supabase/server.ts)

Used in Server Components and Server Actions. Reads cookies for the current session:

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

This client respects RLS policies — queries return only data the authenticated user is allowed to see.

Admin Client (lib/supabase/admin.ts)

Uses the service role key to bypass RLS. Used only in Server Actions for admin operations:

import { createClient } from "@supabase/supabase-js"

export const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

Security: The service role key is never exposed to the client. It only runs in Server Actions and API routes.

Auth Pages

Login (/login)

  • Email and password sign-in via supabase.auth.signInWithPassword()
  • Google OAuth via supabase.auth.signInWithOAuth({ provider: 'google' })
  • GitHub OAuth via supabase.auth.signInWithOAuth({ provider: 'github' })
  • Error messages for invalid credentials
  • Redirects to /account on success

Register (/register)

  • Email, password, and full name
  • Calls supabase.auth.signUp() with user_metadata: { full_name }
  • Shows "Check your email" confirmation message
  • Database trigger creates the profile automatically

Forgot Password (/forgot-password)

  • Calls supabase.auth.resetPasswordForEmail()
  • Shows success state after sending

OAuth Callback (/auth/callback)

Handles the OAuth code exchange after social login:

// Exchanges the code for a session
const { searchParams } = new URL(request.url)
const code = searchParams.get("code")
const supabase = await createClient()
await supabase.auth.exchangeCodeForSession(code)

Email Confirmation (/auth/confirm)

Handles email verification links for new signups.

Middleware

The middleware.ts file at the project root handles:

  1. Session refresh — updates the auth cookie on every request
  2. Route protection — redirects unauthenticated users from /account/*
  3. Admin protection — redirects non-admin users from /admin/*
  4. Auth redirect — redirects authenticated users away from /login and /register
// Simplified middleware logic
if (pathname.startsWith("/account") && !user) {
  redirect("/login")
}
if (pathname.startsWith("/admin") && !isAdmin) {
  redirect("/")
}
if ((pathname === "/login" || pathname === "/register") && user) {
  redirect("/account")
}

Admin Access

Admin access is controlled via app_metadata.role:

  1. The seed data creates an admin user: admin@example.com / password123
  2. For your own admin, set app_metadata in the Supabase Dashboard:
    • Go to Authentication > Users
    • Click on the user
    • Edit app_metadata to include {"role": "admin"}

The is_admin() database function checks this value in RLS policies:

SELECT auth.jwt() -> 'app_metadata' ->> 'role' = 'admin'

useUser Hook

The hooks/use-user.ts hook provides the current user in client components:

const { user, loading } = useUser()

It listens for auth state changes and updates automatically on login/logout.

Session Management

  • Sessions are stored in HTTP-only cookies via @supabase/ssr
  • The middleware refreshes sessions on every request
  • Sessions expire based on your Supabase project settings (default: 1 hour, with refresh)
  • Logout calls supabase.auth.signOut() and clears the cookie