From d1f8115e8e1bc3a5ecf0e13119dd241fa1a37590 Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 5 Feb 2026 12:33:20 +0100 Subject: [PATCH 1/2] feat: add client config extension hook --- .../1.getting-started/3.client-setup.md | 21 +++++++- src/module.ts | 52 ++++++++++++++++++- src/runtime/app/composables/useUserSession.ts | 33 +++++++----- src/runtime/config.ts | 5 +- src/types/hooks.ts | 27 ++++++++-- 5 files changed, 115 insertions(+), 23 deletions(-) diff --git a/docs/content/1.getting-started/3.client-setup.md b/docs/content/1.getting-started/3.client-setup.md index fa08165b..8e9db0be 100644 --- a/docs/content/1.getting-started/3.client-setup.md +++ b/docs/content/1.getting-started/3.client-setup.md @@ -23,7 +23,7 @@ export default defineClientAuth(({ siteUrl }) => ({ ``` ::note -The helper creates a factory function that the module calls with the correct `baseURL` at runtime. +`defineClientAuth` returns a config factory. The module calls it with the resolved `baseURL` and creates the client at runtime. :: ## Using Plugins @@ -41,6 +41,25 @@ export default defineClientAuth({ }) ``` +## Extending Client Config via Hook + +Modules can inject client plugins without requiring users to edit `app/auth.config.ts`. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + hooks: { + 'better-auth:client:extend'(config) { + config.plugins = [ + { + from: 'better-auth/client/plugins', + name: 'adminClient', + }, + ] + }, + }, +}) +``` + ## Common Plugin Combinations ### Admin + Two Factor diff --git a/src/module.ts b/src/module.ts index f7b218d1..a2eecc7d 100644 --- a/src/module.ts +++ b/src/module.ts @@ -3,6 +3,7 @@ import type { BetterAuthPlugin } from 'better-auth' import type { BetterAuthModuleOptions } from './runtime/config' import type { AuthRouteRules } from './runtime/types' import type { CasingOption } from './schema-generator' +import type { ClientExtendConfig } from './types/hooks' import { randomBytes } from 'node:crypto' import { existsSync, readFileSync, writeFileSync } from 'node:fs' import { mkdir, writeFile } from 'node:fs/promises' @@ -436,13 +437,60 @@ export type { AuthMeta, AuthMode, AuthRouteRules, UserMatch, RequireSessionOptio addTypeTemplate({ filename: 'types/nuxt-better-auth-client.d.ts', getContents: () => ` -import type createAppAuthClient from '${clientConfigPath}' +import type { createAuthClient } from 'better-auth/vue' +import type getClientConfig from '${clientConfigPath}' +type _ClientConfig = ReturnType declare module '#nuxt-better-auth' { - export type AppAuthClient = ReturnType + export type AppAuthClient = ReturnType<(typeof createAuthClient)<_ClientConfig>> } `, }) + // Client config extension via hook + const extendedClientConfig: ClientExtendConfig = {} + await nuxt.callHook('better-auth:client:extend', extendedClientConfig) + + const hasClientExtensions = extendedClientConfig.plugins && extendedClientConfig.plugins.length > 0 + + if (hasClientExtensions) { + const pluginImports = extendedClientConfig.plugins!.map((plugin, index) => { + const importName = plugin.name || `plugin${index}` + return plugin.name + ? `import { ${plugin.name} as ${importName} } from '${plugin.from}'` + : `import ${importName} from '${plugin.from}'` + }).join('\n') + + const pluginInvocations = extendedClientConfig.plugins!.map((plugin, index) => { + const importName = plugin.name || `plugin${index}` + if (plugin.options) + return `${importName}(${JSON.stringify(plugin.options)})` + return `${importName}()` + }).join(', ') + + const clientExtensionsCode = ` +import getUserClientConfig from '${clientConfigPath}' +${pluginImports} + +// Extended plugins from better-auth:client:extend hook +const extendedPlugins = [${pluginInvocations}] + +export default function getClientConfig(baseURL) { + const userConfig = getUserClientConfig(baseURL) + return { + ...userConfig, + plugins: [...extendedPlugins, ...(userConfig.plugins || [])], + } +} +` + const clientExtTemplate = addTemplate({ + filename: 'better-auth/client-extended.mjs', + getContents: () => clientExtensionsCode, + write: true, + }) + nuxt.options.alias['#auth/client'] = clientExtTemplate.dst + consola.info('Client config extended via better-auth:client:extend hook') + } + // HMR nuxt.hook('builder:watch', async (_event, relativePath) => { if (relativePath.includes('auth.config')) { diff --git a/src/runtime/app/composables/useUserSession.ts b/src/runtime/app/composables/useUserSession.ts index 3cc2de7b..76370d0c 100644 --- a/src/runtime/app/composables/useUserSession.ts +++ b/src/runtime/app/composables/useUserSession.ts @@ -1,18 +1,21 @@ -import type { AppAuthClient, AuthSession, AuthUser } from '#nuxt-better-auth' +import type { AuthSession, AuthUser } from '#nuxt-better-auth' import type { ComputedRef, Ref } from 'vue' -import createAppAuthClient from '#auth/client' +import getClientConfig from '#auth/client' import { computed, nextTick, useRequestHeaders, useRequestURL, useRuntimeConfig, useState, watch } from '#imports' +import { createAuthClient } from 'better-auth/vue' + +type AuthClient = ReturnType export interface SignOutOptions { onSuccess?: () => void | Promise } export interface UseUserSessionReturn { - client: AppAuthClient | null + client: AuthClient | null session: Ref user: Ref loggedIn: ComputedRef ready: ComputedRef - signIn: NonNullable['signIn'] - signUp: NonNullable['signUp'] + signIn: NonNullable['signIn'] + signUp: NonNullable['signUp'] signOut: (options?: SignOutOptions) => Promise waitForSession: () => Promise fetchSession: (options?: { headers?: HeadersInit, force?: boolean }) => Promise @@ -20,11 +23,13 @@ export interface UseUserSessionReturn { } // Singleton client instance to ensure consistent state across all useUserSession calls -let _client: AppAuthClient | null = null -function getClient(baseURL: string): AppAuthClient { - if (!_client) - _client = createAppAuthClient(baseURL) - return _client +let _client: AuthClient | null = null +function getClient(baseURL: string): AuthClient { + if (!_client) { + const config = getClientConfig(baseURL) + _client = createAuthClient(config) + } + return _client! } export function useUserSession(): UseUserSessionReturn { @@ -32,8 +37,8 @@ export function useUserSession(): UseUserSessionReturn { const requestURL = useRequestURL() // Client only - create better-auth client for client-side operations (singleton) - const client: AppAuthClient | null = import.meta.client - ? getClient(runtimeConfig.public.siteUrl || requestURL.origin) + const client: AuthClient | null = import.meta.client + ? getClient((runtimeConfig.public.siteUrl as string) || requestURL.origin) : null // Shared state via useState for SSR hydration @@ -99,8 +104,8 @@ export function useUserSession(): UseUserSessionReturn { } // Wrap signIn methods to wait for session sync before calling onSuccess - type SignIn = NonNullable['signIn'] - type SignUp = NonNullable['signUp'] + type SignIn = NonNullable['signIn'] + type SignUp = NonNullable['signUp'] // Wraps onSuccess callback to sync session before executing function wrapOnSuccess(cb: (ctx: unknown) => void | Promise) { diff --git a/src/runtime/config.ts b/src/runtime/config.ts index f97f409e..e38895bd 100644 --- a/src/runtime/config.ts +++ b/src/runtime/config.ts @@ -2,7 +2,6 @@ import type { BetterAuthOptions } from 'better-auth' import type { ClientOptions } from 'better-auth/client' import type { CasingOption } from '../schema-generator' import type { ServerAuthContext } from './types/augment' -import { createAuthClient } from 'better-auth/vue' // Re-export for declaration merging with generated types export type { ServerAuthContext } @@ -56,10 +55,10 @@ export function defineServerAuth(config: T | ((ctx: return typeof config === 'function' ? config : () => config } -export function defineClientAuth(config: T | ((ctx: ClientAuthContext) => T)): (baseURL: string) => ReturnType> { +export function defineClientAuth(config: T | ((ctx: ClientAuthContext) => T)): (baseURL: string) => T & { baseURL: string } { return (baseURL: string) => { const ctx: ClientAuthContext = { siteUrl: baseURL } const resolved = typeof config === 'function' ? config(ctx) : config - return createAuthClient({ ...resolved, baseURL }) + return { ...resolved, baseURL } } } diff --git a/src/types/hooks.ts b/src/types/hooks.ts index a693c1a3..c36ffe7a 100644 --- a/src/types/hooks.ts +++ b/src/types/hooks.ts @@ -1,12 +1,33 @@ import type { BetterAuthOptions } from 'better-auth' +export interface ClientPluginImport { + /** Import path to the plugin file (e.g., 'my-module/runtime/client-plugin') */ + from: string + /** Named export to import (e.g., 'myPlugin'). If not specified, uses default export. */ + name?: string + /** Options to pass to the plugin function */ + options?: Record +} + +export interface ClientExtendConfig { + /** Plugin imports to add to the client config */ + plugins?: ClientPluginImport[] +} + declare module '@nuxt/schema' { interface NuxtHooks { /** - * Extend better-auth config with additional plugins or options. - * Called after user's auth.config.ts is loaded. - * @param config - Partial config to merge into the auth options + * Extend better-auth server config with additional plugins or options. + * Called during schema generation for NuxtHub database. + * @param config - Partial config to merge into the server auth options */ 'better-auth:config:extend': (config: Partial) => void | Promise + + /** + * Extend better-auth client config with additional plugins. + * Called during module setup, affects runtime client. + * @param config - Plugin imports to merge into the client auth options + */ + 'better-auth:client:extend': (config: ClientExtendConfig) => void | Promise } } From 608d25fc9fc290f4d50e7cecd00fb68d4038c828 Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 5 Feb 2026 19:58:27 +0100 Subject: [PATCH 2/2] fix(docs): clarify custom DB CLI + deps --- docs/content/3.guides/3.custom-database.md | 39 +++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/content/3.guides/3.custom-database.md b/docs/content/3.guides/3.custom-database.md index 641c15f9..d0baae65 100644 --- a/docs/content/3.guides/3.custom-database.md +++ b/docs/content/3.guides/3.custom-database.md @@ -13,6 +13,16 @@ Better Auth supports any database through adapters. Use this guide when you need | OAuth-only, no persistence | [Database-less Mode](/guides/database-less-mode) | | Existing database, custom setup | **This guide** | +## Install Better Auth + +Adapter imports come from the `better-auth` package, so you must install it in your app. + +```bash +pnpm add better-auth +``` + +`better-auth` is a peer dependency of `@onmax/nuxt-better-auth`. Keep the major versions aligned to avoid mismatches. + ## Drizzle Adapter ```ts [server/auth.config.ts] @@ -66,11 +76,38 @@ export default defineServerAuth({ You must create the required tables manually. Better Auth provides a CLI to generate schemas: ```bash -npx better-auth generate +npx @better-auth/cli@latest generate ``` This generates migration files for your adapter. Apply them with your database tool. +### CLI config (Nuxt) + +The CLI reads a Better Auth instance from `auth.ts`, not your `server/auth.config.ts`. Add a dedicated `auth.ts` file for CLI generation. + +```ts [auth.ts] +import { betterAuth } from 'better-auth' +import { drizzleAdapter } from 'better-auth/adapters/drizzle' +import { drizzle } from 'drizzle-orm/node-postgres' + +const db = drizzle(process.env.DATABASE_URL!) + +export const auth = betterAuth({ + database: drizzleAdapter(db, { provider: 'pg' }), + emailAndPassword: { enabled: true }, +}) +``` + +If you use Prisma or Kysely, swap the adapter section to match your setup. + +Use `--config` if you place the file elsewhere: + +```bash +npx @better-auth/cli@latest generate --config server/auth/auth.ts +``` + +Keep this config in sync with `server/auth.config.ts` so the generated schema matches your runtime plugins and options. + ::callout{icon="i-lucide-book" to="https://www.better-auth.com/docs/concepts/database"} See **Better Auth database docs** for full schema reference and adapter options. ::