Skip to content

[@clerk/clerk-js] <SignUp/> sends 2 verification code emails per sign-up: both POST /sign_ups and POST /prepare_verification trigger email send (Flutter analog #117) #8684

Closed as not planned

Description

@jackc625

Preflight

Description

The prebuilt <SignUp/> widget from @clerk/react@6.4.2 sends two verification-code emails for a single sign-up submission. The Network panel shows the widget makes two POST requests during one submit, and both server endpoints trigger a verification email send:

  1. POST /v1/client/sign_ups?__clerk_api_version=2025-11-10 — signUp.create({ emailAddress, password })
  2. POST /v1/client/sign_ups/<id>/prepare_verification?__clerk_api_version=2025-11-10 — signUp.prepareEmailAddressVerification({ strategy: 'email_code' })

The user receives two emails with different codes; the first one is invalidated by the second.

Why this is a real bug (not a duplicate of #887)

This is the prebuilt-widget analog of the documented behavior in #887 (closed 2023-03-03), where Clerk contributor @gkats explained:

the signIn.create() method will also prepare a verification under the hood when called with a strategy such as "email_code". That's why you get two codes. The first one comes from signIn.create() and becomes invalid as soon as the second one is generated. When you call signIn.create() with a "phone_code", "email_code" or "email_link" strategy you don't need to also prepare the verification afterwards.

That explanation applies symmetrically to signUp.create(): when the application's configured strategy is email_code (Dashboard → User & Authentication → Email, Phone, Username → Email address → ✅ Email verification code), signUp.create() auto-prepares a verification on the server side, and the widget's subsequent explicit prepareEmailAddressVerification call generates a second code.

The #887 resolution was a workaround for custom sign-in flows: omit the strategy from signIn.create(), then call prepareFirstFactor() manually. Users of the prebuilt <SignUp/> widget cannot apply this workaround — the widget controls both calls internally. Rebuilding sign-up as a custom useSignUp() flow defeats the purpose of the prebuilt component.

So either:

  • (a) The prebuilt <SignUp/> widget should detect that signUp.create() already initiated verification (returned status with the right verification primed) and skip the redundant prepareEmailAddressVerification call; OR
  • (b) signUp.create() should NOT auto-prepare a verification (the widget should be the sole driver of when verification preparation happens, matching what the workaround in signIn.prepareFirstFactor sending two emails with different codes #887 recommends for custom flows).

For comparison, the same root-cause shape was filed against the Flutter SDK and is the documented behavior there too: clerk/clerk-sdk-flutter#117 (createSignUp + follow-up triggering two emails).

Reproduction

Minimal repro is the prebuilt <SignUp/> widget with password + email_code strategy. No custom code, no useSignUp() flow.

// main.tsx
import { ClerkProvider } from "@clerk/react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ClerkProvider publishableKey={CLERK_KEY}>
      <App />
    </ClerkProvider>
  </StrictMode>
);

// sign-up.tsx
import { SignUp } from "@clerk/react";

export function SignUpPage() {
  return (
    <SignUp
      routing="path"
      path="/sign-up"
      signInUrl="/sign-in"
    />
  );
}

Clerk Dashboard → User & Authentication → Email, Phone, Username → Email address → Verification methods:

  • ✅ Email verification code
  • ❌ Email verification link

(Only ONE strategy enabled. Issue reproduces regardless — not a dashboard misconfig.)

Steps

  1. pnpm --filter web build && pnpm --filter web preview (prod-build, no StrictMode dev-time side effects)
  2. Open the preview URL, navigate to /sign-up
  3. DevTools → Network tab → filter clerk. → clear log
  4. Enter a fresh email + valid password, click Continue
  5. Observe Network panel + inbox

Observed

Endpoint Method Count Status
/v1/client/sign_ups?__clerk_api_version=2025-11-10 POST 1 200
/v1/client/sign_ups/<id>/prepare_verification?__clerk_api_version=2025-11-10 POST 1 200
/v1/client/sign_ups/<id> PATCH 0 —
Verification emails received within 60s — 2 first invalidated by second per #887

Expected

Exactly 1 verification email per sign-up submission from the prebuilt <SignUp/> widget — without requiring users to drop the widget and write a custom useSignUp() flow.

SDK / Environment

  • @clerk/react: ^6.4.2
  • @clerk/clerk-js (transitive, bundled with @clerk/react@6.4.2)
  • React: 19.0.x + react-dom@19.0.x
  • Build tool: Vite 5, prod build (pnpm build && pnpm preview)
  • Browser: Chrome (latest stable)
  • API version (from request URL): __clerk_api_version=2025-11-10
  • Dashboard strategy: Email verification code enabled; Email verification link disabled

Workaround

The workaround documented in #887 — omit strategy from the create() call, then explicitly call the prepare method — requires rebuilding sign-up as a custom useSignUp() flow. This is not viable for users of the prebuilt <SignUp/> component (the widget owns those calls).

No workaround within the prebuilt widget is known.

References

Activity

  1. github-actions commented on Jun 28, 2026

    @github-actions
    Contributor

    Hello 👋

    We currently close issues after 40 days of inactivity. It's been 30 days since the last update here. If we missed this issue, please reply here. Otherwise, we'll close this issue in 10 days.

    As a friendly reminder: The best way to see an issue fixed is to open a pull request. If you're not sure how to do that, please check out our contributing guide.

    Thanks for being a part of the Clerk community! 🙏

  2. jackc625 commented on Jul 7, 2026

    @jackc625
    Author

    Still reproduces on current clerk-js@6.x — please keep open (not stale, not a dup of #887). Adding the minimal reproduction that was missing.

    Minimal reproduction (one-click):

    Fresh Vite + @clerk/react app — a single prebuilt <SignUp/>, nothing else. A throwaway public dev-instance key is committed, so it's zero-setup: open it, sign up with any email, and you receive two email_code emails. Network panel shows the two POSTs that each trigger a send:

    1. POST /v1/client/sign_ups — signUp.create({ emailAddress, password })
    2. POST /v1/client/sign_ups/<id>/prepare_verification — signUp.prepareEmailAddressVerification({ strategy: 'email_code' })

    The first code is invalidated by the second.

    This isn't version drift. @clerk/react doesn't pin @clerk/clerk-js (its only deps are tslib + @clerk/shared); the widget is hot-loaded from Clerk's CDN at the major version, so this reproduces on whatever clerk-js@6.x you serve today regardless of the SDK version a consumer pins. An SDK bump doesn't change it.

    Not a duplicate of #887. #887 is the sign-in side, and its fix (omit strategy from create(), then call the prepare method yourself) is a custom-flow workaround. The prebuilt <SignUp/> widget owns both the create() and the prepareEmailAddressVerification() calls internally, so there's no consumer-side lever to apply it short of dropping the widget and rebuilding sign-up with useSignUp().

    One triage question — which fix would you accept, so I can open a PR against packages/clerk-js?
    (a) the widget skips the redundant prepareEmailAddressVerification when signUp.create({ strategy: 'email_code' }) has already primed verification, or
    (b) create() doesn't auto-prepare, leaving the widget as the sole driver.

    Happy to follow up with a PR (starting with a failing test in the SignUp flow suite) once you point me at the preferred direction.

  3. github-actions commented on Aug 7, 2026

    @github-actions
    Contributor

    Hello 👋

    We currently close issues after 40 days of inactivity. It's been 30 days since the last update here. If we missed this issue, please reply here. Otherwise, we'll close this issue in 10 days.

    As a friendly reminder: The best way to see an issue fixed is to open a pull request. If you're not sure how to do that, please check out our contributing guide.

    Thanks for being a part of the Clerk community! 🙏

  4. chton commented on Aug 10, 2026

    @chton

    This issue still exists, please do not close it without a resolution.

  5. SPerekrestova commented on Aug 24, 2026

    @SPerekrestova

    +1, faced the same issue

  6. github-actions commented on Sep 24, 2026

    @github-actions
    Contributor

    Hello 👋

    We currently close issues after 40 days of inactivity. It's been 30 days since the last update here. If we missed this issue, please reply here. Otherwise, we'll close this issue in 10 days.

    As a friendly reminder: The best way to see an issue fixed is to open a pull request. If you're not sure how to do that, please check out our contributing guide.

    Thanks for being a part of the Clerk community! 🙏

  7. github-actions commented on Oct 10, 2026

    @github-actions
    Contributor

    Hello again 👋

    After 40 days of no activity, we'll close this issue. Keep in mind, I'm just a robot, so if I've closed this issue in error, please reply here and my human colleagues will reopen it.

    As a friendly reminder: The best way to see an issue fixed is to open a pull request. If you're not sure how to do that, please check out our contributing guide.

    Thanks for being a part of the Clerk community! 🙏

  8. thermcampos commented on Oct 10, 2026

    @thermcampos

    The issue is still happening. Can we keep it open? If no one is working on it, I can try finding some time next week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions