Skip to content

ZeptoMail (transactional email SMTP relay)

QR setu's own transactional-email sender (ADR-0008), and — as of the auth work (QRS-261, ADR-0018) — the mandatory SMTP relay behind every Supabase Auth email: sign-up/sign-in OTP, password reset, email verification/change, reauthentication, and the 7 security-notification templates. Branded templates live at supabase/templates/ (see that directory's README); this page covers getting mail actually delivered.

Why custom SMTP is not optional

Verified against Supabase's docs: the built-in mailer is capped at 2 emails/hour and "will refuse to deliver messages to addresses that are not part of the project's team." Below custom SMTP, no real merchant can ever receive a sign-in OTP — this is why it is on the auth critical path (P1 of the auth plan), not a later polish pass.

ADR-0008 already decided ZeptoMail as the platform's transactional sender (India DC, high sender reputation). This page consumes that decision; it does not re-litigate it. Once custom SMTP is enabled, Supabase imposes its own rate limit — 30 messages/hour by default — raise it deliberately at Authentication → Rate Limits before launch.

Sender identity

no-reply@qrsetu.com, consistent display name, both Dev and Prod projects.

DNS setup — Cloudflare, not Hostinger

qrsetu.com is registered at Hostinger, but Cloudflare (free plan) is authoritative for DNS — every record below goes in Cloudflare → the domain → DNS → Records. Nothing goes in Hostinger; Hostinger's own DNS panel is not in the resolution path at all once a domain points at Cloudflare's nameservers.

Get the values first: ZeptoMail console → Domains → add qrsetu.com → it shows a DKIM selector, DKIM value, and a bounce CNAME target. Use those exact values — they vary per account and per data centre (India DC) — never copy example values from this page or elsewhere.

#TypeNameValueCloudflare proxyTTL
1TXT<selector>._domainkeythe DKIM value from ZeptoMailn/aAuto
2CNAMEbounce-zem (see note)the target from ZeptoMailDNS only — OFFAuto
3TXT@v=spf1 include:zeptomail.net ~alln/aAuto
4TXT_dmarcv=DMARC1; p=none; rua=mailto:dmarc@qrsetu.com; fo=1n/aAuto

Both record NAMES are account-specific. Do not guess them, and do not trust this table's placeholders.

This page previously wrote row 2's name as plain bounce. For qrsetu.com ZeptoMail actually issued bounce-zem, and the DKIM selector is a number (31152624), not a word. That cost a real debugging session on 2026-08-01: a check for bounce.qrsetu.com and four guessed DKIM selectors all returned NXDOMAIN, and the conclusion drawn — "the ZeptoMail records were never added"was wrong. They were present and verified the whole time.

The rule: read the exact Host values off the ZeptoMail console (Domains → the domain → DNS records), then resolve those. A negative lookup on a name you guessed is evidence of nothing. As of 2026-08-01 both records resolve and ZeptoMail reports qrsetu.com Verified.

Four Cloudflare-specific traps, in the order people hit them

  1. The bounce CNAME must be "DNS only" (grey cloud), not proxied (orange cloud). Cloudflare proxies CNAMEs by default, which rewrites the record to Cloudflare IPs and breaks return-path validation, so ZeptoMail cannot verify the domain and SPF alignment fails. Click the orange cloud icon to turn it grey. This is the single most common cause of "I added everything and it still won't verify."
  2. Don't type the domain into the Name field. Cloudflare appends it automatically. Enter <selector>._domainkey, not <selector>._domainkey.qrsetu.com — the latter silently becomes <selector>._domainkey.qrsetu.com.qrsetu.com.
  3. Only one SPF TXT record on the root, ever. If @ already has a v=spf1 record (Hostinger may have added one at transfer), merge the include: into the existing record rather than adding a second — two SPF records is a permanent SPF failure, not a fallback that tries the second if the first doesn't match. Check before adding.
  4. Skip Cloudflare's "Email Routing" onboarding prompt if it appears. It adds its own MX/SPF records that will conflict with these. This domain only sends through ZeptoMail; it does not receive mail through Cloudflare.

Click Verify in ZeptoMail once records are added. Propagation on Cloudflare is usually minutes; ZeptoMail documents up to 24-48 hours for full verification, which is why this is a lead-time item to start early, not a launch-week task.

DMARC policy — start at p=none, tighten later

Deliberately monitor-only to start, so a misconfiguration can't blackhole OTP delivery while still under observation. Read the aggregate reports (rua=) for about a week, confirm 100% pass, then tighten to p=quarantine and eventually p=reject. Going straight to p=reject before SPF/DKIM alignment is actually confirmed is how teams break their own transactional mail on day one.

⚠ Current state (2026-08-01): DNS is done, the relay is pointed at the wrong provider

Do not read the sections above as "outstanding work" — the slow part is finished. What remains is a misconfiguration, measured rather than assumed (QRS-285):

  • Done: ZeptoMail domain verification. DKIM 31152624._domainkey.qrsetu.com and CNAME bounce-zem.qrsetu.comcluster89.zeptomail.in both resolve; the console reports Verified.
  • Wrong: Supabase Auth → SMTP Settings on both projects points at smtp.hostinger.com:465, not ZeptoMail. Custom SMTP is enabled, so Supabase reaches that relay and is rejected: a live OTP request to Dev returns HTTP 500 and the auth log reads 535 "5.7.8 Error: authentication failed".
  • Also wrong: SPF publishes v=spf1 include:_spf.mail.hostinger.com ~allno zeptomail include. So even once the credential is corrected, ZeptoMail-sent mail fails SPF. Remember there may only ever be ONE SPF record on the root: merge the include, never add a second.
  • Weak: _dmarc is a bare v=DMARC1; p=none with no rua=, so no aggregate reports are being collected.

Why nobody noticed: Supabase rolls the signup back when the confirmation email fails, so a failed OTP leaves no orphan auth.users row — it looks exactly like nothing happened. Google sign-in is unaffected and works, which is why the app appeared healthy.

Verifying delivery — evidence, not the dashboard's green tick

A green verification tick in ZeptoMail means the DNS records exist; it does not prove a receiving mailbox provider accepts them. Send a real OTP to both a Gmail and an Outlook address, open the raw message headers on each, and confirm all three:

dkim=pass
spf=pass
dmarc=pass

Also check visually in both light and dark email-client themes — the shared shell's dark-mode CSS (supabase/templates/_shell.mjs) is honoured by Apple/iOS Mail and some Gmail clients but ignored by Outlook desktop, which is an inherent HTML-email limitation, not a bug to chase.

Promotion

Auth SMTP settings, rate limits, and every email template are Supabase project settings, not files this repo's migrations capture. Promote to Dev first, verify, then Prod — same order as everything else in supabase/docs/PROMOTION_RUNBOOK.md — and record the promotion there alongside migrations/EFs/secrets so the runbook stays the one place that lists everything that must move together.

supabase/templates/ (the branded template source) · ADR-0008 (ZeptoMail decision) · ADR-0018 (auth architecture — resolves ADR-0008's open "ride Supabase's native OTP vs a bespoke EF" question in favour of native OTP + this SMTP relay) · QRS-261 (the auth pre-flight work that made custom SMTP a blocking dependency rather than a nice-to-have).