nuxt-x-auth-better
nuxt-x-auth-better
Better Auth frontend layer for Nuxt 4. Provides 19 auto-imported XAuth-prefixed components, useXAuth and useOrganization composables, global auth and organization middleware, 18 pre-built pages, and a fully typed app.config.ts interface — all built on Nuxt UI v4. Pairs with @xenterprises/fastify-x-auth-better on the backend.
What the consumer writes
The layer ships batteries included: all auth pages, routes, layouts, middleware, and components come from the layer. A minimal consumer is an extends entry, one environment variable, and an optional app/app.config.ts for branding and feature flags.
npm install @xenterprises/nuxt-x-auth-better better-auth
better-auth (^1.0.0, verified on the 1.6.x line) and nuxt (^4.0.0) are peer dependencies — install them in the consuming app.
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@xenterprises/nuxt-x-auth-better'],
})
# .env — required: the base URL of the Better Auth backend
NUXT_PUBLIC_X_AUTH_BASE_URL=https://api.example.com
// app/app.config.ts — must live in app/, not the project root
export default defineAppConfig({
xAuth: {
features: {
oauth: true,
organization: true,
},
oauthProviders: [
{ id: 'google', label: 'Google', icon: 'i-simple-icons-google' },
{ id: 'github', label: 'GitHub', icon: 'i-simple-icons-github' },
],
ui: {
brandName: 'Acme',
tagline: 'Sign in to your account',
},
},
})
That is the whole consumer surface: /auth/login, /auth/signup, /auth/forgot-password, and the rest of the 18 pages work immediately against the backend, with route protection enforced by the global middleware.
x.auth.baseUrl points back at the Nuxt app, SSR session probes recurse — fetch /auth/get-session hits a 404 page, the auth middleware runs again, and the request loop hangs the server render. (This exact hang was found and fixed in the layer's own E2E setup, which points the client at a dead port instead.) For same-origin setups behind a reverse proxy that forwards /auth/* to the backend, leave baseUrl empty.Installation
npm install @xenterprises/nuxt-x-auth-better better-auth
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@xenterprises/nuxt-x-auth-better'],
})
The layer declares the public.x.auth runtime config schema itself, so the environment variable NUXT_PUBLIC_X_AUTH_BASE_URL is picked up automatically. To wire a differently-named variable, override the runtime config in the consumer's nuxt.config.ts:
export default defineNuxtConfig({
extends: ['@xenterprises/nuxt-x-auth-better'],
runtimeConfig: {
public: {
x: {
auth: {
baseUrl: process.env.MY_AUTH_API_URL || '',
authPath: '/auth',
},
},
},
},
})
Override defaults in app/app.config.ts (every key shown with its layer default):
// app/app.config.ts
export default defineAppConfig({
xAuth: {
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,
organization: false,
teams: false,
},
publicRoutes: [],
plugins: {
admin: false,
stripe: false,
},
oauthProviders: [],
ui: {
showLogo: true,
showBrandName: true,
brandName: '',
tagline: '',
layout: 'centered',
},
},
})
What the Layer Provides
Components (auto-imported, XAuth prefix):
XAuthLogin— Email/password login form with optional magic link and OAuthXAuthSignup— Registration form with name, email, and passwordXAuthForgotPassword— Password reset request formXAuthMagicLink— Magic link email input formXAuthOAuthButton— Single OAuth provider button (Google, GitHub, etc.)XAuthOAuthButtonGroup— Multiple OAuth provider buttons fromapp.config.tsXAuthHandler— Callback handler for OAuth and magic link redirectsXAuthResendVerification— Resend email verification form (non-disclosing)XAuthActiveSessions— List and revoke the current user's sessions/devicesXAuthOrganizationSwitcher— List and navigate between organizationsXAuthOrganizationCreate— Create a new organizationXAuthOrganizationSettings— Edit or delete organizationXAuthOrganizationMembers— Manage members and rolesXAuthOrganizationInvite— Invite a new memberXAuthOrganizationInvitations— List pending invitationsXAuthTeamList— List teams in an organizationXAuthTeamCreate— Create a teamXAuthTeamSettings— Edit or delete teamXAuthTeamMembers— Manage team members
Composables:
useXAuth()— Full Better Auth client wrapper with session state, auth methods, and user managementuseOrganization()— Organizations & Teams state, CRUD, members, invitations, teams, and access control
Pages:
/auth/login— Login page/auth/signup— Signup page/auth/forgot-password— Forgot password page/auth/magic-link— Magic link request page/auth/reset-password— Reset password page (token from email link)/auth/handler/[...slug]— OAuth callback and magic link verification handler/auth/logout— Logout action page/org— Organization list / switcher/org/create— Create a new organization/org/:slug— Organization dashboard/org/:slug/settings— Organization settings/org/:slug/members— Member list/org/:slug/invite— Invite member/org/:slug/invitations— Pending invitations/org/:slug/teams— Team list/org/:slug/teams/create— Create team/org/:slug/teams/:teamId— Team settings + members/auth/handler/invitation?id=...— Public invitation accept handler
Middleware:
auth.global— Global route guard; redirects unauthenticated users to login and authenticated users away from guest-only routesorganization.global— Protects organization routes, resolves org by slug, enforces membership and role rules
App Config Options
Configure under the xAuth key in app/app.config.ts:
| Option | Type | Default | Description |
|---|---|---|---|
redirects.login | string | '/auth/login' | Login page path |
redirects.signup | string | '/auth/signup' | Signup page path |
redirects.afterLogin | string | '/' | Redirect after successful login |
redirects.afterSignup | string | '/' | Redirect after successful signup |
redirects.afterLogout | string | '/auth/login' | Redirect after logout |
redirects.forgotPassword | string | '/auth/forgot-password' | Forgot password page path |
publicRoutes | string | [] | Extra routes treated as public by auth.global (merged with the built-in /auth/handler, /auth/logout) |
features.oauth | boolean | false | Show OAuth provider buttons |
features.magicLink | boolean | false | Show magic link option |
features.otp | boolean | false | Show OTP input |
features.forgotPassword | boolean | true | Show forgot password link |
features.signup | boolean | true | Show signup link |
features.organization | boolean | false | Enable Organizations & Teams features |
features.teams | boolean | false | Enable teams (also requires organization.teams: true) |
plugins.admin | boolean | false | Opt in to the Better Auth adminClient plugin |
plugins.stripe | boolean | false | Opt in to the Better Auth stripeClient plugin (no-op with a console warning unless @better-auth/stripe is installed — better-auth 1.6.x does not export it) |
organization.enabled | boolean | false | Enable organization support |
organization.teams | boolean | true | Enable teams when organizations are on |
organization.allowUserToCreateOrganization | boolean | true | Allow users to create organizations |
organization.requireMemberEmailVerification | boolean | true | Require email verification for invitation acceptance |
organization.defaultRole | string | 'member' | Default role for new organization members |
organization.protectedRoutes | array | ['/org'] | Routes protected by organization.global middleware |
organization.redirects.create | string | '/org/create' | Create organization page path |
organization.redirects.list | string | '/org' | Organization list page path |
organization.redirects.settings | string | '/org/:slug/settings' | Organization settings page path |
organization.redirects.invitations | string | '/org/:slug/invitations' | Invitations page path |
organization.redirects.notMember | string | '/org' | Redirect when user is not an org member |
organization.redirects.noPermission | string | '/org' | Redirect when user lacks required role/permissions |
oauthProviders | array | [] | OAuth providers: { id, label, icon } |
ui.showLogo | boolean | true | Show logo on auth pages |
ui.showBrandName | boolean | true | Show brand name on auth pages |
ui.logoUrl | string | '' | Logo image URL shown above the auth card |
ui.brandName | string | '' | Brand name text |
ui.tagline | string | '' | Tagline below brand name |
ui.layout | 'centered' | 'split' | 'centered' | Auth page layout |
ui.background.enabled | boolean | true | Show background on auth pages |
ui.background.imageUrl | string | '' | Background image URL |
ui.background.overlayOpacity | number | 55 | Overlay opacity (0–100) |
ui.background.blur | boolean | true | Blur background image |
ui.card.glass | boolean | false | Glass morphism card effect |
ui.card.glassIntensity | 'subtle' | 'medium' | 'strong' | 'medium' | Glass effect intensity |
ui.card.logoUrl | string | '' | Logo image URL inside the auth card |
ui.split.heroPosition | 'left' | 'right' | 'left' | Hero panel position in split layout |
ui.split.heroImageUrl | string | '' | Hero panel background image URL |
ui.split.headline | string | '' | Headline in split layout hero |
ui.split.subheadline | string | '' | Subheadline in split layout hero |
ui.split.features | string | [] | Feature list in split layout hero |
ui.form.icon | string | '' | Iconify icon shown above auth forms |
ui.form.showSeparator | boolean | true | Show the "or" separator between form and OAuth buttons |
ui.legal.copyright | string | '' | Copyright text in footer |
ui.legal.links | array | [] | Footer links: { label, to } |
Runtime Config
| Key | Env Variable | Default | Description |
|---|---|---|---|
public.x.auth.baseUrl | NUXT_PUBLIC_X_AUTH_BASE_URL | '' | Base URL of the Better Auth API server (leave empty only for same-origin proxy setups) |
public.x.auth.authPath | NUXT_PUBLIC_X_AUTH_AUTH_PATH | '/auth' | Better Auth mount path on the API server |
The full auth URL is constructed as baseUrl + authPath (e.g. https://api.example.com/auth).
useXAuth() Composable
const {
user, // ComputedRef<AuthUser | null>
isAuthenticated, // ComputedRef<boolean>
isLoading, // ComputedRef<boolean>
emailSent, // Ref<boolean>
codeSent, // Ref<boolean>
needsEmailVerification, // Ref<boolean> — true when login fails with EMAIL_NOT_VERIFIED
session, // ComputedRef<Session | null>
sessionError, // ComputedRef<Error | null>
config, // ComputedRef<AuthConfig>
authClient, // Better Auth client instance
login, // (email, password) => Promise<AuthUser | null>
signup, // (email, password, name?) => Promise<AuthUser | null>
logout, // () => Promise<boolean>
forgotPassword, // (email) => Promise<boolean>
resetPassword, // (token, newPassword) => Promise<true | { error }>
loginWithProvider, // (providerName) => Promise<boolean>
sendMagicLink, // (email, options?) => Promise<boolean>
getCurrentUser, // () => Promise<AuthUser | null>
getToken, // () => Promise<string | null>
getAuthHeaders, // () => Promise<{ Authorization: string }>
handleMagicLinkCallback, // (token) => Promise<true | { error }>
verifyEmail, // (token) => Promise<true | { error }>
resendVerificationEmail, // (email) => Promise<boolean> — non-disclosing
listSessions, // () => Promise<AuthSession[]>
revokeSession, // (token) => Promise<boolean>
changePassword, // (currentPassword, newPassword) => Promise<true | { error }>
updateUser, // ({ name?, image? }) => Promise<true | { error }>
deleteAccount, // () => Promise<boolean>
resetState, // () => void
} = useXAuth()
Minimal Usage Example
<script setup>
const { user, isAuthenticated, logout } = useXAuth()
</script>
<template>
<div v-if="isAuthenticated">
Welcome, {{ user?.name }}
<UButton @click="logout">Sign Out</UButton>
</div>
<XAuthLogin v-else />
</template>
OAuth Providers Example
// app/app.config.ts
export default defineAppConfig({
xAuth: {
features: { oauth: true },
oauthProviders: [
{ id: 'google', label: 'Google', icon: 'i-simple-icons-google' },
{ id: 'github', label: 'GitHub', icon: 'i-simple-icons-github' },
],
},
})
<XAuthOAuthButtonGroup />
Global Middleware Behavior
The auth.global middleware runs on every route navigation:
| Route type | Authenticated | Unauthenticated |
|---|---|---|
Guest-only (/auth/login, /auth/signup, etc.) | Redirect to afterLogin | Allow |
Public (/auth/handler/*, /auth/logout) | Allow | Allow |
| All other routes | Allow | Redirect to login |
Extra public routes (beyond the built-in /auth/handler and /auth/logout) can be registered via xAuth.publicRoutes in app/app.config.ts.
Organizations & Teams
Enable features.organization: true in app.config.ts and add the Better Auth organization plugin on your server to activate the full Organizations & Teams implementation. See the dedicated guides for details:
- Organizations & Teams — enabling, components, pages, middleware, and server setup
- useOrganization — full composable API reference
Layer Architecture
| Path | Purpose |
|---|---|
nuxt.config.ts | Registers @nuxt/ui, injects runtime config schema |
app/app.config.ts | All configurable options under xAuth namespace with TypeScript type augmentation |
app/composables/useXAuth.ts | Better Auth client wrapper — all session state and auth methods |
app/composables/useOrganization.ts | Organizations & Teams state, CRUD, members, invitations, teams, and access control |
app/components/XAuth/ | 19 auto-imported XAuth-prefixed components |
app/pages/auth/ | 8 pre-built auth pages |
app/pages/org/ | 10 pre-built organization and team pages |
app/layouts/auth.vue | Auth layout (centered card or split panel) |
app/middleware/auth.global.ts | Global route guard |
app/middleware/organization.global.ts | Organization route guard |
app/plugins/auth-token.ts | Token injection plugin |
app/utils/ | fieldMapper utility |
Environment Variables
| Variable | Required | Description |
|---|---|---|
NUXT_PUBLIC_X_AUTH_BASE_URL | Yes | Base URL of the Better Auth API server (e.g. https://api.example.com) — never the Nuxt app's own URL (see the SSR recursion callout above) |
Breaking changes
2026-07 (next minor): the vestigial xAuth.tokens app-config block and the dead useCookieStorage util (app/utils/cookieStorage.ts) were removed — Better Auth manages its own cookies. Consumers referencing either must drop the usage.
