How to Add Email Notifications That Reach the Inbox
Sending email is one API call. Getting it delivered is a reputation problem, and reputation attaches to your domain — which means a careless bulk send can quietly break your password reset emails for weeks.
The authentication setup is genuinely non-optional now. Google and Yahoo both enforce SPF, DKIM, and DMARC for bulk senders, and mail without them lands in spam or is rejected. The good news is it's a one-time DNS task, and the subdomain strategy that protects your main domain costs nothing to adopt at the start.
Protect the domain, separate the streams
Deliverability is earned by the sending domain. The two decisions that matter most are made before you send anything.
- ›Send from a subdomain (`mail.example.com`), not your apex. A reputation problem then stays contained instead of affecting your corporate mail.
- ›Separate transactional from marketing entirely — different subdomains, ideally different providers. A newsletter that draws spam complaints must not be able to sink password resets.
- ›Set up SPF, DKIM, and DMARC before the first send. Retrofitting after a bad reputation is far slower than starting clean.
- ›Warm up gradually. Going from zero to 50,000 emails in a day looks exactly like a compromised account.
- ›One-click unsubscribe is required for bulk mail, and honoring it instantly is the cheapest deliverability protection there is.
The approaches
Transactional email APIs (Resend, Postmark, SendGrid, SES)
// best for: Password resets, receipts, notifications — anything triggered by a user action.
- AWS
- Express.js
- Next.js
- Django
- Laravel
- Ruby on Rails
One API call per message, with delivery tracking, bounce handling, and webhooks for opens and complaints. Postmark has the strongest reputation for transactional deliverability and deliberately refuses bulk marketing, which is precisely why its IP reputation stays clean. Resend is the modern developer-experience pick. SES is by far the cheapest at volume and gives you the least hand-holding.
Send asynchronously. An API call inside the request that creates a user couples signup to your email provider's availability — their outage becomes failed registrations. Queue the send and retry.
Handle bounce and complaint webhooks. Continuing to send to an address that hard-bounced is one of the fastest ways to damage a sending reputation, and providers will eventually suspend you for it.
# Three DNS records. All three are now required by Gmail
# and Yahoo for bulk senders.
# SPF — which servers may send as this domain.
mail.example.com. TXT "v=spf1 include:_spf.provider.com -all"
# DKIM — cryptographic signature. Provider gives you the key.
resend._domainkey.mail.example.com. TXT "v=DKIM1; k=rsa; p=MIGf..."
# DMARC — what to do when SPF/DKIM fail, and where to report.
# Start at p=none to collect reports, then tighten to quarantine.
_dmarc.mail.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com"The gotcha
SPF permits a maximum of 10 DNS lookups, and each `include:` counts — often several, since providers nest their own includes. Adding a fourth or fifth vendor silently exceeds the limit, SPF returns permerror, and mail that was delivering fine starts failing with no configuration change on your side.
Templating and rendering
// best for: Any email a customer actually reads.
- React
- Next.js
- Django
- Laravel
- Ruby on Rails
- NestJS
Email HTML is roughly 1999-era HTML. Outlook renders with Word's engine, flexbox and grid support is inconsistent, and CSS must be inlined because several clients strip `<style>` blocks. This is why table-based layouts persist — not nostalgia, but the only thing that renders reliably everywhere.
React Email and MJML both solve this by letting you write components and compiling down to the table soup clients need. React Email is particularly good if your app is already React, since you can share design tokens between the product and its emails.
Always send a plain-text alternative. Some clients prefer it, some users insist on it, and spam filters treat HTML-only mail with more suspicion.
Dark mode is now a real consideration: many clients invert colors, and a logo that assumed a white background disappears.
The gotcha
Images are blocked by default in most clients, so an email whose content is a single image shows an empty rectangle with a broken-image icon. Every image needs meaningful `alt` text, and the message must make sense with images off — including the call to action, which should be a styled link, not an image.
Notification preferences and digests
// best for: Products that generate more than a handful of notifications per user.
- PostgreSQL
- Supabase
- Redis
- Django
- Laravel
Once a product sends several kinds of email, per-type preferences stop being a nicety. Without them, a user annoyed by one notification type unsubscribes from everything, including messages they wanted, or marks you as spam — which is materially worse for deliverability than losing the subscriber.
Digests are the other lever. Batching activity into one daily or weekly email dramatically reduces volume and complaint rate while usually increasing engagement. The implementation is a scheduled job that aggregates unsent notifications per user, so the interesting part is the state tracking rather than the sending.
Always include a footer link to preferences alongside unsubscribe. Giving people a way to reduce email is what stops them from eliminating it.
The gotcha
Not suppressing notifications for activity the user performed themselves. Emailing someone about their own comment is the fastest way to train them that your notifications are noise, and it's a filter that's easy to forget on every new notification type.
Common pitfalls
Sending from your apex domain
A deliverability problem then affects everything at that domain, including mail your team sends from their own accounts. Use a subdomain so reputation damage is contained and recoverable.
Sending synchronously in the request
The email provider's latency becomes your endpoint's latency, and their outage becomes your failed signups. Queue every send, retry with backoff, and let the user's request complete.
Ignoring bounces and complaints
Repeatedly mailing addresses that hard-bounced is a reliable way to get suspended. Consume the bounce and complaint webhooks and add those addresses to a permanent suppression list.
Mixing marketing and transactional streams
Spam complaints from a campaign damage the reputation that delivers your password resets. Separate the domains and preferably the providers, so the two reputations are independent.
Questions people actually ask
Why is my email going to spam?
Check SPF, DKIM, and DMARC first — missing authentication is the most common cause and Gmail and Yahoo now enforce it. After that: new domain with no reputation, sudden volume increase, spam-triggering content, or a shared IP with poor history.
Do I need a dedicated IP?
Only above roughly 100,000 messages a month with consistent volume. Below that a shared pool is better — a dedicated IP with low volume never establishes reputation, and the warm-up is a real project.
How do I test emails before sending?
Use a provider sandbox or a catch-all inbox in staging, and a rendering preview service to check Outlook and mobile clients. Also test with images blocked and in dark mode, which is where most real rendering problems show up.