// Base auth instance without "server-only" - can be used in seed scripts import { betterAuth, type BetterAuthOptions } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { nextCookies } from "better-auth/next-js"; import { admin as adminPlugin } from "better-auth/plugins"; import { pgDb } from "lib/db/pg/db.pg"; import { headers } from "next/headers"; import { AccountTable, SessionTable, UserTable, VerificationTable, } from "lib/db/pg/schema.pg"; import { getAuthConfig } from "./config"; import logger from "logger"; import { userRepository } from "lib/db/repository"; import { DEFAULT_USER_ROLE, USER_ROLES } from "app-types/roles"; import { admin, editor, user, ac } from "./roles"; const { emailAndPasswordEnabled, signUpEnabled, socialAuthenticationProviders, } = getAuthConfig(); const options = { secret: process.env.BETTER_AUTH_SECRET!, plugins: [ adminPlugin({ defaultRole: DEFAULT_USER_ROLE, adminRoles: [USER_ROLES.ADMIN], ac, roles: { admin, editor, user, }, }), nextCookies(), ], baseURL: process.env.BETTER_AUTH_URL || process.env.NEXT_PUBLIC_BASE_URL, user: { changeEmail: { enabled: true, }, deleteUser: { enabled: true, }, }, database: drizzleAdapter(pgDb, { provider: "pg", schema: { user: UserTable, session: SessionTable, account: AccountTable, verification: VerificationTable, }, }), databaseHooks: { user: { create: { before: async (user) => { // This hook ONLY runs during user creation (sign-up), not on sign-in // Use our optimized getIsFirstUser function with caching const isFirstUser = await getIsFirstUser(); // Set role based on whether this is the first user const role = isFirstUser ? USER_ROLES.ADMIN : DEFAULT_USER_ROLE; logger.info( `User creation hook: ${user.email} will get role: ${role} (isFirstUser: ${isFirstUser})`, ); return { data: { ...user, role, }, }; }, }, }, }, emailAndPassword: { enabled: emailAndPasswordEnabled, disableSignUp: !signUpEnabled, }, session: { cookieCache: { enabled: true, maxAge: 60 * 60, }, expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated) }, advanced: { useSecureCookies: process.env.NO_HTTPS == "1" ? false : process.env.NODE_ENV === "production", database: { generateId: false, }, }, account: { accountLinking: { trustedProviders: ( Object.keys( socialAuthenticationProviders, ) as (keyof typeof socialAuthenticationProviders)[] ).filter((key) => socialAuthenticationProviders[key]), }, }, socialProviders: socialAuthenticationProviders, } satisfies BetterAuthOptions; export const auth = betterAuth({ ...options, plugins: [...(options.plugins ?? [])], }); export const getSession = async () => { const reqHeaders = await headers(); try { const session = await auth.api.getSession({ headers: reqHeaders, }); return session ?? null; } catch (error) { logger.error("Error getting session:", error); return null; } }; // Cache the first user check to avoid repeated DB queries let isFirstUserCache: boolean | null = null; export const getIsFirstUser = async () => { // If we already know there's at least one user, return false immediately // This in-memory cache prevents any DB calls once we know users exist if (isFirstUserCache === false) { return false; } try { // Direct database query - simple and reliable const userCount = await userRepository.getUserCount(); const isFirstUser = userCount === 0; // Once we have at least one user, cache it permanently in memory if (!isFirstUser) { isFirstUserCache = false; } return isFirstUser; } catch (error) { logger.error("Error checking if first user:", error); // Cache as false on error to prevent repeated attempts isFirstUserCache = false; return false; } };