Authentication
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:
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:
- User enters their email on the OTP login page
- System sends a 6-digit code to their email
- User enters the code to authenticate
- Session is created upon successful verification
Password reset
The password reset flow:
- User clicks "Forgot password" and enters their email
- System sends a password reset link
- User clicks the link and sets a new password
- 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:
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:
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 verificationresetPasswordTemplate.ts- Password resetotpTemplate.ts- OTP code deliverychangeEmailTemplate.ts- Email change confirmation
{{action_url}} and {{site_name}} that are replaced with actual values.Customizing email templates
To customize an email template:
- Open the template file in
server/email-templates/ - Modify the HTML as needed
- Keep placeholders for dynamic content
- Test by triggering the email flow
Example:
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:
- Install the better-auth plugin for your provider
- Configure the provider in
server/utils/auth.ts - Add environment variables for client ID and secret
- 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:
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()