This is a demo site.Purchase now

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

Environment variables

Before using authentication, you need to configure these environment variables in your .env file:

# Authentication (better-auth)
# Generate with: openssl rand -base64 32
BETTER_AUTH_SECRET=your-secret-key-here
BETTER_AUTH_URL=http://localhost:3000
  • BETTER_AUTH_SECRET - A random secret key used for signing tokens and cookies. Generate a secure value using openssl rand -base64 32 or see the better-auth documentation for other generation methods.
  • BETTER_AUTH_URL - The base URL of your application. Use http://localhost:3000 for local development and your production domain when deployed.
Never commit your actual BETTER_AUTH_SECRET to version control. Keep it secure and regenerate it if exposed.

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 { PrismaClient } from '@prisma/client'
import { emailOTP } from 'better-auth/plugins'

const prisma = new PrismaClient()

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()],
})
The actual implementation in server/utils/auth.ts includes additional functionality like custom email sending logic, database hooks for user creation, and email change verification. The simplified version above shows the core configuration structure.

What's configured

The authentication setup includes:

  • Email templates - Custom HTML templates with automatic variable replacement (logo, site name, URLs).
  • Email sending - Integration with your email service (Resend) for all auth emails.
  • Database hooks - Automatically creates UserData records when users sign up.
  • Email change - Secure email change flow with verification sent to the old email.
UserData is a custom database table for storing user preferences and other user-related data points beyond what better-auth provides. It's not essential to the authentication system and can be removed if you don't need it. User data is accessible through the user Pinia store.

To customize any of these, edit server/utils/auth.ts directly.

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 email to the user's inbox
  3. User opens the email and clicks the reset link
  4. User is redirected to the set password page
  5. User enters and confirms their new password
  6. Password is updated and user can log in with the new credentials

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'
import { emailOTPClient } from 'better-auth/client/plugins'

export const authClient = createAuthClient({
  plugins: [emailOTPClient()],
})

The authClient is auto-imported and available throughout your app.

Usage examples

To see real-world usage of the auth client, check out these components in the template:

  • app/components/auth/PasswordLoginForm.vue - Email/password sign in
  • app/components/auth/RegisterForm.vue - User registration
  • app/components/auth/OtpLoginForm.vue - OTP authentication
  • app/components/auth/ResetPasswordForm.vue - Request password reset
  • app/components/auth/SetPasswordForm.vue - Reset password with token
  • app/components/settings/ChangeEmailForm.vue - Email change
  • app/components/settings/ChangeNameForm.vue - Update user info
  • app/stores/user.ts - Session management and sign out

For the complete client API reference and all available methods, see the better-auth client documentation.

Change the baseURL to your production domain when deploying. You can use environment variables for this.

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

Pages are automatically protected by default via the global auth middleware (app/middleware/auth.global.ts). Any route that isn't in the public routes list requires authentication and will redirect unauthenticated users to /auth/login.

You don't need to add any middleware to protect pages - they're secure out of the box.

Making pages public

To make a specific page public (accessible without authentication), you have two options:

Option 1: Use the public layout

<script setup>
definePageMeta({
  layout: 'public',
})
</script>

Option 2: Add to the public routes list

Edit app/middleware/auth.global.ts and add your route to either:

  • publicPrefixes - For route prefixes (e.g., /blog/ makes all blog routes public)
  • exactPublicRoutes - For exact path matches (e.g., /pricing)
app/middleware/auth.global.ts
const publicPrefixes = ['/auth/', '/blog/', '/checkout/', '/docs']
const exactPublicRoutes = ['/', '/pricing']

Why secure by default?

This template implements secure by default authentication: all routes start as protected, and you explicitly mark which ones should be public. This is a security best practice where systems are configured with maximum security from the outset.

The key advantage: If you forget to configure a route, it fails closed (protected) rather than fails open (exposed). This prevents accidental data exposure.

Additional benefits:

  • Better scalability - Most SaaS apps have 5-10 public pages (landing, pricing, blog) and 50+ protected pages (dashboard, settings, admin). With secure by default, you configure the minority, not the majority.
  • Lower cognitive load - You only ask "Should this be public?" for marketing pages. You don't need to remember which pages contain sensitive data or might need protection later.
  • Aligns with security principles - Follows the principle of least privilege (access denied by default, granted explicitly) and zero-trust architecture (nothing trusted until explicitly allowed).

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>
`

Security features

Better-auth provides built-in security features:

  • Secure password hashing - Passwords are never stored in plain text.
  • HTTP-only cookies - Session tokens are not accessible via JavaScript.
  • Secure, SameSite cookies - Protection against CSRF attacks.
  • Secure email verification - Verification and reset tokens expire and are single-use.
  • Session management - Automatic expiration and secure token handling.

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.

Common tasks

Get the current user

In a component:

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

In an API route:

// Use requireAuth when you only need the userId
const userId = await requireAuth(event)

// Use event.context when you need other user properties (email, name, etc.)
const user = event.context.user
if (!user) {
  throw createError({ statusCode: 401, message: 'Unauthorized' })
}

Check if user is authenticated

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

<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 { logout } = useUserStore()

async function handleSignOut() {
  await logout()
}
</script>

Refresh user data

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

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.