Unknown argument refreshTokenExpiresAt - with Prisma #2171

Closed
opened 2026-03-13 09:31:53 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @elie222 on GitHub (Oct 21, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I'm using Prisma + Better Auth. I updated Better Auth to the latest version and started getting this error which broke log in:

Unknown argument refreshTokenExpiresAt. Available options are marked with ?.
inbox-zero-ai:dev: GET /api/auth/callback/google

Current vs. Expected behavior

User can log in

What version of Better Auth are you using?

1.3.28

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Aug 11 21:16:34 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6020",
    "release": "24.6.0",
    "cpuCount": 12,
    "cpuModel": "Apple M2 Pro",
    "totalMemory": "32.00 GB",
    "freeMemory": "0.75 GB"
  },
  "node": {
    "version": "v22.15.1",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.2"
  },
  "frameworks": null,
  "databases": null,
  "betterAuth": {
    "version": "Unknown",
    "config": null
  }
}

Which area(s) are affected? (Select all that apply)

Package, Backend, Client

Auth config (if applicable)

// based on: https://github.com/vercel/platforms/blob/main/lib/auth.ts

import { sso } from "@better-auth/sso";
import { createContact as createLoopsContact } from "@inboxzero/loops";
import { createContact as createResendContact } from "@inboxzero/resend";
import type { Prisma } from "@prisma/client";
import type { Account, AuthContext, User } from "better-auth";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { nextCookies } from "better-auth/next-js";
import { cookies, headers } from "next/headers";
import { env } from "@/env";
import { trackDubSignUp } from "@/utils/dub";
import {
  isGoogleProvider,
  isMicrosoftProvider,
} from "@/utils/email/provider-types";
import { encryptToken } from "@/utils/encryption";
import { captureException } from "@/utils/error";
import { getContactsClient as getGoogleContactsClient } from "@/utils/gmail/client";
import { SCOPES as GMAIL_SCOPES } from "@/utils/gmail/scopes";
import { createScopedLogger } from "@/utils/logger";
import { getContactsClient as getOutlookContactsClient } from "@/utils/outlook/client";
import { SCOPES as OUTLOOK_SCOPES } from "@/utils/outlook/scopes";
import { updateAccountSeats } from "@/utils/premium/server";
import prisma from "@/utils/prisma";

const logger = createScopedLogger("auth");

export const betterAuthConfig = betterAuth({
  advanced: {
    database: {
      generateId: false,
    },
  },
  logger: {
    level: "info",
    log: (level, message, ...args) => {
      switch (level) {
        case "info":
          logger.info(message, { args });
          break;
        case "error":
          logger.error(message, { args });
          break;
      }
    },
  },
  baseURL: env.NEXT_PUBLIC_BASE_URL,
  trustedOrigins: [env.NEXT_PUBLIC_BASE_URL],
  secret: env.AUTH_SECRET || env.NEXTAUTH_SECRET,
  emailAndPassword: {
    enabled: false,
  },
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
  plugins: [
    nextCookies(),
    sso({
      disableImplicitSignUp: false,
      organizationProvisioning: { disabled: true },
    }),
  ],
  session: {
    modelName: "Session",
    fields: {
      token: "sessionToken",
      expiresAt: "expires",
    },
    cookieCache: {
      enabled: true,
      maxAge: 60 * 60 * 24 * 30, // 30 days
    },
    expiresIn: 60 * 60 * 24 * 30, // 30 days
    updateAge: 60 * 60 * 24 * 3, // 1 day (every 1 day the session expiration is updated)
  },
  account: {
    modelName: "Account",
    fields: {
      accountId: "providerAccountId",
      providerId: "provider",
      refreshToken: "refresh_token",
      accessToken: "access_token",
      accessTokenExpiresAt: "expires_at",
      idToken: "id_token",
    },
  },
  verification: {
    modelName: "VerificationToken",
    fields: {
      value: "token",
      expiresAt: "expires",
    },
  },
  socialProviders: {
    google: {
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
      scope: [...GMAIL_SCOPES],
      accessType: "offline",
      prompt: "select_account consent",
      disableIdTokenSignIn: true,
    },
    microsoft: {
      clientId: env.MICROSOFT_CLIENT_ID || "",
      clientSecret: env.MICROSOFT_CLIENT_SECRET || "",
      scope: [...OUTLOOK_SCOPES],
      tenantId: "common",
      prompt: "consent",
      disableIdTokenSignIn: true,
    },
  },
  events: {
    signIn: handleSignIn,
  },
  databaseHooks: {
    account: {
      create: {
        after: async (account: Account) => {
          await handleLinkAccount(account);
        },
      },
      update: {
        after: async (account: Account) => {
          await handleLinkAccount(account);
        },
      },
    },
  },
  onAPIError: {
    throw: true,
    onError: (error: unknown, ctx: AuthContext) => {
      logger.error("Auth API encountered an error", { error, ctx });
    },
    errorURL: "/login/error",
  },
});

async function handleSignIn({
  user,
  isNewUser,
}: {
  user: User;
  isNewUser: boolean;
}) {
  if (isNewUser && user.email) {
    const loops = createLoopsContact(
      user.email,
      user.name?.split(" ")?.[0],
    ).catch((error) => {
      const alreadyExists =
        error instanceof Error && error.message.includes("409");
      if (!alreadyExists) {
        logger.error("Error creating Loops contact", {
          email: user.email,
          error,
        });
        captureException(error, undefined, user.email);
      }
    });

    const resend = createResendContact({ email: user.email }).catch((error) => {
      logger.error("Error creating Resend contact", {
        email: user.email,
        error,
      });
      captureException(error, undefined, user.email);
    });

    const dub = trackDubSignUp(user).catch((error) => {
      logger.error("Error tracking Dub sign up", {
        email: user.email,
        error,
      });
      captureException(error, undefined, user.email);
    });

    await Promise.all([loops, resend, dub]);
  }

  if (isNewUser && user.email && user.id) {
    await Promise.all([
      handlePendingPremiumInvite({ email: user.email }),
      handleReferralOnSignUp({
        userId: user.id,
        email: user.email,
      }),
    ]);
  }
}
async function handlePendingPremiumInvite({ email }: { email: string }) {
  logger.info("Handling pending premium invite", { email });

  // Check for pending invite
  const premium = await prisma.premium.findFirst({
    where: { pendingInvites: { has: email } },
    select: {
      id: true,
      pendingInvites: true,
      lemonSqueezySubscriptionItemId: true,
      stripeSubscriptionId: true,
      _count: { select: { users: true } },
    },
  });

  if (
    premium?.lemonSqueezySubscriptionItemId ||
    premium?.stripeSubscriptionId
  ) {
    // Add user to premium and remove from pending invites
    await prisma.premium.update({
      where: { id: premium.id },
      data: {
        users: { connect: { email } },
        pendingInvites: {
          set: premium.pendingInvites.filter((e: string) => e !== email),
        },
      },
    });
  }

  logger.info("Added user to premium from invite", { email });
}

export async function handleReferralOnSignUp({
  userId,
  email,
}: {
  userId: string;
  email: string;
}) {
  try {
    const cookieStore = await cookies();
    const referralCookie = cookieStore.get("referral_code");

    if (!referralCookie?.value) {
      logger.info("No referral code found in cookies", { email });
      return;
    }

    const referralCode = referralCookie.value;
    logger.info("Processing referral for new user", {
      email,
      referralCode,
    });

    // Import the createReferral function
    const { createReferral } = await import("@/utils/referral/referral-code");
    await createReferral(userId, referralCode);
    logger.info("Successfully created referral", {
      email,
      referralCode,
    });
  } catch (error) {
    logger.error("Error processing referral on sign up", {
      error,
      userId,
      email,
    });
    // Don't throw error - referral failure shouldn't prevent sign up
    captureException(error, {
      extra: { userId, email, location: "handleReferralOnSignUp" },
    });
  }
}

// TODO: move into email provider instead of checking the provider type
async function getProfileData(providerId: string, accessToken: string) {
  if (isGoogleProvider(providerId)) {
    const contactsClient = getGoogleContactsClient({ accessToken });
    const profileResponse = await contactsClient.people.get({
      resourceName: "people/me",
      personFields: "emailAddresses,names,photos",
    });

    return {
      email: profileResponse.data.emailAddresses
        ?.find((e) => e.metadata?.primary)
        ?.value?.toLowerCase(),
      name: profileResponse.data.names?.find((n) => n.metadata?.primary)
        ?.displayName,
      image: profileResponse.data.photos?.find((p) => p.metadata?.primary)?.url,
    };
  }

  if (isMicrosoftProvider(providerId)) {
    const client = getOutlookContactsClient({ accessToken });
    try {
      const profileResponse = await client.getUserProfile();

      // Get photo separately as it requires a different endpoint
      let photoUrl = null;
      try {
        const photo = await client.getUserPhoto();
        if (photo) {
          photoUrl = photo;
        }
      } catch (error) {
        logger.info("User has no profile photo", { error });
      }

      return {
        email:
          profileResponse.mail?.toLowerCase() ||
          profileResponse.userPrincipalName?.toLowerCase(),
        name: profileResponse.displayName,
        image: photoUrl,
      };
    } catch (error) {
      logger.error("Error fetching Microsoft profile data", { error });
      throw error;
    }
  }
}

async function handleLinkAccount(account: Account) {
  let primaryEmail: string | null | undefined;
  let primaryName: string | null | undefined;
  let primaryPhotoUrl: string | null | undefined;

  try {
    if (!account.accessToken) {
      logger.error(
        "[linkAccount] No access_token found in data, cannot fetch profile.",
      );
      throw new Error("Missing access token during account linking.");
    }
    const profileData = await getProfileData(
      account.providerId,
      account.accessToken,
    );

    if (!profileData?.email) {
      logger.error("[handleLinkAccount] No email found in profile data");
    }

    primaryEmail = profileData?.email;
    primaryName = profileData?.name;
    primaryPhotoUrl = profileData?.image;

    if (!primaryEmail) {
      logger.error(
        "[linkAccount] Primary email could not be determined from profile.",
      );
      throw new Error("Primary email not found for linked account.");
    }

    const user = await prisma.user.findUnique({
      where: { id: account.userId },
      select: { email: true, name: true, image: true },
    });

    if (!user?.email) {
      logger.error("[linkAccount] No user email found", {
        userId: account.userId,
      });
      return;
    }

    // --- Create/Update the corresponding EmailAccount record ---
    const emailAccountData: Prisma.EmailAccountUpsertArgs = {
      where: { email: profileData?.email },
      update: {
        userId: account.userId,
        accountId: account.id,
        name: primaryName,
        image: primaryPhotoUrl,
      },
      create: {
        email: primaryEmail,
        userId: account.userId,
        accountId: account.id,
        name: primaryName,
        image: primaryPhotoUrl,
      },
    };
    await prisma.emailAccount.upsert(emailAccountData);

    // Handle premium account seats
    await updateAccountSeats({ userId: account.userId }).catch((error) => {
      logger.error("[linkAccount] Error updating premium account seats:", {
        userId: account.userId,
        error,
      });
      captureException(error, { extra: { userId: account.userId } });
    });

    logger.info("[linkAccount] Successfully linked account", {
      email: user.email,
      userId: account.userId,
      accountId: account.id,
    });
  } catch (error) {
    logger.error("[linkAccount] Error during linking process:", {
      userId: account.userId,
      error,
    });
    captureException(error, {
      extra: { userId: account.userId, location: "linkAccount" },
    });
    throw error;
  }
}

export async function saveTokens({
  tokens,
  accountRefreshToken,
  providerAccountId,
  emailAccountId,
  provider,
}: {
  tokens: {
    access_token?: string;
    refresh_token?: string;
    expires_at?: number;
  };
  accountRefreshToken: string | null;
  provider: string;
} & ( // provide one of these:
  | {
      providerAccountId: string;
      emailAccountId?: never;
    }
  | {
      emailAccountId: string;
      providerAccountId?: never;
    }
)) {
  const refreshToken = tokens.refresh_token ?? accountRefreshToken;

  if (!refreshToken) {
    logger.error("Attempted to save null refresh token", { providerAccountId });
    captureException("Cannot save null refresh token", {
      extra: { providerAccountId },
    });
    return;
  }

  const data = {
    access_token: tokens.access_token,
    expires_at: tokens.expires_at ? new Date(tokens.expires_at * 1000) : null,
    refresh_token: refreshToken,
  };

  if (emailAccountId) {
    // Encrypt tokens in data directly
    // Usually we do this in prisma-extensions.ts but we need to do it here because we're updating the account via the emailAccount
    // We could also edit prisma-extensions.ts to handle this case but this is easier for now
    if (data.access_token)
      data.access_token = encryptToken(data.access_token) || undefined;
    if (data.refresh_token)
      data.refresh_token = encryptToken(data.refresh_token) || "";

    await prisma.emailAccount.update({
      where: { id: emailAccountId },
      data: { account: { update: data } },
    });
  } else {
    if (!providerAccountId) {
      logger.error("No providerAccountId found in database", {
        emailAccountId,
      });
      captureException("No providerAccountId found in database", {
        extra: { emailAccountId },
      });
      return;
    }

    return await prisma.account.update({
      where: {
        provider_providerAccountId: {
          provider,
          providerAccountId,
        },
      },
      data,
    });
  }
}

export const auth = async () =>
  betterAuthConfig.api.getSession({ headers: await headers() });

Additional context

No response

Originally created by @elie222 on GitHub (Oct 21, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce I'm using Prisma + Better Auth. I updated Better Auth to the latest version and started getting this error which broke log in: Unknown argument `refreshTokenExpiresAt`. Available options are marked with ?. inbox-zero-ai:dev: GET /api/auth/callback/google ### Current vs. Expected behavior User can log in ### What version of Better Auth are you using? 1.3.28 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Aug 11 21:16:34 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6020", "release": "24.6.0", "cpuCount": 12, "cpuModel": "Apple M2 Pro", "totalMemory": "32.00 GB", "freeMemory": "0.75 GB" }, "node": { "version": "v22.15.1", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.2" }, "frameworks": null, "databases": null, "betterAuth": { "version": "Unknown", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Package, Backend, Client ### Auth config (if applicable) ```typescript // based on: https://github.com/vercel/platforms/blob/main/lib/auth.ts import { sso } from "@better-auth/sso"; import { createContact as createLoopsContact } from "@inboxzero/loops"; import { createContact as createResendContact } from "@inboxzero/resend"; import type { Prisma } from "@prisma/client"; import type { Account, AuthContext, User } from "better-auth"; import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { nextCookies } from "better-auth/next-js"; import { cookies, headers } from "next/headers"; import { env } from "@/env"; import { trackDubSignUp } from "@/utils/dub"; import { isGoogleProvider, isMicrosoftProvider, } from "@/utils/email/provider-types"; import { encryptToken } from "@/utils/encryption"; import { captureException } from "@/utils/error"; import { getContactsClient as getGoogleContactsClient } from "@/utils/gmail/client"; import { SCOPES as GMAIL_SCOPES } from "@/utils/gmail/scopes"; import { createScopedLogger } from "@/utils/logger"; import { getContactsClient as getOutlookContactsClient } from "@/utils/outlook/client"; import { SCOPES as OUTLOOK_SCOPES } from "@/utils/outlook/scopes"; import { updateAccountSeats } from "@/utils/premium/server"; import prisma from "@/utils/prisma"; const logger = createScopedLogger("auth"); export const betterAuthConfig = betterAuth({ advanced: { database: { generateId: false, }, }, logger: { level: "info", log: (level, message, ...args) => { switch (level) { case "info": logger.info(message, { args }); break; case "error": logger.error(message, { args }); break; } }, }, baseURL: env.NEXT_PUBLIC_BASE_URL, trustedOrigins: [env.NEXT_PUBLIC_BASE_URL], secret: env.AUTH_SECRET || env.NEXTAUTH_SECRET, emailAndPassword: { enabled: false, }, database: prismaAdapter(prisma, { provider: "postgresql", }), plugins: [ nextCookies(), sso({ disableImplicitSignUp: false, organizationProvisioning: { disabled: true }, }), ], session: { modelName: "Session", fields: { token: "sessionToken", expiresAt: "expires", }, cookieCache: { enabled: true, maxAge: 60 * 60 * 24 * 30, // 30 days }, expiresIn: 60 * 60 * 24 * 30, // 30 days updateAge: 60 * 60 * 24 * 3, // 1 day (every 1 day the session expiration is updated) }, account: { modelName: "Account", fields: { accountId: "providerAccountId", providerId: "provider", refreshToken: "refresh_token", accessToken: "access_token", accessTokenExpiresAt: "expires_at", idToken: "id_token", }, }, verification: { modelName: "VerificationToken", fields: { value: "token", expiresAt: "expires", }, }, socialProviders: { google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, scope: [...GMAIL_SCOPES], accessType: "offline", prompt: "select_account consent", disableIdTokenSignIn: true, }, microsoft: { clientId: env.MICROSOFT_CLIENT_ID || "", clientSecret: env.MICROSOFT_CLIENT_SECRET || "", scope: [...OUTLOOK_SCOPES], tenantId: "common", prompt: "consent", disableIdTokenSignIn: true, }, }, events: { signIn: handleSignIn, }, databaseHooks: { account: { create: { after: async (account: Account) => { await handleLinkAccount(account); }, }, update: { after: async (account: Account) => { await handleLinkAccount(account); }, }, }, }, onAPIError: { throw: true, onError: (error: unknown, ctx: AuthContext) => { logger.error("Auth API encountered an error", { error, ctx }); }, errorURL: "/login/error", }, }); async function handleSignIn({ user, isNewUser, }: { user: User; isNewUser: boolean; }) { if (isNewUser && user.email) { const loops = createLoopsContact( user.email, user.name?.split(" ")?.[0], ).catch((error) => { const alreadyExists = error instanceof Error && error.message.includes("409"); if (!alreadyExists) { logger.error("Error creating Loops contact", { email: user.email, error, }); captureException(error, undefined, user.email); } }); const resend = createResendContact({ email: user.email }).catch((error) => { logger.error("Error creating Resend contact", { email: user.email, error, }); captureException(error, undefined, user.email); }); const dub = trackDubSignUp(user).catch((error) => { logger.error("Error tracking Dub sign up", { email: user.email, error, }); captureException(error, undefined, user.email); }); await Promise.all([loops, resend, dub]); } if (isNewUser && user.email && user.id) { await Promise.all([ handlePendingPremiumInvite({ email: user.email }), handleReferralOnSignUp({ userId: user.id, email: user.email, }), ]); } } async function handlePendingPremiumInvite({ email }: { email: string }) { logger.info("Handling pending premium invite", { email }); // Check for pending invite const premium = await prisma.premium.findFirst({ where: { pendingInvites: { has: email } }, select: { id: true, pendingInvites: true, lemonSqueezySubscriptionItemId: true, stripeSubscriptionId: true, _count: { select: { users: true } }, }, }); if ( premium?.lemonSqueezySubscriptionItemId || premium?.stripeSubscriptionId ) { // Add user to premium and remove from pending invites await prisma.premium.update({ where: { id: premium.id }, data: { users: { connect: { email } }, pendingInvites: { set: premium.pendingInvites.filter((e: string) => e !== email), }, }, }); } logger.info("Added user to premium from invite", { email }); } export async function handleReferralOnSignUp({ userId, email, }: { userId: string; email: string; }) { try { const cookieStore = await cookies(); const referralCookie = cookieStore.get("referral_code"); if (!referralCookie?.value) { logger.info("No referral code found in cookies", { email }); return; } const referralCode = referralCookie.value; logger.info("Processing referral for new user", { email, referralCode, }); // Import the createReferral function const { createReferral } = await import("@/utils/referral/referral-code"); await createReferral(userId, referralCode); logger.info("Successfully created referral", { email, referralCode, }); } catch (error) { logger.error("Error processing referral on sign up", { error, userId, email, }); // Don't throw error - referral failure shouldn't prevent sign up captureException(error, { extra: { userId, email, location: "handleReferralOnSignUp" }, }); } } // TODO: move into email provider instead of checking the provider type async function getProfileData(providerId: string, accessToken: string) { if (isGoogleProvider(providerId)) { const contactsClient = getGoogleContactsClient({ accessToken }); const profileResponse = await contactsClient.people.get({ resourceName: "people/me", personFields: "emailAddresses,names,photos", }); return { email: profileResponse.data.emailAddresses ?.find((e) => e.metadata?.primary) ?.value?.toLowerCase(), name: profileResponse.data.names?.find((n) => n.metadata?.primary) ?.displayName, image: profileResponse.data.photos?.find((p) => p.metadata?.primary)?.url, }; } if (isMicrosoftProvider(providerId)) { const client = getOutlookContactsClient({ accessToken }); try { const profileResponse = await client.getUserProfile(); // Get photo separately as it requires a different endpoint let photoUrl = null; try { const photo = await client.getUserPhoto(); if (photo) { photoUrl = photo; } } catch (error) { logger.info("User has no profile photo", { error }); } return { email: profileResponse.mail?.toLowerCase() || profileResponse.userPrincipalName?.toLowerCase(), name: profileResponse.displayName, image: photoUrl, }; } catch (error) { logger.error("Error fetching Microsoft profile data", { error }); throw error; } } } async function handleLinkAccount(account: Account) { let primaryEmail: string | null | undefined; let primaryName: string | null | undefined; let primaryPhotoUrl: string | null | undefined; try { if (!account.accessToken) { logger.error( "[linkAccount] No access_token found in data, cannot fetch profile.", ); throw new Error("Missing access token during account linking."); } const profileData = await getProfileData( account.providerId, account.accessToken, ); if (!profileData?.email) { logger.error("[handleLinkAccount] No email found in profile data"); } primaryEmail = profileData?.email; primaryName = profileData?.name; primaryPhotoUrl = profileData?.image; if (!primaryEmail) { logger.error( "[linkAccount] Primary email could not be determined from profile.", ); throw new Error("Primary email not found for linked account."); } const user = await prisma.user.findUnique({ where: { id: account.userId }, select: { email: true, name: true, image: true }, }); if (!user?.email) { logger.error("[linkAccount] No user email found", { userId: account.userId, }); return; } // --- Create/Update the corresponding EmailAccount record --- const emailAccountData: Prisma.EmailAccountUpsertArgs = { where: { email: profileData?.email }, update: { userId: account.userId, accountId: account.id, name: primaryName, image: primaryPhotoUrl, }, create: { email: primaryEmail, userId: account.userId, accountId: account.id, name: primaryName, image: primaryPhotoUrl, }, }; await prisma.emailAccount.upsert(emailAccountData); // Handle premium account seats await updateAccountSeats({ userId: account.userId }).catch((error) => { logger.error("[linkAccount] Error updating premium account seats:", { userId: account.userId, error, }); captureException(error, { extra: { userId: account.userId } }); }); logger.info("[linkAccount] Successfully linked account", { email: user.email, userId: account.userId, accountId: account.id, }); } catch (error) { logger.error("[linkAccount] Error during linking process:", { userId: account.userId, error, }); captureException(error, { extra: { userId: account.userId, location: "linkAccount" }, }); throw error; } } export async function saveTokens({ tokens, accountRefreshToken, providerAccountId, emailAccountId, provider, }: { tokens: { access_token?: string; refresh_token?: string; expires_at?: number; }; accountRefreshToken: string | null; provider: string; } & ( // provide one of these: | { providerAccountId: string; emailAccountId?: never; } | { emailAccountId: string; providerAccountId?: never; } )) { const refreshToken = tokens.refresh_token ?? accountRefreshToken; if (!refreshToken) { logger.error("Attempted to save null refresh token", { providerAccountId }); captureException("Cannot save null refresh token", { extra: { providerAccountId }, }); return; } const data = { access_token: tokens.access_token, expires_at: tokens.expires_at ? new Date(tokens.expires_at * 1000) : null, refresh_token: refreshToken, }; if (emailAccountId) { // Encrypt tokens in data directly // Usually we do this in prisma-extensions.ts but we need to do it here because we're updating the account via the emailAccount // We could also edit prisma-extensions.ts to handle this case but this is easier for now if (data.access_token) data.access_token = encryptToken(data.access_token) || undefined; if (data.refresh_token) data.refresh_token = encryptToken(data.refresh_token) || ""; await prisma.emailAccount.update({ where: { id: emailAccountId }, data: { account: { update: data } }, }); } else { if (!providerAccountId) { logger.error("No providerAccountId found in database", { emailAccountId, }); captureException("No providerAccountId found in database", { extra: { emailAccountId }, }); return; } return await prisma.account.update({ where: { provider_providerAccountId: { provider, providerAccountId, }, }, data, }); } } export const auth = async () => betterAuthConfig.api.getSession({ headers: await headers() }); ``` ### Additional context _No response_
GiteaMirror added the bug label 2026-03-13 09:31:53 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 21, 2025):

This error happens because Better Auth now expects your Prisma Account model to include a refreshTokenExpiresAt field (or its mapped equivalent). If your schema doesn't have this field, or if the mapping is incorrect, Prisma will throw the "Unknown argument" error during login attempts. This requirement became stricter after recent updates, and a bug in earlier versions caused mapping issues for custom field names, but that was fixed in September 2025 [PR #4569].

To fix this, make sure your Prisma schema for the Account model includes a field for refresh token expiration. For example:

model Account {
  // ... other fields ...
  accessTokenExpiresAt  DateTime?
  refreshTokenExpiresAt DateTime?
  // ... other fields ...
}

If you use custom field names in your Better Auth config, ensure your Prisma schema matches those names exactly. After updating your schema, run prisma generate and apply any necessary migrations.

If you already have a custom mapping, double-check that your config and schema are in sync. The mapping bug should be resolved in your version, so the main issue is likely a missing or mismatched field in your schema [source].

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Oct 21, 2025): <!-- Answer --> This error happens because Better Auth now expects your Prisma Account model to include a `refreshTokenExpiresAt` field (or its mapped equivalent). If your schema doesn't have this field, or if the mapping is incorrect, Prisma will throw the "Unknown argument" error during login attempts. This requirement became stricter after recent updates, and a bug in earlier versions caused mapping issues for custom field names, but that was fixed in September 2025 [[PR #4569](https://github.com/better-auth/better-auth/pull/4569)]. To fix this, make sure your Prisma schema for the Account model includes a field for refresh token expiration. For example: ```prisma model Account { // ... other fields ... accessTokenExpiresAt DateTime? refreshTokenExpiresAt DateTime? // ... other fields ... } ``` If you use custom field names in your Better Auth config, ensure your Prisma schema matches those names exactly. After updating your schema, run `prisma generate` and apply any necessary migrations. If you already have a custom mapping, double-check that your config and schema are in sync. The mapping bug should be resolved in your version, so the main issue is likely a missing or mismatched field in your schema [[source](https://github.com/better-auth/better-auth/issues/4564)]. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/5731652d-eca5-4659-a010-ac60a67f3033?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/5731652d-eca5-4659-a010-ac60a67f3033?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/5731652d-eca5-4659-a010-ac60a67f3033?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/5731652d-eca5-4659-a010-ac60a67f3033?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/5731652d-eca5-4659-a010-ac60a67f3033?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/5731652d-eca5-4659-a010-ac60a67f3033?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/5731652d-eca5-4659-a010-ac60a67f3033?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/5466)
Author
Owner

@elie222 commented on GitHub (Oct 21, 2025):

I believe this is missing from your docs

@elie222 commented on GitHub (Oct 21, 2025): I believe this is missing from your docs
Author
Owner

@Bekacru commented on GitHub (Oct 28, 2025):

We have refreshTokenExpiresAt field mentioned in the docs - https://www.better-auth.com/docs/concepts/database#account

@Bekacru commented on GitHub (Oct 28, 2025): We have `refreshTokenExpiresAt` field mentioned in the docs - https://www.better-auth.com/docs/concepts/database#account
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2171