Authentication

Learn how authentication works with better-auth in this boilerplate

This boilerplate uses better-auth for a complete, production-ready authentication system with email verification, password reset, OTP login, and more.

Overview

The authentication system provides:

  • Email/password authentication with email verification
  • OTP (one-time password) login via email
  • Password reset with secure token handling
  • Email change functionality with verification
  • Session management with cookie caching for performance
  • Type-safe client with auto-imported composables

Authentication configuration

The auth configuration is located in server/utils/auth.ts:

server/utils/auth.ts
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import { emailOTP } from 'better-auth/plugins'

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // 1 day
    cookieCache: {
      enabled: true,
      maxAge: 5 * 60, // 5 minutes
    },
  },
  database: prismaAdapter(prisma, {
    provider: 'postgresql',
  }),
  plugins: [emailOTP()],
})

Key features explained

Email verification

When a user signs up, they receive an email with a verification link. Until verified, they cannot log in.

The email template is in server/email-templates/verifyEmailTemplate.ts and uses your configured email service (Resend by default).

OTP login

Users can request a one-time password sent to their email for passwordless login:

  1. User enters their email on the OTP login page
  2. System sends a 6-digit code to their email
  3. User enters the code to authenticate
  4. Session is created upon successful verification
OTP codes expire after a configured time period for security.

Password reset

The password reset flow:

  1. User clicks "Forgot password" and enters their email
  2. System sends a password reset link
  3. User clicks the link and sets a new password
  4. Password is updated and user can log in

Session management

Sessions are managed efficiently with:

  • 7-day expiration - Sessions last 7 days by default
  • Cookie caching - Reduces database queries by caching session data
  • Automatic refresh - Sessions update every 24 hours
  • Secure cookies - HTTP-only, secure, and SameSite protected

Client-side usage

Auth client

The auth client is initialized in app/utils/auth-client.ts:

app/utils/auth-client.ts
import { createAuthClient } from 'better-auth/vue'

export const authClient = createAuthClient()

export const { signIn, signUp, signOut, useSession } = authClient

These composables are auto-imported and available throughout your app.

User store

The centralized user store (app/stores/user.ts) provides reactive authentication state:

const userStore = useUserStore()
const { user, isAuthenticated, isLoading } = storeToRefs(userStore)

Protecting pages

Use the auth middleware to protect routes:

<script setup>
definePageMeta({
  middleware: 'auth',
})
</script>

This redirects unauthenticated users to /auth/login.

Protecting API endpoints

Use the requireAuth utility in your API routes:

server/api/protected.ts
import { requireAuth } from '~/server/utils/require-auth'

export default defineEventHandler(async event => {
  const { user } = await requireAuth(event)

  return {
    message: `Hello ${user.name}!`,
  }
})

Authentication pages

The boilerplate includes pre-built authentication pages:

  • /auth/login - Email/password login
  • /auth/register - User registration
  • /auth/otp-login - OTP (passwordless) login
  • /auth/reset-password - Password reset request
  • /auth/set-password - Set new password (from reset link)

All pages are styled with shadcn-vue components and follow best practices.

Email templates

Email templates are HTML-based and located in server/email-templates/:

  • verifyEmailTemplate.ts - Email verification
  • resetPasswordTemplate.ts - Password reset
  • otpTemplate.ts - OTP code delivery
  • changeEmailTemplate.ts - Email change confirmation
Templates include placeholders like {{action_url}} and {{site_name}} that are replaced with actual values.

Customizing email templates

To customize an email template:

  1. Open the template file in server/email-templates/
  2. Modify the HTML as needed
  3. Keep placeholders for dynamic content
  4. Test by triggering the email flow

Example:

server/email-templates/verifyEmailTemplate.ts
export const verifyEmailTemplate = `
<!DOCTYPE html>
<html>
<body>
  <h1>Verify your email for {{site_name}}</h1>
  <p>Click the link below to verify:</p>
  <a href="{{action_url}}">Verify Email</a>
</body>
</html>
`

Session data

Session data is stored in your PostgreSQL database using Prisma. The schema is in prisma/schema.prisma:

model Session {
  id        String   @id
  expiresAt DateTime
  token     String   @unique
  createdAt DateTime
  updatedAt DateTime
  ipAddress String?
  userAgent String?
  userId    String
  user      User     @relation(fields: [userId], references: [id])
}

Security considerations

The authentication system includes:

  • Secure password hashing - Passwords are never stored in plain text
  • CSRF protection - Prevents cross-site request forgery
  • Rate limiting - Protects against brute force attacks (configure in server/utils/rate-limit.ts)
  • HTTP-only cookies - JavaScript cannot access session tokens
  • Secure email verification - Tokens expire and are single-use

Extending authentication

Adding OAuth providers

To add OAuth providers like Google or GitHub:

  1. Install the better-auth plugin for your provider
  2. Configure the provider in server/utils/auth.ts
  3. Add environment variables for client ID and secret
  4. Add buttons to your login page

See the better-auth documentation for provider-specific guides.

Custom authentication logic

To add custom logic after user registration:

server/utils/auth.ts
export const auth = betterAuth({
  // ... other config
  hooks: {
    after: {
      signUp: async ({ user }) => {
        // Send welcome email
        // Create default user settings
        // Track in analytics
      },
    },
  },
})

Common tasks

Get the current user

In a component:

<script setup>
const userStore = useUserStore()
const { user, isAuthenticated } = storeToRefs(userStore)
</script>

In an API route:

const { user } = await requireAuth(event)

Check if user is authenticated

<template>
  <div v-if="isAuthenticated">Welcome back, {{ user.name }}!</div>
  <div v-else>
    <Button as-child>
      <NuxtLink to="/auth/login">Log in</NuxtLink>
    </Button>
  </div>
</template>

Sign out programmatically

<script setup>
const { signOut } = authClient

async function handleSignOut() {
  await signOut()
  navigateTo('/auth/login')
}
</script>

Refresh user data

const userStore = useUserStore()
await userStore.fetchUser()

Reference

For the most up-to-date information on better-auth features and best practices, always refer to the official documentation at better-auth.com.