[PR #2760] [CLOSED] Add customTypes support to Email OTP plugin #4471

Closed
opened 2026-03-13 11:48:19 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/better-auth/better-auth/pull/2760
Author: @totigm
Created: 5/23/2025
Status: Closed

Base: v1.3Head: email-otp/add-custom-types


📝 Commits (10+)

  • 0ded5b1 Add custom types to email OTP
  • 8437273 Update Email OTP docs
  • eb40756 Update Email OTP Client
  • 56da3b3 Update client docs
  • ae00dcf Linter fix
  • fb8b1ac Add tests
  • 635c333 Merge branch 'v1.3' into pr/2760
  • 03ea11f Merge remote-tracking branch 'origin/v1.3' into pr/2760
  • 416c61e chore: lock file
  • fd50e3d check if email is verified before update and avoid creating a session if it already exist

📊 Changes

6 files changed (+241 additions, -92 deletions)

View changed files

📝 docs/content/docs/plugins/email-otp.mdx (+90 -3)
📝 packages/better-auth/package.json (+1 -1)
📝 packages/better-auth/src/plugins/email-otp/client.ts (+7 -2)
📝 packages/better-auth/src/plugins/email-otp/email-otp.test.ts (+59 -4)
📝 packages/better-auth/src/plugins/email-otp/index.ts (+83 -81)
📝 pnpm-lock.yaml (+1 -1)

📄 Description

Description

This PR enhances the Email OTP plugin and its client to allow you to add your own customTypes alongside the default OTP types, with full TypeScript literal-union inference on both server handlers and client calls. It also updates the documentation to include a dedicated Custom Types section and clear examples for using custom OTP types in your server and client code.
Additionally, it adds new tests verifying customTypes support for both server and client flows.

What’s changed

Server plugin (emailOTP)

  • Conditional merging of default and custom types. z.enum(types) now includes both default and custom types:
    DefaultEmailOtpType | CustomType
    // e.g. "sign-in" | "email-verification"  | "forget-password" | "set-password" | "your-own-type"
    
  • If customTypes is omitted, you still get the defaults:
    "email-verification" | "sign-in" | "forget-password"
    

Server call

The generated API client’s sendVerificationOTP method now correctly infers the type parameter as the union of default and any customTypes you provided:

auth.api.sendVerificationOTP({
  body: {
    type: "set-password",  // "sign-in" | "email-verification"  | "forget-password" | "set-password" | "your-own-type"
    email: "test@test.com",
  },
});

Client plugin (emailOTPClient)

  • Made the client factory generic over CustomType extends string and accept the same customTypes?: readonly CustomType[].
  • Stubbed a no-op EmailOTPOptions<CustomType> so only the TS types are inferred:
    $InferServerPlugin: {} as ReturnType<typeof emailOTP<CustomType>>,
    
  • With customTypes = ["set-password", "your-own-type"], you now get:
    client.emailOtp.sendVerificationOtp({
      email: string,
      type: "sign-in" | "email-verification"  | "forget-password" | "set-password" | "your-own-type"
    })
    
    And without it, you still get the default types.

Documentation

  • Added a Custom Types section in the MDX docs.
  • Clarified that customTypes extend (don’t overwrite) the default OTP types and must be passed to the client plugin for autocompletion.

Zod v4 import note

  • We import from zod/v4 (while staying on v3 in package.json) to pick up fixes in generic inference for z.object().
  • In v3, z.object() could mark required fields T | undefined due to addQuestionMarks, breaking our strict type field.
  • This temporary import ensures our type is always required and correctly narrowed.

Why this change

Previously, you were limited to the three default OTP types and could not add any extras. For example, I wanted to do a custom OTP email for users that logged in with a social provider at first, but wanted to set a password now. Now users can set their own types depending on their needs, while still defaulting to the same we had before.

Code examples

Server usage

import { betterAuth } from "better-auth"
import { emailOTP } from "better-auth/plugins"

const auth = betterAuth({
  plugins: [
    emailOTP({
      customTypes: ["set-password", "your-own-type"],
      async sendVerificationOTP({ email, otp, type }) {
        if (type === "set-password") {
          // Send OTP to set password for first time
        } else {
          // Handle other types
        }
      },
    }),
  ],
});

Client usage

import { createAuthClient } from "better-auth/client"
import { emailOTPClient } from "better-auth/client/plugins"

const client = createAuthClient({
  plugins: [
    emailOTPClient({
      customTypes: ["set-password", "your-own-type"],
    }),
  ],
});

await client.emailOtp.sendVerificationOtp({
  email: "test@test.com",
  type: "set-password", // "sign-in" | "email-verification"  | "forget-password" | "set-password" | "your-own-type"
});

Tests

  • Custom OTP type "set-password"
    Verifies that the plugin correctly handles a custom OTP type in both server and client setups.

  • Client flow
    should send OTP with correct type via client

  • Server API flow
    should send OTP with correct type via server api


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/better-auth/better-auth/pull/2760 **Author:** [@totigm](https://github.com/totigm) **Created:** 5/23/2025 **Status:** ❌ Closed **Base:** `v1.3` ← **Head:** `email-otp/add-custom-types` --- ### 📝 Commits (10+) - [`0ded5b1`](https://github.com/better-auth/better-auth/commit/0ded5b1fa516c6e4944d20cfdbd05ad4f735de25) Add custom types to email OTP - [`8437273`](https://github.com/better-auth/better-auth/commit/84372737c8b437b2570a0c2d9b1f84580e26e710) Update Email OTP docs - [`eb40756`](https://github.com/better-auth/better-auth/commit/eb40756fadae27584a2db960828c70df530c27e2) Update Email OTP Client - [`56da3b3`](https://github.com/better-auth/better-auth/commit/56da3b3b2f33f3c2fa11299acd54c3280f38add0) Update client docs - [`ae00dcf`](https://github.com/better-auth/better-auth/commit/ae00dcfb4bfccc833528a2dc542652fcd9c9e16d) Linter fix - [`fb8b1ac`](https://github.com/better-auth/better-auth/commit/fb8b1ac5f891741725f249531db5904407337ed4) Add tests - [`635c333`](https://github.com/better-auth/better-auth/commit/635c333fc9ab2fe3b5f8898c5bd1ba45ca06a1f2) Merge branch 'v1.3' into pr/2760 - [`03ea11f`](https://github.com/better-auth/better-auth/commit/03ea11fde73f3339e37f4507b65738811553c4d2) Merge remote-tracking branch 'origin/v1.3' into pr/2760 - [`416c61e`](https://github.com/better-auth/better-auth/commit/416c61ee69bc7f339c03deb90cae2eeec4a760de) chore: lock file - [`fd50e3d`](https://github.com/better-auth/better-auth/commit/fd50e3d7d4c16b3bb77610ad4d0cbea8cbabf831) check if email is verified before update and avoid creating a session if it already exist ### 📊 Changes **6 files changed** (+241 additions, -92 deletions) <details> <summary>View changed files</summary> 📝 `docs/content/docs/plugins/email-otp.mdx` (+90 -3) 📝 `packages/better-auth/package.json` (+1 -1) 📝 `packages/better-auth/src/plugins/email-otp/client.ts` (+7 -2) 📝 `packages/better-auth/src/plugins/email-otp/email-otp.test.ts` (+59 -4) 📝 `packages/better-auth/src/plugins/email-otp/index.ts` (+83 -81) 📝 `pnpm-lock.yaml` (+1 -1) </details> ### 📄 Description ## Description This PR enhances the `Email OTP plugin` and its client to allow you to add your own `customTypes` alongside the default OTP types, with full TypeScript literal-union inference on both server handlers and client calls. It also updates the documentation to include a dedicated **Custom Types** section and clear examples for using custom OTP types in your server and client code. Additionally, it adds new tests verifying `customTypes` support for both server and client flows. ## What’s changed ### Server plugin (`emailOTP`) - Conditional merging of default and custom types. `z.enum(types)` now includes both default and custom types: ```ts DefaultEmailOtpType | CustomType // e.g. "sign-in" | "email-verification" | "forget-password" | "set-password" | "your-own-type" ``` - If `customTypes` is omitted, you still get the defaults: ```ts "email-verification" | "sign-in" | "forget-password" ``` ### Server call The generated API client’s `sendVerificationOTP` method now correctly infers the `type` parameter as the union of default and any `customTypes` you provided: ```ts title="server-call.ts" auth.api.sendVerificationOTP({ body: { type: "set-password", // "sign-in" | "email-verification" | "forget-password" | "set-password" | "your-own-type" email: "test@test.com", }, }); ``` ### Client plugin (`emailOTPClient`) - Made the client factory generic over `CustomType extends string` and accept the same `customTypes?: readonly CustomType[]`. - Stubbed a no-op `EmailOTPOptions<CustomType>` so only the TS types are inferred: ```ts $InferServerPlugin: {} as ReturnType<typeof emailOTP<CustomType>>, ``` - With `customTypes = ["set-password", "your-own-type"]`, you now get: ```ts client.emailOtp.sendVerificationOtp({ email: string, type: "sign-in" | "email-verification" | "forget-password" | "set-password" | "your-own-type" }) ``` And without it, you still get the default types. ### Documentation - Added a **Custom Types** section in the MDX docs. - Clarified that `customTypes` **extend** (don’t overwrite) the default OTP types and must be passed to the client plugin for autocompletion. ### Zod v4 import note - We import from `zod/v4` (while staying on v3 in `package.json`) to pick up fixes in generic inference for `z.object()`. - In v3, `z.object()` could mark required fields `T | undefined` due to `addQuestionMarks`, breaking our strict `type` field. - This temporary import ensures our `type` is always required and correctly narrowed. ## Why this change Previously, you were limited to the three default OTP types and could not add any extras. For example, I wanted to do a custom OTP email for users that logged in with a social provider at first, but wanted to set a password now. Now users can set their own types depending on their needs, while still defaulting to the same we had before. ## Code examples ### Server usage ```ts title="auth.ts" import { betterAuth } from "better-auth" import { emailOTP } from "better-auth/plugins" const auth = betterAuth({ plugins: [ emailOTP({ customTypes: ["set-password", "your-own-type"], async sendVerificationOTP({ email, otp, type }) { if (type === "set-password") { // Send OTP to set password for first time } else { // Handle other types } }, }), ], }); ``` ### Client usage ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" import { emailOTPClient } from "better-auth/client/plugins" const client = createAuthClient({ plugins: [ emailOTPClient({ customTypes: ["set-password", "your-own-type"], }), ], }); await client.emailOtp.sendVerificationOtp({ email: "test@test.com", type: "set-password", // "sign-in" | "email-verification" | "forget-password" | "set-password" | "your-own-type" }); ``` ### Tests - **Custom OTP type `"set-password"`** Verifies that the plugin correctly handles a custom OTP type in both server and client setups. - **Client flow** `should send OTP with correct type via client` - **Server API flow** `should send OTP with correct type via server api` --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-03-13 11:48:19 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#4471