Stop Building Login Systems from Scratch!
If you've ever built a member registration/login system from scratch using Node.js in a full-stack project, you know how painful it can be.
You need to salt passwords with bcrypt hashing, issue JWT tokens, handle token expiration and refresh tokens, implement "forgot password" email logic... not to mention the headache of OAuth (Google/GitHub login).
And if you make one mistake in any step, hackers could potentially steal your entire member database.
In modern full-stack development of 2026, professional engineers don't reinvent the wheel. We use BASS (Backend as a Service) solutions. And Supabase is the chosen brain for our Vibe Tutor platform.
Why Choose Supabase?
Many compare it to Firebase. While Firebase is great, its underlying NoSQL database isn't ideal for e-commerce/knowledge payment systems like ours that require strict relationships between "members" and "multiple orders." A relational database (RDBMS) is undoubtedly the safer choice.
Supabase is built on PostgreSQL, the world's most powerful open-source relational database. It not only provides lightning-fast APIs but also comes with a flawless built-in Auth system.
Implementing Supabase Auth in Next.js 15
With Next.js's App Router architecture splitting environments into Server and Client components, we need to prepare two types of Supabase Clients.
This is all configured in our project's src/utils/supabase/ folder.
1. client.ts (For Frontend Browser)
Used primarily in components with "use client", like your login form component.
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
2. server.ts (For Backend Server)
Used in Server Components or API Routes to authenticate users on the backend. It handles cookie reading/writing.
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options)
})
} catch (error) {
// In Server Components, sometimes we can't set cookies directly
}
},
},
}
)
}
Email Magic Link Login in Action
On Vibe Tutor's login page (src/app/login/page.tsx), we implement the currently most popular frictionless Magic Link login method.
Users don't need to set or remember passwords. They just enter their email, and the system sends them a secure link to click for instant access.
Check out this core login logic:
"use client";
import { createClient } from "@/utils/supabase/client";
const handleLogin = async (e) => {
e.preventDefault();
const supabase = createClient();
// Call Supabase's signInWithOtp to send magic link
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
// Redirect to member dashboard after login
emailRedirectTo: `${location.origin}/auth/callback?next=/dashboard`,
},
});
if (error) {
alert("Failed to send email. Please try again later.");
} else {
alert("Magic login link sent to your email! Please check your inbox.");
}
};
It's that simple! We don't need to configure SMTP servers for sending emailsโSupabase handles everything internally.
Auth Callback Handling Mechanism
When users click the magic link in their email (which looks like https://your-site.com/auth/callback?code=xxx), we intercept it in the API Route at src/app/auth/callback/route.ts to exchange the URL's code for real login credentials (Session Cookie).
import { NextResponse } from 'next/server'
import { createClient } from '@/utils/supabase/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/dashboard' // Default redirect to member center
if (code) {
const supabase = await createClient()
// Exchange code for user session
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
}
}
// Handle failed verification
return NextResponse.redirect(`${origin}/login?error=InvalidToken`)
}
After this exchange completes, a secure HttpOnly cookie is written to the browser.
From then on, when users browse any page, we can instantly identify them on the server with await supabase.auth.getUser() and check their purchase permissions.
Now that you've mastered Supabase's powerful yet elegant authentication system, we'll next dive into the core of our platform: PostgreSQL database design, where we'll explore the mysterious vt_purchases order table.