nuxt-x-auth
nuxt-x-auth
Provider-agnostic authentication layer for Nuxt. Register once and configure a single provider in app.config.ts — all UI components, route guards, and the useXAuth composable work identically regardless of the backend. Supports Stack Auth, Better Auth, Neon Auth, and Local JWT out of the box.
Installation
npm install @xenterprises/nuxt-x-auth
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@xenterprises/nuxt-x-auth'],
})
Configuration
All configuration lives in app.config.ts under the xAuth key.
// app.config.ts
export default defineAppConfig({
xAuth: {
provider: 'stack', // 'stack' | 'better-auth' | 'neon-auth' | 'local'
redirects: {
login: '/auth/login',
signup: '/auth/signup',
afterLogin: '/',
afterSignup: '/',
afterLogout: '/auth/login',
forgotPassword: '/auth/forgot-password',
},
features: {
oauth: false,
magicLink: false,
otp: false,
forgotPassword: true,
signup: true,
},
oauthProviders: [
{ id: 'google', label: 'Google', icon: 'i-logos-google-icon' },
{ id: 'github', label: 'GitHub', icon: 'i-logos-github-icon' },
],
tokens: {
accessCookie: 'x_auth_access',
refreshCookie: 'x_auth_refresh',
hasRefresh: true,
},
ui: {
showLogo: true,
logoUrl: '',
brandName: 'My App',
tagline: 'Welcome back',
layout: 'centered', // 'centered' | 'split'
background: {
type: 'gradient', // 'gradient' | 'image' | 'solid'
imageUrl: '',
overlayOpacity: 50,
},
card: {
glass: false,
glassIntensity: 'medium', // 'subtle' | 'medium' | 'strong'
},
split: {
heroPosition: 'left', // 'left' | 'right'
heroImageUrl: '',
headline: '',
subheadline: '',
features: [],
},
form: {
icon: '',
showSeparator: true,
},
},
},
})
provider options
| Value | Backend |
|---|---|
"stack" | Stack Auth (default) |
"better-auth" | Better Auth |
"neon-auth" | Neon Auth (Better Auth wrapper) |
"local" | Local JWT API |
endpoints overrides (Local JWT / custom)
endpoints: {
login: '/auth/login',
signup: '/auth/signup',
logout: '/auth/logout',
refresh: '/auth/refresh',
me: '/auth/me',
forgotPassword: '/auth/forgot-password',
resetPassword: '/auth/reset-password',
}
fieldMapping overrides
Normalize provider-specific fields to the unified AuthUser shape:
fieldMapping: {
id: 'sub', // default tries: id, sub, user_id
name: 'full_name', // default tries: displayName, name, full_name, fullName
avatar: 'image', // default tries: avatarUrl, image, profile_picture
emailVerified: 'verified',
}
useXAuth Composable
const {
// State
user, // Ref<AuthUser | null>
isLoading, // Ref<boolean>
isAuthenticated, // ComputedRef<boolean>
emailSent, // Ref<boolean> — true after forgotPassword/sendMagicLink
codeSent, // Ref<boolean> — true after sendOtp
// Core
login, // (email, password) => Promise<AuthUser | null>
signup, // (email, password) => Promise<AuthUser | null>
logout, // () => Promise<boolean>
// Password
forgotPassword, // (email) => Promise<boolean>
resetPassword, // (code, newPassword) => Promise<true | { error }>
// OAuth
loginWithProvider, // (providerName, options?) => Promise<boolean>
// OTP
sendOtp, // (email) => Promise<boolean>
verifyOtp, // (code) => Promise<AuthUser | null>
// Magic Link
sendMagicLink, // (email, options?) => Promise<boolean>
handleMagicLinkCallback, // (code) => Promise<true | { error }>
// Utilities
getCurrentUser, // () => Promise<AuthUser | null>
getToken, // () => Promise<string | null>
getAuthHeaders, // () => Promise<Record<string, string>>
resetState, // () => void
// Info
providerType, // AuthProviderType
config, // ComputedRef<AuthConfig>
} = useXAuth()
AuthUser shape
All providers normalize their user data to this structure:
{
id: string
email: string
name: string
avatar?: string
emailVerified: boolean
metadata?: Record<string, any>
}
Examples
// Login
const user = await login('user@example.com', 'password')
// OAuth
await loginWithProvider('google')
// OTP flow
await sendOtp('user@example.com') // sends code
await verifyOtp('123456') // signs in
// Magic link flow
await sendMagicLink('user@example.com')
// user clicks link → /auth/handler/magic-link-callback
// page calls:
await handleMagicLinkCallback(route.query.code)
// Get token for API calls
const headers = await getAuthHeaders()
// { Authorization: 'Bearer eyJ...' }
Pre-Built Pages
The layer registers these pages automatically:
| Route | Component | Description |
|---|---|---|
/auth/login | XAuthLogin | Email/password + OAuth + magic link/OTP links |
/auth/signup | XAuthSignup | Registration form |
/auth/forgot-password | XAuthForgotPassword | Password reset request |
/auth/magic-link | XAuthMagicLink | Magic link request |
/auth/otp | XAuthOtp | OTP verification |
/auth/logout | — | Calls logout() and redirects |
/auth/handler/[...slug] | XAuthHandler | OAuth/magic link callbacks |
Components
All components are auto-imported with the XAuth prefix.
<XAuthLogin />
Full login form — email/password, optional OAuth buttons, links to signup/forgot password.
<XAuthSignup />
Registration form — email/password with confirmation.
<XAuthForgotPassword />
Password reset request form. Shows success state after submission (emailSent becomes true).
<XAuthMagicLink />
Magic link request form. Shows success state after email is sent.
<XAuthMagicLinkCallback />
Handles the magic link callback URL. Reads the code query param and calls handleMagicLinkCallback.
<XAuthOtp />
Two-step OTP: email input → code verification. Uses sendOtp then verifyOtp.
<XAuthOAuthButton />
Single OAuth provider button.
| Prop | Type | Description |
|---|---|---|
provider | { id, label, icon } | Provider definition |
<XAuthOAuthButtonGroup />
Renders all configured oauthProviders from app.config.ts as a button group.
<XAuthHandler />
Handles auth callbacks (OAuth redirects, magic link completions). Reads the route slug and dispatches to the appropriate callback handler.
<XAuthForm />
Low-level form container used internally by the page components. Provides the shared card/layout/branding UI shell.
Global Route Middleware
A global auth.global.ts middleware runs on every navigation:
- Guest-only routes (
/auth/login,/auth/signup,/auth/forgot-password,/auth/magic-link,/auth/otp) — authenticated users are redirected toredirects.afterLogin. - Public routes (
/auth/handler/*,/auth/logout) — always accessible, middleware skips. - All other routes — unauthenticated users are redirected to
redirects.login.
Layouts
| Layout | Description |
|---|---|
auth | Auth pages layout (full-screen, no nav) |
default | Default app layout |
Environment Variables
Configure the active provider via runtimeConfig (or environment variables):
| Variable | Provider | Description |
|---|---|---|
NUXT_PUBLIC_STACK_PROJECT_ID | Stack Auth | Stack project ID |
NUXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY | Stack Auth | Publishable client key |
NUXT_PUBLIC_BETTER_AUTH_BASE_URL | Better Auth | Better Auth API base URL |
NUXT_PUBLIC_NEON_AUTH_PROJECT_ID | Neon Auth | Neon project ID |
NUXT_PUBLIC_NEON_AUTH_BRANCH_ID | Neon Auth | Neon branch ID |
NUXT_PUBLIC_NEON_AUTH_BASE_URL | Neon Auth | Neon Auth base URL |
NUXT_PUBLIC_LOCAL_AUTH_BASE_URL | Local JWT | Your API base URL |
How It Works
useXAuth reads appConfig.xAuth.provider at call time and instantiates the matching provider adapter (useStackAuthProvider, useBetterAuthProvider, useNeonAuthProvider, or useLocalAuthProvider). Each adapter implements the AuthProvider interface — the same method signatures regardless of backend. useXAuth wraps every method with loading state, toast notifications (via useToast()), and post-action redirects derived from appConfig.xAuth.redirects. All UI components consume useXAuth internally so swapping providers requires only changing the provider value in app.config.ts. The auth plugin (auth-token.ts) attaches the active token to outgoing requests. Token storage uses cookies (names configurable via tokens.accessCookie / tokens.refreshCookie).
