import { betterAuth } from 'better-auth'; import { getMigrations } from 'better-auth/db'; import { nextCookies } from 'better-auth/next-js'; import { admin, magicLink, organization } from 'better-auth/plugins'; import { Pool } from 'pg'; declare global { // eslint-disable-next-line no-var var __fiscalAuthPgPool: Pool | undefined; } type BetterAuthInstance = ReturnType; let authInstance: BetterAuthInstance | null = null; let migrationPromise: Promise | null = null; function parseCsvList(value: string | undefined) { return (value ?? '') .split(',') .map((entry) => entry.trim()) .filter((entry) => entry.length > 0); } function isPostgresConnectionString(value: string | undefined) { if (!value) { return false; } try { const protocol = new URL(value).protocol.toLowerCase(); return protocol === 'postgres:' || protocol === 'postgresql:'; } catch { return /^postgres(?:ql)?:\/\//i.test(value); } } function splitSqlStatements(sqlText: string) { return sqlText .split(';') .map((statement) => statement.trim()) .filter((statement) => statement.length > 0); } function buildPostgresMigrationPlan(sqlText: string) { const immediateStatements: string[] = []; const deferredIndexStatements: string[] = []; const addIndexPattern = /^alter table\s+([^\s]+)\s+add\s+index\s+([^\s]+)\s+\((.+)\)$/i; for (const rawStatement of splitSqlStatements(sqlText)) { const statement = rawStatement.replace(/\s+/g, ' ').trim(); const match = statement.match(addIndexPattern); if (!match) { immediateStatements.push(statement); continue; } const [, tableName, indexName, columns] = match; deferredIndexStatements.push(`create index if not exists ${indexName} on ${tableName} (${columns})`); } return [...immediateStatements, ...deferredIndexStatements]; } async function runPostgresMigrations(pool: Pool, sqlText: string) { const statements = buildPostgresMigrationPlan(sqlText); if (statements.length === 0) { return; } const client = await pool.connect(); try { await client.query('BEGIN'); for (const statement of statements) { await client.query(statement); } await client.query('COMMIT'); } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } function getPool() { const connectionString = process.env.DATABASE_URL?.trim(); if (!connectionString) { throw new Error('DATABASE_URL is required for Better Auth PostgreSQL adapter.'); } if (!globalThis.__fiscalAuthPgPool) { globalThis.__fiscalAuthPgPool = new Pool({ connectionString }); } return globalThis.__fiscalAuthPgPool; } function buildAuth() { const adminUserIds = parseCsvList(process.env.BETTER_AUTH_ADMIN_USER_IDS); const trustedOrigins = parseCsvList(process.env.BETTER_AUTH_TRUSTED_ORIGINS); const baseURL = process.env.BETTER_AUTH_BASE_URL?.trim() || process.env.BETTER_AUTH_URL?.trim() || undefined; const secret = process.env.BETTER_AUTH_SECRET?.trim() || undefined; return betterAuth({ database: getPool(), baseURL, secret, emailAndPassword: { enabled: true }, trustedOrigins: trustedOrigins.length > 0 ? trustedOrigins : undefined, plugins: [ admin(adminUserIds.length > 0 ? { adminUserIds } : undefined), magicLink({ sendMagicLink: async ({ email, url }) => { console.info(`[better-auth] Magic link requested for ${email}: ${url}`); } }), organization(), nextCookies() ] }); } export function getAuth() { if (!authInstance) { authInstance = buildAuth(); } return authInstance; } export async function ensureAuthSchema() { const auth = getAuth(); const connectionString = process.env.DATABASE_URL?.trim(); if (!migrationPromise) { migrationPromise = (async () => { const migrations = await getMigrations(auth.options); if (isPostgresConnectionString(connectionString)) { const sql = await migrations.compileMigrations(); await runPostgresMigrations(getPool(), sql); return; } await migrations.runMigrations(); })(); } try { await migrationPromise; } catch (error) { migrationPromise = null; throw error; } return auth; }