The Architectural Shift: Client-Side vs. Server-Side Auth
When you build a standard React SPA using Vite, your authentication logic usually lives entirely in the browser. You likely have a useAuth hook, a ProtectedRoutes component using React Router, and a JWT stored in memory or local storage.
Moving to Next.js (specifically the App Router) changes the fundamental "where" of authentication. In Next.js, auth happens on the server before the page even renders. This prevents the "flash of unauthenticated content" common in SPAs and improves security by utilizing HTTP-only cookies.
If you are planning a large-scale transition, you can streamline the structural changes by using tools like ViteToNext.AI to automate the conversion of your Vite components into Next.js-compatible structures, but you will still need to manually rethink your auth provider's implementation.
1. Supabase: From supabase-js to ssr package
In a Vite app, you likely initialized Supabase like this:
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(URL, KEY);
The Next.js Pattern
In Next.js, you must use @supabase/ssr. This is because the client needs to be able to read and write cookies across Server Components, Client Components, and Middleware.
You'll need a utility to create a server client:
// utils/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, {
cookies: {
getAll() { return cookieStore.getAll() },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))
},
},
})
}
The Strategy: Use Middleware to refresh the user session. This ensures that every time a user navigates to a new route, the server checks if their session is still valid before rendering the page.
2. Clerk: The Easiest Transition
Clerk is arguably the easiest provider to migrate from Vite to Next.js because they provide high-level components that abstract the heavy lifting.
The Vite Pattern
In Vite, you wrap your app in <ClerkProvider> and use <SignedIn> or <SignedOut> tags. Route protection is typically handled in your router config.
The Next.js Pattern
Next.js allows Clerk to handle protection at the edge. Instead of wrapping routes in components, you use a middleware.ts file:
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)', '/']);
export default clerkMiddleware((auth, request) => {
if (!isPublicRoute(request)) {
auth().protect();
}
});
This is significantly more performant than the Vite approach because the request is intercepted before it hits your page logic, reducing TTFB (Time to First Byte).
3. Auth.js (NextAuth): The Custom Powerhouse
If you were using a custom backend or a generic OIDC provider in Vite, you were likely managing local storage tokens manually. In Next.js, Auth.js is the gold standard.
Implementation Pattern
Unlike Vite where you might have an authContext.tsx, Auth.js uses a centralized configuration inside auth.ts.
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub],
})
To protect a page in Vite, you would check if (!user) return <Loading />. In Next.js, you do it directly in the Server Component:
// app/dashboard/page.tsx
import { auth } from "@/auth"
export default async function Page() {
const session = await auth()
if (!session) return <div>Not authenticated</div>
return <div>Welcome {session.user?.name}</div>
}
Common Pitfalls During Migration
- Hydration Mismatches: Accessing auth state in a Client Component that differs from the Server's state will throw an error. Always prioritize server-side checks.
- Environment Variables: Vite uses
VITE_prefixes; Next.js usesNEXT_PUBLIC_. Remember that sensitive keys (likeSUPABASE_SERVICE_ROLE) must never have theNEXT_PUBLIC_prefix, as they should only exist on the server. - The "Window" trap: In Vite, you might check
window.localStorage. In Next.js,windowis undefined during SSR. Shift your state management to cookie-based libraries provided by your auth vendor.
Conclusion
Migrating auth from Vite to Next.js is less about changing code and more about changing your mental model. You are moving from a "Client-First" approach to a "Server-First" approach. By leveraging Middleware and Server Components, you gain a more secure, faster app that doesn't leak sensitive logic to the browser.
Further reading: ViteToNext.AI Migration Guide
Top comments (0)