{"post":{"id":"83","slug":"nextjs-auth-supabase-nextauth-part-2","title":"Next.js Authentication with Supabase and NextAuth: A Deep Dive (Part 2 of 3)","excerpt":"Part 2 of the series dives into the real implementation — full sign-in and sign-up flows powered by Supabase, JWT session callbacks, route protection, and proper error handling with NextAuth.js.","content":"\r\n# Next.js Authentication with Supabase and NextAuth: A Deep Dive (Part 2 of 3)\r\n\r\n> **By Sidharrth Mahadevan** | Published: February 27, 2025\r\n> [Original Article on Medium](https://medium.com/@sidharrthnix/next-js-authentication-with-supabase-and-nextauth-a-deep-dive-part-2-5fa43563989a)\r\n\r\n---\r\n\r\n## Introduction\r\n\r\nWelcome to **Part 2** of our Next.js Authentication series! In [Part 1](https://medium.com/@sidharrthnix/next-js-authentication-with-supabase-and-nextauth-js-part-1-of-3-76dc97d3a345), we set up the basics of NextAuth and Supabase — ensuring we had a minimal configuration and environment variables ready.\r\n\r\nNow we go further: implementing **real sign-in and sign-up flows**, wiring up Supabase under the hood, managing sessions with JWT callbacks, and protecting routes. By the end, you'll see exactly how the frontend interacts with NextAuth and how Supabase does the heavy lifting. Ready? Let's do this! 🚀\r\n\r\n---\r\n\r\n## Series Overview\r\n\r\n- **Part 1**: Basic setup, minimal NextAuth config, and project structure\r\n- **Part 2**: Full sign-in/sign-up implementation, session management, and protecting routes *(You are here! 👈)*\r\n- **Part 3**: Advanced features (password reset, profile updates) and extra security tips\r\n\r\n---\r\n\r\n## Key Players\r\n\r\nBefore diving into code, here's a quick summary of the roles each piece plays:\r\n\r\n- **NextAuth & CredentialsProvider** — Main auth library + provider that handles email/password logic.\r\n- **`supabase`** — The client for your Supabase instance, where users are actually created and validated.\r\n- **`CustomUser` & `CustomSession`** — TypeScript interfaces giving structure to user data and session shape.\r\n\r\n---\r\n\r\n## 1. Implementing the Auth Handlers\r\n\r\nIn your `app/api/auth/[...nextauth]/route.ts`, define the actual sign-up and sign-in logic:\r\n\r\n```ts\r\nimport NextAuth, { NextAuthOptions } from 'next-auth';\r\nimport CredentialsProvider from 'next-auth/providers/credentials';\r\nimport { supabase } from '@/lib/supabase';\r\n\r\n// Custom TypeScript interfaces\r\ninterface CustomUser {\r\n  id: string;\r\n  email: string;\r\n}\r\n\r\ninterface CustomSession {\r\n  user: CustomUser;\r\n  accessToken?: string;\r\n}\r\n\r\n// Auth handler functions\r\nconst authHandlers = {\r\n  async handleSignup(email: string, password: string) {\r\n    const { data, error } = await supabase.auth.signUp({\r\n      email,\r\n      password,\r\n      options: {\r\n        emailRedirectTo: `${process.env.NEXTAUTH_URL}`,\r\n      },\r\n    });\r\n\r\n    if (error) {\r\n      console.error('[AUTH] Signup error:', error);\r\n      throw new Error(error.message);\r\n    }\r\n\r\n    if (!data.user?.id) {\r\n      throw new Error('Signup successful. Please check your email to confirm.');\r\n    }\r\n\r\n    return { id: data.user.id, email: data.user.email! };\r\n  },\r\n\r\n  async handleSignin(email: string, password: string) {\r\n    const { data, error } = await supabase.auth.signInWithPassword({\r\n      email,\r\n      password,\r\n    });\r\n\r\n    if (error) {\r\n      console.error('[AUTH] Signin error:', error);\r\n      throw new Error(error.message);\r\n    }\r\n\r\n    return { id: data.user.id, email: data.user.email! };\r\n  },\r\n};\r\n```\r\n\r\n---\r\n\r\n## 2. The Full NextAuth Configuration\r\n\r\n```ts\r\nexport const authOptions: NextAuthOptions = {\r\n  providers: [\r\n    CredentialsProvider({\r\n      credentials: {\r\n        email:    { label: 'Email',    type: 'email'    },\r\n        password: { label: 'Password', type: 'password' },\r\n        mode:     { label: 'Mode',     type: 'text'     },\r\n      },\r\n      async authorize(credentials) {\r\n        if (!credentials?.email || !credentials?.password) return null;\r\n\r\n        try {\r\n          if (credentials.mode === 'signup') {\r\n            return await authHandlers.handleSignup(\r\n              credentials.email,\r\n              credentials.password\r\n            );\r\n          } else {\r\n            return await authHandlers.handleSignin(\r\n              credentials.email,\r\n              credentials.password\r\n            );\r\n          }\r\n        } catch (error: any) {\r\n          throw new Error(error.message);\r\n        }\r\n      },\r\n    }),\r\n  ],\r\n\r\n  session: { strategy: 'jwt' },\r\n\r\n  callbacks: {\r\n    async jwt({ token, user }) {\r\n      // Runs after authorize() — store user data in the JWT\r\n      if (user) {\r\n        token.userId = user.id;\r\n        token.email  = user.email;\r\n        token.lastUpdated = Date.now();\r\n      }\r\n      return token;\r\n    },\r\n\r\n    async session({ session, token }) {\r\n      // Runs whenever the session is checked — shape the client-facing session\r\n      if (token) {\r\n        session.user = {\r\n          ...session.user,\r\n          id: token.userId as string,\r\n        };\r\n      }\r\n      return session;\r\n    },\r\n  },\r\n\r\n  pages: {\r\n    signIn: '/auth/signin',\r\n    error:  '/auth/error',\r\n  },\r\n\r\n  secret: process.env.NEXTAUTH_SECRET,\r\n};\r\n\r\nconst handler = NextAuth(authOptions);\r\nexport { handler as GET, handler as POST };\r\n```\r\n\r\n### How the Flow Works\r\n\r\n```\r\nFrontend form → signIn('credentials', { email, password, mode: 'signup' })\r\n  → NextAuth sees mode='signup'\r\n    → calls handleSignup in route.ts\r\n      → Supabase creates user\r\n        → JWT callback stores userId in token\r\n          → Session callback exposes userId to client\r\n```\r\n\r\n---\r\n\r\n## 3. Understanding the Callbacks\r\n\r\n### `jwt` Callback\r\nRuns right after `authorize()` succeeds. Use it to store data in the JWT token — such as `userId`, `email`, and a `lastUpdated` timestamp. By storing this in the JWT you avoid querying the database on every request.\r\n\r\n### `session` Callback\r\nRuns whenever the session is checked — for instance, when you call `useSession()` in a React component or `getServerSession()` on the server. It shapes the session object the client sees, pulling info like `userId` out of the token and inserting it into `session.user`.\r\n\r\n---\r\n\r\n## 4. The Sign-In Page\r\n\r\n```tsx\r\n// app/auth/signin/page.tsx\r\n'use client';\r\n\r\nimport { signIn } from 'next-auth/react';\r\nimport { useState } from 'react';\r\nimport { useRouter } from 'next/navigation';\r\n\r\nexport default function SignInPage() {\r\n  const router = useRouter();\r\n  const [email, setEmail]       = useState('');\r\n  const [password, setPassword] = useState('');\r\n  const [mode, setMode]         = useState<'signin' | 'signup'>('signin');\r\n  const [error, setError]       = useState('');\r\n\r\n  const handleSubmit = async (e: React.FormEvent) => {\r\n    e.preventDefault();\r\n    setError('');\r\n\r\n    const result = await signIn('credentials', {\r\n      email,\r\n      password,\r\n      mode,\r\n      redirect: false,\r\n    });\r\n\r\n    if (result?.error) {\r\n      setError(result.error);\r\n    } else {\r\n      router.push('/dashboard');\r\n    }\r\n  };\r\n\r\n  return (\r\n    <form onSubmit={handleSubmit}>\r\n      <input\r\n        type=\"email\"\r\n        value={email}\r\n        onChange={(e) => setEmail(e.target.value)}\r\n        placeholder=\"Email\"\r\n        required\r\n      />\r\n      <input\r\n        type=\"password\"\r\n        value={password}\r\n        onChange={(e) => setPassword(e.target.value)}\r\n        placeholder=\"Password\"\r\n        required\r\n      />\r\n      <button type=\"submit\">\r\n        {mode === 'signin' ? 'Sign In' : 'Sign Up'}\r\n      </button>\r\n      <button type=\"button\" onClick={() => setMode(mode === 'signin' ? 'signup' : 'signin')}>\r\n        Switch to {mode === 'signin' ? 'Sign Up' : 'Sign In'}\r\n      </button>\r\n      {error && <p style={{ color: 'red' }}>{error}</p>}\r\n    </form>\r\n  );\r\n}\r\n```\r\n\r\n---\r\n\r\n## 5. Protecting Routes\r\n\r\nUse `getServerSession` in Server Components or API routes to protect pages:\r\n\r\n```ts\r\nimport { getServerSession } from 'next-auth';\r\nimport { authOptions } from '@/app/api/auth/[...nextauth]/route';\r\nimport { redirect } from 'next/navigation';\r\n\r\nexport default async function DashboardPage() {\r\n  const session = await getServerSession(authOptions);\r\n\r\n  if (!session) {\r\n    redirect('/auth/signin');\r\n  }\r\n\r\n  return <h1>Welcome, {session.user?.email}</h1>;\r\n}\r\n```\r\n\r\n---\r\n\r\n## 6. Sign-Out\r\n\r\n```tsx\r\n'use client';\r\nimport { signOut } from 'next-auth/react';\r\n\r\nexport function SignOutButton() {\r\n  return (\r\n    <button onClick={() => signOut({ callbackUrl: '/' })}>\r\n      Sign Out\r\n    </button>\r\n  );\r\n}\r\n```\r\n\r\n---\r\n\r\n## 🔜 What's Next?\r\n\r\nIn **Part 3**, we'll tackle:\r\n\r\n- **Password reset flows** using Supabase email links.\r\n- **Email verification** and confirmation handling.\r\n- **Additional security tips** — rate limiting, token expiry, and more.\r\n\r\n---\r\n\r\n## 😊 Conclusion\r\n\r\nYou now have a complete, working authentication system:\r\n\r\n- **Sign-up** — Creates a new user in Supabase via `supabase.auth.signUp`.\r\n- **Sign-in** — Validates credentials via `supabase.auth.signInWithPassword`.\r\n- **Session management** — JWT callbacks store and expose `userId` cleanly.\r\n- **Route protection** — `getServerSession` guards your private pages.\r\n\r\nStay tuned for Part 3 — the final piece of the puzzle!\r\n\r\n---\r\n\r\n## Resources\r\n\r\n- [NextAuth.js Official Docs](https://next-auth.js.org/getting-started/introduction)\r\n- [Supabase Auth Docs](https://supabase.com/docs/guides/auth/server-side/nextjs)\r\n  ","category":"Tutorial","author":"Sidharrth Mahadevan","published_at":"2025-02-27T00:00:00.000Z","read_time":"7 min read","image_url":"https://miro.medium.com/v2/resize:fit:1100/format:webp/1*zS0XAPP2WSvVAjfoiaojOw.png","tags":["Next.js","Authentication","Supabase","NextAuth","JavaScript"],"featured":false,"created_at":"2026-04-01T07:09:20.224Z","image_attribution":"Generated by AI-Hub"}}