Loading...
Loading...
Provides Better Auth authentication integration patterns for NestJS backend and Next.js frontend with Drizzle ORM and PostgreSQL. Use when implementing authentication - Setting up Better Auth with NestJS backend, Integrating Next.js App Router frontend, Configuring Drizzle ORM schema with PostgreSQL, Implementing social login (GitHub, Google, etc.), Adding plugins (2FA, Organization, SSO, Magic Link, Passkey), Email/password authentication with session management, Creating protected routes and middleware
npx skill4agent add giuseppe-trisciuoglio/developer-kit better-auth# Backend (NestJS)
npm install better-auth @auth/drizzle-adapter
npm install drizzle-orm pg
npm install -D drizzle-kit
# Frontend (Next.js)
npm install better-authsrc/
├── auth/
│ ├── auth.module.ts # Auth module configuration
│ ├── auth.controller.ts # Auth HTTP endpoints
│ ├── auth.service.ts # Business logic
│ ├── auth.guard.ts # Route protection
│ └── schema.ts # Drizzle auth schema
├── database/
│ ├── database.module.ts # Database module
│ └── database.service.ts # Drizzle connection
└── main.tsapp/
├── (auth)/
│ ├── sign-in/
│ │ └── page.tsx # Sign in page
│ └── sign-up/
│ └── page.tsx # Sign up page
├── (dashboard)/
│ ├── dashboard/
│ │ └── page.tsx # Protected page
│ └── layout.tsx # With auth check
├── api/
│ └── auth/
│ └── [...auth]/route.ts # Auth API route
├── layout.tsx # Root layout
└── middleware.ts # Auth middleware
lib/
├── auth.ts # Better Auth client
└── utils.tsnpm install drizzle-orm pg @auth/drizzle-adapter better-auth
npm install -D drizzle-kitdrizzle.config.tsnpx drizzle-kit generate
npx drizzle-kit migrate// 1. Create auth instance
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: { ...schema }
}),
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}
}
});
// 2. Create auth controller
@Controller('auth')
export class AuthController {
@All('*')
async handleAuth(@Req() req: Request, @Res() res: Response) {
return auth.handler(req);
}
}/auth/*// middleware.ts
import { auth } from '@/lib/auth';
export default auth((req) => {
if (!req.auth && req.nextUrl.pathname.startsWith('/dashboard')) {
const newUrl = new URL('/sign-in', req.nextUrl.origin);
return Response.redirect(newUrl);
}
});
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*']
};/sign-in/dashboard/*// 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>
);
}// Enable 2FA plugin
export const auth = betterAuth({
plugins: [
twoFactor({
issuer: 'MyApp',
otpOptions: {
digits: 6,
period: 30
}
})
]
});
// Client-side enable 2FA
const { data, error } = await authClient.twoFactor.enable({
password: 'user-password'
});// Server-side: Configure 2FA with OTP sending
export const auth = betterAuth({
plugins: [
twoFactor({
issuer: 'MyApp',
otpOptions: {
async sendOTP({ user, otp }, ctx) {
// Send OTP via email, SMS, or other method
await sendEmail({
to: user.email,
subject: 'Your verification code',
body: `Code: ${otp}`
});
}
}
})
]
});
// Client-side: Verify TOTP and trust device
const verify2FA = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyTotp({
code,
trustDevice: true // Device trusted for 30 days
});
if (data) {
// Redirect to dashboard
router.push('/dashboard');
}
};// Server-side: Configure passkey plugin
import { passkey } from '@better-auth/passkey';
export const auth = betterAuth({
plugins: [
passkey({
rpID: 'example.com', // Relying Party ID (your domain)
rpName: 'My App', // Display name
advanced: {
webAuthnChallengeCookie: 'my-app-passkey'
}
})
]
});
// Client-side: Register passkey
const registerPasskey = async () => {
const { data, error } = await authClient.passkey.register({
name: 'My Device'
});
if (data) {
console.log('Passkey registered successfully');
}
};
// Client-side: Sign in with passkey
const signInWithPasskey = async () => {
await authClient.signIn.passkey({
autoFill: true, // Enable conditional UI
fetchOptions: {
onSuccess() {
router.push('/dashboard');
}
}
});
};// Component with conditional UI support
'use client';
import { useEffect } from 'react';
import { authClient } from '@/lib/auth/client';
export default function SignInPage() {
useEffect(() => {
// Check for conditional mediation support
if (!PublicKeyCredential.isConditionalMediationAvailable ||
!PublicKeyCredential.isConditionalMediationAvailable()) {
return;
}
// Enable passkey autofill
void authClient.signIn.passkey({ autoFill: true });
}, []);
return (
<form>
<label htmlFor="email">Email:</label>
<input
type="email"
name="email"
autoComplete="username webauthn"
/>
<label htmlFor="password">Password:</label>
<input
type="password"
name="password"
autoComplete="current-password webauthn"
/>
<button type="submit">Sign In</button>
</form>
);
}// Enable 2FA - backup codes are generated automatically
const enable2FA = async (password: string) => {
const { data, error } = await authClient.twoFactor.enable({
password
});
if (data) {
// IMPORTANT: Display backup codes to user immediately
console.log('Backup codes (save these securely):');
data.backupCodes.forEach((code: string) => {
console.log(code);
});
// Show TOTP URI as QR code
const qrCodeUrl = data.totpURI;
displayQRCode(qrCodeUrl);
}
};
// Recover with backup code
const recoverWithBackupCode = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyBackupCode({
code
});
if (data) {
// Allow user to disable 2FA or set up new authenticator
router.push('/settings/2fa');
}
};// NestJS Guard
@Controller('dashboard')
@UseGuards(AuthGuard)
export class DashboardController {
@Get()
getDashboard(@Request() req) {
return req.user;
}
}// Next.js Server Component
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function Dashboard() {
const session = await auth();
if (!session) {
redirect('/sign-in');
}
return <div>Welcome {session.user.name}</div>;
}// Get session in API route
const session = await auth.api.getSession({
headers: await headers()
});
// Get session in Server Component
const session = await auth();
// Get session in Client Component
'use client';
import { useSession } from '@/lib/auth/client';
const { data: session } = useSession();openssl rand -base64 32ngrok.env.gitignore{
"dependencies": {
"better-auth": "^1.2.0",
"@auth/drizzle-adapter": "^1.0.0",
"drizzle-orm": "^0.35.0",
"pg": "^8.12.0",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/config": "^3.0.0"
},
"devDependencies": {
"drizzle-kit": "^0.24.0",
"@types/pg": "^8.11.0"
}
}{
"dependencies": {
"better-auth": "^1.2.0",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}BETTER_AUTH_SECRETBETTER_AUTH_URLnpx better-auth typegenbetter-authdrizzle-kit pushcredentials: 'include'references/nestjs-setup.mdreferences/nextjs-setup.mdreferences/plugins.mdassets/Assets/env.example# Database
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# Better Auth
BETTER_AUTH_SECRET=your-secret-key-min-32-chars
BETTER_AUTH_URL=http://localhost:3000
# OAuth Providers
AUTH_GITHUB_CLIENT_ID=your-github-client-id
AUTH_GITHUB_CLIENT_SECRET=your-github-client-secret
AUTH_GOOGLE_CLIENT_ID=your-google-client-id
AUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret
# Email (for magic links and verification)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your-smtp-user
SMTP_PASSWORD=your-smtp-password
SMTP_FROM=noreply@example.com
# Session (optional, for Redis)
REDIS_URL=redis://localhost:6379