Skip to Content
🚀 APSO is now in public beta. Get started →
GuidesAuthBetter Auth

Better Auth

Better Auth is the recommended authentication solution for APSO backends. It provides a complete, type-safe authentication system.

Overview

FeatureSupport
Email/Passwordâś…
OAuth Providersâś…
Magic Linksâś…
Two-Factor Authâś…
Session Managementâś…
Password Resetâś…
Email Verificationâś…

Installation

npm install better-auth

Configuration

Basic Setup

// src/lib/auth.ts import { betterAuth } from 'better-auth'; import { Pool } from 'pg'; export const auth = betterAuth({ database: new Pool({ connectionString: process.env.DATABASE_URL, }), emailAndPassword: { enabled: true, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 1 day }, }); export type Session = typeof auth.$Infer.Session;

With OAuth Providers

// src/lib/auth.ts import { betterAuth } from 'better-auth'; export const auth = betterAuth({ database: new Pool({ connectionString: process.env.DATABASE_URL, }), emailAndPassword: { enabled: true, }, socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }, github: { clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }, }, });

NestJS Integration

Auth Module

// src/modules/auth/auth.module.ts import { Module } from '@nestjs/common'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { auth } from '../../lib/auth'; @Module({ controllers: [AuthController], providers: [ AuthService, { provide: 'BETTER_AUTH', useValue: auth, }, ], exports: [AuthService], }) export class AuthModule {}

Auth Controller

// src/modules/auth/auth.controller.ts import { Controller, Post, Body, Get, Req, Res } from '@nestjs/common'; import { Request, Response } from 'express'; import { auth } from '../../lib/auth'; @Controller('auth') export class AuthController { @Post('sign-up') async signUp( @Body() body: { email: string; password: string; name: string }, @Req() req: Request, @Res() res: Response, ) { return auth.api.signUpEmail({ body: { email: body.email, password: body.password, name: body.name, }, asResponse: true, headers: req.headers, }); } @Post('sign-in') async signIn( @Body() body: { email: string; password: string }, @Req() req: Request, @Res() res: Response, ) { return auth.api.signInEmail({ body, asResponse: true, headers: req.headers, }); } @Post('sign-out') async signOut(@Req() req: Request, @Res() res: Response) { return auth.api.signOut({ asResponse: true, headers: req.headers, }); } @Get('session') async getSession(@Req() req: Request) { const session = await auth.api.getSession({ headers: req.headers, }); return session; } }

Auth Guard

// src/common/guards/auth.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { auth } from '../../lib/auth'; @Injectable() export class AuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const session = await auth.api.getSession({ headers: request.headers, }); if (!session) { throw new UnauthorizedException(); } request.user = session.user; request.session = session.session; return true; } }

Organization Support

Custom User Fields

// src/lib/auth.ts import { betterAuth } from 'better-auth'; export const auth = betterAuth({ database: new Pool({ connectionString: process.env.DATABASE_URL, }), user: { additionalFields: { organizationId: { type: 'string', required: true, }, role: { type: 'string', defaultValue: 'member', }, }, }, });

Organization Middleware

// src/common/middleware/organization.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; @Injectable() export class OrganizationMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { if (req.user && !req.user.organizationId) { // Handle users without organization // Redirect to org setup or throw error } next(); } }

Frontend Integration

React Client

// src/lib/auth-client.ts import { createAuthClient } from 'better-auth/react'; export const authClient = createAuthClient({ baseURL: process.env.NEXT_PUBLIC_API_URL, }); export const { signIn, signUp, signOut, useSession, } = authClient;

Login Component

// components/LoginForm.tsx 'use client'; import { useState } from 'react'; import { signIn } from '../lib/auth-client'; export function LoginForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { await signIn.email({ email, password }); window.location.href = '/dashboard'; } catch (err) { setError('Invalid credentials'); } }; return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" /> <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" /> {error && <p className="text-red-500">{error}</p>} <button type="submit">Sign In</button> </form> ); }

Session Hook

// components/UserMenu.tsx 'use client'; import { useSession, signOut } from '../lib/auth-client'; export function UserMenu() { const { data: session, isPending } = useSession(); if (isPending) return <div>Loading...</div>; if (!session) return <a href="/login">Sign In</a>; return ( <div> <span>{session.user.name}</span> <button onClick={() => signOut()}>Sign Out</button> </div> ); }

Two-Factor Authentication

// src/lib/auth.ts import { betterAuth } from 'better-auth'; import { twoFactor } from 'better-auth/plugins'; export const auth = betterAuth({ plugins: [ twoFactor({ issuer: 'YourApp', }), ], });

Environment Variables

# .env DATABASE_URL=postgresql://... BETTER_AUTH_SECRET=your-secret-key # OAuth Providers GOOGLE_CLIENT_ID=... GOOGLE_CLIENT_SECRET=... GITHUB_CLIENT_ID=... GITHUB_CLIENT_SECRET=...
Last updated on