Firebase email guides
How to send email from Firebase
Firebase is great for building apps quickly, but it does not provide a general-purpose transactional email API that you can safely call from your frontend.
For production email, the usual pattern is to send from a trusted backend, such as Firebase Cloud Functions. That keeps API keys and provider credentials away from browser code and gives you a controlled place to send password resets, verification emails, welcome emails, notifications and billing messages.
The basic shape is usually the same whichever email route you choose:
- Your app triggers an action.
- Firebase calls trusted backend code.
- The backend sends the email.
- The email provider accepts, queues or rejects the message.
The difference is how much email work you have to do yourself.
The three main ways to send email from Firebase
There are three practical routes:
- Use Firebase’s built-in email options and the Trigger Email extension.
- Use Firebase Cloud Functions with an email provider such as Resend, Mailgun, SendGrid or SES.
- Use Firebase Cloud Functions with EmailsDone.
All three can work. The important difference is how much template, HTML and delivery plumbing you want to own.
Why email should be sent from the backend
Do not put email provider API keys in frontend code.
If your app is built with React, Vue, Angular, Flutter Web or another browser-based frontend, anything shipped to the browser should be treated as public. Even environment variables with frontend prefixes can end up in the built client bundle.
That means transactional email should normally be sent from trusted backend code, such as Firebase Cloud Functions.
A simple Firebase email flow looks like this:
- the user performs an action in your app
- the app calls a Firebase Function
- the function validates the request
- the function sends the email using a provider or email service
The Firebase Function part is not usually the painful bit. The painful bit is everything after that: templates, HTML, variables, retries, bounces, unsubscribes and keeping every app email consistent.
Option 1: Firebase Trigger Email extension
Firebase has an official Trigger Email extension. The usual pattern is that your app or backend writes a document to Firestore, and the extension sends an email based on that document.
That can be a good fit if you want a Firebase-native approach and are happy using Firestore as the email queue.
The trade-off is that you still need to think about the actual email content. You need subjects, HTML, text, template data and a structure for each type of email your app sends.
For one email, that is fine.
For a production app with welcome emails, verification emails, password resets, login codes, payment failed emails, trial ending emails, export ready emails and notifications, it becomes another small project.
Option 2: Firebase Cloud Functions with an email provider
Another common approach is to create a Firebase Function and call an email provider directly.
For example, you might use Resend, Mailgun, SendGrid, Postmark or SES.
The shape is usually:
export const sendWelcomeEmail = onCall(async (request) => {
// validate user
// load user/app data
// call email provider API
});
This gives you control, but you own the email layer.
You still need to create templates, decide where they live, write HTML, test rendering, handle provider responses and avoid scattering email code around your app.
Again, the API call is not the hard bit.
The hard bit is building all the boring production email around it.
Option 3: Firebase Cloud Functions with EmailsDone
EmailsDone uses the same safe backend pattern, but removes the template work.
You still call it from Firebase Cloud Functions or another trusted backend. You still keep the API key server-side. The difference is that common transactional emails are already templates.
Install the library:
npm install emailsdone
Create a small email helper:
import { EmailsDone } from "emailsdone";
const emailsDone = EmailsDone.fromApiKey(process.env.EMAILSDONE_API_KEY!);
export async function sendWelcomeEmail(email: string, actionUrl: string) {
return emailsDone
.authentication()
.welcome(actionUrl)
.send(email);
}
Then your Firebase Function can call that helper when the relevant app event happens.
For example:
await sendWelcomeEmail(user.email, "https://app.example.com/get-started");
The important difference is that you are not building the welcome email template from scratch. You are selecting the app email you need and passing the data required by that template.
The same approach works for common flows like:
- welcome emails
- verify email
- password reset
- login codes
- notifications
- billing emails
- trial ending emails
- export ready emails
The practical difference
With a normal provider, you usually write code like:
await resend.emails.send({
from: "Acme <hello@example.com>",
to: user.email,
subject: "Welcome to Acme",
html: `
<h1>Welcome</h1>
<p>Thanks for signing up...</p>
<a href="${actionUrl}">Get started</a>
`
});
That works, but now you own the template.
With EmailsDone, the same intent is closer to:
await emailsDone
.authentication()
.welcome(actionUrl)
.send(user.email);
The function still runs securely in Firebase. The API key still stays server-side. But the email itself is already handled.
Prompt-first setup
If you are using Claude Code, Cursor, Codex or another coding agent, you can also generate a Firebase-specific implementation prompt.
The prompt tells the agent to:
- inspect the app before editing
- use Firebase Cloud Functions or trusted backend code
- keep the EmailsDone API key server-side
- install the official EmailsDone Node library
- create one small EmailService/helper
- wire only the selected templates
- avoid inventing custom HTML emails
- run local checks where available
That is useful because email touches auth, backend config and app flows. A focused prompt gives the coding agent enough structure to implement the feature without turning it into a messy rewrite.
Which Firebase email option should you choose?
Use Firebase Trigger Email if you want a Firebase-native extension and are happy managing the email content and templates yourself.
Use a provider like Resend, Mailgun, SendGrid or SES if you want full control over the email API, HTML, layout and provider setup.
Use EmailsDone if you want the safe Firebase backend pattern, but do not want to spend time creating and maintaining transactional email templates for every common app flow.
The boring answer is usually the best one:
Send from trusted backend code. Keep secrets out of the browser. Do not turn email into another project unless you really want to own it.