The Architectural Shift: Client-Side vs. Server-Side Auth
When you build a React app with Vite, your authentication usually lives entirely in the browser. You likely use a useAuth hook, store your session in memory or local storage, and use a provider to wrap your entire <App />.
Transitioning to Next.js changes the game. You aren't just shifting from a SPA to a framework; you are shifting from client-side state management to a hybrid model where the server (SSR) and the edge (Middleware) need to know who the user is before the page even renders.
In this guide, we will look at how to migrate the three most popular auth providers—Supabase, Clerk, and Auth.js (formerly NextAuth)—while avoiding the common pitfalls of the window is not defined error.
1. Supabase: From auth.onAuthStateChange to Cookies
In a Vite environment, Supabase works by persisting the session to localStorage. In Next.js, this isn't enough because the server cannot read your browser's local storage.
The Vite Way (Client-Only)
// App.tsx
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
setSession(session);
});
The Next.js Way (SSR-Ready)
You must transition to using the @supabase/ssr package. This allows you to sync the auth state into cookies, making the session accessible in Server Components and Middleware.
- Server Client: Create a utility to initialize the Supabase client using
cookies()fromnext/headers. - Middleware: Use middleware to refresh the session before the route loads. This prevents the flickering "unauthenticated" state common in Vite-to-Next migrations.
2. Clerk: The Easiest Transition Path
Clerk is arguably the most seamless migration path because it handles the cookie syncing for you. However, the logic moves from the ClerkProvider in your Vite main.tsx to the Next.js layout.tsx.
Migration Steps:
- Replace your Vite environment variables (
VITE_CLERK_PUBLISHABLE_KEY) with Next.js variables (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY). - Wrap your root layout in
<ClerkProvider>. - Use the
auth()helper for Server Components anduseAuth()for Client Components.
One significant benefit here is Middleware Protection. Instead of manually checking for a user object in every useEffect like you did in Vite, you can define public and private routes in a single middleware.ts file.
3. Auth.js (NextAuth): The Go-To for Custom Backends
If you were using a custom JWT solution or a complex OAuth flow in Vite, Auth.js is your destination. Unlike Vite, where you might have manually managed refresh tokens, Auth.js handles the lifecycle of the session automatically via a hidden API route (/api/auth/*).
The Pattern:
-
Define the Config: Move your auth logic to
auth.tsin the root. -
Session Access: Instead of context providers, use
const session = await auth()in your Server Components. It’s faster, more secure, and reduces the bundle size sent to the client.
Handling the Migration Friction
Migrating auth logic is often the most tedious part of moving frameworks because a single mistake leads to broken redirects or infinite loops. If you are looking to speed up this process, ViteToNext.AI can help automate the structural conversion of your Vite components and hooks into Next.js-ready code, saving hours of manual refactoring.
Common Pitfalls to Avoid
The "Use Client" Directive
In Vite, every component is a client component. In Next.js, your auth hooks (useSession, useUser) will fail if you don't add the 'use client' directive at the top of the file. Always try to fetch your user data in a Server Component first and pass it down as props to minimize client-side overhead.
Environmental Variables
Remember that Vite uses import.meta.env, while Next.js uses process.env. If your auth provider initialization code isn't updated, your client will silently fail to connect to the auth API, leaving you wondering why your login button does nothing.
Protecting Routes
In Vite, you probably used a <ProtectedRoute> component that returned a Navigate component from React Router. In Next.js, the best practice is to handle this in middleware.ts or via a server-side redirect:
import { auth } from "./auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session) redirect("/login");
return <div>Welcome, {session.user.name}</div>;
}
Conclusion
Migrating authentication from Vite to Next.js is less about rewriting your logic and more about shifting where that logic executes. By embracing cookies over local storage and leveraging Middleware, you gain a more secure and faster application. Whether you choose the plug-and-play nature of Clerk, the flexibility of Supabase, or the control of Auth.js, the key is to ensure your server is always aware of the user's state.
Further reading: Step-by-step automation for Vite to Next.js migration
Top comments (0)