X Enterprises

nuxt-x-auth-better

Nuxt 4 authentication layer powered by Better Auth — 19 XAuth-prefixed components, useXAuth and useOrganization composables, global auth and organization middleware, 18 pre-built pages, and a fully typed app.config.ts interface built on Nuxt UI v4.

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.

Point the base URL at the Better Auth backend, never at the Nuxt app itself. This layer requires a reachable Better Auth server URL. If 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 OAuth
  • XAuthSignup — Registration form with name, email, and password
  • XAuthForgotPassword — Password reset request form
  • XAuthMagicLink — Magic link email input form
  • XAuthOAuthButton — Single OAuth provider button (Google, GitHub, etc.)
  • XAuthOAuthButtonGroup — Multiple OAuth provider buttons from app.config.ts
  • XAuthHandler — Callback handler for OAuth and magic link redirects
  • XAuthResendVerification — Resend email verification form (non-disclosing)
  • XAuthActiveSessions — List and revoke the current user's sessions/devices
  • XAuthOrganizationSwitcher — List and navigate between organizations
  • XAuthOrganizationCreate — Create a new organization
  • XAuthOrganizationSettings — Edit or delete organization
  • XAuthOrganizationMembers — Manage members and roles
  • XAuthOrganizationInvite — Invite a new member
  • XAuthOrganizationInvitations — List pending invitations
  • XAuthTeamList — List teams in an organization
  • XAuthTeamCreate — Create a team
  • XAuthTeamSettings — Edit or delete team
  • XAuthTeamMembers — Manage team members

Composables:

  • useXAuth() — Full Better Auth client wrapper with session state, auth methods, and user management
  • useOrganization() — 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 routes
  • organization.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:

OptionTypeDefaultDescription
redirects.loginstring'/auth/login'Login page path
redirects.signupstring'/auth/signup'Signup page path
redirects.afterLoginstring'/'Redirect after successful login
redirects.afterSignupstring'/'Redirect after successful signup
redirects.afterLogoutstring'/auth/login'Redirect after logout
redirects.forgotPasswordstring'/auth/forgot-password'Forgot password page path
publicRoutesstring[]Extra routes treated as public by auth.global (merged with the built-in /auth/handler, /auth/logout)
features.oauthbooleanfalseShow OAuth provider buttons
features.magicLinkbooleanfalseShow magic link option
features.otpbooleanfalseShow OTP input
features.forgotPasswordbooleantrueShow forgot password link
features.signupbooleantrueShow signup link
features.organizationbooleanfalseEnable Organizations & Teams features
features.teamsbooleanfalseEnable teams (also requires organization.teams: true)
plugins.adminbooleanfalseOpt in to the Better Auth adminClient plugin
plugins.stripebooleanfalseOpt 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.enabledbooleanfalseEnable organization support
organization.teamsbooleantrueEnable teams when organizations are on
organization.allowUserToCreateOrganizationbooleantrueAllow users to create organizations
organization.requireMemberEmailVerificationbooleantrueRequire email verification for invitation acceptance
organization.defaultRolestring'member'Default role for new organization members
organization.protectedRoutesarray['/org']Routes protected by organization.global middleware
organization.redirects.createstring'/org/create'Create organization page path
organization.redirects.liststring'/org'Organization list page path
organization.redirects.settingsstring'/org/:slug/settings'Organization settings page path
organization.redirects.invitationsstring'/org/:slug/invitations'Invitations page path
organization.redirects.notMemberstring'/org'Redirect when user is not an org member
organization.redirects.noPermissionstring'/org'Redirect when user lacks required role/permissions
oauthProvidersarray[]OAuth providers: { id, label, icon }
ui.showLogobooleantrueShow logo on auth pages
ui.showBrandNamebooleantrueShow brand name on auth pages
ui.logoUrlstring''Logo image URL shown above the auth card
ui.brandNamestring''Brand name text
ui.taglinestring''Tagline below brand name
ui.layout'centered' | 'split''centered'Auth page layout
ui.background.enabledbooleantrueShow background on auth pages
ui.background.imageUrlstring''Background image URL
ui.background.overlayOpacitynumber55Overlay opacity (0–100)
ui.background.blurbooleantrueBlur background image
ui.card.glassbooleanfalseGlass morphism card effect
ui.card.glassIntensity'subtle' | 'medium' | 'strong''medium'Glass effect intensity
ui.card.logoUrlstring''Logo image URL inside the auth card
ui.split.heroPosition'left' | 'right''left'Hero panel position in split layout
ui.split.heroImageUrlstring''Hero panel background image URL
ui.split.headlinestring''Headline in split layout hero
ui.split.subheadlinestring''Subheadline in split layout hero
ui.split.featuresstring[]Feature list in split layout hero
ui.form.iconstring''Iconify icon shown above auth forms
ui.form.showSeparatorbooleantrueShow the "or" separator between form and OAuth buttons
ui.legal.copyrightstring''Copyright text in footer
ui.legal.linksarray[]Footer links: { label, to }

Runtime Config

KeyEnv VariableDefaultDescription
public.x.auth.baseUrlNUXT_PUBLIC_X_AUTH_BASE_URL''Base URL of the Better Auth API server (leave empty only for same-origin proxy setups)
public.x.auth.authPathNUXT_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 typeAuthenticatedUnauthenticated
Guest-only (/auth/login, /auth/signup, etc.)Redirect to afterLoginAllow
Public (/auth/handler/*, /auth/logout)AllowAllow
All other routesAllowRedirect 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:

Layer Architecture

PathPurpose
nuxt.config.tsRegisters @nuxt/ui, injects runtime config schema
app/app.config.tsAll configurable options under xAuth namespace with TypeScript type augmentation
app/composables/useXAuth.tsBetter Auth client wrapper — all session state and auth methods
app/composables/useOrganization.tsOrganizations & 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.vueAuth layout (centered card or split panel)
app/middleware/auth.global.tsGlobal route guard
app/middleware/organization.global.tsOrganization route guard
app/plugins/auth-token.tsToken injection plugin
app/utils/fieldMapper utility

Environment Variables

VariableRequiredDescription
NUXT_PUBLIC_X_AUTH_BASE_URLYesBase 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.

Copyright © 2026