This guide is specifically for when you have a Nuxt 3 + Drizzle Setup (I wrote a guide for this part too.) and now want to use Better-Auth with it. It is highly opinionated to what I use and what works for my projects. I use pnpm, but you can use every package manager you like of course.
TLDR: GitHub Example Repo
I wrote this guide while migrating a project from Supabase Auth to Better Auth, so maybe this helps :) Let me know if something's unclear!
Let's start with the server/database side of the installation.
Install better-auth
pnpm add better-auth
Then, add these two new variables into your .env
. You can generate a random string as a auth secret, or use the generator at the Better Auth Installation Guide.
BETTER_AUTH_SECRET="YOUR_RANDOM_SECRET"
BETTER_AUTH_URL="http://localhost:3000" # Base URL of your app
Add Server Auth Utils
Create new server/utils/auth.ts
. This is the server-side better-auth client to work with. You also notice that this connects directly with your Drizzle Database instance.
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { db } from './db'
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
},
database: drizzleAdapter(db, {
provider: 'pg',
}),
})
Generate Auth DB schema
Now you can use the better-auth CLI to generate and migrate your new auth-related tables.
npx @better-auth/cli generate --config server/utils/auth.ts
It will ask you if you want to generate a schema, you can confirm that. it will generate a auth-schema.ts
file in your project root. Move this file to server/db/auth-schema.ts
.
In this file, there are a few things which I'd personally change.
1. Cuid2 IDs
I prefer to use automatically generated cuid2 ids. So for every table that's been generated, replace the id field with this:
// ...
import { createId } from '@paralleldrive/cuid2'
export const user = pgTable('user', {
id: text('id').primaryKey().$defaultFn(createId),
// ...
})
// ...
2. Timestamp with TZ
Instead of using the regular timestamps, I prefer to use timestamptz to also store the timezone. For every timestamp()
usage, add the withTimezone: true
option
// ...
export const user = pgTable('user', {
// ...
createdAt: timestamp('created_at', {
withTimezone: true,
}).notNull(),
updatedAt: timestamp('updated_at', {
withTimezone: true,
}).notNull(),
})
// ...
Add schema to drizzle config
Now you should have 2 schema files in your /server/db
directory. schema.ts
and auth-schema.ts
For drizzle to notice this new schema, modify your drizzle.config.ts
:
// ...
schema: ['./server/db/schema.ts', './server/db/auth-schema.ts'],
// ...
Also update your /server/utils/db.ts
:
// ...
import * as schema from '../db/schema'
import * as authSchema from '../db/auth-schema'
// ...
export const db = drizzle(queryClient, {
schema: {
...schema,
...authSchema,
},
})
Migrate Auth DB Schema
After you have created your schema file, generate a migration file it with pnpm db:generate
which is just an alias script for (drizzle-kit generate
). The new sql migration file should create everything you need. You can migrate it depending on your migration strategy. I use pnpm db:migrate
script which just runs drizzle-kit migrate
.
Create Server API Catchall Endpoint
For the Nuxt API to be generated, create the following endpoint:
/server/api/[...auth].ts
import { auth } from '@/server/utils/auth'
export default defineEventHandler(event => {
return auth.handler(toWebRequest(event))
})
That's all to make your whole Auth API work! Awesome!
Implement Client Composable
To have a global reusable composable to get the current user session, use something like this:
composables/auth.ts
import { createAuthClient } from 'better-auth/vue'
export const useAuthClient = () => {
return createAuthClient({
plugins: [],
})
}
export const useAuthUser = async () => {
const authClient = useAuthClient()
return {
user: (await authClient.useSession(useFetch)).data.value?.user,
}
}
Implement Client-Side Middleware
To protect your routes, you can use a global middleware like this in middleware/auth.global.ts
export default defineNuxtRouteMiddleware(async to => {
const isUserNavigatingToTheApp = to.path.startsWith('/app')
if (!isUserNavigatingToTheApp) {
return
}
const { data: loggedIn } = await useAuthClient().useSession(useFetch)
if (!loggedIn.value) {
return navigateTo('/auth/login')
}
})
This makes sure that all routes that are /app
or /app/...
are protected and will be redirected to login if no authenticated user (session) has been found.
Implement Login
Again, super opinionated – This is just an example how you can implement a basic login screen with zod and Nuxt UI.
<template>
<div>
<h1>Login</h1>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField label="Email" name="email">
<UInput v-model="state.email" />
</UFormField>
<UFormField label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormField>
<UButton type="submit">Submit</UButton>
</UForm>
</div>
</template>
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '#ui/types'
const { signIn } = useAuthClient()
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string(),
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({
email: undefined,
password: undefined,
})
const pending = ref(false)
const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
await signIn.email(
{
email: event.data.email,
password: event.data.password,
},
{
onRequest: () => {
pending.value = true
},
onResponse: () => {
pending.value = false
},
onSuccess: () => {
navigateTo('/app')
},
onError: errorContext => {
toast.add({
title: 'Error',
description: errorContext.error.message,
color: 'red',
})
},
}
)
}
</script>
Better Examples are in the Better Auth Docs for email authentication.
Disabling SSR
Highly opinionated too, but for everything auth & app related, I tend to just disable SSR in the nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/app/*': {
ssr: false,
},
'/auth/*': {
ssr: false,
},
},
})
Going further
Congrats! 🚀 You now have a very simple Drizzle & Better Auth setup with Nuxt 3.
It is also incredibly easy to add social logins, passkeys or whatever, but that's kind of out of scope. The docs are really really well made – check them out.
You can also check out my example repo where I installed Nuxt UI, Drizzle and Better-Auth in an example: https://github.com/madebyfabian/nuxt-drizzle-better-auth