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

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

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

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

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

  1. Hydration Mismatches: Do not check for a user session inside a useEffect and 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.
  2. Environment Variables: Remember that in Vite, variables start with VITE_. In Next.js, they must start with NEXT_PUBLIC_ to be accessible on the client. Auth secrets (like SUPABASE_SERVICE_ROLE_KEY) should never have this prefix.
  3. 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)