Introduction
Moving a frontend application from a Single Page Application (SPA) architecture—typically built with Vite—to a Server-Side Rendered (SSR) framework like Next.js is a significant architectural shift. While the UI components often stay the same, the way you handle authentication changes fundamentally.
In a Vite app, auth is usually client-side only: you fetch a JWT, store it in localStorage or a cookie, and use a React Context provider to manage the session. In Next.js, auth happens at the edge, on the server, and in the browser.
This guide explores how to migrate the three most popular auth providers—Supabase, Clerk, and Auth.js—while avoiding common pitfalls.
The Core Difference: Client-Side vs. Server-Side Auth
In a Vite environment, your auth logic looks like this:
- User logs in.
- API returns a token.
- Token is stored in
localStorage. - Every
useEffectoruseQuerysends this token in the header.
In Next.js, this is an anti-pattern. Because Next.js renders content on the server (SSR) or during build time (SSG), the server needs to know who the user is before the page reaches the browser. This requires HttpOnly Cookies.
1. Migrating Supabase Auth
If you are using Supabase in Vite, you likely use the @supabase/supabase-js client. In Next.js, you need to switch to @supabase/auth-helpers-nextjs or the newer @supabase/ssr package.
The Vite Way (Client Only):
// Initializing in Vite
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(URL, KEY);
The Next.js Way (Server-Side Logic):
You must create a client that can access cookies. In your middleware.ts, you’ll need to refresh the session so the user stays logged in across SSR requests.
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({ request })
const supabase = createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, {
cookies: {
get(name: string) { return request.cookies.get(name)?.value },
set(name: string, value: string, options: CookieOptions) {
response.cookies.set({ name, value, ...options })
},
},
})
await supabase.auth.getUser()
return response
}
2. Migrating Clerk
Clerk is arguably the easiest to migrate because they handle the heavy lifting of session synchronization between the client and server.
In Vite, you wrap your app in <ClerkProvider>. In Next.js, the pattern is similar, but you gain access to auth() for Server Components and useAuth() for Client Components.
Key Migration Step: Move your environment variables from VITE_CLERK_PUBLISHABLE_KEY to NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY. Then, implement the clerkMiddleware() to protect your routes at the infrastructure level rather than relying on useEffect redirects, which cause layout shifts.
3. The Transition to Auth.js (formerly NextAuth)
If you were using a custom backend or Firebase with Vite, many developers choose to switch to Auth.js when moving to Next.js. Auth.js is built specifically for the Next.js ecosystem.
Unlike Vite where you might have a useAuth hook talking to a /login endpoint, Auth.js provides a unified configuration file (auth.ts) that handles OAuth providers, credentials, and session management automatically.
Handling the Migration Complexity
Rewriting your entire authentication layer, handling middleware, and refactoring every API call to be server-aware is time-consuming. If you are looking to speed up the transition, using a tool like ViteToNext.AI can help automate the structural conversion of your Vite project into a Next.js App Router format, allowing you to focus purely on the auth logic.
Common Pitfalls to Avoid
1. The "Window is not defined" Error
In Vite, you can check localStorage.getItem('token') anywhere. In Next.js, this will crash your build during SSR. Always wrap client-only auth checks in a useEffect or use the 'use client' directive sparingly.
2. Protecting Routes
In Vite, you protect routes in your router (like React Router). In Next.js, it is best practice to protect routes in middleware.ts. This prevents the browser from downloading the page code entirely if the user is unauthorized, which is a significant security and performance boost.
3. Data Fetching
In Vite: client -> fetch -> backend.
In Next.js: Server Component -> database.
If you are logged in, the Server Component can fetch data directly from your DB or Supabase using the server-side session, eliminating the need for an intermediate API route in many cases.
Conclusion
Migrating auth from Vite to Next.js isn't just about changing libraries; it's about changing your mindset from "browser-first" to "server-first." By leveraging Middleware and HttpOnly cookies via tools like Clerk or Supabase SSR, you create a more secure and faster experience for your users.
Further reading: How to automate your migration with ViteToNext.AI
Top comments (0)