import { betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { db } from '@/lib/db'; /** * Better Auth server configuration. * * Sessions are stored in PostgreSQL (not Redis) per SECURITY-GUIDELINES.md §1.2. * The drizzle adapter handles session persistence via the existing `sessions` table. */ /** * In dev, allow requests from any LAN IP so the same `pnpm dev` instance can * serve both localhost (Mac) and the LAN IP (iPhone on Wi-Fi). In production, * trustedOrigins is locked down to NEXT_PUBLIC_APP_URL only. */ /** * In dev, allow localhost + any LAN-IP origin so the same `pnpm dev` instance * can serve both Mac (localhost) and iPhone-on-Wi-Fi (192.168.x.x). The * function form is preferred over a static list because the LAN IP can vary * across networks. In production, lock down to NEXT_PUBLIC_APP_URL only. */ const isProd = process.env.NODE_ENV === 'production'; const DEV_ORIGIN_PATTERNS = [ /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/, /^https?:\/\/192\.168\.\d+\.\d+(:\d+)?$/, /^https?:\/\/10\.\d+\.\d+\.\d+(:\d+)?$/, ]; const trustedOrigins: (request?: Request) => Promise = async (request) => { if (isProd) { const prodUrl = process.env.NEXT_PUBLIC_APP_URL; return prodUrl ? [prodUrl] : []; } const origin = request?.headers.get('origin') ?? ''; if (origin && DEV_ORIGIN_PATTERNS.some((re) => re.test(origin))) { return [origin]; } return ['http://localhost:3000', 'http://localhost:3001']; }; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: 'pg', }), trustedOrigins, emailAndPassword: { enabled: true, minPasswordLength: 9, // Accounts are admin-created only — no self-service email verification flow. requireEmailVerification: false, }, session: { // Enable cookie-level session caching to reduce DB reads (5-minute cache). cookieCache: { enabled: true, maxAge: 5 * 60, }, // Absolute session lifetime: 24 hours. expiresIn: 60 * 60 * 24, // Refresh the session whenever the user is active in the last 25% of its lifetime (6h). updateAge: 60 * 60 * 6, }, advanced: { cookiePrefix: 'pn-crm', defaultCookieAttributes: { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict' as const, }, }, logger: { disabled: false, level: 'error' as const, }, }); export type Session = typeof auth.$Infer.Session; export type User = typeof auth.$Infer.Session.user;