Skip to content

Using secondaryStorage gives TypeError: expiresAt.getTime is not a function when calling auth.api.updateUser() #8156

@hammer-ai

Description

@hammer-ai

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I have the following setup:

apps/web/src/lib/auth.ts

import {betterAuth, BetterAuthOptions} from 'better-auth/minimal'
import {prismaAdapter} from 'better-auth/adapters/prisma'
import {customSession} from 'better-auth/plugins'
import {prisma} from 'database/src/client'
import {AUTH_COOKIE_PREFIX, DESKTOP_APP_CLIENT_ID, DESKTOP_APP_PROTOCOL, getURL, MAX_PASSWORD_CHARS, MAX_USERNAME_CHARS, MIN_PASSWORD_CHARS, MIN_USERNAME_CHARS} from 'utils'
import {nextCookies} from 'better-auth/next-js'
import {electron} from '@better-auth/electron'
import {waitUntil} from '@vercel/functions'
import {kv} from '@vercel/kv'

const options = {
  appName: APP_NAME,
  database: prismaAdapter(prisma, {
    provider: 'postgresql',
    usePlural: true
  }),
  baseURL: BASE_URL,
  secondaryStorage: {
    get: async (key) => await kv.get(`better-auth:${key}`),
    set: async (key, value, ttl) => {
      if (ttl) {
        await kv.set(`better-auth:${key}`, value, {ex: ttl})
      } else {
        await kv.set(`better-auth:${key}`, value)
      }
    },
    delete: async (key) => {
      await kv.del(`better-auth:${key}`)
    }
  },
  secret: process.env.BETTER_AUTH_SECRET!,
  trustedOrigins: [
    'http://localhost:3000',
    'https://my-app.com',
  ],
  socialProviders: {
    google: {
      prompt: 'select_account',
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!
    }
  },
  emailAndPassword: {
    enabled: true,
  },
  advanced: {
    cookiePrefix: AUTH_COOKIE_PREFIX,
    useSecureCookies: process.env.NODE_ENV === 'production',
    backgroundTasks: {
      handler: (promise) => waitUntil(promise)
    }
  },
  account: {
    modelName: 'account',
    encryptOAuthTokens: true,
    accountLinking: {
      enabled: true,
      trustedProviders: [
        'email-password',
        'google',
        'discord'
      ]
    }
  },
  user: {
    modelName: 'user',
    additionalFields: {
      bio: {
        type: 'string',
        required: false
      },
      backgroundImage: {
        type: 'string',
        required: false
      },
      status: {
        type: 'string',
        required: false
      }
    },
    deleteUser: {
      enabled: true
    }
  },
  session: {
    modelName: 'session',
    expiresIn: 30 * 24 * 60 * 60, // 30 days
    // https://www.better-auth.com/docs/guides/optimizing-for-performance#cookie-cache
    cookieCache: {
      enabled: true,
      maxAge: 5 * 60 // Cache duration in seconds - 5 minutes
    }
  },
  verification: {
    modelName: 'verification'
  },
  rateLimit: {
    enabled: true,
    window: 60,
    max: 100,
    customRules: {
      '/sign-in/email': {window: 60, max: 5},
      '/sign-up/email': {window: 60, max: 5},
      '/forget-password': {window: 60, max: 5}
    }
  },
  ...other code,
  plugins: [
    ...other code,
  ]
} satisfies BetterAuthOptions

export const auth = betterAuth({
  ...options,
  plugins: [
    ...(options.plugins ?? []),
    customSession(({user, session}) => Promise.resolve({
      user: {
        ...user,
        ...other code
      },
      session
    }), options),
    nextCookies() // make sure this is the last plugin in the array
  ]
})

and

apps/web/src/lib/auth-client.ts
import {customSessionClient, inferAdditionalFields, usernameClient} from 'better-auth/client/plugins'
import {createAuthClient} from 'better-auth/react'
import type {auth} from './auth'
import {AUTH_COOKIE_PREFIX, DESKTOP_APP_CLIENT_ID, DESKTOP_APP_PROTOCOL, getURL} from 'utils'
import {electronProxyClient} from '@better-auth/electron/proxy'

export const authClient = createAuthClient({
  baseURL: getURL(),
  plugins: [
    electronProxyClient({
      clientID: DESKTOP_APP_CLIENT_ID,
      cookiePrefix: AUTH_COOKIE_PREFIX,
      protocol: {
        scheme: DESKTOP_APP_PROTOCOL
      }
    }),
    usernameClient(),
    customSessionClient<typeof auth>(),
    inferAdditionalFields<typeof auth>()
  ],
  fetchOptions: {
    onError(e) {
      if (e.error.status === 429) {
        toastIt({text: 'Too many requests. Please try again later.', type: ToastType.Error})
      }
    }
  }
})

Current vs. Expected behavior

When I have secondaryStorage enabled (i.e. not commented out), when I update the user on the server with:

await auth.api.updateUser({
      body: {
        status: 'pending',
      },
      headers: await headers()
    })

I get:

TypeError: expiresAt.getTime is not a function
    at async POST (src/app/api/user/update-user-profile/route.ts:63:5)
  61 |     // Use auth.api.updateUser to update user and session together.
  62 |     // This ensures the session reflects the updated values immediately.
> 63 |     await auth.api.updateUser({
     |     ^
  64 |       body: {
  65 |         status: 'pending',

What version of Better Auth are you using?

1.5.0-beta.19

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin",
    "release": "25.3.0",
    "cpuCount": 12,
    "cpuModel": "Apple M2 Pro",
    "totalMemory": "32.00 GB",
    "freeMemory": "3.64 GB"
  },
  "node": {
    "version": "v21.7.3",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.5.0"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "^15.5.9"
    },
    {
      "name": "react",
      "version": "^19.2.1"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "Unknown",
    "config": null,
    "error": "Missing API key. Pass it to the constructor `new Resend(\"re_123\")`"
  }
}

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

Backend

Metadata

Metadata

Assignees

Labels

bugSomething isn't workinglockedLocked conversations after being closed for 7 days

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions