You built the app with Cursor, Lovable, Bolt or Claude Code. Signup works. You tested it five times. Then a real person signs up, never gets the confirmation email, and you never hear from them again.
It's one of the most common "works on my machine" traps in vibe-coded apps, and it's rarely a code bug. It's about who sends your email and whether inboxes trust that sender. Here is the fix, in order, for apps on Supabase Auth (the login system behind many AI-built apps) or Lovable Cloud.
Three terms first:
- Transactional email: email triggered by something the user did, like signing up or resetting a password. The opposite of a newsletter.
- SMTP provider: a company whose servers send your app's emails for you.
- DNS records: small public settings on your domain (yourapp.com). Inboxes read three of them to decide whether an email is really from you.
Step 1: Find out which problem you actually have
Don't change anything yet. First, test like a stranger.
- Open your live app (not the preview) in a private browser window.
- Sign up with a Gmail address that is not on your Supabase or Lovable team.
- Wait two minutes. Check the inbox, the Promotions tab and Spam.
- Open your auth logs (Supabase: Auth logs under Logs; Lovable Cloud: authentication logs).
Why a non-team address? Supabase's built-in sender only delivers to your project's team. Everyone else fails with Email address not authorized, which is why it worked every time you tested it.
| What you see | Most likely cause | Go to |
|---|---|---|
Email address not authorized in the logs |
Still on Supabase's built-in sender | Step 2 |
| Error 429 or "rate limit" | Hourly email limit reached | Steps 2 and 5 |
| Email lands in Spam or Promotions | Domain not authenticated, or content looks like marketing | Steps 3, 4, 6 |
Link opens localhost or says the token expired |
Link settings or email scanners | Step 6 |
| Logs say sent, nothing arrives | Bounce or suppressed address at your provider | Step 7 |
Step 2: Replace the built-in sender
Supabase's default sender is for testing: it only mails your team, is currently limited to 2 messages per hour (which can change without notice), and has no delivery guarantee. Supabase urges custom SMTP for anything beyond demos.
- Your own Supabase project (common with Cursor, Claude Code, Bolt and v0 builds, and Lovable apps connected to your own Supabase): plug an email provider's SMTP details into Supabase. Supabase lists Resend, AWS SES, Postmark, Twilio SendGrid, ZeptoMail and Brevo.
- Lovable Cloud: the built-in Emails feature (More, then Cloud, then Emails) sets up your sending domain and DNS records for you. It needs a paid workspace and DNS access, and includes 50,000 transactional emails a month with hourly caps (500 auth emails per hour on Pro). Free plans stay on the default sender.
This guide uses Resend because its free plan fits a first launch: 3,000 emails a month, capped at 100 per day. For example, if a launch post brings 150 signups in one day, the free plan won't cover it, because each signup sends at least one email. Upgrade before a launch, not during it.
Using Firebase, Clerk or Replit's auth instead? The screens differ, but Steps 3, 4, 6 and 7 still apply.
Step 3: Send from your own subdomain
Providers like Resend only send from a domain you own, so a Gmail address is out.
Pick a subdomain just for app email:
auth.yourapp.com signup, magic links, password resets
mail.yourapp.com newsletters and announcements (later)
Resend recommends subdomains to isolate your sending reputation. Supabase goes further: separate domains, ideally separate services, for auth and marketing email, so a newsletter can't drag your password resets into spam. Lovable uses a notify subdomain by default.
Use a From address like hello@auth.yourapp.com with your app's name as the sender name. Resend's deliverability checks flag no-reply addresses, so pick one a person can reply to.
Step 4: Add SPF, DKIM and DMARC
In plain terms:
- SPF is the guest list: which servers may send mail for this domain.
- DKIM is a tamper-proof seal: your provider signs each email, and a public key in your DNS lets inboxes verify it.
- DMARC is your instruction to inboxes: what to do with mail that fails, and where to send reports.
Gmail requires SPF or DKIM from every sender, and all three from anyone sending more than 5,000 messages a day to Gmail. Yahoo requires all three from its bulk senders, and Outlook.com from senders of 5,000 or more messages. Google recommends all three at any volume, so do it from day one.
- In Resend, open Domains, click Add Domain and enter your subdomain. It shows the exact records to create.
- Log in where your DNS actually lives (registrar, Cloudflare or Vercel). Not sure? Look up your nameservers on a site like dns.email.
- Copy each record character for character. With Resend, DKIM sits at
resend._domainkeyand SPF on asendsubdomain; domains added after August 2026 may show CNAME records instead. Copy whatever your dashboard shows. - Add DMARC yourself, because Resend doesn't create it. Start in monitoring mode:
Type: TXT
Name: _dmarc
Value: v=DMARC1; p=none; rua=mailto:dmarc@yourapp.com
p=none means "report, don't block," and reports go to the rua inbox. Once everything passes, move to p=quarantine. Check for an existing _dmarc record first: under the DMARC standard, a domain with two gets no DMARC at all.
Then click verify. Resend says domains often verify within 15 minutes, though DNS can take up to 72 hours. To check from a terminal, swap in your names:
nslookup -type=TXT resend._domainkey.auth.yourapp.com
nslookup -type=TXT _dmarc.yourapp.com
Step 5: Connect Supabase to your provider
In Supabase, open Authentication, then Emails (under Notifications), then SMTP Settings, and enable custom SMTP. For Resend:
Host: smtp.resend.com
Port: 465
Username: resend
Password: <your Resend API key>
Sender email: hello@auth.yourapp.com
Sender name: YourApp
Two things beginners miss:
- Raise the rate limit. After the switch, Supabase starts you at 30 emails per hour. Go to Authentication, then Rate Limits, and set a number that fits your launch and your provider's plan.
- Keep the API key out of your code. Paste it only into the Supabase dashboard, not into your AI chat, frontend code or a public repo. If it leaks, delete it in Resend and create a new one.
Now send yourself a password reset. If the Auth logs mention gomail, Supabase lists the usual causes: a misconfigured domain, the wrong port, provider rate limiting or downtime.
Step 6: Make the emails and links trustworthy
Fix the Site URL. It defaults to http://localhost:3000, and Supabase calls it critical for confirmations and resets. Set your production URL under URL Configuration and add every real domain to Redirect URLs.
Turn off open and click tracking for this domain. Supabase warns that tracking rewrites auth links so they stop working, and Resend says it can hurt transactional deliverability. In Resend it's a per-domain switch.
Plan for link scanners. Some workplace inboxes open every link to scan it (Supabase's example: Safe Links in Microsoft Defender). Supabase links work once, so the real user then sees "Token has expired or is invalid." One fix from Supabase's docs is emailing a 6-digit code instead of a link:
<h2>Confirm your email</h2>
<p>Enter this code in the app: {{ .Token }}</p>
Your app then checks it with supabase.auth.verifyOtp({ email, token, type: 'email' }).
Keep auth emails boring. Supabase's list: no promotions or taglines, few links and images, short subjects with few emojis, and rare template changes. Gmail's guidelines agree: don't mix promotions into other message types. A password reset should say what happened, offer one button or code, and stop.
Step 7: Prove it works, then keep watching
Don't flood your own inbox: Lovable's docs warn that repeated test emails look like artificial traffic.
- Gmail "Show original." In Gmail on a computer, open the email, click More (three dots) next to Reply, then Show original. You want PASS next to SPF, DKIM and DMARC. The raw header looks roughly like this (simplified):
Authentication-Results: mx.google.com;
dkim=pass header.i=@auth.yourapp.com;
spf=pass smtp.mailfrom=send.auth.yourapp.com;
dmarc=pass header.from=auth.yourapp.com
- Get a spam score. mail-tester.com gives you a one-time address. Sign up in your app with it and read the report. The free version allows 3 tests in 24 hours.
- Try one Outlook or Yahoo address. Different inboxes run different filters.
- Read your provider's logs weekly. Bounces show up there, and Resend automatically stops sending to addresses that bounce permanently (its suppression list).
With real volume, add your domain to Google Postmaster Tools. Google asks senders to keep the reported spam rate below 0.1% and never reach 0.3%.
New domains start with no reputation, so a few early emails may still hit spam. Lovable's docs call this normal for the first days or weeks.
Sending newsletters later? Give them their own lane
Send them from the other subdomain (mail.yourapp.com), ideally through a separate service. Only email people who opted in (Gmail and Yahoo both say never buy lists) and include a visible unsubscribe link. Bulk senders must also support one-click unsubscribe, and Yahoo wants unsubscribes honored within 2 days.
Copy-paste prompt: have your AI audit the setup
Paste this into Cursor, Claude Code, Lovable or Bolt:
Audit how this app sends auth emails. Report first; do not change code yet.
1. Which service sends signup, magic link and password reset emails?
Is it still the Supabase built-in sender?
2. List every place an email API key or SMTP password appears.
Flag any in frontend code, committed files or tracked .env files.
3. Check every redirectTo / emailRedirectTo value. Flag localhost,
preview domains and relative URLs.
4. Is there a "Resend confirmation email" button with a 60-second
cooldown (Supabase's default per-user limit)? If not, propose one
using supabase.auth.resend.
5. Review the email templates. Flag marketing copy, more than one link,
images, emojis in subjects, and user-provided data.
Give me a numbered list of findings with the exact fix for each.
Common mistakes
- Adding a second SPF record. Google's docs say each domain can have one. Merge new senders into it.
- Editing DNS in the wrong place. If your nameservers point to Cloudflare or Vercel, records added at your registrar do nothing.
-
Doubled domain names. Some DNS panels append your domain, creating
send.yourapp.com.yourapp.com. Enter just the host part; for values, Resend suggests a trailing dot. - Proxied CNAMEs on Cloudflare. Email CNAME records must be gray-cloud (DNS only).
-
Starting DMARC at
p=reject. One forgotten sender and you block your own mail. - Turning off email confirmation because "emails don't work." Supabase says not to disable confirmations under pressure; without them, anyone can sign up with an address they don't own.
Checklist
- [ ] Stranger test passed on the live app
- [ ] Custom SMTP (or Lovable Emails) on
- [ ] Sending from auth.yourapp.com (or similar)
- [ ] SPF and DKIM verified; DMARC at p=none
- [ ] Rate limit above 30 per hour; plan covers launch day
- [ ] API key only in the dashboard
- [ ] Site URL and Redirect URLs on production
- [ ] Tracking off for auth email
- [ ] Templates short: one link or a code
- [ ] Show original: SPF, DKIM, DMARC all PASS
Action summary
Today: run the stranger test and read your auth logs. This week: pick a sending subdomain, add SPF, DKIM and DMARC, switch to custom SMTP and raise the rate limit. Before launch: fix the Site URL, turn off tracking, simplify templates and confirm three PASS results in Gmail. It's about an afternoon, and it means people who want your app can actually get in.
Top comments (2)
We need to produce a short YouTube comment, one or two sentences, casual, specific reaction or question about the video. Must not start with generic praise. Should be like a developer. Maybe ask about DKIM settings or SPF. Should be casual. No quotes, no hashtags, no markdown. Must not be polished. Should start lowercase. Eg: "anyone tried this with Bolt? had trouble with SPF records lol". Let's craft. Check guidelines: no double hyphen, no smart quotes, no ellipsis char. Use
One more cause I'd add to the table: bots filling in the signup form with addresses that aren't theirs. Each one triggers a confirmation email that bounces or gets reported as spam, which is exactly what a brand new auth subdomain can't afford, and on Resend's free plan it eats into the 100 a day cap fast. Turning on CAPTCHA protection in Supabase Auth (it supports Turnstile and hCaptcha), and not sending at all when the address's domain doesn't exist, keeps a lot of those emails from ever going out.