Skip to content

[Medium] Auth mail may still send from Supabase's built-in SMTP; email rendering unverified in a real inbox #61

Description

@novaspatial

This was generated by AI while working the issue.

Severity: Medium (deliverability + credibility on a client-facing product taking payments). One ruling plus a set of checks that need a real inbox — none of it is provable from the repo, which is why it had no tracker home until now.

Background — what already shipped

Two rounds of email work landed on main and are not in question here:

  • Recovery (password-reset) verified end-to-end on production, 2026-08-01. It was never broken — both dashboard templates already link to /auth/confirm?token_hash={{ .TokenHash }}&type=…, which is what confirm/page.tsx expects. It read as unverified because nobody had ever run it (recovery_sent_at was null on all three accounts). Evidence: POST /api/auth/reset-password stamped recovery_sent_at at 15:22:49Z; after click-through it returned to null (GoTrue clears it on consumption) with updated_at/last_sign_in_at at 15:26:32Z.
  • A real rendering defect in both auth templates, fixed the same day. They shipped light-theme text (#18181b headline) with no background-color while declaring color-scheme: light dark — so dark-mode clients, which then skip auto-inversion, rendered a near-black headline on a dark background. They also used an inline <svg> logo (Gmail strips it, Outlook doesn't support it) and a raw & in the href. Corrected copy was applied to both templates.
  • App-sent email brought onto the same visual language (3e7301c). Every resend.emails.send() call had been text-only, so a client got a designed "Confirm your email" followed by a plain-text receipt and plain-text status notifications. There is now a shared shell — src/lib/email/layout.ts (renderEmailHtml) — and the receipt, four status notifications, and the contact notice all render through it, with text kept on every send as the multipart alternative and the fallback.

What is actually open

1. The ruling: Supabase built-in SMTP vs Resend for auth mail

App mail goes through Resend from NOVA Spatial <noreply@nova-spatial.com> (src/lib/resend.ts:10). Auth mail — confirm and recovery — is sent by Supabase, and if its SMTP is still the built-in sender, those two arrive from a mail.app.supabase.io address.

That is two problems at once: the built-in sender is rate-limited and shares reputation across every project on it (deliverability), and a client resetting a password on a paid product gets mail from a vendor domain they have never heard of, moments after a receipt from nova-spatial.com (credibility). It also means the one email flow that gates account access is the one not covered by the domain's own SPF/DKIM.

The question: point Supabase Auth at Resend SMTP so all mail leaves from noreply@nova-spatial.com, or accept the built-in sender for launch?

If Resend: Supabase dashboard → Project Settings → Authentication → SMTP Settings, host smtp.resend.com, port 465, user resend, password = a Resend API key, sender noreply@nova-spatial.com. The domain is already verified in Resend (app mail sends from it today), so there is no DNS work — it is a form to fill in. Receipt: trigger one reset and check the From: header.

2. Checks that need a real inbox or a second device

Verified in the repo: explicit backgrounds, canonical hosts, correct CTA targets in the rendered HTML. Not verified: how any of it actually renders — no client renders HTML the way a browser does.

  • Open a confirm mail and a recovery mail in a dark-mode inbox (Gmail app and Apple Mail are the two that matter) and confirm the headline is legible. This is the specific defect that was fixed blind.
  • Same for the app-sent mail through the new shell — receipt and one status notification.
  • Outlook/SafeLinks retest: corporate inboxes rewrite links and pre-fetch them, which can consume a single-use token before the client clicks. Send to a corporate address if you have one.
  • Cross-device: request the link on desktop, open it on a phone.
  • Stale-link UX: click an already-used or expired link and confirm the copy explains what happened instead of dumping an error.
  • Resend end-to-end: the resend button on the confirm screen, all the way to a delivered mail.

3. One post-deploy confirmation owed

The HTML shell has never been exercised against a real send — local sends are impossible while the .env.local demo kill switches are on. After the next deploy:

  • Trigger one real status change and confirm the HTML notification arrives and looks right.

Why this is ready-for-human

Item 1 is a ruling plus a dashboard form. Items 2–3 need inboxes, devices, and a live deploy. There is no code change pending on any of them — if a render check turns up a defect, that becomes its own fix.

Activity

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions