Troubleshooting guide

Users can't sign in: auth email in spam, blocked or never sent

The app worked for you, but after launch real users can't get in. Codes land in spam, reset links fail, or no email arrives. There is rarely a single cause. Sign-in depends on four things working at once: an identity service, an email provider, your DNS and your host's outbound network.

13 min read Updated 24 Oct 2026Last reviewed 9 Nov 2026How we collect cases

What you'll see

  • Sign-in codes or magic links reach your own inbox, but real users find them in spam or never get them.
  • Password reset links fail, or the identity service returns intermittent 5xx errors such as 502.
  • OAuth sign-in hangs or errors when the account is first created, with a message like "Database error saving new user".
  • Email reaches your own verified address and your team, and fails for everyone else.
  • Mail stops mid-day after a burst of sign-ups, or the provider reports a rate limit or daily quota.
  • All email stops at once, and the logs show SMTP authentication failures or a disabled or invalid API key.

Why it happens

Cause 1

The sending domain isn't authenticated

Gmail requires every sender to set up SPF or DKIM. Senders of more than 5,000 messages a day to Gmail must also publish DMARC, and the From domain must align with the SPF or DKIM domain. A new domain has no sending reputation, so unauthenticated mail from it goes to spam or is rejected.

Cause 2

You're still in a sandbox or on the default sender

A new Amazon SES account starts in a sandbox in each region. It can only send to verified addresses or domains, with at most 200 messages per 24 hours and 1 per second. Supabase's built-in email service only delivers to members of the project's team. It is limited to 2 messages an hour and is best-effort, with no SLA.

Cause 3

Rate limits and plan caps hit at launch

Launch is often the first time many people sign up at once. Free-plan caps and default limits that testing never reached start refusing mail. With custom SMTP, Supabase Auth starts at 30 messages an hour until you raise it.

Cause 4

Your host blocks outbound SMTP

Railway disables SMTP on its Free, Trial and Hobby plans. Render's free web services cannot send outbound traffic on ports 25, 465 or 587. The SMTP connection times out or is refused, and the sign-in request waiting on it fails or hangs.

Cause 5

Account creation fails inside the identity service

Many apps use a database trigger on the users table to create a profile row. The sign-up fails if that function refers to a missing table, has no permission to write outside the auth schema, or violates a foreign key constraint. On Supabase this shows up as "Database error saving new user", often during the first OAuth sign-in.

How to fix it

  1. Publish SPF, DKIM and DMARC for the sending domain

    Add the SPF and DKIM records your email provider gives you, then a DMARC TXT record at _dmarc on the same domain. Google recommends starting with p=none and a dedicated mailbox for reports. Tighten the policy once the reports show your mail passing.

    _dmarc.example.com  TXT  "v=DMARC1; p=none; rua=mailto:[email protected]"
  2. Leave the sandbox and the default sender before launch

    On SES, request production access; AWS gives an initial response within 96 hours. On Supabase, configure custom SMTP, then raise the email rate limit on the Rate Limits page to match the sign-ups you expect.

    aws sesv2 put-account-details \
      --production-access-enabled \
      --mail-type TRANSACTIONAL \
      --website-url https://example.com \
      --contact-language EN
  3. Send over the provider's HTTPS API instead of SMTP

    If your host blocks or throttles SMTP ports, call your email provider's HTTPS send endpoint from the server. Railway recommends HTTPS email APIs on every plan, including plans where SMTP is allowed.

  4. Fix the new-user trigger

    Check the Auth logs for the exact database error. Make sure the table the trigger writes to exists. Run the function as security definer with an empty search_path so the auth role can write outside the auth schema.

    create or replace function public.handle_new_user()
    returns trigger
    language plpgsql
    security definer set search_path = ''
    as $$
    begin
      insert into public.profiles (id) values (new.id);
      return new;
    end;
    $$;
  5. Handle bounces and complaints, and send sign-in mail separately

    Subscribe to your provider's bounce and complaint events, and stop sending to addresses that hard-bounce. Send sign-in mail separately from lifecycle and marketing mail, so complaints about a newsletter are less likely to affect delivery of sign-in codes.

  6. Add one-click unsubscribe to lifecycle mail

    Gmail requires one-click unsubscribe (RFC 8058) on marketing and subscribed mail from senders of more than 5,000 messages a day. The DKIM signature must cover both headers, and the URL must accept a POST.

    List-Unsubscribe: <https://example.com/unsubscribe/opaque-token>
    List-Unsubscribe-Post: List-Unsubscribe=One-Click
  7. Alert on send failures and sign-in success rate

    Page someone when the email provider returns an auth error, such as a disabled key or a failed SMTP login. Also track the share of started sign-ins that complete, so you notice a delivery problem before users write to support.

Check it's fixed

  • Sign up with new Gmail, Outlook and Yahoo addresses that are not on your team or verified list. Confirm the code arrives in the inbox, not spam.
  • In Gmail, open the message and choose Show original. SPF, DKIM and DMARC should all read PASS.
  • Complete a first-time OAuth sign-up and a password reset, then confirm the Auth logs show no database or 5xx errors.
  • Watch the sending domain in Google Postmaster Tools and keep the reported spam rate below 0.3%.

Specific fixes

Stuck on one exact error? These pages go deeper on one symptom each.

Fix it with Gemmein

Until you add a domain with a purpose of email (or both), sign-in codes arrive from Gemmein's shared address as "<App name> (via Gemmein)"; once that domain is set up, the code arrives from its account word. The Domains page lists every record the domain needs, each with its own status.

  1. Build sign-in on the two email-code calls

    g.auth.sendEmailCode(email) emails an 8-digit code and g.auth.verifyEmailCode({ email, code }) completes the sign-in; build the code field for 8 digits, digits only. Once a new code's email has been sent it replaces the earlier one, which then stops working even if its email arrives later: tell people to use the newest email.

    await g.auth.sendEmailCode(email);
    await g.auth.verifyEmailCode({ email, code });
  2. Add your domain with a purpose of email

    Add the domain on the Domains page with a purpose of email (or both); for email it needs six records: the one proving the domain is yours, DKIM, SPF, the bounce MX, the reply MX and DMARC. With email on, the app sends from account@ (sign-in codes, receipts), support@ and news@ on mail.<your domain>; with email off, the app keeps Gemmein's sender.

  3. Publish the records and read their status

    When your DNS provider has approved Gemmein's template, Records offers Connect automatically; otherwise the Records list shows every record to add by hand, with whether each one is found, missing or has the wrong value. If the records are not found yet, the domain stays pending and Gemmein checks the ownership record on its own; the Email card says "Authenticated: SPF, DKIM, DMARC" when all three are found, and until then names the ones not found yet.

  4. Show the wait and the retry

    Sending codes is capped at 3 per email address and 20 per IP per app, each per 15 minutes; past that the answer is 429 with resetAt, so show the wait and disable the resend button between attempts. If the code email cannot be sent the answer is 503 email_send_failed, which is retryable: show "Try again in a moment".

Questions

Why do sign-in emails reach me but not my users?

You are probably still in a sandbox or on a default sender that only delivers to verified or team addresses. Amazon SES and Supabase's built-in service both work this way. Your own address is verified, so testing never hit the restriction.


Do I need DMARC if I only send a few sign-in codes?

Gmail requires SPF or DKIM from every sender, and DMARC only above 5,000 messages a day. DMARC at p=none is still worth adding: it costs one DNS record and sends you reports on who is sending as your domain.


Why does SMTP work locally but time out once deployed?

Some hosts block outbound SMTP on certain plans. Railway blocks it on Free, Trial and Hobby, and Render blocks ports 25, 465 and 587 on free web services. Your laptop has no such block. Switch to the provider's HTTPS API.


Should sign-in codes carry an unsubscribe header?

Gmail's one-click unsubscribe requirement applies to marketing and subscribed messages. Add it to lifecycle and marketing mail, and send that mail as a separate stream so it can't hurt delivery of sign-in codes.


Sources

Last reviewed 9 Nov 2026. What changed: No changes: steps re-checked against 8 official page(s). Checked against: support.google.com, knowledge.workspace.google.com, rfc-editor.org, docs.aws.amazon.com, supabase.com, supabase.com, docs.railway.com, render.com. How we check this

← Back to the full report