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
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 usingopenssl rand -base64 32or see the better-auth documentation for other generation methods.BETTER_AUTH_URL- The base URL of your application. Usehttp://localhost:3000for local development and your production domain when deployed.
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:
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()],
})
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
UserDatarecords 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:
- 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 email to the user's inbox
- User opens the email and clicks the reset link
- User is redirected to the set password page
- User enters and confirms their new password
- 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:
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 inapp/components/auth/RegisterForm.vue- User registrationapp/components/auth/OtpLoginForm.vue- OTP authenticationapp/components/auth/ResetPasswordForm.vue- Request password resetapp/components/auth/SetPasswordForm.vue- Reset password with tokenapp/components/settings/ChangeEmailForm.vue- Email changeapp/components/settings/ChangeNameForm.vue- Update user infoapp/stores/user.ts- Session management and sign out
For the complete client API reference and all available methods, see the better-auth client documentation.
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)
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:
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>
`
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:
- 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.
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()