What happened
Hosted Supabase project, 2026-08-02 ~02:37 UTC. Two POST /auth/v1/otp requests for the same not-yet-existing email address were in flight concurrently (~300 ms apart; email OTP via supabase-js signInWithOtp, auth-js 2.110.x, shouldCreateUser default). The first request created the auth user and sent the code (audit log shows exactly one user_confirmation_requested). The second hit the partial unique index on email during user creation and surfaced a raw 500:
POST | 500 | /auth/v1/otp
error: "failed to close prepared statement: ERROR: current transaction is aborted, commands ignored
until end of transaction block (SQLSTATE 25P02): ERROR: duplicate key value violates unique
constraint \"users_email_partial_key\" (SQLSTATE 23505)"
error_code: unexpected_failure · duration 327ms
To reproduce (untested sketch)
We have not re-run this against a live project (it creates a throwaway user and sends a real email), but the collision should reproduce with two sufficiently concurrent requests for the same never-before-seen address — the observed race had the requests ~300 ms apart against a winning request that took 327 ms, so the loser must land inside the winner's user-creation transaction:
EMAIL="race-$(date +%s)@your-domain.example" # fresh address, domain you control
URL="https://<project-ref>.supabase.co/auth/v1/otp"
BODY=$(printf '{"email":"%s","create_user":true}' "$EMAIL")
for i in 1 2; do
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$URL" -H "apikey: $ANON_KEY" -H "Content-Type: application/json" -d "$BODY" &
done
wait
Observed outcome: one 200, one 500 (unexpected_failure, the error above). Expected: two 200s (loser falls through to the existing-user OTP path, as a retry a second later would) or a structured 4xx.
System information
- Supabase: hosted (supabase.com); incident 2026-08-02 ~02:37 UTC, so whatever Auth build was deployed to hosted projects at that date
- Client: supabase-js 2.110.8 (resolved @supabase/auth-js 2.110.8), browser web app
- Flow:
signInWithOtp email OTP, shouldCreateUser default (true), PKCE
- Endpoint:
POST /auth/v1/otp
Why this seems worth hardening server-side
Two concurrent signups for one email is a predictable race (double-submit, multiple forms on one page, network-layer duplication), but the losing request currently fails with unexpected_failure out of an aborted transaction rather than a structured response. A client cannot distinguish this from a genuine server outage. Expected behavior would be a graceful path — e.g. detect the conflict and fall through to the existing-user OTP path (idempotent-ish, matching what a retry a second later would do), or a structured 4xx (409/429).
In our case the user happened to be unaffected (the winning request's success reached the client first), but if the ordering flips, an email+OTP signup flow shows a hard failure at the first step while a valid code is already in the user's inbox.
Notes
- We ruled out an app-level double submit on our side (single form submit; client instrumented since) — but regardless of where a duplicate originates, the collision handling in the
/otp user-creation path is server-side.
- Client-side error observability for this class was also degraded at the time by the 5xx
"{}"-message issue fixed in supabase-js#2587 — worth noting only as context for why such 500s can go unnoticed.
- Searched open/closed issues here for
/otp idempotency, users_email_partial_key, and concurrent-signup races — found no existing report.
What happened
Hosted Supabase project, 2026-08-02 ~02:37 UTC. Two
POST /auth/v1/otprequests for the same not-yet-existing email address were in flight concurrently (~300 ms apart; email OTP via supabase-jssignInWithOtp, auth-js 2.110.x,shouldCreateUserdefault). The first request created the auth user and sent the code (audit log shows exactly oneuser_confirmation_requested). The second hit the partial unique index on email during user creation and surfaced a raw 500:To reproduce (untested sketch)
We have not re-run this against a live project (it creates a throwaway user and sends a real email), but the collision should reproduce with two sufficiently concurrent requests for the same never-before-seen address — the observed race had the requests ~300 ms apart against a winning request that took 327 ms, so the loser must land inside the winner's user-creation transaction:
Observed outcome: one
200, one500(unexpected_failure, the error above). Expected: two200s (loser falls through to the existing-user OTP path, as a retry a second later would) or a structured 4xx.System information
signInWithOtpemail OTP,shouldCreateUserdefault (true), PKCEPOST /auth/v1/otpWhy this seems worth hardening server-side
Two concurrent signups for one email is a predictable race (double-submit, multiple forms on one page, network-layer duplication), but the losing request currently fails with
unexpected_failureout of an aborted transaction rather than a structured response. A client cannot distinguish this from a genuine server outage. Expected behavior would be a graceful path — e.g. detect the conflict and fall through to the existing-user OTP path (idempotent-ish, matching what a retry a second later would do), or a structured 4xx (409/429).In our case the user happened to be unaffected (the winning request's success reached the client first), but if the ordering flips, an email+OTP signup flow shows a hard failure at the first step while a valid code is already in the user's inbox.
Notes
/otpuser-creation path is server-side."{}"-message issue fixed in supabase-js#2587 — worth noting only as context for why such 500s can go unnoticed./otpidempotency,users_email_partial_key, and concurrent-signup races — found no existing report.