Nuxt Integration

Integrate Better Auth with Nuxt.

Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the installation.

Mount the handler

Mount the Better Auth handler to a catch-all Nitro route. Create a file at server/api/auth/[...all].ts:

server/api/auth/[...all].ts
import { auth } from "~~/lib/auth"; // import your auth config

export default defineEventHandler((event) => {
	return auth.handler(toWebRequest(event));
});

You can change the mount path in your Better Auth configuration, but keeping it as /api/auth/[...all] is recommended.

Create a client

Create an auth client using better-auth/vue so useSession returns Vue refs:

lib/auth-client.ts
import { createAuthClient } from "better-auth/vue";

export const authClient = createAuthClient();
export const { signIn, signUp, signOut, useSession } = authClient;

Use the session

Use authClient.useSession(useFetch) inside a page setup() to load the session on the server and hydrate it on the client. Pass Nuxt's useFetch so the request is made with the incoming cookies and the payload is reused during hydration:

app/pages/index.vue
<script setup lang="ts">
import { authClient } from "~~/lib/auth-client";

const { data: session } = await authClient.useSession(useFetch);
</script>

<template>
	<div v-if="session">
		<p>Welcome, {{ session.user.name }}</p>
		<button @click="authClient.signOut()">Sign out</button>
	</div>
	<button v-else @click="authClient.signIn.social({ provider: 'github' })">
		Continue with GitHub
	</button>
</template>

For client-only widgets like popovers or menus, call authClient.useSession() without an argument. It returns a reactive ref that updates on sign-in and sign-out.

Protect pages

Create a named route middleware and opt pages into it with definePageMeta:

app/middleware/auth.ts
import { authClient } from "~~/lib/auth-client";

export default defineNuxtRouteMiddleware(async (to) => {
	const { data: session } = await authClient.useSession(useFetch);
	if (!session.value) {
		return navigateTo({ path: "/login", query: { redirect: to.fullPath } });
	}
});
app/pages/dashboard.vue
<script setup lang="ts">
definePageMeta({ middleware: "auth" });
</script>

Rename the file to app/middleware/auth.global.ts to run it on every route. Always return the navigateTo(...) call, since calling it without return is a no-op.

Protect server routes

Call auth.api.getSession with the incoming event.headers and guard the route with createError:

server/api/me.get.ts
import { auth } from "~~/lib/auth";

export default defineEventHandler(async (event) => {
	const session = await auth.api.getSession({ headers: event.headers });
	if (!session?.user) {
		throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
	}
	return { user: session.user };
});

If you need to reuse this across many routes, factor the check into your own server/utils/ helper.

Use the client during SSR

Aside from useSession(useFetch), authClient actions don't forward cookies during SSR by default, so they return as unauthenticated. Two ways to handle this:

Approach A: call them on the client only. Wrap the consumer in <ClientOnly> or guard with import.meta.client:

<script setup lang="ts">
import { authClient } from "~~/lib/auth-client";

const accounts = ref<Awaited<ReturnType<typeof authClient.listAccounts>>["data"]>();

onMounted(async () => {
	const { data } = await authClient.listAccounts();
	accounts.value = data;
});
</script>

Approach B: create a request-scoped client that forwards cookies during SSR. Add a useAuth composable and combine it with useAsyncData:

app/composables/useAuth.ts
import { createAuthClient } from "better-auth/vue";

export function useAuth() {
	const url = useRequestURL();
	const headers = import.meta.server ? useRequestHeaders(["cookie"]) : undefined;
	return createAuthClient({
		baseURL: url.origin,
		fetchOptions: { headers },
	});
}
app/pages/settings.vue
<script setup lang="ts">
const { data: accounts } = await useAsyncData("accounts", () =>
	useAuth().listAccounts().then((res) => res.data),
);
</script>

Reuse with a Nuxt layer

When you want to reuse the same auth setup across multiple Nuxt apps (for example a marketing site and a dashboard), extract lib/auth.ts, server/, app/middleware/, and app/composables/ into a Nuxt layer and extend it from each app:

nuxt.config.ts
export default defineNuxtConfig({
	extends: ["./layers/auth"],
});

Resources & examples