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'],
})
What the consumer writes
The layer ships the batteries: pre-built auth pages (/auth/login, /auth/signup, /auth/forgot-password, /auth/magic-link, /auth/otp, /auth/logout, /auth/handler/[...slug]), a global route middleware, and all UI components. The minimal consumer setup is an extends entry, one app/app.config.ts selecting the provider, and the env vars for that provider:
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@xenterprises/nuxt-x-auth'],
})
// app/app.config.ts — must live in app/, not the project root
export default defineAppConfig({
xAuth: {
provider: 'stack', // 'stack' | 'better-auth' | 'neon-auth' | 'local'
// Everything else has working defaults — override only what you need:
redirects: { afterLogin: '/dashboard' },
features: { oauth: true },
oauthProviders: [
{ id: 'google', label: 'Google', icon: 'i-logos-google-icon' },
],
ui: { brandName: 'My App' },
},
})
# .env — only the vars for the provider you selected (Stack Auth shown)
NUXT_PUBLIC_STACK_PROJECT_ID=...
NUXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=...
That's it — visiting /auth/login renders a working login screen for the selected provider. Switching backends later is a one-line change to xAuth.provider plus the matching env vars; no component or page code changes.
ssr: false) and its app.config.ts overrides only take effect when the file lives in the consumer's app/ directory — at the project root it is silently ignored.Configuration
All configuration lives in app/app.config.ts under the xAuth key. The block below shows the layer's shipped defaults (with oauthProviders filled in as an example — the default is an empty array):
// app/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: '', // e.g. 'My App'
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/reset-password | XAuthResetPassword | New-password form consuming the reset link's ?code=/?token= |
/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).
<XAuthResetPassword />
New-password form reached from the reset email link. Accepts the reset credential as ?code= (Stack Auth) or ?token= (Better Auth / Neon Auth / Local JWT), shows an "Invalid reset link" state when it's missing, and a success state after resetPassword(code, newPassword) succeeds. See ResetPassword.
<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 with a built-in brand icon. The provider prop is a string id ('google', 'github', 'facebook', 'twitter', 'microsoft', 'apple', 'linkedin', 'discord', 'spotify').
<XAuthOAuthButtonGroup />
Renders all configured oauthProviders from app.config.ts as a button group. Accepts an optional providers prop to override the list; renders nothing unless xAuth.features.oauth is true.
<XAuthHandler />
Handles auth callbacks (OAuth redirects, magic link completions, password reset, email verification). Requires a type prop ('magic-link' | 'password-reset' | 'oauth' | 'email-verification'); the /auth/handler/[...slug] page maps the route slug to it.
<XAuthForm />
Low-level schema-driven form renderer used internally by the page components. Takes declarative fields, OAuth providers, a Zod schema, and a submit label, and emits submit with the validated data.
Global Route Middleware
A global auth.global.ts middleware runs on every navigation:
- Guest-only routes (
/auth/login,/auth/signup,/auth/forgot-password,/auth/reset-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.
Middleware opt-outs
// app/app.config.ts
export default defineAppConfig({
xAuth: {
middleware: {
enabled: true, // false disables the global guard entirely —
// manage protection yourself (e.g. definePageMeta middleware)
guestOnly: undefined, // set to replace the built-in guest-only list
public: undefined, // set to extend the public list
},
},
})
Per-page kill switches
xAuth.pages.* disables individual shipped auth pages — a disabled route 404s in the middleware even though the layer registers it. (xAuth.features.* only hides links/buttons; pages.* removes the route itself.)
// app/app.config.ts
export default defineAppConfig({
xAuth: {
pages: {
login: true,
signup: true,
forgotPassword: true,
resetPassword: true,
magicLink: true,
otp: true,
},
},
})
All pages stay routable by default (pages itself defaults to undefined). Disabling login also breaks the middleware's redirect target — pair it with middleware.enabled: false or a custom redirects.login.
Page SEO Titles
Every shipped auth page sets its own <title> via useSeoMeta: Sign in (/auth/login), Create account (/auth/signup), Forgot password (/auth/forgot-password), Reset password (/auth/reset-password), Sign in with a magic link (/auth/magic-link), Sign in with a code (/auth/otp).
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) provides $getAuthToken, so sibling layers (e.g. a dashboard layer's useXFetch) can retrieve the active token without depending on a specific provider. Token storage uses cookies (names configurable via tokens.accessCookie / tokens.refreshCookie).
