import { expo } from "@better-auth/expo"; import { passkey } from "@better-auth/passkey"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { betterAuth } from "better-auth/minimal"; import { nextCookies } from "better-auth/next-js"; import { anonymous, magicLink, twoFactor, organization, admin, lastLoginMethod, } from "better-auth/plugins"; import * as schema from "@turbostarter/db/schema"; import { creditTransaction, customer } from "@turbostarter/db/schema"; import { db } from "@turbostarter/db/server"; import { EmailTemplate } from "@turbostarter/email"; import { sendEmail } from "@turbostarter/email/server"; import { getLocaleFromRequest } from "@turbostarter/i18n/server"; import { NodeEnv } from "@turbostarter/shared/constants"; import { logger } from "@turbostarter/shared/logger"; import { generateId } from "@turbostarter/shared/utils"; import { env } from "./env"; import { getUrl } from "./lib/utils"; import { AuthProvider, SocialProvider, VerificationType } from "./types"; /** * Credits for new free-tier users. * Configurable via FREE_TIER_CREDITS env var. Defaults: 10000 (dev), 100 (prod). */ const FREE_TIER_CREDITS = env.FREE_TIER_CREDITS ?? (env.NODE_ENV === NodeEnv.DEVELOPMENT ? 10000 : 100); export const auth = betterAuth({ appName: "TurboStarter", session: { cookieCache: { enabled: true, maxAge: 5 * 60, // 5 minutes }, }, user: { deleteUser: { enabled: true, sendDeleteAccountVerification: async ({ user, url }, request) => sendEmail({ to: user.email, template: EmailTemplate.DELETE_ACCOUNT, locale: getLocaleFromRequest(request), variables: { url: getUrl({ request, url, type: VerificationType.DELETE_ACCOUNT, }).toString(), }, }), }, changeEmail: { enabled: true, updateEmailWithoutVerification: true, sendChangeEmailConfirmation: async ({ user, newEmail, url }, request) => sendEmail({ to: user.email, template: EmailTemplate.CHANGE_EMAIL, locale: getLocaleFromRequest(request), variables: { url: getUrl({ request, url, type: VerificationType.CONFIRM_EMAIL, }).toString(), newEmail, }, }), }, }, trustedOrigins: [ "chrome-extension://", "turbostarter://", /* Needed only for Apple ID authentication */ "https://appleid.apple.com", ...(env.NODE_ENV === NodeEnv.DEVELOPMENT ? ["http://localhost*", "https://localhost*"] : []), ...(process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(",") ?? []), ], emailAndPassword: { enabled: true, requireEmailVerification: true, sendResetPassword: async ({ user, url }, request) => sendEmail({ to: user.email, template: EmailTemplate.RESET_PASSWORD, locale: getLocaleFromRequest(request), variables: { url, }, }), }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }, request) => sendEmail({ to: user.email, template: EmailTemplate.CONFIRM_EMAIL, locale: getLocaleFromRequest(request), variables: { url: getUrl({ request, url, type: VerificationType.CONFIRM_EMAIL, }).toString(), }, }), }, database: drizzleAdapter(db, { provider: "pg", schema, }), databaseHooks: { user: { create: { after: async (user) => { // Auto-create customer record with free credits on signup const customerId = generateId(); try { await db.transaction(async (tx) => { await tx.insert(customer).values({ id: customerId, userId: user.id, customerId: `free_${user.id}`, status: "active", plan: "free", credits: FREE_TIER_CREDITS, }); await tx.insert(creditTransaction).values({ id: generateId(), customerId, amount: FREE_TIER_CREDITS, type: "signup", reason: "Welcome credits for new user", balanceAfter: FREE_TIER_CREDITS, }); }); logger.info(`Created customer with ${FREE_TIER_CREDITS} credits for user ${user.id}`); } catch (error) { // Log but don't fail user creation if customer creation fails logger.error("Failed to create customer for user", { userId: user.id, error }); } }, }, }, }, plugins: [ magicLink({ sendMagicLink: async ({ email, url }, ctx) => sendEmail({ to: email, template: EmailTemplate.MAGIC_LINK, locale: getLocaleFromRequest(ctx?.request), variables: { url: getUrl({ request: ctx?.request, url, type: VerificationType.MAGIC_LINK, }).toString(), }, }), }), passkey(), twoFactor(), anonymous(), admin(), organization({ sendInvitationEmail: async ( { invitation, inviter, organization }, request, ) => { const url = getUrl({ request, }); url.searchParams.set("invitationId", invitation.id); url.searchParams.set("email", invitation.email); return sendEmail({ to: invitation.email, template: EmailTemplate.ORGANIZATION_INVITATION, locale: getLocaleFromRequest(request), variables: { url: url.toString(), inviter: inviter.user.name, organization: organization.name, }, }); }, }), lastLoginMethod({ customResolveMethod: (ctx) => { switch (ctx.path) { case "/magic-link/verify": return AuthProvider.MAGIC_LINK; case "/passkey/verify-authentication": return AuthProvider.PASSKEY; default: return null; } }, }), expo(), nextCookies(), ], socialProviders: { [SocialProvider.APPLE]: { clientId: env.APPLE_CLIENT_ID, clientSecret: env.APPLE_CLIENT_SECRET, appBundleIdentifier: env.APPLE_APP_BUNDLE_IDENTIFIER, }, [SocialProvider.GOOGLE]: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, }, [SocialProvider.GITHUB]: { clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET, }, }, advanced: { cookiePrefix: "turbostarter", cookies: { state: { attributes: { sameSite: "none", secure: true, }, }, }, }, logger: { log: (level, ...args) => logger[level](...args), }, }); export type AuthErrorCode = keyof typeof auth.$ERROR_CODES; export type Session = typeof auth.$Infer.Session; export type User = Session["user"]; export type Invitation = typeof auth.$Infer.Invitation; export type Organization = typeof auth.$Infer.Organization; export type ActiveOrganization = typeof auth.$Infer.ActiveOrganization; export type Member = typeof auth.$Infer.Member;