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. Sign-in and email delivery came up in 7 verified builder reports in our data. 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.

7 verified cases10 min read Updated 27 Sep 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 24 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 a complaint about a newsletter can't hurt 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. Logging it isn't enough. Track the share of started sign-ins that complete, so you see a delivery problem within minutes, 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%.

Fix it with Gemmein

Gemmein sends sign-in codes itself, so you have no separate email provider or SMTP connection to set up for sign-in. Codes go out as "<App name> (via Gemmein)" until you verify your own domain, and then they come from that domain's account word. The limits are 3 codes per address and 20 per IP per app, each per 15 minutes, and there is no daily cap on sign-in emails.

  1. Send and verify codes with the SDK

    Sign-in takes two calls: one sends the code and the other checks it. Gemmein sends the code email, so you don't need to set up SMTP or an email service. Check that the email field isn't blank before you call, because a required field that is empty is refused with 400 missing_params.

    await g.auth.sendEmailCode(email);
    await g.auth.verifyEmailCode({ email, code });
  2. Add your domain for email on the Domains page

    Add the domain with a purpose of email (or both). The page lists every record it needs, each with its own status. Once the domain is verified, sign-in codes come from its account word instead of Gemmein's shared address.

  3. Branch on the error code when a limit is hit

    Each app allows 3 codes per address and 20 per IP, each per 15 minutes, and the number of sign-in emails sent in a day is never capped. A rate limit is refused as denied (429) and carries resetAt, so branch on err.code, show err.message, and tell the user when to try again.

    try {
      await g.auth.sendEmailCode(email);
    } catch (err) {
      // errorText and retryAt are your app's own UI state
      errorText = err.message;
      if (err.code === "denied") retryAt = err.resetAt;
    }
  4. Rehearse with real emails before launch

    Locally, npx gemmein dev prints codes to the terminal and sends no email. The dev rail sends real sign-in codes by email, so test delivery to real inboxes there before go-live.

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

← Back to the full report