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 migrate
Checkpoint: Verify tables created: psql $DATABASE_URL -c "\dt" should show user, account, session, verification_token tables.
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-session returns { 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 /dashboard when 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 32 for BETTER_AUTH_SECRET
- HTTPS Required: OAuth callbacks need HTTPS (use
ngrok for local testing)
- Session Expiration: Configure based on security requirements (7 days default)
- Database Indexing: Add indexes on
email, userId for 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 typegen for full TypeScript coverage
Constraints and Warnings
Security Notes
- Never commit secrets: Add
.env to .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
Reference Implementations
references/nestjs-setup.md - Complete NestJS backend setup
references/nextjs-setup.md - Complete Next.js frontend setup
references/plugins.md - Plugin configuration (2FA, passkey, organizations, SSO, magic link)
references/mfa-2fa.md - Detailed MFA/2FA guide
references/passkey.md - Detailed passkey implementation
references/schema.md - Drizzle schema reference
references/social-providers.md - Social provider configuration
1---2name: better-auth-83description: Provides Better Auth integration patterns for NestJS backend and Next.js frontend with Drizzle ORM and PostgreSQL. Use when setting up Better Auth with NestJS backend, integrating Next.js App Router frontend, configuring Drizzle ORM schema, implementing social login (GitHub, Google), adding plugins (2FA, Organization, SSO, Magic Link, Passkey), implementing email/password authentication with session management, or creating protected routes and middleware.4---5
6# Better Auth Integration Guide
7
8## Overview
9
10Better 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.
11
12## When to Use
13
14- Setting up Better Auth with NestJS backend
15- Integrating Next.js App Router frontend
16- Configuring Drizzle ORM schema with PostgreSQL
17- Implementing social login (GitHub, Google, Facebook, Microsoft)
18- Adding MFA/2FA with TOTP, passkey passwordless auth, or magic links
19- Managing trusted devices and backup codes for account recovery
20- Building multi-tenant apps with organizations or SSO
21- Creating protected routes with session management
22
23## Quick Start
24
25### Installation
26
27```bash
28# Backend (NestJS)
29npm install better-auth @auth/drizzle-adapter drizzle-orm pg
30npm install -D drizzle-kit
31
32# Frontend (Next.js)
33npm install better-auth
34```
35
36### 4-Phase Setup
37
381. **Database**: Install Drizzle, configure schema, run migrations
392. **Backend**: Create Better Auth instance with NestJS module
403. **Frontend**: Configure auth client, create pages, add middleware
414. **Plugins**: Add 2FA, passkey, organizations as needed
42
43See `references/nestjs-setup.md` for complete backend setup, `references/plugins.md` for plugin configuration.
44
45## Instructions
46
47### Phase 1: Database Setup
48
491. **Install dependencies**
50 ```bash
51 npm install drizzle-orm pg @auth/drizzle-adapter better-auth
52 npm install -D drizzle-kit
53 ```
54
552. **Create Drizzle config** (`drizzle.config.ts`)
56 ```typescript
57 import { defineConfig } from 'drizzle-kit';
58 export default defineConfig({
59 schema: './src/auth/schema.ts',
60 out: './drizzle',
61 dialect: 'postgresql',
62 dbCredentials: { url: process.env.DATABASE_URL! },
63 });
64 ```
65
663. **Generate and run migrations**
67 ```bash
68 npx drizzle-kit generate
69 npx drizzle-kit migrate
70 ```
71
72 **Checkpoint**: Verify tables created: `psql $DATABASE_URL -c "\dt"` should show `user`, `account`, `session`, `verification_token` tables.
73
74### Phase 2: Backend Setup (NestJS)
75
761. **Create database module** - Set up Drizzle connection service
77
782. **Configure Better Auth instance**
79 ```typescript
80 // src/auth/auth.instance.ts
81 import { betterAuth } from 'better-auth';
82 import { drizzleAdapter } from '@auth/drizzle-adapter';
83 import * as schema from './schema';
84
85 export const auth = betterAuth({
86 database: drizzleAdapter(schema, { provider: 'postgresql' }),
87 emailAndPassword: { enabled: true },
88 socialProviders: {
89 github: {
90 clientId: process.env.AUTH_GITHUB_CLIENT_ID!,
91 clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!,
92 }
93 }
94 });
95 ```
96
973. **Create auth controller**
98 ```typescript
99 @Controller('auth')
100 export class AuthController {
101 @All('*')
102 async handleAuth(@Req() req: Request, @Res() res: Response) {
103 return auth.handler(req);
104 }
105 }
106 ```
107
108 **Checkpoint**: Test endpoint `GET /auth/get-session` returns `{ session: null }` when unauthenticated (no error).
109
110### Phase 3: Frontend Setup (Next.js)
111
1121. **Configure auth client** (`lib/auth.ts`)
113 ```typescript
114 import { createAuthClient } from 'better-auth/client';
115 export const authClient = createAuthClient({
116 baseURL: process.env.NEXT_PUBLIC_APP_URL!
117 });
118 ```
119
1202. **Add middleware** (`middleware.ts`)
121 ```typescript
122 import { auth } from '@/lib/auth';
123 export default auth((req) => {
124 if (!req.auth && req.nextUrl.pathname.startsWith('/dashboard')) {
125 return Response.redirect(new URL('/sign-in', req.nextUrl.origin));
126 }
127 });
128 export const config = { matcher: ['/dashboard/:path*'] };
129 ```
130
1313. **Create sign-in page** with form or social buttons
132
133 **Checkpoint**: Navigating to `/dashboard` when logged out should redirect to `/sign-in`.
134
135### Phase 4: Advanced Features
136
137Add plugins from `references/plugins.md`:
138
139- **2FA**: `twoFactor({ issuer: 'AppName', otpOptions: { sendOTP } })`
140- **Passkey**: `passkey({ rpID: 'domain.com', rpName: 'App' })`
141- **Organizations**: `organization({ avatar: { enabled: true } })`
142- **Magic Link**: `magicLink({ sendMagicLink })`
143- **SSO**: `sso({ saml: { enabled: true } })`
144
145 **Checkpoint**: After adding plugins, re-run migrations and verify new tables exist.
146
147## Examples
148
149### Example 1: Server Component with Session
150
151**Input**: Display user data in a Next.js Server Component.
152
153```tsx
154// app/dashboard/page.tsx
155import { auth } from '@/lib/auth';
156import { redirect } from 'next/navigation';
157
158export default async function DashboardPage() {
159 const session = await auth();
160
161 if (!session) {
162 redirect('/sign-in');
163 }
164
165 return (
166 <div>
167 <h1>Welcome, {session.user.name}</h1>
168 <p>Email: {session.user.email}</p>
169 </div>
170 );
171}
172```
173
174**Output**: Renders user info for authenticated users; redirects unauthenticated to sign-in.
175
176### Example 2: 2FA TOTP Verification with Trusted Device
177
178**Input**: User has 2FA enabled and wants to sign in, marking device as trusted.
179
180```typescript
181// Server: Configure 2FA with OTP sending
182export const auth = betterAuth({
183 plugins: [
184 twoFactor({
185 issuer: 'MyApp',
186 otpOptions: {
187 async sendOTP({ user, otp }, ctx) {
188 await sendEmail({
189 to: user.email,
190 subject: 'Your verification code',
191 body: `Code: ${otp}`
192 });
193 }
194 }
195 })
196 ]
197});
198
199// Client: Verify TOTP and trust device
200const verify2FA = async (code: string) => {
201 const { data } = await authClient.twoFactor.verifyTotp({
202 code,
203 trustDevice: true // Device trusted for 30 days
204 });
205
206 if (data) {
207 router.push('/dashboard');
208 }
209};
210```
211
212**Output**: User authenticated; device trusted for 30 days without 2FA prompt.
213
214### Example 3: Passkey Registration and Login
215
216**Input**: Enable passkey (WebAuthn) authentication for passwordless login.
217
218```typescript
219// Server
220import { passkey } from '@better-auth/passkey';
221export const auth = betterAuth({
222 plugins: [
223 passkey({
224 rpID: 'example.com',
225 rpName: 'My App',
226 })
227 ]
228});
229
230// Client: Register passkey
231const registerPasskey = async () => {
232 const { data } = await authClient.passkey.register({
233 name: 'My Device'
234 });
235};
236
237// Client: Sign in with autofill
238const signInWithPasskey = async () => {
239 await authClient.signIn.passkey({
240 autoFill: true, // Browser suggests passkey
241 });
242};
243```
244
245**Output**: Users can register and authenticate with biometrics, PIN, or security keys.
246
247For more examples (backup codes, organizations, magic link, conditional UI), see `references/plugins.md` and `references/passkey.md`.
248
249## Best Practices
250
2511. **Environment Variables**: Store all secrets in `.env`, add to `.gitignore`
2522. **Secret Generation**: Use `openssl rand -base64 32` for `BETTER_AUTH_SECRET`
2533. **HTTPS Required**: OAuth callbacks need HTTPS (use `ngrok` for local testing)
2544. **Session Expiration**: Configure based on security requirements (7 days default)
2555. **Database Indexing**: Add indexes on `email`, `userId` for performance
2566. **Error Handling**: Return generic errors without exposing sensitive details
2577. **Rate Limiting**: Add to auth endpoints to prevent brute force attacks
2588. **Type Safety**: Use `npx better-auth typegen` for full TypeScript coverage
259
260## Constraints and Warnings
261
262### Security Notes
263
264- **Never commit secrets**: Add `.env` to `.gitignore`; never commit OAuth secrets or DB credentials
265- **Validate redirect URLs**: Always validate OAuth redirect URLs to prevent open redirects
266- **Hash passwords**: Better Auth handles password hashing automatically; never implement custom hashing
267- **Session storage**: For production, use Redis or another scalable session store
268- **HTTPS Only**: Always use HTTPS for authentication in production
269- **Email Verification**: Always implement email verification for password-based auth
270
271### Known Limitations
272
273- Better Auth requires Node.js 18+ for Next.js App Router support
274- Some OAuth providers require specific redirect URL formats
275- Passkeys require HTTPS and compatible browsers
276- Organization features require additional database tables
277
278## Resources
279
280### Documentation
281
282- [Better Auth](https://www.better-auth.com) - Official documentation
283- [Drizzle ORM](https://orm.drizzle.team) - Database ORM
284- [NestJS](https://docs.nestjs.com) - Backend framework
285- [Next.js](https://nextjs.org/docs/app) - Frontend framework
286
287### Reference Implementations
288
289- `references/nestjs-setup.md` - Complete NestJS backend setup
290- `references/nextjs-setup.md` - Complete Next.js frontend setup
291- `references/plugins.md` - Plugin configuration (2FA, passkey, organizations, SSO, magic link)
292- `references/mfa-2fa.md` - Detailed MFA/2FA guide
293- `references/passkey.md` - Detailed passkey implementation
294- `references/schema.md` - Drizzle schema reference
295- `references/social-providers.md` - Social provider configuration