Every time I started a new project, auth killed my momentum.
Setting up JWT, hashing passwords, managing sessions, handling refresh tokens... before writing a single line of actual product code I'd already wasted 2 days.
Then I found Supabase Auth. I had a fully working login system in my Next.js app in 10 minutes flat. Here's exactly how.
Why auth is such a pain normally
If you've ever built auth from scratch you know the drill:
- Install bcrypt, hash every password manually
- Set up JWT, handle token expiry, refresh tokens
- Store sessions, manage cookies securely
- Handle edge cases like "what if the token expires mid-session"
- Pray nothing is vulnerable
It's not that it's impossible. It's that it's boring, repetitive work that has nothing to do with your actual product.
Supabase handles ALL of that for you. You just call a function. That's it.
What we're building
- Email/password authentication
- Protected routes
- Session management
- Sign in, Sign up, Sign out
Prerequisites
- Next.js app set up
- Supabase account (free at supabase.com)
- Basic knowledge of React
Step 1 — Create a Supabase project
- Go to supabase.com
- Click New Project
- Fill in your project name and database password
- Wait for it to spin up (~2 minutes)
Once ready, go to Settings → API and copy:
Project URL-
anon publickey
Step 2 — Install Supabase in your Next.js app
npm install @supabase/supabase-js @supabase/ssr
Step 3 — Set up environment variables
Create a .env.local file in your root:
NEXT_PUBLIC_SUPABASE_URL=your_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
Don't commit this to GitHub. Make sure .env.local is in your .gitignore — if it's not, add it now.
Step 4 — Create the Supabase client
Create lib/supabase.js:
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)
}
I keep this in a lib folder so I can import it anywhere in the app without repeating myself.
Step 5 — Sign up page
Create app/signup/page.jsx:
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase'
import { useRouter } from 'next/navigation'
export default function SignUp() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const router = useRouter()
const supabase = createClient()
const handleSignUp = async () => {
const { error } = await supabase.auth.signUp({ email, password })
if (error) {
setError(error.message)
} else {
router.push('/dashboard')
}
}
return (
<div>
<h1>Create account</h1>
<input
type="email"
placeholder="your@email.com"
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="password"
onChange={(e) => setPassword(e.target.value)}
/>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button onClick={handleSignUp}>Sign Up</button>
</div>
)
}
Nothing fancy. Grab email and password, pass it to Supabase, redirect to dashboard. Done.
Step 6 — Sign in page
Create app/signin/page.jsx:
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase'
import { useRouter } from 'next/navigation'
export default function SignIn() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const router = useRouter()
const supabase = createClient()
const handleSignIn = async () => {
const { error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) {
setError(error.message)
} else {
router.push('/dashboard')
}
}
return (
<div>
<h1>Welcome back</h1>
<input
type="email"
placeholder="your@email.com"
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="password"
onChange={(e) => setPassword(e.target.value)}
/>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button onClick={handleSignIn}>Sign In</button>
</div>
)
}
Quick note — make sure you use
signInWithPasswordnotsignIn. I spent 20 minutes confused the first time because of this 😅 Supabase has multiple sign in methods so you have to be specific.
Step 7 — Protect your dashboard
This is the part most tutorials skip completely. What's the point of auth if anyone can just go to /dashboard directly in the URL bar?
Create app/dashboard/page.jsx:
import { createClient } from '@/lib/supabase'
import { redirect } from 'next/navigation'
export default async function Dashboard() {
const supabase = createClient()
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
redirect('/signin')
}
return (
<div>
<h1>Hey {session.user.email} 👋</h1>
<p>You're in.</p>
</div>
)
}
No session? Get out. Simple as that.
Step 8 — Sign out
Add this wherever makes sense in your app:
const supabase = createClient()
const handleSignOut = async () => {
await supabase.auth.signOut()
router.push('/signin')
}
<button onClick={handleSignOut}>Sign out</button>
You just saved yourself 2 days
Most developers waste hours on auth before writing a single feature. You just skipped all of that in 10 minutes.
Your users can now sign up, log in, and access protected pages. That's a real app. Now go build the actual product.
In under 10 minutes you now have:
- ✅ Email/password sign up
- ✅ Sign in with session management
- ✅ Protected dashboard route
- ✅ Sign out functionality
The bigger picture
Auth is just the beginning of what Supabase can do. Once your users can log in you've got a real app — now you can start building the actual product.
If you're building a SaaS product and feeling stuck, I write about real problems I run into while building with Next.js and Supabase.
I'm Krishna — full stack dev from Kathmandu, Nepal. I've been building SaaS products and helping clients turn ideas into real web apps.
Got a project in mind? Let's talk → krishna011.com.np
Top comments (0)