[GH-ISSUE #5356] The custom plugin removes extra attributes on the user added by other plugins #18851

Closed
opened 2026-04-15 17:32:10 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @scesbron on GitHub (Oct 16, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5356

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Get this project with the branch "custom-plugin-bug": https://github.com/scesbron/solid-auth/tree/custom-plugin-bug

  1. Run "bun install" to install the dependencies
  2. Run "tsc" to view the typescript error
  3. Remove the "customSession" plugin in the "auth.ts" file
  4. Run "tsc" again the typescript error is gone

Current vs. Expected behavior

Adding the custom session plugin should not remove the user attributes added by other plugins from the type definition

What version of Better Auth are you using?

1.3.27

System info

{
  "system": {
    "platform": "linux",
    "arch": "x64",
    "version": "#85-Ubuntu SMP PREEMPT_DYNAMIC Thu Sep 18 15:26:59 UTC 2025",
    "release": "6.8.0-85-generic",
    "cpuCount": 8,
    "cpuModel": "Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz",
    "totalMemory": "15.30 GB",
    "freeMemory": "3.39 GB"
  },
  "node": {
    "version": "v20.17.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.8.2"
  },
  "frameworks": [
    {
      "name": "solid",
      "version": "1.9.7"
    }
  ],
  "databases": [
    {
      "name": "postgres",
      "version": "3.4.7"
    },
    {
      "name": "drizzle",
      "version": "0.44.6"
    }
  ],
  "betterAuth": {
    "version": "1.3.27",
    "config": {
      "secret": "[REDACTED]",
      "emailAndPassword": {
        "enabled": true
      },
      "plugins": [
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": "Failed to create user",
              "USER_ALREADY_EXISTS": "User already exists.",
              "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.",
              "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself",
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users",
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users",
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users",
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]",
              "BANNED_USER": "You have been banned from this application",
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user",
              "NO_DATA_TO_UPDATE": "No data to update",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users",
              "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself"
            },
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            }
          }
        },
        {
          "name": "two-factor",
          "config": {
            "id": "two-factor",
            "endpoints": {},
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "user": {
                "fields": {
                  "twoFactorEnabled": {
                    "type": "boolean",
                    "required": false,
                    "defaultValue": false,
                    "input": false
                  }
                }
              },
              "twoFactor": {
                "fields": {
                  "secret": {
                    "type": "string",
                    "required": true,
                    "returned": false
                  },
                  "backupCodes": {
                    "type": "string",
                    "required": true,
                    "returned": false
                  },
                  "userId": {
                    "type": "string",
                    "required": true,
                    "returned": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  }
                }
              }
            },
            "rateLimit": [
              {
                "window": 10,
                "max": 3
              }
            ],
            "$ERROR_CODES": {
              "OTP_NOT_ENABLED": "OTP not enabled",
              "OTP_HAS_EXPIRED": "OTP has expired",
              "TOTP_NOT_ENABLED": "TOTP not enabled",
              "TWO_FACTOR_NOT_ENABLED": "Two factor isn't enabled",
              "BACKUP_CODES_NOT_ENABLED": "Backup codes aren't enabled",
              "INVALID_BACKUP_CODE": "Invalid backup code",
              "INVALID_CODE": "Invalid code",
              "TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE": "Too many attempts. Please request a new code.",
              "INVALID_TWO_FACTOR_COOKIE": "Invalid two factor cookie"
            }
          }
        },
        {
          "name": "custom-session",
          "config": {
            "id": "custom-session",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {}
          }
        }
      ]
    }
  }
}

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

Types

Auth config (if applicable)


Additional context

No response

Originally created by @scesbron on GitHub (Oct 16, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5356 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Get this project with the branch "custom-plugin-bug": https://github.com/scesbron/solid-auth/tree/custom-plugin-bug 1. Run "bun install" to install the dependencies 2. Run "tsc" to view the typescript error 3. Remove the "customSession" plugin in the "auth.ts" file 4. Run "tsc" again the typescript error is gone ### Current vs. Expected behavior Adding the custom session plugin should not remove the user attributes added by other plugins from the type definition ### What version of Better Auth are you using? 1.3.27 ### System info ```bash { "system": { "platform": "linux", "arch": "x64", "version": "#85-Ubuntu SMP PREEMPT_DYNAMIC Thu Sep 18 15:26:59 UTC 2025", "release": "6.8.0-85-generic", "cpuCount": 8, "cpuModel": "Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz", "totalMemory": "15.30 GB", "freeMemory": "3.39 GB" }, "node": { "version": "v20.17.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.8.2" }, "frameworks": [ { "name": "solid", "version": "1.9.7" } ], "databases": [ { "name": "postgres", "version": "3.4.7" }, { "name": "drizzle", "version": "0.44.6" } ], "betterAuth": { "version": "1.3.27", "config": { "secret": "[REDACTED]", "emailAndPassword": { "enabled": true }, "plugins": [ { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": "Failed to create user", "USER_ALREADY_EXISTS": "User already exists.", "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.", "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself", "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions", "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users", "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users", "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions", "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users", "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]", "BANNED_USER": "You have been banned from this application", "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user", "NO_DATA_TO_UPDATE": "No data to update", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users", "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself" }, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } } } }, { "name": "two-factor", "config": { "id": "two-factor", "endpoints": {}, "hooks": { "after": [ {} ] }, "schema": { "user": { "fields": { "twoFactorEnabled": { "type": "boolean", "required": false, "defaultValue": false, "input": false } } }, "twoFactor": { "fields": { "secret": { "type": "string", "required": true, "returned": false }, "backupCodes": { "type": "string", "required": true, "returned": false }, "userId": { "type": "string", "required": true, "returned": false, "references": { "model": "user", "field": "id" } } } } }, "rateLimit": [ { "window": 10, "max": 3 } ], "$ERROR_CODES": { "OTP_NOT_ENABLED": "OTP not enabled", "OTP_HAS_EXPIRED": "OTP has expired", "TOTP_NOT_ENABLED": "TOTP not enabled", "TWO_FACTOR_NOT_ENABLED": "Two factor isn't enabled", "BACKUP_CODES_NOT_ENABLED": "Backup codes aren't enabled", "INVALID_BACKUP_CODE": "Invalid backup code", "INVALID_CODE": "Invalid code", "TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE": "Too many attempts. Please request a new code.", "INVALID_TWO_FACTOR_COOKIE": "Invalid two factor cookie" } } }, { "name": "custom-session", "config": { "id": "custom-session", "hooks": { "after": [ {} ] }, "endpoints": {} } } ] } } } ``` ### Which area(s) are affected? (Select all that apply) Types ### Auth config (if applicable) ```typescript ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-15 17:32:10 -05:00
Author
Owner

@scesbron commented on GitHub (Oct 16, 2025):

Another problem I have with the custom plugin, in my project I redefine the error codes with

const ERROR_CODES: Record<keyof typeof authClient.$ERROR_CODES, string> = {
	USER_ALREADY_EXISTS: "L'utilisateur existe déjà",
        // ...
}

This generates an error if I forgot to translate an error code. But with the custom plugin if I add the error codes from the 2fa module then I have a typescript error that says that the error codes from the 2fa module do not exist.

<!-- gh-comment-id:3411771276 --> @scesbron commented on GitHub (Oct 16, 2025): Another problem I have with the custom plugin, in my project I redefine the error codes with ```typescript const ERROR_CODES: Record<keyof typeof authClient.$ERROR_CODES, string> = { USER_ALREADY_EXISTS: "L'utilisateur existe déjà", // ... } ``` This generates an error if I forgot to translate an error code. But with the custom plugin if I add the error codes from the 2fa module then I have a typescript error that says that the error codes from the 2fa module do not exist.
Author
Owner

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

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

This happens because your custom plugin (custom-session) does not define a $ERROR_CODES property. In Better Auth, only plugins that explicitly provide $ERROR_CODES contribute to the merged type for authClient.$ERROR_CODES. If a plugin is missing this property, TypeScript drops error codes from other plugins (like two-factor) from the inferred type, which leads to the errors you’re seeing when using Record<keyof typeof authClient.$ERROR_CODES, string> for translations. This is a known limitation in the current plugin type merging logic [source].

Workaround:
You can extract your plugin configs into constants and manually merge their error code types to form a combined type for your translations. For example:

const adminPluginConfig = admin();
const twoFactorPluginConfig = twoFactor();

type AdminErrorCodes = keyof typeof adminPluginConfig.$ERROR_CODES;
type TwoFactorErrorCodes = keyof typeof twoFactorPluginConfig.$ERROR_CODES;

type AllErrorCodes = AdminErrorCodes | TwoFactorErrorCodes;

const ERROR_CODES: Record<AllErrorCodes, string> = {
  USER_ALREADY_EXISTS: "L'utilisateur existe déjà",
  OTP_NOT_ENABLED: "OTP non activé",
  // ...
};

Alternatively, ensure every plugin (including custom ones) defines a $ERROR_CODES property, even if it's just an empty object, to avoid breaking the type merging [source].

This is a TypeScript type safety issue only—your plugins still work at runtime. For a complete, type-safe list of error codes, referencing the server-side auth.$ERROR_CODES is the most reliable way [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:3411798246 --> @dosubot[bot] commented on GitHub (Oct 16, 2025): <!-- Greeting --> Hi @scesbron! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This happens because your custom plugin (`custom-session`) does not define a `$ERROR_CODES` property. In Better Auth, only plugins that explicitly provide `$ERROR_CODES` contribute to the merged type for `authClient.$ERROR_CODES`. If a plugin is missing this property, TypeScript drops error codes from other plugins (like two-factor) from the inferred type, which leads to the errors you’re seeing when using `Record<keyof typeof authClient.$ERROR_CODES, string>` for translations. This is a known limitation in the current plugin type merging logic [[source](https://github.com/better-auth/better-auth/issues/5098)]. **Workaround:** You can extract your plugin configs into constants and manually merge their error code types to form a combined type for your translations. For example: ```typescript const adminPluginConfig = admin(); const twoFactorPluginConfig = twoFactor(); type AdminErrorCodes = keyof typeof adminPluginConfig.$ERROR_CODES; type TwoFactorErrorCodes = keyof typeof twoFactorPluginConfig.$ERROR_CODES; type AllErrorCodes = AdminErrorCodes | TwoFactorErrorCodes; const ERROR_CODES: Record<AllErrorCodes, string> = { USER_ALREADY_EXISTS: "L'utilisateur existe déjà", OTP_NOT_ENABLED: "OTP non activé", // ... }; ``` Alternatively, ensure every plugin (including custom ones) defines a `$ERROR_CODES` property, even if it's just an empty object, to avoid breaking the type merging [[source](https://github.com/better-auth/better-auth/issues/5098)]. This is a TypeScript type safety issue only—your plugins still work at runtime. For a complete, type-safe list of error codes, referencing the server-side `auth.$ERROR_CODES` is the most reliable way [[source](https://github.com/better-auth/better-auth/issues/5110)]. <!-- 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/b603c043-3d3f-44da-81d6-f16b6244a561?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/b603c043-3d3f-44da-81d6-f16b6244a561?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/b603c043-3d3f-44da-81d6-f16b6244a561?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/b603c043-3d3f-44da-81d6-f16b6244a561?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/b603c043-3d3f-44da-81d6-f16b6244a561?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/b603c043-3d3f-44da-81d6-f16b6244a561?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/b603c043-3d3f-44da-81d6-f16b6244a561?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/5356)
Author
Owner

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

The custom session overrides the default returned object. So whenever you call getSession it only returns the data returned from the custom fn. I'll look into the error code issue but seems like a duplicate of #5098

<!-- gh-comment-id:3413129678 --> @Bekacru commented on GitHub (Oct 16, 2025): The custom session overrides the default returned object. So whenever you call `getSession` it only returns the data returned from the custom fn. I'll look into the error code issue but seems like a duplicate of #5098
Author
Owner

@scesbron commented on GitHub (Oct 17, 2025):

@Bekacru do you mean that this is a normal behavior that the custom session overrides the default returned object? This means that once I use the custom session plugin there is no way to have my attributes from other plugins correctly type?

<!-- gh-comment-id:3414115796 --> @scesbron commented on GitHub (Oct 17, 2025): @Bekacru do you mean that this is a normal behavior that the custom session overrides the default returned object? This means that once I use the custom session plugin there is no way to have my attributes from other plugins correctly type?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18851