This boilerplate uses better-auth for a complete, production-ready authentication system with email verification, password reset, OTP login, and more.
The authentication system provides:
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.BETTER_AUTH_SECRET to version control. Keep it secure and regenerate it if exposed.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.The authentication setup includes:
UserData records when users sign up.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.
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).
Users can request a one-time password sent to their email for passwordless login:
The password reset flow:
Sessions are managed efficiently with:
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.
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 outFor 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.The centralized user store (app/stores/user.ts) provides reactive authentication state:
const userStore = useUserStore()
const { user, isAuthenticated, isLoading } = storeToRefs(userStore)
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.
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']
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:
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}!`,
}
})
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 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.To customize an email template:
server/email-templates/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>
`
Better-auth provides built-in security features:
To add OAuth providers like Google or GitHub:
server/utils/auth.tsSee the better-auth documentation for provider-specific guides.
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' })
}
<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>
<script setup>
const { logout } = useUserStore()
async function handleSignOut() {
await logout()
}
</script>
const userStore = useUserStore()
await userStore.fetchSession()