Why Sending Email Deserves Extra Scrutiny
Email is one of the easiest features to ask AI for, and one of the easiest to get wrong in ways you won't see from the outside. AI can produce a working sendEmail() call, an "Email sent!" message on screen, and a message that arrives cleanly in your own inbox during testing — and it will look finished. What's easy to miss: whether real users' mail actually reaches their inbox instead of their spam folder, whether the sending key is exposed where a visitor could take it, and whether the app can be tricked into emailing the wrong person or the wrong content.
The good news is the same as with accounts, payments, and uploads: you don't need to run mail infrastructure yourself to do this safely. You need to avoid one common mistake — trying to deliver mail directly from your own app server instead of using a hosted provider — and confirm a few things AI tools often skip by default: that the sending happens on your backend where the key stays secret, that your sending domain is authenticated so receiving systems can verify it, and that the recipient and contents come from your own verified data, never straight from whatever a request claimed.
A message that "sends successfully" has not necessarily been delivered. Your code getting a 200 response from an email service only means the service accepted the request — not that the mail reached an inbox instead of a spam folder or a silent block. Password resets and verification links are the emails users notice most when they fail to arrive, and the ones they can't work around. Treat "did it reach the inbox?" as part of the feature, not an afterthought.
The Safe Default: A Hosted Email API
Just as a hosted auth provider takes password storage off your plate, a hosted email service takes mail delivery infrastructure off your plate: it operates the sending network, retries temporary failures, and tells you when a message bounced or drew a spam complaint. Your app makes one authenticated API call from its backend (or uses the provider's authenticated SMTP relay); the service handles delivery. Trying to deliver mail directly from your own app server means managing IP reputation, warm-up, feedback loops, and blocklists — specialist work that hosted services exist to remove.
- Resend — a developer-focused email API with a clean setup, a natural fit for a vibe coded app; the free plan sends up to 3,000 emails a month, capped at 100 per day, with no credit card required to start.
- Postmark — built around transactional email with a strong deliverability reputation, which is exactly what password resets and receipts need; the free developer plan covers 100 emails a month for testing and doesn't expire, with paid plans starting at $15 a month for 10,000 emails.
- Amazon SES — a low-cost pay-as-you-go option at $0.10 per 1,000 emails, billed per recipient; in exchange it's more infrastructure to wire up and configure, so reach for it when volume makes the price matter rather than for your first version.
- SendGrid (now a Twilio product) — a large, established provider; note that its permanent free plan has become a time-limited trial, so check the current terms before you build on the free tier assuming it stays free.
Domain authentication is a foundation for deliverability, not a guarantee of the inbox. SPF authorizes sending systems, DKIM adds a verifiable signature, and DMARC tells receivers what to do when authentication does not align with the address users see in the From field. Follow the provider's exact DNS setup: some ask you to publish SPF records, while others manage SPF through their own return-path and only require you to add DKIM. Then publish a DMARC policy for your domain using the provider's or your domain host's instructions. Authentication lets receivers verify your mail, while reputation, complaint and bounce rates, message content, and recipient engagement still affect placement. Don't send real user mail from an unverified domain (or impersonate a free @gmail.com-style address whose DNS you don't control) and expect reliable delivery.
The Prompt to Ask For
Name the hosted service explicitly so AI doesn't invent self-managed delivery, and ask for backend sending, key safety, and a verified sender in the same prompt so none of them is left as an afterthought:
Add transactional email to my app using [Resend / Postmark / Amazon SES] for [password reset / email verification / receipts / notifications]. Send the email from my backend, never from the browser, and read the API key from a server-side environment variable — never hardcode it or expose it to client code. The final recipient must be loaded from my own database record for the account the verified action belongs to, not copied blindly from the request. For password reset, use the submitted address only to look up the account, return the same generic response whether or not it exists, and send a short-lived, single-use reset link created by my auth system only to the stored address. Do not put any unvalidated user input into the email's subject, headers, or the "to"/"from"/"reply-to" fields. Tell me which DNS records I need to add to authenticate my sending domain, and give me a way to send a test message before I wire it into real user actions.
When the AI returns code, check for these things before you consider it done:
- Sending happens on the server, and the API key lives in an environment variable — not in client-side code, not in a committed file. If the key can be read from anything the browser downloads, treat it as already leaked.
- The final recipient comes from your own trusted data — the signed-in user's stored address or the account record found during a reset flow — not from a request field that a caller could use to redirect someone else's receipt or notification.
- No raw user input is placed into subjects, headers, or address fields — the next section explains why an attacker controlling those can turn your app into a spam relay.
- Domain authentication is part of setup — the provider dashboard shows your domain as verified, with SPF and DKIM in place, before you rely on delivery.
Why a "Send Email" Button Isn't a Delivered, Safe Email
The dangerous shortcut with email is letting the browser decide who gets mailed and what it says. If your frontend calls an endpoint that emails "whatever address and message the request contained," anyone can call that endpoint directly — not just through your form — and use your verified domain and your sending quota to deliver mail of their choosing. That's how a small app quietly becomes a tool for sending spam or phishing in your name, which also wrecks the sending reputation your real mail depends on.
Sending must happen on your backend, triggered by a verified event, with the final recipient looked up from your own data. A password-reset form necessarily accepts an address, but the server should use it only to find the account and then mail the address stored on that record; its on-screen response should look the same even when no account exists, so the form does not reveal who has signed up. A receipt goes to the buyer your verified payment record names. When the recipient and content are derived from records you trust, a malicious request has nothing to redirect. When an endpoint will send arbitrary content to an arbitrary address from the request, it is an open door.
There's a related, quieter failure that has nothing to do with attackers: sending the right email to the wrong person. If a notification loop reuses a variable, an off-by-one selects the wrong user, or a test address is left wired in, your app can cheerfully deliver one user's receipt, reset link, or private alert to someone else. Because email leaves your system and can't be recalled, that's a data leak, not a bug you can quietly patch. Verify that the address is loaded fresh from the correct record for each message, and test with two separate accounts to confirm each one only ever receives its own mail — the same two-account discipline used for accounts and file access.
Show me exactly where each email's final recipient address comes from. Confirm it's loaded from my database for the specific account the action belongs to, and that no general email endpoint will send arbitrary content to an arbitrary address from the request body. For password reset, confirm the submitted address is used only for account lookup, the public response does not reveal whether the account exists, and the auth system creates an expiring, single-use link. Then give me a test with two different accounts and one nonexistent address so I can verify that each account only receives its own mail and all three reset requests show the same public response.
Transactional vs Marketing Email
Not all email is the same, and mixing the two is a common way to get your whole domain filtered. Transactional email is a direct response to something a specific user did: a reset link, a verification code, a receipt, an alert they asked for. Marketing email is something you decided to send — announcements, newsletters, re-engagement nudges — often to many people at once. The safety rules differ.
- Keep marketing out of transactional flows. Don't slip a promotion into a password-reset email. Beyond annoying users, in many places marketing mail carries legal requirements (a real sender identity and a working unsubscribe link) that transactional mail doesn't, and blurring them puts your deliverability and compliance at risk.
- Anything promotional needs a working unsubscribe, and you must honor it. If your app grows into sending updates or newsletters, every such message needs a genuine one-click way out, and opting out has to actually stop the mail. Ask AI to store and respect a per-user email preference, not just render an unsubscribe link that does nothing.
- Rate-limit and never send in a loop by accident. A retry that re-sends on every attempt, or a loop that emails once per row, can fire hundreds of messages in seconds — burning your daily free-tier quota, and looking exactly like abuse to your provider. Send each transactional message once per event, and cap how often a given user can trigger one.
Test Mode Before Live
Email is unusually easy to test wrong, because during development every message goes to you and everything looks fine. The failures show up only with real, varied recipients. A few habits close that gap:
- Send to a real external inbox before launch, not just your own. Mail to your own domain often skips the spam checks that mail to an outside provider goes through. Send a test to an address on a different provider and confirm it lands in the inbox, not spam — and that the links and formatting survive.
- Use the provider's sandbox or test key while building. Most services offer a test mode or a way to send only to verified addresses, so a bug in development can't spray mail at real people. Switch to the live key only when the flow is verified, and keep test and live keys in separate environment variables.
- Watch bounces and complaints once real mail flows. Every provider reports bounced messages and spam complaints. A rising bounce or complaint rate is an early warning that your deliverability — and your sending reputation — is in trouble, well before users start telling you their reset link never came.
Pre-Launch Email Checklist
- Hosted email API, not raw SMTP from your own server — the service maintains the sending reputation your delivery depends on.
- API key server-side in an environment variable — never in client code, never committed; separate keys for test and live.
- Sending domain authenticated — the provider's required SPF or return-path setup complete, DKIM verified, and a DMARC policy published for your domain; necessary for trust, but not an inbox guarantee.
- Final recipient loaded from your own trusted data — including after account lookup in a password-reset flow, never blindly copied from the request.
- No unvalidated input in subjects, headers, or address fields — so your send endpoint can't be turned into a spam relay.
- Two-account test passed — each account only ever receives email meant for it.
- Marketing separated from transactional — with a real, honored unsubscribe on anything promotional.
- Sending rate-limited and never looping — one message per event, with a cap per user.
- Delivered to a real external inbox and checked — inbox not spam, with bounce and complaint monitoring on.
Related Guides
Adding User Accounts to Your Vibe Coded App
Email verification and password reset are where most apps first need to send mail — the accounts guide this one builds on.
Adding Payments to Your Vibe Coded App
Receipts and payment alerts are transactional email too — hosted checkout, webhooks, and what to verify before real money moves.
Growing Your Vibe Coded App
Adding features, handling real users, and keeping an eye on cost and reliability as your app grows.
Sanitizing Code and Data Before Sending to AI
What to scrub before pasting code or credentials into an AI conversation — including email API keys.