Next.js Authentication with Supabase and NextAuth: A Deep Dive (Part 2 of 3) | AI-Hub Blog | AI Hub
Tutorial
Next.js Authentication with Supabase and NextAuth: A Deep Dive (Part 2 of 3)
S
Sidharrth Mahadevan
AI Research Lead
February 27, 2025
7 min read
Generated by AI-Hub
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.
Next.js Authentication with Supabase and NextAuth: A Deep Dive (Part 2 of 3)
Welcome to Part 2 of our Next.js Authentication series! In Part 1, we set up the basics of NextAuth and Supabase — ensuring we had a minimal configuration and environment variables ready.
Now 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! 🚀
Deepen Your Knowledge
Continue exploring related insights and research in Tutorial.
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
mode: { label: 'Mode', type: 'text' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
try {
if (credentials.mode === 'signup') {
return await authHandlers.handleSignup(
credentials.email,
credentials.password
);
} else {
return await authHandlers.handleSignin(
credentials.email,
credentials.password
);
}
} catch (error: any) {
throw new Error(error.message);
}
},
}),
],
session: { strategy: 'jwt' },
callbacks: {
async jwt({ token, user }) {
// Runs after authorize() — store user data in the JWT
if (user) {
token.userId = user.id;
token.email = user.email;
token.lastUpdated = Date.now();
}
return token;
},
async session({ session, token }) {
// Runs whenever the session is checked — shape the client-facing session
if (token) {
session.user = {
...session.user,
id: token.userId as string,
};
}
return session;
},
},
pages: {
signIn: '/auth/signin',
error: '/auth/error',
},
secret: process.env.NEXTAUTH_SECRET,
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
How the Flow Works
Frontend form → signIn('credentials', { email, password, mode: 'signup' })
→ NextAuth sees mode='signup'
→ calls handleSignup in route.ts
→ Supabase creates user
→ JWT callback stores userId in token
→ Session callback exposes userId to client
3. Understanding the Callbacks
jwt Callback
Runs 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.
session Callback
Runs 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.