[GH-ISSUE #6442] Clerk migration guide uses wrong admin field and time unit #10517

Closed
opened 2026-04-13 06:42:51 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @tjx666 on GitHub (Dec 1, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/6442

Summary

  • The Clerk migration guide/script writes banExpiresAt to the user record, but the admin plugin schema exposes banExpires (no "At"). The value is dropped/ignored.
  • lockout_expires_in_seconds from Clerk is seconds-from-now; the guide writes that raw number into a timestamp column without multiplying by 1000, so stored value becomes ~1970-01-01 or is rejected.

Repro

  1. Follow https://www.better-auth.com/docs/guides/clerk-migration-guide#enable-email-and-password-optional and run the sample migration script (current docs as of 2025-12-01).
  2. Observe the user insert payload contains banExpiresAt: clerkUser.lockout_expires_in_seconds.
  3. With admin plugin enabled, the inserted row has ban_expires null/ignored.
  4. If insert is forced, ban_expires receives an epoch-adjacent date because the value is a small integer in seconds.

Expected

Actual

  • Migration script uses banExpiresAt and writes the raw seconds number; ban data is lost or invalid.

Suggestion

  • Update the guide/sample to use banExpires and convert seconds to milliseconds.

Environment: Better Auth docs as of 2025-12-01; admin plugin enabled; PostgreSQL schema aligns with the plugin docs.

Originally created by @tjx666 on GitHub (Dec 1, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/6442 ## Summary - The Clerk migration guide/script writes `banExpiresAt` to the user record, but the admin plugin schema exposes `banExpires` (no "At"). The value is dropped/ignored. - `lockout_expires_in_seconds` from Clerk is seconds-from-now; the guide writes that raw number into a timestamp column without multiplying by 1000, so stored value becomes ~1970-01-01 or is rejected. ## Repro 1) Follow https://www.better-auth.com/docs/guides/clerk-migration-guide#enable-email-and-password-optional and run the sample migration script (current docs as of 2025-12-01). 2) Observe the user insert payload contains `banExpiresAt: clerkUser.lockout_expires_in_seconds`. 3) With admin plugin enabled, the inserted row has `ban_expires` null/ignored. 4) If insert is forced, `ban_expires` receives an epoch-adjacent date because the value is a small integer in seconds. ## Expected - Field name should be `banExpires` to match the admin plugin schema (docs: https://www.better-auth.com/docs/plugins/admin#schema). - Value should convert Clerk seconds-from-now into a Date, e.g. `banExpires: new Date(Date.now() + lockout_expires_in_seconds * 1000)`. ## Actual - Migration script uses `banExpiresAt` and writes the raw seconds number; ban data is lost or invalid. ## Suggestion - Update the guide/sample to use `banExpires` and convert seconds to milliseconds. Environment: Better Auth docs as of 2025-12-01; admin plugin enabled; PostgreSQL schema aligns with the plugin docs.
GiteaMirror added the locked label 2026-04-13 06:42:51 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Dec 1, 2025):

You’re right—there was a mismatch in the Clerk migration guide: it used banExpiresAt instead of the correct banExpires field, and wrote Clerk’s lockout_expires_in_seconds (seconds-from-now) directly into the database without converting to a proper timestamp. The admin plugin schema expects banExpires as a date/timestamp, so the old script caused the value to be dropped or stored as an invalid epoch date guide source admin schema.

This was fixed in November 2025: the migration guide now uses banExpires and expects Clerk’s timestamp in milliseconds, so you should pass the value directly (no need to multiply by 1000 if you’re using the latest Clerk API) PR #5745. If you migrated data using the old guide, update your records by renaming banExpiresAt to banExpires and converting any seconds values to milliseconds (multiply by 1000) to restore correct ban expiration dates issue #6441.

Let me know if you need a sample script for fixing existing data!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3595549940 --> @dosubot[bot] commented on GitHub (Dec 1, 2025): <!-- Answer --> You’re right—there was a mismatch in the Clerk migration guide: it used `banExpiresAt` instead of the correct `banExpires` field, and wrote Clerk’s `lockout_expires_in_seconds` (seconds-from-now) directly into the database without converting to a proper timestamp. The admin plugin schema expects `banExpires` as a date/timestamp, so the old script caused the value to be dropped or stored as an invalid epoch date [guide source](https://github.com/better-auth/better-auth/blob/33eeb8b21474a00a535c3b4c91057d7f0205028a/docs/content/docs/guides/clerk-migration-guide.mdx#L92-L299) [admin schema](https://github.com/better-auth/better-auth/blob/33eeb8b21474a00a535c3b4c91057d7f0205028a/packages/better-auth/src/plugins/admin/schema.ts#L4-L27). This was fixed in November 2025: the migration guide now uses `banExpires` and expects Clerk’s timestamp in milliseconds, so you should pass the value directly (no need to multiply by 1000 if you’re using the latest Clerk API) [PR #5745](https://github.com/better-auth/better-auth/pull/5745). If you migrated data using the old guide, update your records by renaming `banExpiresAt` to `banExpires` and converting any seconds values to milliseconds (multiply by 1000) to restore correct ban expiration dates [issue #6441](https://github.com/better-auth/better-auth/issues/6441). Let me know if you need a sample script for fixing existing data! <!-- 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/99a0aedb-8c67-4b10-a906-b7d1b5dfce76?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/99a0aedb-8c67-4b10-a906-b7d1b5dfce76?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/99a0aedb-8c67-4b10-a906-b7d1b5dfce76?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/99a0aedb-8c67-4b10-a906-b7d1b5dfce76?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/99a0aedb-8c67-4b10-a906-b7d1b5dfce76?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/99a0aedb-8c67-4b10-a906-b7d1b5dfce76?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/99a0aedb-8c67-4b10-a906-b7d1b5dfce76?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=You%E2%80%99re%20right%E2%80%94there%20was%20a%20mismatch%20in%20the%20Clerk%20migration%20guide%3A%20it%20used%20%60banExpiresAt%60%20instead%20of%20the%20correct%20%60banExpires%60%20field%2C%20and%20wrote%20Clerk%E2%80%99s%20%60lockout_expires_in_seconds%60%20%28seconds-from-now%29%20directly%20into%20the%20database%20without%20converting%20to%20a%20proper%20timestamp.%20The%20admin%20plugin%20schema%20expects%20%60banExpires%60%20as%20a%20date/timestamp%2C%20so%20the%20old%20script%20caused%20the%20value%20to%20be%20dropped%20or%20stored%20as%20an%20invalid%20epoch%20date%C2%A0%5Bguide%20source%5D%28https%3A//github.com/better-auth/better-auth/blob/33eeb8b21474a00a535c3b4c91057d7f0205028a/docs/content/docs/guides/clerk-migration-guide.mdx%23L92-L299%29%C2%A0%5Badmin%20schema%5D%28https%3A//github.com/better-auth/better-auth/blob/33eeb8b21474a00a535c3b4c91057d7f0205028a/packages/better-auth/src/plugins/admin/schema.ts%23L4-L27%29.%0A%0AThis%20was%20fixed%20in%20November%202025%3A%20the%20migration%20guide%20now%20uses%20%60banExpires%60%20and%20expects%20Clerk%E2%80%99s%20timestamp%20in%20milliseconds%2C%20so%20you%20should%20pass%20the%20value%20directly%20%28no%20need%20to%20multiply%20by%201000%20if%20you%E2%80%99re%20using%20the%20latest%20Clerk%20API%29%C2%A0%5BPR%20%235745%5D%28https%3A//github.com/better-auth/better-auth/pull/5745%29.%20If%20you%20migrated%20data%20using%20the%20old%20guide%2C%20update%20your%20records%20by%20renaming%20%60banExpiresAt%60%20to%20%60banExpires%60%20and%20converting%20any%20seconds%20values%20to%20milliseconds%20%28multiply%20by%201000%29%20to%20restore%20correct%20ban%20expiration%20dates%C2%A0%5Bissue%20%236441%5D%28https%3A//github.com/better-auth/better-auth/issues/6441%29.%0A%0ALet%20me%20know%20if%20you%20need%20a%20sample%20script%20for%20fixing%20existing%20data%21)&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/6442)
Author
Owner

@tjx666 commented on GitHub (Dec 1, 2025):

Add a more intuitive fragment to facilitate positioning:

// How to write the current document/script
banExpiresAt: clerkUser?.lockout_expires_in_seconds,

// Should be (Field name and unit are corrected)
banExpires: clerkUser?.lockout_expires_in_seconds
  ? new Date(Date.now() + clerkUser.lockout_expires_in_seconds * 1000)
  : undefined,

Reason: The admin plug-in schema only has banExpires, and lockout_expires_in_seconds is the "number of seconds from now", which needs to be multiplied by 1000 to convert to a timestamp and then save timestamptz.

<!-- gh-comment-id:3595552155 --> @tjx666 commented on GitHub (Dec 1, 2025): Add a more intuitive fragment to facilitate positioning: ```ts // How to write the current document/script banExpiresAt: clerkUser?.lockout_expires_in_seconds, // Should be (Field name and unit are corrected) banExpires: clerkUser?.lockout_expires_in_seconds ? new Date(Date.now() + clerkUser.lockout_expires_in_seconds * 1000) : undefined, ``` Reason: The admin plug-in schema only has `banExpires`, and `lockout_expires_in_seconds` is the "number of seconds from now", which needs to be multiplied by 1000 to convert to a timestamp and then save timestamptz.
Author
Owner

@GautamBytes commented on GitHub (Dec 1, 2025):

On it!!

<!-- gh-comment-id:3595872391 --> @GautamBytes commented on GitHub (Dec 1, 2025): On it!!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10517