Migrating from nuxt-auth-utils
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.
Why Migrate?
| nuxt-auth-utils | Nuxt Better Auth |
|---|---|
| Session-only (cookie storage) | Database-backed user records |
| Manual OAuth handlers per provider | Declarative OAuth config |
| Generic session object | Full TypeScript inference from plugins |
| Limited plugin ecosystem | Rich plugins (admin, 2FA, passkeys, orgs) |
| Manual session management | Built-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.
Migrate OAuth Providers
Before:
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:
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/**.
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):
export default defineNuxtRouteMiddleware((to) => {
const { loggedIn } = useUserSession()
if (!loggedIn.value && to.path.startsWith('/app')) {
return navigateTo('/login')
}
})
After (route rules):
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:
export default defineEventHandler(async (event) => {
const session = await requireUserSession(event)
return { userId: session.user.id }
})
After:
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-utils | nuxt-better-auth | Notes |
|---|---|---|
useUserSession().user | useUserSession().user | Now typed from config |
useUserSession().fetch() | useUserSession().fetchSession() | Renamed |
useUserSession().clear() | useUserSession().signOut() | Renamed + server call |
setUserSession(event, data) | N/A | Handled by Better Auth |
getUserSession(event) | getUserSession(event) | Returns { user, session } |
requireUserSession(event) | requireUserSession(event, opts?) | Supports field matching |
clearUserSession(event) | N/A | Use client signOut() |
defineOAuth*EventHandler | socialProviders config | Declarative |
Special Cases
Password Hashing Migration
$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.
- Do not import a legacy hash as a usable Better Auth credential
- Direct users through a verified password-reset flow
- 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:
- Export your WebAuthn credentials from the old format
- Install the Better Auth WebAuthn plugin
- Import credentials to the new schema
Data Migration
Better Auth creates user, session, and account tables. After setup, run schema generation:
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.