How to Integrate Supabase Auth with Next.js (JavaScript Only)


Step 1: Create useAuth hook
// src/hooks/useAuth.js
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import supabase from '@/libs/supabase/client'
const AuthContext = createContext({
session: null,
user: null,
loading: true,
signUp: async () => { },
signIn: async () => { },
signInWithGoogle: async () => { },
signOut: async () => { },
forgotPassword: async () => { }
});
export const AuthProvider = ({ children }) => {
const [session, setSession] = useState(null);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchSession = async () => {
const { data: { session }, error } = await supabase.auth.getSession();
if (error) {
console.error('Error fetching session:', error);
} else {
setSession(session);
setUser(session?.user ?? null);
}
setLoading(false);
};
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (_event, session) => {
setSession(session);
setUser(session?.user ?? null);
setLoading(false);
}
);
fetchSession();
return () => {
subscription?.unsubscribe();
};
}, []);
const signUp = async (email, password) => {
const { data, error } = await supabase.auth.signUp({
email,
password,
});
if (error) throw error;
return data;
};
const signIn = async (email, password) => {
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) throw new Error(error.message);
};
const signInWithGoogle = async () => {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: `${window.location.origin}/auth/callback` }
});
if (error) throw error;
};
const signOut = async () => {
const { error } = await supabase.auth.signOut();
if (error) throw error;
window.location.href = '/';
};
const forgotPassword = async (email) => {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/auth/reset-password`,
});
if (error) throw error;
};
return (
<AuthContext.Provider
value={{
session,
user,
loading,
signUp,
signIn,
signInWithGoogle,
signOut,
forgotPassword
}}
>
{!loading && children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within an AuthProvider');
return context;
};
Step 2: Provide AuthProvider to the layout.js and create supabase client
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { AuthProvider } from '@/hooks/useAuth'
import Navbar from "@/components/Navbar";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}
// src/libs/supabase/client.js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, {
auth: {
persistSession: true,
autoRefreshToken: true,
detectSessionInUrl: true,
},
});
export default supabase;
// make sure in main_directory/.env.local
// NEXT_PUBLIC_SUPABASE_URL=https://your_supabase_url.supabase.co
// NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
// refer: https://supabase.com/dashboard/project/settings/api
Step 3 : Create authentication page for signup/login
//app/auth/page.jsx
'use client';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import dynamic from 'next/dynamic';
const LoginForm = dynamic(() => import('@/components/authentication/LoginForm'), { ssr: false });
const SignUpForm = dynamic(() => import('@/components/authentication/SignUpForm'), { ssr: false });
export default function AuthPage() {
const { user, loading } = useAuth();
const router = useRouter();
const searchParams = useSearchParams();
const initialMode = searchParams.get('status') === 'signup' ? 'signup' : 'login';
const [authMode, setAuthMode] = useState(initialMode);
useEffect(() => {
if (!loading && user) {
router.push('/');
}
}, [user, loading, router]);
const handleModeChange = (mode) => {
setAuthMode(mode);
const newUrl = `/auth?status=${mode}`;
router.push(newUrl);
};
return (
<div className="min-h-screen flex flex-col justify-center items-center px-4 bg-white">
<div className="bg-white rounded-xl shadow-md p-6 w-full max-w-md">
<h2 className="text-center text-2xl font-bold mb-4">
{authMode === 'login' ? 'Login to Your App Name' : 'Sign Up for Your App Name'}
</h2>
<div className="flex justify-center space-x-4 mb-6">
<button
onClick={() => handleModeChange('login')}
className={`px-4 py-2 rounded-md text-sm font-medium ${authMode === 'login' ? 'bg-orange-500 text-white' : 'bg-gray-200 text-gray-700'
}`}
>
Login
</button>
<button
onClick={() => handleModeChange('signup')}
className={`px-4 py-2 rounded-md text-sm font-medium ${authMode === 'signup' ? 'bg-orange-500 text-white' : 'bg-gray-200 text-gray-700'
}`}
>
Sign Up
</button>
</div>
{/* Login or Signup Form */}
<div>
{authMode === 'login' ? (
<>
<LoginForm />
</>
) : (
<SignUpForm />
)}
</div>
</div>
</div>
);
}
Step 4: Create Authentication sub components
Create Account Component
// src/components/authentication/SignUpForm.js
'use client'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { Mail, Lock, Loader2, Eye, EyeOff } from 'lucide-react'
import { useAuth } from '@/hooks/useAuth'
export default function SignUpForm() {
const [loading, setLoading] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const { signUp } = useAuth()
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm()
const onSubmit = async (data) => {
try {
setLoading(true)
await signUp(data.email, data.password) // Removed username
alert('Account created successfully')
reset()
} catch (error) {
alert(error.message || 'Something went wrong')
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 font-sans max-w-md mx-auto">
{/* Email */}
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium">Email</label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
id="email"
type="email"
placeholder="Enter your email"
className="w-full pl-10 pr-3 py-2 border rounded-md focus:outline-none focus:ring"
/>
</div>
{errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
</div>
{/* Password */}
<div className="space-y-2">
<label htmlFor="password" className="text-sm font-medium">Password</label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
{...register('password', {
required: 'Password is required',
minLength: {
value: 8,
message: 'Password must be at least 8 characters',
},
})}
id="password"
type={showPassword ? 'text' : 'password'}
placeholder="Create a password"
className="w-full pl-10 pr-10 py-2 border rounded-md focus:outline-none focus:ring"
/>
<button
type="button"
onClick={() => setShowPassword((prev) => !prev)}
className="absolute right-3 top-2.5 text-gray-500"
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
{errors.password && <p className="text-sm text-red-500">{errors.password.message}</p>}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white py-2 rounded-md flex items-center justify-center hover:bg-blue-700 transition"
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Create Account
</button>
</form>
)
}
Login Form Component
// src/components/authentication/LoginForm.js
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { Mail, Lock, Eye, EyeOff, Loader2 } from 'lucide-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import supabase from '@/libs/supabase/client';
const ForgotPasswordForm = dynamic(() => import('@/components/authentication/ForgotPasswordPage'), {
ssr: false,
});
export default function LoginForm() {
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showForgot, setShowForgot] = useState(false);
const router = useRouter();
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = async (data) => {
try {
setLoading(true);
const { error } = await supabase.auth.signInWithPassword({
email: data.email,
password: data.password,
});
if (error) {
alert('Login failed: ' + error.message);
} else {
alert('Login successful!');
router.push('/myprofile');
}
} catch (err) {
alert('Login Failed');
} finally {
setLoading(false);
}
};
const signInWithGoogle = async () => {
const { error } = await supabase.auth.signInWithOAuth({ provider: 'google' });
if (!error) router.push('/myprofile');
if (error) alert('Google Sign-in failed: ' + error.message);
};
return (
<>
<div className='space-y-6 max-w-md mx-auto p-6 border rounded-md shadow-md bg-white'>
<form
onSubmit={handleSubmit(onSubmit)}
className=""
>
<div className="space-y-1">
<label htmlFor="email" className="block text-sm font-medium">Email</label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
type="email"
id="email"
placeholder="Enter your email"
className="w-full pl-10 pr-3 py-2 border rounded focus:outline-none focus:ring-2 text-black"
/>
</div>
{errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
</div>
<div className="space-y-1">
<label htmlFor="password" className="block text-sm font-medium">Password</label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
{...register('password', { required: 'Password is required' })}
type={showPassword ? 'text' : 'password'}
id="password"
placeholder="Enter your password"
className="w-full pl-10 pr-10 py-2 border rounded focus:outline-none focus:ring-2 text-black"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-2.5"
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
{errors.password && <p className="text-sm text-red-500">{errors.password.message}</p>}
</div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center items-center gap-2 px-4 py-2 bg-blue-500 text-black rounded hover:bg-blue-600 transition"
>
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
Login
</button>
<div className="relative my-4">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white rounded-3xl px-2 text-gray-500">Or continue with</span>
</div>
</div>
<button
type="button"
onClick={signInWithGoogle}
disabled={loading}
className="w-full flex items-center justify-center gap-2 border px-4 py-2 rounded-2xl"
>
<img
src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg"
alt="Google"
className="h-5 w-5"
/>
<span className="text-sm font-medium">Continue with Google</span>
</button>
</form>
<div className="mt-4 text-center">
<div
type="button"
onClick={() => setShowForgot((prev) => !prev)}
className="text-sm text-blue-600 hover:underline"
>
<ForgotPasswordForm />
</div>
</div>
</div>
</>
);
}
Forgot Password Component
// src/components/authentication/ForgotPasswordPage.js
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth } from '@/hooks/useAuth';
export default function ForgotPasswordPage() {
const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [message, setMessage] = useState(null);
const { forgotPassword } = useAuth();
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm();
const onSubmit = async (data) => {
setLoading(true);
setMessage(null);
try {
await forgotPassword(data.email);
alert('✅ Password reset instructions sent!');
setIsOpen(false);
reset();
} catch (err) {
setMessage('❌ ' + (err.message || 'Something went wrong.'));
} finally {
setLoading(false);
}
};
return (
<div className="w-full">
<button
onClick={() => setIsOpen(!isOpen)}
className="text-sm text-black bg-cyan-200 px-4 py-1 rounded-full hover:bg-cyan-300"
>
Forgot password?
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.2 }}
className="mt-2 p-4 bg-gray-50 rounded-md border border-gray-200 shadow-sm"
>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-3">
<div>
<input
id="email"
type="email"
placeholder="Enter your email"
className="w-full px-3 py-2 border rounded-md text-sm"
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email',
},
})}
/>
{errors.email && (
<p className="text-xs text-red-500 mt-1">
{errors.email.message}
</p>
)}
</div>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => {
setIsOpen(false);
reset();
}}
className="text-xs px-3 py-1 bg-gray-200 rounded-md"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="text-xs px-3 py-1 bg-blue-600 text-white rounded-md"
>
{loading ? 'Sending...' : 'Send'}
</button>
</div>
{message && (
<p className="text-xs text-center mt-1">{message}</p>
)}
</form>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Step 6: Create Reset Password Page
// app/auth/reset-password/page.jsx
"use client"
import { useState, useEffect } from 'react'
import { Eye, EyeOff, Loader2 } from 'lucide-react'
import { useAuth } from '@/hooks/useAuth'
import supabase from '@/libs/supabase/client'
export default function ResetPasswordPage() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
const { resetPassword } = useAuth()
useEffect(() => {
supabase.auth.getUser().then(({ data, error }) => {
if (error || !data.user) {
setError('Invalid or expired reset link')
} else {
setError(null)
}
setLoading(false)
})
}, [])
const handleSubmit = async (e) => {
e.preventDefault()
if (password !== confirmPassword) {
alert('Passwords do not match')
return
}
try {
await supabase.auth.updateUser({ password })
alert({
title: 'Password Updated',
description: 'You can now log in',
})
window.location.href = '/auth'
} catch (error) {
alert({
variant: 'destructive',
title: 'Reset Failed',
description: error.message,
})
}
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Loader2 className="h-12 w-12 animate-spin text-orange-500" />
</div>
)
}
if (error) {
return (
<div className="min-h-screen flex flex-col items-center justify-center p-4 text-center">
<h1 className="text-2xl font-bold text-red-500 mb-4">Error</h1>
<p className="text-gray-600">{error}</p>
<button
onClick={() => (window.location.href = '/auth')}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
>
Request New Reset Link
</button>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-gray-100">
<form onSubmit={handleSubmit} className="w-full max-w-md bg-white p-6 rounded shadow space-y-6">
<div className="text-center">
<h1 className="text-3xl font-bold">Reset Password</h1>
<p className="text-gray-600">Enter your new password</p>
</div>
{[{
label: 'New Password', id: 'password', value: password, onChange: setPassword, show: showPassword, toggle: setShowPassword
}, {
label: 'Confirm Password', id: 'confirm', value: confirmPassword, onChange: setConfirmPassword, show: showConfirm, toggle: setShowConfirm
}].map(({ label, id, value, onChange, show, toggle }) => (
<div key={id}>
<label htmlFor={id} className="block font-medium mb-1">{label}</label>
<div className="relative">
<input
id={id}
type={show ? 'text' : 'password'}
required
minLength={6}
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full border px-3 py-2 rounded pr-10"
/>
<button
type="button"
onClick={() => toggle(!show)}
className="absolute right-3 top-2.5 text-gray-500"
aria-label="Toggle visibility"
>
{show ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
))}
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 rounded font-semibold"
>
Reset Password
</button>
</form>
</div>
)
}
Step 7 : Create Callback for Auth Flow
// app/auth/callback/page.jsx
'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import supabase from '@/libs/supabase/client'
import { Loader2 } from 'lucide-react'
export default function AuthCallback() {
const router = useRouter()
useEffect(() => {
const handleRedirect = async () => {
const hash = window.location.hash
const params = new URLSearchParams(hash.slice(1))
const type = params.get('type')
const access_token = params.get('access_token')
if (type !== 'recovery' || !access_token) {
router.push('/auth?error=Invalid or expired link')
return
}
// Let Supabase auto-detect session from URL
await supabase.auth.getSession()
router.push('/auth/reset-password')
}
handleRedirect()
}, [router])
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<Loader2 className="h-12 w-12 animate-spin text-orange-500" />
</div>
)
}
Step 8 : My Profile page
// app/myprofile/page.js
'use client'
import { useAuth } from '@/hooks/useAuth';
import Image from 'next/image';
import { useState } from 'react';
const ProfilePage = () => {
const { user, signOut } = useAuth();
if (!user) return <p>Loading user data...</p>;
const email = user.email;
const avatar = user.user_metadata?.avatar_url || '/default-avatar.png';
const fullName = user.user_metadata?.full_name || 'Anonymous';
const createdAt = new Date(user.created_at).toLocaleDateString();
const handleLogout = async () => {
try {
await signOut();
} catch (err) {
alert('Failed to logout!');
}
};
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-100 p-6">
<div className="bg-white rounded-2xl shadow-xl p-6 max-w-sm w-full text-center">
<Image
src={avatar}
alt="Profile Picture"
width={100}
height={100}
className="rounded-full mx-auto mb-4"
/>
<h2 className="text-2xl font-bold mb-1">{fullName}</h2>
<p className="text-gray-600 mb-1">{email}</p>
<p className="text-sm text-gray-500 mb-4">Member since: {createdAt}</p>
<button
onClick={handleLogout}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition"
>
Logout
</button>
</div>
</div>
);
};
export default ProfilePage;
In this post, I walk you through a complete authentication flow in Next.js using JavaScript—including login, signup, forgot password, reset password, and Google provider integration. I’ve shared detailed implementation steps to help you build a secure and user-friendly auth system. Hope you find it useful and enjoyable!
Subscribe to my newsletter
Read articles from Dynamic Phillic directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
