[GH-ISSUE #4187] $onUpdate is not set for updatedAt field in psql schema #9855

Closed
opened 2026-04-13 05:37:39 -05:00 by GiteaMirror · 12 comments
Owner

Originally created by @yvbbrjdr on GitHub (Aug 24, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4187

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Setup better-auth with drizzle (provider=pg).
  2. Run npx @better-auth/cli generate.

Current vs. Expected behavior

Currently, it outputs a schema which looks like

// ...

export const user = pgTable("user", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  emailVerified: boolean("email_verified")
    .$defaultFn(() => false)
    .notNull(),
  image: text("image"),
  createdAt: timestamp("created_at")
    .$defaultFn(() => /* @__PURE__ */ new Date())
    .notNull(),
  updatedAt: timestamp("updated_at")
    .$defaultFn(() => /* @__PURE__ */ new Date())
    .notNull(),
});

// ...

For the updatedAt field, it's missing

    .$onUpdate(() => /* @__PURE__ */ new Date())

What version of Better Auth are you using?

1.3.7

System info

System:
    OS: macOS 26.0
    CPU: (8) arm64 Apple M1 Pro
    Memory: 101.58 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
  database: drizzleAdapter(db, {
    provider: "pg",
  }),
});

Additional context

No response

Originally created by @yvbbrjdr on GitHub (Aug 24, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4187 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Setup better-auth with drizzle (provider=pg). 2. Run `npx @better-auth/cli generate`. ### Current vs. Expected behavior Currently, it outputs a schema which looks like ```typescript // ... export const user = pgTable("user", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: boolean("email_verified") .$defaultFn(() => false) .notNull(), image: text("image"), createdAt: timestamp("created_at") .$defaultFn(() => /* @__PURE__ */ new Date()) .notNull(), updatedAt: timestamp("updated_at") .$defaultFn(() => /* @__PURE__ */ new Date()) .notNull(), }); // ... ``` For the `updatedAt` field, it's missing ```typescript .$onUpdate(() => /* @__PURE__ */ new Date()) ``` ### What version of Better Auth are you using? 1.3.7 ### System info ```bash System: OS: macOS 26.0 CPU: (8) arm64 Apple M1 Pro Memory: 101.58 MB / 16.00 GB Shell: 5.9 - /bin/zsh ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, database: drizzleAdapter(db, { provider: "pg", }), }); ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-13 05:37:39 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 24, 2025):

Hi @yvbbrjdr! I'm Dosu and I’m helping the better-auth team.

The omission of .$onUpdate(() => new Date()) for the updatedAt field in the generated Drizzle schema is not intentional or documented—it's likely an oversight in the schema template logic. Without .$onUpdate, the updatedAt field won't automatically update when records are modified, which is the expected behavior for most user/auth tables in Drizzle ORM.

To fix this, manually add .$onUpdate(() => new Date()) to the updatedAt field in your generated schema. This will ensure the timestamp updates correctly on record changes. There are no tickets or docs explaining this omission, and recent code and test snapshots confirm it's still missing in the latest release (source). If you want official support for this, you could suggest it for a future release.

Let me know if you need help with the implementation or have other questions!

To reply, just mention @dosu.


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

<!-- gh-comment-id:3217586743 --> @dosubot[bot] commented on GitHub (Aug 24, 2025): <!-- Greeting --> Hi @yvbbrjdr! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The omission of `.$onUpdate(() => new Date())` for the `updatedAt` field in the generated Drizzle schema is not intentional or documented—it's likely an oversight in the schema template logic. Without `.$onUpdate`, the `updatedAt` field won't automatically update when records are modified, which is the expected behavior for most user/auth tables in Drizzle ORM. To fix this, manually add `.$onUpdate(() => new Date())` to the `updatedAt` field in your generated schema. This will ensure the timestamp updates correctly on record changes. There are no tickets or docs explaining this omission, and recent code and test snapshots confirm it's still missing in the latest release ([source](https://github.com/better-auth/better-auth/pull/4132)). If you want official support for this, you could suggest it for a future release. Let me know if you need help with the implementation or have other questions! <!-- 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/a681c16d-ffb9-499a-a106-f488e880b5c6?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/a681c16d-ffb9-499a-a106-f488e880b5c6?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/a681c16d-ffb9-499a-a106-f488e880b5c6?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/a681c16d-ffb9-499a-a106-f488e880b5c6?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/a681c16d-ffb9-499a-a106-f488e880b5c6?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/a681c16d-ffb9-499a-a106-f488e880b5c6?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/a681c16d-ffb9-499a-a106-f488e880b5c6?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/4187)
Author
Owner

@yvbbrjdr commented on GitHub (Aug 24, 2025):

@dosu

expected behavior for most user/auth tables in Drizzle ORM

Why?

<!-- gh-comment-id:3217606010 --> @yvbbrjdr commented on GitHub (Aug 24, 2025): @dosu > expected behavior for most user/auth tables in Drizzle ORM Why?
Author
Owner

@dosubot[bot] commented on GitHub (Aug 24, 2025):

It's considered best practice in most ORMs and relational databases to auto-update the updatedAt field whenever a record changes, especially for user/auth tables. This helps with auditability, debugging, and compliance—knowing when data was last modified is crucial for tracking changes and troubleshooting issues. Drizzle ORM provides the .$onUpdate method specifically for this purpose: it ensures the timestamp is refreshed automatically on updates.

In the Better Auth codebase, the omission of .$onUpdate for updatedAt isn't documented or explained—it's likely just an oversight in the schema generator logic, not a deliberate design choice. The expectation to auto-update comes from general database conventions, not from anything specific in Better Auth or Drizzle ORM docs or code. If you want this behavior, you should manually add .$onUpdate(() => new Date()) to your schema field. 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

<!-- gh-comment-id:3217608511 --> @dosubot[bot] commented on GitHub (Aug 24, 2025): <!-- Answer --> It's considered best practice in most ORMs and relational databases to auto-update the `updatedAt` field whenever a record changes, especially for user/auth tables. This helps with auditability, debugging, and compliance—knowing when data was last modified is crucial for tracking changes and troubleshooting issues. Drizzle ORM provides the `.$onUpdate` method specifically for this purpose: it ensures the timestamp is refreshed automatically on updates. In the Better Auth codebase, the omission of `.$onUpdate` for `updatedAt` isn't documented or explained—it's likely just an oversight in the schema generator logic, not a deliberate design choice. The expectation to auto-update comes from general database conventions, not from anything specific in Better Auth or Drizzle ORM docs or code. If you want this behavior, you should manually add `.$onUpdate(() => new Date())` to your schema field. [source](https://github.com/better-auth/better-auth/blob/577cfb4ab3b16d22f36892c0e843f36abcee80c3/packages/cli/src/generators/drizzle.ts) <!-- 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/03a03352-37ce-488a-ad99-bc0e0273f2eb?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/03a03352-37ce-488a-ad99-bc0e0273f2eb?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/03a03352-37ce-488a-ad99-bc0e0273f2eb?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/03a03352-37ce-488a-ad99-bc0e0273f2eb?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/03a03352-37ce-488a-ad99-bc0e0273f2eb?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/03a03352-37ce-488a-ad99-bc0e0273f2eb?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/03a03352-37ce-488a-ad99-bc0e0273f2eb?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/4187)
Author
Owner

@iamsg97 commented on GitHub (Aug 25, 2025):

Hello @yvbbrjdr 👋🏻
I am working to solve this, will share the branch details shortly. Then once the solution is ready, I might reach out to understand how to test it thoroughly.
Thanks in advance ✌🏻

<!-- gh-comment-id:3221336912 --> @iamsg97 commented on GitHub (Aug 25, 2025): Hello @yvbbrjdr 👋🏻 I am working to solve this, will share the branch details shortly. Then once the solution is ready, I might reach out to understand how to test it thoroughly. Thanks in advance ✌🏻
Author
Owner

@iamsg97 commented on GitHub (Aug 26, 2025):

Hello @yvbbrjdr, Happy Tuesday 👋🏻
Here is the change that I think should solve the issue. But I need some guidance on the tests.

What I am looking 👀 for 💭

  • I create a drizzle integration with better-auth ( this is fine)
  • It will be helpful if you can share me your auth config and schema.

Reason for the ask is it would save me some time, and I would make sure that I am not doing which is out of scope of this ticket 🥹

cc: @ping-maxwell @Kinfe123

<!-- gh-comment-id:3223762615 --> @iamsg97 commented on GitHub (Aug 26, 2025): Hello @yvbbrjdr, Happy Tuesday 👋🏻 [Here](https://github.com/iamsg97/better-auth/commit/24dc130652fb3646c31019984ba5b5e7035d0515) is the change that I think should solve the issue. But I need some guidance on the tests. What I am looking 👀 for 💭 - I create a drizzle integration with better-auth (✅ this is fine) - It will be helpful if you can share me your auth config and schema. > Reason for the ask is it would save me some time, and I would make sure that I am not doing which is out of scope of this ticket 🥹 cc: @ping-maxwell @Kinfe123
Author
Owner

@iamsg97 commented on GitHub (Aug 26, 2025):

Hello @yvbbrjdr 👋🏻
In continuation to the above, by any means do the config of yours look something similar. I was going through the better-auth docs, and I could make something like this (I am assuming, I might not be correct here)

import { createFieldAttribute } from "better-auth";

const options: BetterAuthOptions = {
  // keep other options as needed...
  user: {
    modelName: "user",
    // either put these in `additionalFields` or in a plugin schema; generator merges schemas.
    additionalFields: {
      // model property -> FieldAttribute
      name: createFieldAttribute("string", { required: true }),
      email: createFieldAttribute("string", { required: true, unique: true }),
      emailVerified: createFieldAttribute("boolean", {
        required: true,
        // make this a function so generator emits .$defaultFn(() => false)
        defaultValue: () => false,
      }),
      image: createFieldAttribute("string", { required: false }),
      createdAt: createFieldAttribute("date", {
        required: true,
        // function default produces .$defaultFn(() => /* @__PURE__ */ new Date())
        defaultValue: () => /* @__PURE__ */ new Date(),
      }),
      updatedAt: createFieldAttribute("date", {
        required: true,
        defaultValue: () => /* @__PURE__ */ new Date(),
        // explicit onUpdate isn't necessary because the generator infers onUpdate for fields named updated/updatedAt/updated_at,
        // but you can add a non-typed `onUpdate: true` or a function if desired.
        // onUpdate: true
      }),
    },
  },
};

If the above makes sense, all we need to add is a new property like below:

continuing from the above code comments

updatedAt: createFieldAttribute("date", {
        required: true,
        defaultValue: () => /* @__PURE__ */ new Date(),
        // explicit onUpdate isn't necessary because the generator infers onUpdate for fields named updated/updatedAt/updated_at,
        // but you can add a non-typed `onUpdate: true` or a function if desired.
        onUpdate: () => /* @__PURE__ */ new Date()
      })
<!-- gh-comment-id:3225010077 --> @iamsg97 commented on GitHub (Aug 26, 2025): Hello @yvbbrjdr 👋🏻 In continuation to the above, by any means do the config of yours look something similar. I was going through the better-auth docs, and I could make something like this (I am assuming, I might not be correct here) ```ts import { createFieldAttribute } from "better-auth"; const options: BetterAuthOptions = { // keep other options as needed... user: { modelName: "user", // either put these in `additionalFields` or in a plugin schema; generator merges schemas. additionalFields: { // model property -> FieldAttribute name: createFieldAttribute("string", { required: true }), email: createFieldAttribute("string", { required: true, unique: true }), emailVerified: createFieldAttribute("boolean", { required: true, // make this a function so generator emits .$defaultFn(() => false) defaultValue: () => false, }), image: createFieldAttribute("string", { required: false }), createdAt: createFieldAttribute("date", { required: true, // function default produces .$defaultFn(() => /* @__PURE__ */ new Date()) defaultValue: () => /* @__PURE__ */ new Date(), }), updatedAt: createFieldAttribute("date", { required: true, defaultValue: () => /* @__PURE__ */ new Date(), // explicit onUpdate isn't necessary because the generator infers onUpdate for fields named updated/updatedAt/updated_at, // but you can add a non-typed `onUpdate: true` or a function if desired. // onUpdate: true }), }, }, }; ``` If the above makes sense, all we need to add is a new property like below: _continuing from the above code comments_ ```ts updatedAt: createFieldAttribute("date", { required: true, defaultValue: () => /* @__PURE__ */ new Date(), // explicit onUpdate isn't necessary because the generator infers onUpdate for fields named updated/updatedAt/updated_at, // but you can add a non-typed `onUpdate: true` or a function if desired. onUpdate: () => /* @__PURE__ */ new Date() }) ```
Author
Owner

@himself65 commented on GitHub (Aug 26, 2025):

Hello @yvbbrjdr 👋🏻 In continuation to the above, by any means do the config of yours look something similar. I was going through the better-auth docs, and I could make something like this (I am assuming, I might not be correct here)

import { createFieldAttribute } from "better-auth";

const options: BetterAuthOptions = {
// keep other options as needed...
user: {
modelName: "user",
// either put these in additionalFields or in a plugin schema; generator merges schemas.
additionalFields: {
// model property -> FieldAttribute
name: createFieldAttribute("string", { required: true }),
email: createFieldAttribute("string", { required: true, unique: true }),
emailVerified: createFieldAttribute("boolean", {
required: true,
// make this a function so generator emits .$defaultFn(() => false)
defaultValue: () => false,
}),
image: createFieldAttribute("string", { required: false }),
createdAt: createFieldAttribute("date", {
required: true,
// function default produces .$defaultFn(() => /* @PURE / new Date())
defaultValue: () => /
@PURE / new Date(),
}),
updatedAt: createFieldAttribute("date", {
required: true,
defaultValue: () => /
@PURE */ new Date(),
// explicit onUpdate isn't necessary because the generator infers onUpdate for fields named updated/updatedAt/updated_at,
// but you can add a non-typed onUpdate: true or a function if desired.
// onUpdate: true
}),
},
},
};
If the above makes sense, all we need to add is a new property like below:

continuing from the above code comments

updatedAt: createFieldAttribute("date", {
required: true,
defaultValue: () => /* @PURE / new Date(),
// explicit onUpdate isn't necessary because the generator infers onUpdate for fields named updated/updatedAt/updated_at,
// but you can add a non-typed onUpdate: true or a function if desired.
onUpdate: () => /
@PURE */ new Date()
})

Hi, just open the PR. I think you are going on the right path. Thanks

<!-- gh-comment-id:3225119095 --> @himself65 commented on GitHub (Aug 26, 2025): > Hello [@yvbbrjdr](https://github.com/yvbbrjdr) 👋🏻 In continuation to the above, by any means do the config of yours look something similar. I was going through the better-auth docs, and I could make something like this (I am assuming, I might not be correct here) > > import { createFieldAttribute } from "better-auth"; > > const options: BetterAuthOptions = { > // keep other options as needed... > user: { > modelName: "user", > // either put these in `additionalFields` or in a plugin schema; generator merges schemas. > additionalFields: { > // model property -> FieldAttribute > name: createFieldAttribute("string", { required: true }), > email: createFieldAttribute("string", { required: true, unique: true }), > emailVerified: createFieldAttribute("boolean", { > required: true, > // make this a function so generator emits .$defaultFn(() => false) > defaultValue: () => false, > }), > image: createFieldAttribute("string", { required: false }), > createdAt: createFieldAttribute("date", { > required: true, > // function default produces .$defaultFn(() => /* @__PURE__ */ new Date()) > defaultValue: () => /* @__PURE__ */ new Date(), > }), > updatedAt: createFieldAttribute("date", { > required: true, > defaultValue: () => /* @__PURE__ */ new Date(), > // explicit onUpdate isn't necessary because the generator infers onUpdate for fields named updated/updatedAt/updated_at, > // but you can add a non-typed `onUpdate: true` or a function if desired. > // onUpdate: true > }), > }, > }, > }; > If the above makes sense, all we need to add is a new property like below: > > _continuing from the above code comments_ > > updatedAt: createFieldAttribute("date", { > required: true, > defaultValue: () => /* @__PURE__ */ new Date(), > // explicit onUpdate isn't necessary because the generator infers onUpdate for fields named updated/updatedAt/updated_at, > // but you can add a non-typed `onUpdate: true` or a function if desired. > onUpdate: () => /* @__PURE__ */ new Date() > }) Hi, just open the PR. I think you are going on the right path. Thanks
Author
Owner

@himself65 commented on GitHub (Aug 26, 2025):

I'm opening the PR to add onUpdate option to schema

<!-- gh-comment-id:3225862337 --> @himself65 commented on GitHub (Aug 26, 2025): I'm opening the PR to add `onUpdate` option to schema
Author
Owner

@iamsg97 commented on GitHub (Aug 27, 2025):

Thanks! I think apart from the tests I am pretty much inclined with you on the solution.
I will go through the tests to understand it ✌🏻

<!-- gh-comment-id:3226597541 --> @iamsg97 commented on GitHub (Aug 27, 2025): Thanks! I think apart from the `tests` I am pretty much inclined with you on the solution. I will go through the `tests` to understand it ✌🏻
Author
Owner

@maelp commented on GitHub (Aug 27, 2025):

I don't have a createFieldAttribute export on better-auth? has it been removed?

<!-- gh-comment-id:3227854435 --> @maelp commented on GitHub (Aug 27, 2025): I don't have a `createFieldAttribute` export on better-auth? has it been removed?
Author
Owner

@iamsg97 commented on GitHub (Aug 28, 2025):

Hello @maelp greetings 👋🏻
I still do see that in the source code 🧑🏻‍💻

Image

May be someone like @ping-maxwell @Kinfe123 from the org can help you with the details, about usage in the latest version!

<!-- gh-comment-id:3231408844 --> @iamsg97 commented on GitHub (Aug 28, 2025): Hello @maelp greetings 👋🏻 I still do see that in the source code 🧑🏻‍💻 <img width="637" height="306" alt="Image" src="https://github.com/user-attachments/assets/09217fd0-f654-4a01-a403-4a130d4000c1" /> May be someone like @ping-maxwell @Kinfe123 from the org can help you with the details, about usage in the latest version!
Author
Owner

@maelp commented on GitHub (Aug 28, 2025):

interesting weird, or perhaps it's on the latest version and I have the one before, because it wouldn't load for me, I'm using 1.3.7

Image
<!-- gh-comment-id:3232388872 --> @maelp commented on GitHub (Aug 28, 2025): interesting weird, or perhaps it's on the latest version and I have the one before, because it wouldn't load for me, I'm using 1.3.7 <img width="1724" height="238" alt="Image" src="https://github.com/user-attachments/assets/b82ef371-ba83-4273-80dd-1cb9f8cac916" />
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9855