Guides

Migrating from nuxt-auth-utils

Step-by-step guide to migrate from nuxt-auth-utils to Nuxt Better Auth.

Use this guide when you already have a Nuxt app on nuxt-auth-utils and want a safe migration order.

This guide covers migrating from nuxt-auth-utils to Nuxt Better Auth.

Significant migration: nuxt-auth-utils stores sessions in encrypted cookies. Better Auth uses database-backed sessions with typed users. Plan accordingly.
This module is a Nuxt wrapper around Better Auth. Most auth logic (OAuth providers, plugins, email/password, etc.) is configured via Better Auth. Refer to their docs for provider-specific setup.

Why Migrate?

nuxt-auth-utilsNuxt Better Auth
Session-only (cookie storage)Database-backed user records
Manual OAuth handlers per providerDeclarative OAuth config
Generic session objectFull TypeScript inference from plugins
Limited plugin ecosystemRich plugins (admin, 2FA, passkeys, orgs)
Manual session managementBuilt-in signIn/signUp/signOut

Key Differences

Session Architecture

nuxt-auth-utils: Arbitrary data in encrypted cookies via NUXT_SESSION_PASSWORD. No database required.

Better Auth: Database stores users + sessions. Cookies contain session tokens referencing DB records.

Composable API

// nuxt-auth-utils
const { user, loggedIn, ready, fetch, clear } = useUserSession()

// nuxt-better-auth
const { user, session, loggedIn, ready, signOut, fetchSession } = useUserSession()
const signInEmail = useSignIn('email')
const client = useAuthClient()

OAuth Handling

// nuxt-auth-utils - Event handler per provider
// server/routes/auth/github.get.ts
export default defineOAuthGitHubEventHandler({
  async onSuccess(event, { user }) {
    await setUserSession(event, { user: { id: user.id, name: user.name } })
    return sendRedirect(event, '/')
  }
})
// nuxt-better-auth - Declarative config
// server/auth.config.ts
import { defineServerAuth } from '@nuxtjs/better-auth/config'
export default defineServerAuth({
  socialProviders: {
    github: {
      clientId: process.env.NUXT_OAUTH_GITHUB_CLIENT_ID!,
      clientSecret: process.env.NUXT_OAUTH_GITHUB_CLIENT_SECRET!
    }
  }
})

Migration Steps

Remove nuxt-auth-utils

npx nuxi module rm nuxt-auth-utils

You can also delete NUXT_SESSION_PASSWORD from your .env file.

Install Nuxt Better Auth

Follow the Installation Guide to set up the module, environment variables, database, and configuration files.

For OAuth-only apps, Database-less Mode stores sessions in JWE cookies similar to nuxt-auth-utils.

Migrate OAuth Providers

Before:

server/routes/auth/github.get.ts
export default defineOAuthGitHubEventHandler({
  config: { scope: ['user:email'] },
  async onSuccess(event, { user }) {
    await setUserSession(event, {
      user: { id: user.id, name: user.name, email: user.email }
    })
    return sendRedirect(event, '/dashboard')
  }
})

After:

server/auth.config.ts
import { defineServerAuth } from '@nuxtjs/better-auth/config'

export default defineServerAuth({
  socialProviders: {
    github: {
      clientId: process.env.NUXT_OAUTH_GITHUB_CLIENT_ID!,
      clientSecret: process.env.NUXT_OAUTH_GITHUB_CLIENT_SECRET!,
      scope: ['user:email']
    }
  }
})

Delete your OAuth route files (server/routes/auth/*.ts) - Better Auth handles routes automatically at /api/auth/**.

See Better Auth OAuth docs for all supported providers and configuration options.

Update Session Usage

Before:

<script setup>
const { user, loggedIn, fetch, clear } = useUserSession()

async function logout() {
  await clear()
  navigateTo('/login')
}
</script>

After:

<script setup>
const { user, loggedIn, signOut } = useUserSession()

async function logout() {
  await signOut()
  navigateTo('/login')
}
</script>

Migrate Route Protection

Before (custom middleware):

middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
  const { loggedIn } = useUserSession()
  if (!loggedIn.value && to.path.startsWith('/app')) {
    return navigateTo('/login')
  }
})

After (route rules):

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/app/**': { auth: { only: 'user', redirectTo: '/login' } },
    '/login': { auth: { only: 'guest', redirectTo: '/app' } },
    '/admin/**': { auth: { user: { role: 'admin' } } }
  },
  auth: {
    preserveRedirect: true,
    redirectQueryKey: 'redirect'
  }
})

Delete your custom auth middleware.

Migrate API Protection

Before:

server/api/protected.get.ts
export default defineEventHandler(async (event) => {
  const session = await requireUserSession(event)
  return { userId: session.user.id }
})

After:

server/api/protected.get.ts
export default defineEventHandler(async (event) => {
  const { user } = await requireUserSession(event)
  return { userId: user.id }
})

With field matching:

const { user } = await requireUserSession(event, {
  user: { role: 'admin' }
})

API Reference

nuxt-auth-utilsnuxt-better-authNotes
useUserSession().useruseUserSession().userNow typed from config
useUserSession().fetch()useUserSession().fetchSession()Renamed
useUserSession().clear()useUserSession().signOut()Renamed + server call
setUserSession(event, data)N/AHandled by Better Auth
getUserSession(event)getUserSession(event)Returns { user, session }
requireUserSession(event)requireUserSession(event, opts?)Supports field matching
clearUserSession(event)N/AUse client signOut()
defineOAuth*EventHandlersocialProviders configDeclarative

Special Cases

Password Hashing Migration

nuxt-auth-utils and Better Auth both use scrypt by default, but their hashes are not compatible. nuxt-auth-utils stores an Adonis-style PHC string such as $scrypt$.... Better Auth uses a different salt:key encoding and different default scrypt parameters. Better Auth cannot verify an unchanged nuxt-auth-utils hash.

Step 0: Test the Migration

Before the cutover, generate a password hash with the nuxt-auth-utils version used by your app. Assert that your migration path accepts the password, then assert that Better Auth can authenticate the account after its password is reset or re-hashed.

Do not remove the old verifier, auth secret, or rollback data until this test passes in staging. If you customized either password hasher, test hashes created with your exact production settings.

Option 1: Require Password Resets

The simplest approach is to require all users to reset their passwords during migration.

  1. Do not import a legacy hash as a usable Better Auth credential
  2. Direct users through a verified password-reset flow
  3. Let Better Auth hash the new password with the hasher configured in emailAndPassword.password (scrypt by default)

Option 2: Support Both Hash Formats (Custom Hashing)

Configure both emailAndPassword.password.hash and emailAndPassword.password.verify. The verifier must recognize the stored format and support both legacy nuxt-auth-utils hashes and new Better Auth hashes. Use Better Auth's hashPassword and verifyPassword exports from better-auth/crypto for its current format, and keep a tested implementation for the exact legacy format you used.

A custom verify function replaces Better Auth's default verifier for every password check. Supporting only the legacy format will lock out users after they set a new password.

Option 3: Gradual Migration (Custom Hashing)

After the dual-format verifier accepts a legacy hash, replace that stored hash with a value from your configured Better Auth hash function. Better Auth does not automatically re-hash legacy values, so implement and test this database update as part of your migration flow. Keep password resets available as a fallback.

WebAuthn Migration

If you used WebAuthn with nuxt-auth-utils:

  1. Export your WebAuthn credentials from the old format
  2. Install the Better Auth WebAuthn plugin
  3. Import credentials to the new schema
See Better Auth WebAuthn documentation for schema requirements.

Data Migration

Better Auth creates user, session, and account tables. After setup, run schema generation:

See Schema Generation to set up database tables.

Troubleshooting

Sessions not persisting

Ensure a singular or versioned auth secret is configured and the database is available.

OAuth redirect errors

Set NUXT_PUBLIC_SITE_URL for Cloudflare Workers, custom domains, and hosts without a supported platform URL variable. For platform domains, the module can use VERCEL_URL (Vercel), CF_PAGES_URL (Cloudflare Pages), or URL (Netlify) when available at runtime. Cloudflare Workers does not supply CF_PAGES_URL.

Type errors on user fields

Run type augmentation - see Type Augmentation.