import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { organization } from "better-auth/plugins"; import { magicLink } from "better-auth/plugins/magic-link"; import { count, eq } from "drizzle-orm"; import { db } from "@/lib/db"; import * as schema from "@/lib/db/schema"; const isProduction = process.env.NODE_ENV === "production"; export const auth = betterAuth({ // ------------------------------------------------------------------------- // Core // ------------------------------------------------------------------------- secret: process.env.BETTER_AUTH_SECRET!, baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3000", // ------------------------------------------------------------------------- // Database adapter (Drizzle + bun:sqlite) // ------------------------------------------------------------------------- database: drizzleAdapter(db, { provider: "sqlite", // Keys must match Better Auth's internal model names (singular). // usePlural: false (default) → "user", "session", "account", "verification" schema: { user: schema.users, session: schema.sessions, account: schema.accounts, verification: schema.verifications, }, }), // ------------------------------------------------------------------------- // Custom user fields // ------------------------------------------------------------------------- user: { additionalFields: { role: { type: "string", required: false, defaultValue: "moderator", input: false, // Not settable by the user directly }, }, }, // ------------------------------------------------------------------------- // Email + password authentication // ------------------------------------------------------------------------- emailAndPassword: { enabled: true, requireEmailVerification: false, minPasswordLength: 8, maxPasswordLength: 128, }, // ------------------------------------------------------------------------- // Database hooks — first registered user becomes admin automatically // ------------------------------------------------------------------------- databaseHooks: { user: { create: { after: async (user) => { // Count all users; if this is the very first, promote to admin const [{ total }] = await db .select({ total: count() }) .from(schema.users); if (total === 1) { await db .update(schema.users) .set({ role: "admin" } as Record) .where(eq(schema.users.id, user.id)); console.log(`[Auth] First user ${user.id} (${user.email}) promoted to admin`); } }, }, }, }, // ------------------------------------------------------------------------- // Plugins // ------------------------------------------------------------------------- plugins: [ // Organization / role support organization(), // Magic link — used for invitation acceptance flows magicLink({ expiresIn: 60 * 60, // 1 hour disableSignUp: true, // magic links are only for invited users sendMagicLink: async ({ email, url, token }) => { // Delegate to the application's email module. The email module is // responsible for importing and calling Resend (or whichever mailer // is configured). We do a dynamic import so that this file does not // pull in email dependencies at auth-initialisation time on the edge. const { sendMagicLinkEmail } = await import("@/lib/email/index"); await sendMagicLinkEmail({ email, url, token }); }, }), ], // ------------------------------------------------------------------------- // Trusted origins — allow env-configured list plus localhost in dev // ------------------------------------------------------------------------- trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS ? process.env.BETTER_AUTH_TRUSTED_ORIGINS.split(",").map((o) => o.trim()) : ["http://localhost:3000"], // ------------------------------------------------------------------------- // Cookie / session security // ------------------------------------------------------------------------- advanced: { useSecureCookies: isProduction, defaultCookieAttributes: { httpOnly: true, secure: isProduction, sameSite: "strict", path: "/", }, }, }); // --------------------------------------------------------------------------- // Type helpers for use across the application // --------------------------------------------------------------------------- export type Auth = typeof auth; /** The server-side session type returned by auth.api.getSession */ export type Session = typeof auth.$Infer.Session.session; /** The user type embedded in every session, with custom additionalFields */ export type User = typeof auth.$Infer.Session.user & { role?: "superadmin" | "admin" | "moderator" | null; }; type RawSession = NonNullable>>; /** Typed wrapper around auth.api.getSession that includes the role field */ export async function getAuthSession( headers: Headers, ): Promise<(Omit & { user: User }) | null> { return auth.api.getSession({ headers }) as Promise< (Omit & { user: User }) | null >; }