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

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

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

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

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

Common Pitfalls During Migration

  1. 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.
  2. Environment Variables: Vite uses VITE_ prefixes; Next.js uses NEXT_PUBLIC_. Remember that sensitive keys (like SUPABASE_SERVICE_ROLE) must never have the NEXT_PUBLIC_ prefix, as they should only exist on the server.
  3. The "Window" trap: In Vite, you might check window.localStorage. In Next.js, window is 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)