DEV Community

Digital dev
Digital dev

Posted on

Migrating Auth from Vite to Next.js: Supabase, Clerk, and Auth.js Patterns That Actually Work

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);
});
Enter fullscreen mode Exit fullscreen mode

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.

  1. Server Client: Create a utility to initialize the Supabase client using cookies() from next/headers.
  2. 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 and useAuth() 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.ts in 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>;
}
Enter fullscreen mode Exit fullscreen mode

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)