Better Auth Integration Guide
Overview
Better Auth is a type-safe authentication framework for TypeScript supporting multiple providers, 2FA, SSO, organizations, and passkeys. This skill covers integration patterns for NestJS backend with Drizzle ORM + PostgreSQL and Next.js App Router frontend.
When to Use
- Setting up Better Auth with NestJS backend
- Integrating Next.js App Router frontend
- Configuring Drizzle ORM schema with PostgreSQL
- Implementing social login (GitHub, Google, Facebook, Microsoft)
- Adding MFA/2FA with TOTP, passkey passwordless auth, or magic links
- Managing trusted devices and backup codes for account recovery
- Building multi-tenant apps with organizations or SSO
- Creating protected routes with session management
Quick Start
Installation
# Backend (NestJS)
npm install better-auth @auth/drizzle-adapter drizzle-orm pg
npm install -D drizzle-kit
# Frontend (Next.js)
npm install better-auth
4-Phase Setup
- Database: Install Drizzle, configure schema, run migrations
- Backend: Create Better Auth instance with NestJS module
- Frontend: Configure auth client, create pages, add middleware
- Plugins: Add 2FA, passkey, organizations as needed
See references/nestjs-setup.md for complete backend setup, references/plugins.md for plugin configuration.
Instructions
Phase 1: Database Setup
-
Install dependencies
npm install drizzle-orm pg @auth/drizzle-adapter better-auth npm install -D drizzle-kit -
Create Drizzle config (
drizzle.config.ts)import { defineConfig } from 'drizzle-kit'; export default defineConfig({ schema: './src/auth/schema.ts', out: './drizzle', dialect: 'postgresql', dbCredentials: { url: process.env.DATABASE_URL! }, }); -
Generate and run migrations
npx drizzle-kit generate npx drizzle-kit migrateCheckpoint: Verify tables created:
psql $DATABASE_URL -c "\dt"should showuser,account,session,verification_tokentables.
Phase 2: Backend Setup (NestJS)
-
Create database module - Set up Drizzle connection service
-
Configure Better Auth instance
// src/auth/auth.instance.ts import { betterAuth } from 'better-auth'; import { drizzleAdapter } from '@auth/drizzle-adapter'; import * as schema from './schema'; export const auth = betterAuth({ database: drizzleAdapter(schema, { provider: 'postgresql' }), emailAndPassword: { enabled: true }, socialProviders: { github: { clientId: process.env.AUTH_GITHUB_CLIENT_ID!, clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!, } } }); -
Create auth controller
@Controller('auth') export class AuthController { @All('*') async handleAuth(@Req() req: Request, @Res() res: Response) { return auth.handler(req); } }Checkpoint: Test endpoint
GET /auth/get-sessionreturns{ session: null }when unauthenticated (no error).
Phase 3: Frontend Setup (Next.js)
-
Configure auth client (
lib/auth.ts)import { createAuthClient } from 'better-auth/client'; export const authClient = createAuthClient({ baseURL: process.env.NEXT_PUBLIC_APP_URL! }); -
Add middleware (
middleware.ts)import { auth } from '@/lib/auth'; export default auth((req) => { if (!req.auth && req.nextUrl.pathname.startsWith('/dashboard')) { return Response.redirect(new URL('/sign-in', req.nextUrl.origin)); } }); export const config = { matcher: ['/dashboard/:path*'] }; -
Create sign-in page with form or social buttons
Checkpoint: Navigating to
/dashboardwhen logged out should redirect to/sign-in.
Phase 4: Advanced Features
Add plugins from references/plugins.md:
-
2FA:
twoFactor({ issuer: 'AppName', otpOptions: { sendOTP } }) -
Passkey:
passkey({ rpID: 'domain.com', rpName: 'App' }) -
Organizations:
organization({ avatar: { enabled: true } }) -
Magic Link:
magicLink({ sendMagicLink }) -
SSO:
sso({ saml: { enabled: true } })Checkpoint: After adding plugins, re-run migrations and verify new tables exist.
Examples
Example 1: Server Component with Session
Input: Display user data in a Next.js Server Component.
// app/dashboard/page.tsx
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const session = await auth();
if (!session) {
redirect('/sign-in');
}
return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p>Email: {session.user.email}</p>
</div>
);
}
Output: Renders user info for authenticated users; redirects unauthenticated to sign-in.
Example 2: 2FA TOTP Verification with Trusted Device
Input: User has 2FA enabled and wants to sign in, marking device as trusted.
// Server: Configure 2FA with OTP sending
export const auth = betterAuth({
plugins: [
twoFactor({
issuer: 'MyApp',
otpOptions: {
async sendOTP({ user, otp }, ctx) {
await sendEmail({
to: user.email,
subject: 'Your verification code',
body: `Code: ${otp}`
});
}
}
})
]
});
// Client: Verify TOTP and trust device
const verify2FA = async (code: string) => {
const { data } = await authClient.twoFactor.verifyTotp({
code,
trustDevice: true // Device trusted for 30 days
});
if (data) {
router.push('/dashboard');
}
};
Output: User authenticated; device trusted for 30 days without 2FA prompt.
Example 3: Passkey Registration and Login
Input: Enable passkey (WebAuthn) authentication for passwordless login.
// Server
import { passkey } from '@better-auth/passkey';
export const auth = betterAuth({
plugins: [
passkey({
rpID: 'example.com',
rpName: 'My App',
})
]
});
// Client: Register passkey
const registerPasskey = async () => {
const { data } = await authClient.passkey.register({
name: 'My Device'
});
};
// Client: Sign in with autofill
const signInWithPasskey = async () => {
await authClient.signIn.passkey({
autoFill: true, // Browser suggests passkey
});
};
Output: Users can register and authenticate with biometrics, PIN, or security keys.
For more examples (backup codes, organizations, magic link, conditional UI), see references/plugins.md and references/passkey.md.
Best Practices
- Environment Variables: Store all secrets in
.env, add to.gitignore - Secret Generation: Use
openssl rand -base64 32forBETTER_AUTH_SECRET - HTTPS Required: OAuth callbacks need HTTPS (use
ngrokfor local testing) - Session Expiration: Configure based on security requirements (7 days default)
- Database Indexing: Add indexes on
email,userIdfor performance - Error Handling: Return generic errors without exposing sensitive details
- Rate Limiting: Add to auth endpoints to prevent brute force attacks
- Type Safety: Use
npx better-auth typegenfor full TypeScript coverage
Constraints and Warnings
Security Notes
- Never commit secrets: Add
.envto.gitignore; never commit OAuth secrets or DB credentials - Validate redirect URLs: Always validate OAuth redirect URLs to prevent open redirects
- Hash passwords: Better Auth handles password hashing automatically; never implement custom hashing
- Session storage: For production, use Redis or another scalable session store
- HTTPS Only: Always use HTTPS for authentication in production
- Email Verification: Always implement email verification for password-based auth
Known Limitations
- Better Auth requires Node.js 18+ for Next.js App Router support
- Some OAuth providers require specific redirect URL formats
- Passkeys require HTTPS and compatible browsers
- Organization features require additional database tables
Resources
Documentation
- Better Auth - Official documentation
- Drizzle ORM - Database ORM
- NestJS - Backend framework
- Next.js - Frontend framework
Reference Implementations
references/nestjs-setup.md- Complete NestJS backend setupreferences/nextjs-setup.md- Complete Next.js frontend setupreferences/plugins.md- Plugin configuration (2FA, passkey, organizations, SSO, magic link)references/mfa-2fa.md- Detailed MFA/2FA guidereferences/passkey.md- Detailed passkey implementationreferences/schema.md- Drizzle schema referencereferences/social-providers.md- Social provider configuration