Firebase email guides

Firebase password reset emails

Firebase Authentication includes password reset emails out of the box.

For many apps, that is exactly what you should use. It is quick, secure, and already connected to Firebase Auth.

But as your app grows, the default password reset flow can start to feel like a separate piece of configuration rather than part of your app. The email template lives in the Firebase Console, the reset action may send users to a Firebase-hosted page unless configured, and visibility into the email itself can be limited.

This guide compares the default Firebase password reset email flow with a more owned approach: generate the secure reset link with Firebase Admin SDK, then send the email through a template-first transactional email service such as EmailsDone.

The important point:

EmailsDone does not replace Firebase Auth. Firebase still creates the secure password reset link. EmailsDone sends the email.

The short version

Use Firebase’s built-in password reset emails if you want the fastest working route.

Use a custom reset email flow if you want more control over:

  • branding
  • email templates
  • reset page routing
  • delivery visibility
  • app-level logging
  • where email configuration lives
  • consistency with the rest of your transactional emails

Firebase’s default flow is a good default.

The custom route is for when account recovery needs to feel like part of your product rather than a Firebase configuration screen.

How Firebase password reset emails work by default

The normal Firebase client-side flow looks something like this:

import { getAuth, sendPasswordResetEmail } from "firebase/auth";

const auth = getAuth();

await sendPasswordResetEmail(auth, "user@example.com");

Firebase handles the reset email for you.

The user receives an email containing a link. When they click it, Firebase verifies the action code and lets them set a new password.

That is a very useful built-in feature.

For a small app, prototype, internal tool or early SaaS project, it may be enough.

What Firebase does well

The built-in Firebase password reset flow has a lot going for it:

  • it is quick to enable
  • it uses Firebase Auth’s secure reset flow
  • it avoids building a backend just to send a reset email
  • it is familiar to Firebase developers
  • it can be customised in the Firebase Console
  • it works without adding another email provider

That matters.

Password reset is not the place to be clever for no reason. If the default flow works for your app, using it is perfectly sensible.

Where the default flow can feel awkward

The awkwardness usually appears later.

At first, “Firebase sends the password reset email” sounds ideal.

Then you start caring about details:

  • Where is the template maintained?
  • Does the email match the rest of the app?
  • Does the reset link open the right page?
  • Can I see whether the email was accepted or queued?
  • Can I keep password reset email code near my other app email code?
  • Can I use the same transactional email layer for welcome, verification, login code and billing emails?

Firebase’s built-in flow works, but it can feel separate from the rest of your application.

That separation is the main reason to consider owning the email layer yourself.

The hosted action page problem

If you use Firebase’s default password reset flow without customising the action URL, users may end up on a Firebase-hosted action page to complete the reset.

That may be acceptable.

But for a polished app, you often want the reset journey to stay inside your own product:

User requests password reset
↓
User receives branded email
↓
User clicks reset link
↓
User lands on your app
↓
User sets new password inside your UI

Firebase supports custom email action handlers. To use one, you create and host your own handler page, then update the Firebase email template to point to that custom action URL.

That works, but it is another thing to learn and configure.

The moment you are already creating your own reset page, the question becomes:

Should the email still live in the Firebase Console, or should the reset email be part of my app’s own email layer?

The template ownership problem

Firebase email templates live in the Firebase Console.

That is convenient at first, but it means password reset email content is maintained in a different place from the rest of your app emails.

For example, your app may eventually have:

  • welcome emails
  • verification emails
  • password reset emails
  • login code emails
  • invitation emails
  • payment failed emails
  • trial ending emails
  • export ready emails

If only the password reset email lives in Firebase Console, you now have two email systems:

Firebase Console templates
+
your app/provider/email templates

That is not always a problem.

But it is another place to remember, another place to configure, and another place where branding or wording can drift.

The metrics and visibility problem

Firebase’s built-in password reset flow is designed to make account recovery work, not to be a full transactional email dashboard.

For some apps, that is fine.

For others, password reset emails are important operational messages. If a user cannot receive one, they may be locked out.

You may want visibility into whether the email was accepted, queued, bounced or suppressed. You may also want password reset emails to appear alongside your other transactional email activity.

That is one of the reasons to send the email yourself while still using Firebase Auth to generate the secure reset link.

The owned route: generate the reset link yourself

Firebase Admin SDK can generate password reset links.

That gives you a useful split:

Firebase Auth:
creates the secure password reset link

Your backend:
decides when to send it

EmailsDone:
sends the password reset email

That means you are not replacing Firebase Auth.

You are only taking ownership of the email delivery layer.

A backend flow might look like this:

User requests password reset
↓
Firebase Function validates the request
↓
Firebase Admin SDK generates the reset link
↓
EmailsDone sends the password reset email
↓
User clicks the link and completes the Firebase reset flow

This gives you more control, but it is more work than the default Firebase client SDK call.

That is the trade-off.

Example: generating a Firebase reset link

In backend code, you can generate a password reset link with Firebase Admin SDK:

import { getAuth } from "firebase-admin/auth";

const resetLink = await getAuth().generatePasswordResetLink(
  "user@example.com",
  {
    url: "https://app.example.com/login",
    handleCodeInApp: true,
  }
);

The reset link is still a Firebase Auth password reset link.

Firebase still handles the action code.

Your app simply controls how the email is sent.

Sending the reset email with EmailsDone

Once you have the reset link, sending the email with EmailsDone is intentionally small:

await emailsDone
  .authentication()
  .passwordReset(resetLink)
  .send("user@example.com");

That call sends a password reset email using a built-in transactional template.

You can still keep the API key server-side in Firebase Functions, use Firebase secrets, validate the request, and apply App Check where appropriate.

The difference is that you do not need to build the password reset email HTML yourself.

Example Firebase Function

A simplified callable function might look like this:

import { onCall, HttpsError } from "firebase-functions/v2/https";
import { defineSecret } from "firebase-functions/params";
import { getAuth } from "firebase-admin/auth";
import { EmailsDone } from "emailsdone";

const emailsDoneApiKey = defineSecret("EMAILSDONE_API_KEY");

export const requestPasswordReset = onCall(
  {
    secrets: [emailsDoneApiKey],
    enforceAppCheck: true,
  },
  async (request) => {
    const email = request.data.email;

    if (typeof email !== "string" || !email.includes("@")) {
      throw new HttpsError("invalid-argument", "Enter a valid email address.");
    }

    const resetLink = await getAuth().generatePasswordResetLink(email, {
      url: "https://app.example.com/login",
      handleCodeInApp: true,
    });

    const emailsDone = EmailsDone.fromApiKey(emailsDoneApiKey.value());

    await emailsDone
      .authentication()
      .passwordReset(resetLink, {
        idempotencyKey: `password-reset-${email}`,
      })
      .send(email);

    return { queued: true };
  }
);

This is more code than Firebase’s built-in client call.

That is the point of the decision.

If you want the shortest path, use Firebase’s built-in reset email.

If you want the reset email to live with the rest of your transactional email system, generate the link server-side and send it yourself.

Be careful with account enumeration

Password reset endpoints can accidentally reveal whether an email address has an account.

For example, avoid returning different public messages like:

No account exists for this email.

and:

Password reset email sent.

A safer public response is usually:

If an account exists for that email address, a reset link will be sent.

Your backend can still log useful internal details, but the frontend should not help attackers discover registered email addresses.

This applies whether you use Firebase’s built-in reset email or send the reset email yourself.

Built-in reset email vs custom reset email

Question Firebase built-in reset email Custom reset email with EmailsDone
Fastest to start Yes No
Uses Firebase Auth reset codes Yes Yes
Sends email automatically Yes No, your backend sends it
Email template location Firebase Console EmailsDone / app email layer
Requires backend code No Yes
Custom reset page Requires action URL configuration Designed around your app flow
Delivery visibility Limited App/API-level visibility
Good for prototypes Yes Usually overkill
Good for polished app flows Sometimes Yes
Best fit Simple Firebase Auth setup Owned transactional email setup

The important distinction is that both routes can use Firebase Auth securely.

The difference is who owns the email layer.

When to use Firebase’s built-in password reset email

Use the built-in Firebase password reset email if:

  • you want the fastest setup
  • the default flow is good enough
  • you do not need detailed delivery visibility
  • you are happy maintaining the template in Firebase Console
  • your app does not yet have a broader transactional email system

This is a perfectly good choice for many apps.

It is the “just make password reset work” route.

When to send password reset emails yourself

Use the custom route if:

  • you already send other transactional emails
  • you want password reset emails in the same email system
  • you want more control over branding and wording
  • you want to keep the reset journey inside your own app
  • you want better operational visibility
  • you do not want email templates split between Firebase Console and your app/provider

This is the “own the account recovery experience” route.

It takes more setup, but it keeps the email layer in one place.

The practical recommendation

Start with Firebase’s built-in password reset email if you are early.

It works, and it is one of the useful things Firebase gives you out of the box.

Move to a custom reset email when the default flow starts getting in the way.

That usually happens when you care about:

  • branded account recovery
  • custom reset pages
  • email visibility
  • template consistency
  • keeping all transactional email in one place

EmailsDone fits the second stage.

It lets Firebase Auth keep doing the secure auth work, while EmailsDone handles the password reset email template and delivery layer.

Firebase still owns the reset code.

Your app owns the flow.

The email template is already done.