The Great Migration: Why Moving Auth is Hard
Moving a frontend application from a Vite-based Single Page Application (SPA) to Next.js is more than just swapping a router. The fundamental shift is in the security model.
In Vite, your authentication usually lives entirely in the browser (client-side). In Next.js, authentication must bridge the gap between the client, Server Components, and API Routes. If you try to copy-paste your Vite auth logic into Next.js, you'll likely face the dreaded "flashing unauthorized state" or hydration mismatches.
In this guide, we’ll look at how to migrate the three most popular auth providers: Supabase, Clerk, and Auth.js (formerly NextAuth).
1. Supabase: Moving from LocalStorage to Cookies
In a standard Vite app, Supabase stores the session in localStorage. This is invisible to the server. When you move to Next.js, you must switch to Cookie-based sessions so that your Server Components can see if a user is logged in before the page even reaches the browser.
The Vite Pattern (Client-only)
// In Vite, this works fine but causes SEO / flashing issues in Next.js
const { data: { user } } = await supabase.auth.getUser();
The Next.js Pattern (SSR Friendly)
You need the @supabase/ssr package. This creates a "Server Client" that can read and write cookies.
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options))
},
},
}
)
}
Migration Tip: Don't forget to set up a Middleware to refresh the session. Unlike Vite, where the client handles refreshes automatically, Next.js needs a middleware to ensure the cookie stays valid during navigation.
2. Clerk: The Easiest Transition
Clerk is arguably the easiest to migrate because their SDK is built with a "Next.js first" mindset. In Vite, you likely used <ClerkProvider> and useUser().
The Challenge
In Next.js, using useUser() or useAuth() forces your component to be a 'use client' component. This negates the performance benefits of Server Components.
The Solution: auth() and currentUser()
Clerk provides server-side utilities that fetch the session without a network request (by parsing the JWT in the headers).
// app/dashboard/page.tsx
import { auth, currentUser } from '@clerk/nextjs/server';
export default async function Page() {
// auth() gives you the ID and metadata
const { userId } = auth();
// currentUser() fetches the full user object from Clerk's cache
const user = await currentUser();
if (!userId) return <div>Not logged in</div>;
return <h1>Welcome, {user?.firstName}</h1>;
}
Migration Tip: If you were using Clerk's RedirectToSignIn component in Vite, replace it with Next.js Middleware. It's much cleaner than handling redirects inside individual route components.
3. Auth.js (NextAuth): The "No-Provider" Pattern
If you were using a custom backend or Firebase with Vite, you might be migrating to Auth.js (NextAuth v5). The biggest change here is moving away from the SessionProvider context.
The Vite Pattern
Wrapping the entire app in a Context Provider and checking status === 'loading'.
The Next.js Pattern
In v5, Auth.js encourages fetching the session directly in the Server Component to avoid the "loading spinner" hell.
// auth.ts
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub],
})
// app/layout.tsx
import { auth } from "@/auth"
export default async function RootLayout({ children }) {
const session = await auth()
return (
<html>
<body>{children}</body>
</html>
)
}
Migration Tip: If your Vite app relied on an external API (like a Django or Go backend), use the jwt and session callbacks in Auth.js to forward your backend's access token to the client.
Comparison Table
| Feature | Vite Approach | Next.js Approach |
|---|---|---|
| Storage | LocalStorage | Cookies (HttpOnly) |
| State | React Context (Loading states) | Server-side Check (Instant load) |
| Routing |
<ProtectedRoute> Wrapper |
Middleware / Server Redirects |
| API Calls | Fetch + Bearer Token | Server Actions + Cookies |
Common Pitfalls to Avoid
- Hydration Mismatches: Do not check for a user session inside a
useEffectand then conditionally render UI. This will cause the server-rendered HTML to differ from the client-rendered HTML. Always try to check the session on the server first. - Environment Variables: Remember that in Vite, variables start with
VITE_. In Next.js, they must start withNEXT_PUBLIC_to be accessible on the client. Auth secrets (likeSUPABASE_SERVICE_ROLE_KEY) should never have this prefix. - Middleware Bloat: Don't put heavy database calls in your middleware. Middleware runs on every single request (including images and CSS). Only use it for parsing session cookies and basic redirect logic.
Conclusion
Migrating auth from Vite to Next.js isn't just a syntax change—it's an architectural one. By moving from a Client-Side Auth model to a Server-First Auth model, you get better security (HttpOnly cookies) and better UX (no more flashing "Login" buttons).
Whether you choose Supabase for its backend features, Clerk for its simplicity, or Auth.js for its flexibility, the key is the same: Let the server handle the session, and let the client handle the interaction.
Top comments (0)