# How to Create Custom Email Templates for Notifications in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 30-45 min
- Compatibility: FlutterFlow Free+ (Cloud Functions require Firebase Blaze plan)
- Last updated: March 2026

## TL;DR

FlutterFlow has no email template builder — send branded notification emails by creating a Firebase Cloud Function that generates HTML with inline CSS (never external stylesheets) and sends via SendGrid or Mailgun. Store template strings in Firestore for admin editing without redeployment. Use HTML table-based layouts for maximum email client compatibility — never use div-based layouts or CSS classes in email HTML.

## Branded Email Notifications from FlutterFlow Apps

FlutterFlow's built-in Firebase push notifications are great for in-app alerts, but email notifications build trust and re-engage users who have churned. The challenge is email rendering: Gmail, Outlook, Apple Mail, and Yahoo all render HTML differently — and most of them strip CSS classes and external stylesheets entirely. This tutorial builds a Cloud Function email system using battle-tested email HTML patterns (table layouts, inline CSS), stores templates in Firestore for admin editing, and triggers sends from your FlutterFlow app.

## Before you start

- Firebase project with Cloud Functions enabled (Blaze plan)
- SendGrid account with a verified sender email or domain
- FlutterFlow project with Firestore configured
- Basic understanding of HTML structure

## Step-by-step guide

### 1. Understand email HTML rules before writing any code

Email HTML is not web HTML. The three most important rules: first, use inline CSS only — write style='color: #333; font-size: 16px;' on every element, never use a stylesheet or CSS classes because most email clients strip them. Second, use HTML tables for layout — div-based layouts break in Outlook, which still uses Microsoft Word as its rendering engine and does not support flexbox or grid. Third, use a maximum width of 600px and always include a plain-text alternative. Images must be hosted on a public URL (use Firebase Storage), not attached. Understanding these constraints before writing code will save you hours of debugging why your emails look broken in certain clients.

**Expected result:** You understand the three core email HTML constraints: inline CSS only, table-based layout, and 600px max width.

### 2. Create a reusable email HTML generator function

In your Firebase Cloud Functions project, create a utility file named emailTemplates.js. This file exports functions that generate complete email HTML strings. Each template accepts an options object with dynamic values like recipientName, subject, and body content. The outer shell of every email should be identical: a full HTML document with a 100%-width outer table, a 600px-wide inner table, a branded header with your logo, a content area, and a footer with unsubscribe and company address. Build this shell once as a getEmailShell() function and wrap each template in it.

```
// functions/emailTemplates.js
function getEmailShell(content, brandColor = '#4F46E5') {
  return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Email</title>
</head>
<body style="margin:0;padding:0;background-color:#f4f4f5;font-family:Arial,sans-serif;">
  <table width="100%" cellpadding="0" cellspacing="0" border="0"
         style="background-color:#f4f4f5;">
    <tr><td align="center" style="padding:24px 16px;">
      <table width="600" cellpadding="0" cellspacing="0" border="0"
             style="max-width:600px;width:100%;background:#ffffff;
                    border-radius:8px;overflow:hidden;
                    box-shadow:0 2px 8px rgba(0,0,0,0.08);">
        <!-- Header -->
        <tr><td style="background-color:${brandColor};padding:24px 32px;">
          <h1 style="margin:0;color:#ffffff;font-size:22px;
                     font-weight:700;letter-spacing:-0.3px;">YourApp</h1>
        </td></tr>
        <!-- Body -->
        <tr><td style="padding:32px;color:#374151;font-size:16px;
                       line-height:1.6;">
          ${content}
        </td></tr>
        <!-- Footer -->
        <tr><td style="background:#f9fafb;padding:20px 32px;
                       border-top:1px solid #e5e7eb;">
          <p style="margin:0;font-size:12px;color:#9ca3af;">
            You are receiving this email because you have an account with YourApp.
            <a href="{{{unsubscribe_url}}}" style="color:#6b7280;">Unsubscribe</a>
          </p>
        </td></tr>
      </table>
    </td></tr>
  </table>
</body>
</html>`;
}

function welcomeEmail({ recipientName }) {
  const content = `
    <p style="margin:0 0 16px;">Hi ${recipientName},</p>
    <p style="margin:0 0 16px;">Welcome to YourApp! Your account is ready.</p>
    <table cellpadding="0" cellspacing="0" border="0" style="margin:24px 0;">
      <tr><td style="background:#4F46E5;border-radius:6px;">
        <a href="https://yourapp.com" style="display:block;padding:12px 24px;
           color:#ffffff;text-decoration:none;font-size:15px;font-weight:600;">
          Get Started
        </a>
      </td></tr>
    </table>
    <p style="margin:0;font-size:14px;color:#6b7280;">
      Questions? Reply to this email anytime.
    </p>`;
  return getEmailShell(content);
}

function orderConfirmationEmail({ recipientName, orderNumber, amount, items }) {
  const itemRows = items.map(item =>
    `<tr>
      <td style="padding:8px 0;border-bottom:1px solid #e5e7eb;">${item.name}</td>
      <td style="padding:8px 0;border-bottom:1px solid #e5e7eb;
                 text-align:right;">$${item.price.toFixed(2)}</td>
    </tr>`
  ).join('');

  const content = `
    <p style="margin:0 0 16px;">Hi ${recipientName}, your order is confirmed!</p>
    <p style="margin:0 0 24px;font-size:14px;color:#6b7280;">
      Order #${orderNumber}
    </p>
    <table width="100%" cellpadding="0" cellspacing="0" border="0">
      ${itemRows}
      <tr>
        <td style="padding:12px 0;"><strong>Total</strong></td>
        <td style="padding:12px 0;text-align:right;">
          <strong>$${amount.toFixed(2)}</strong>
        </td>
      </tr>
    </table>`;
  return getEmailShell(content);
}

module.exports = { welcomeEmail, orderConfirmationEmail };
```

**Expected result:** The emailTemplates.js utility exports two functions that return complete, self-contained HTML email strings ready to send.

### 3. Create the sendEmail Cloud Function with SendGrid

In your Cloud Functions index.js, import the emailTemplates utility and create a callable function named sendNotificationEmail. Store your SendGrid API key as a Firebase Function secret: run 'firebase functions:secrets:set SENDGRID_API_KEY' in your terminal. The function accepts the recipient email, template name, and template variables. It calls the appropriate template function to generate the HTML, then sends via the @sendgrid/mail package. Always include a plain-text version using the text parameter — this improves deliverability and provides a fallback for email clients that do not render HTML.

```
// functions/index.js (sendEmail portion)
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const sgMail = require('@sendgrid/mail');
const { welcomeEmail, orderConfirmationEmail } = require('./emailTemplates');

const templateMap = { welcome: welcomeEmail, order_confirmation: orderConfirmationEmail };

exports.sendNotificationEmail = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Login required');
  }

  const { recipientEmail, templateName, templateVars } = data;
  const templateFn = templateMap[templateName];

  if (!templateFn) {
    throw new functions.https.HttpsError('invalid-argument', `Unknown template: ${templateName}`);
  }

  sgMail.setApiKey(process.env.SENDGRID_API_KEY);

  const html = templateFn(templateVars);

  await sgMail.send({
    to: recipientEmail,
    from: { email: 'hello@yourapp.com', name: 'YourApp' },
    subject: templateVars.subject || 'Notification from YourApp',
    html,
    text: `Hi ${templateVars.recipientName}, please view this email in an HTML-capable client.`,
  });

  return { success: true };
});
```

**Expected result:** Calling sendNotificationEmail from FlutterFlow sends a branded HTML email via SendGrid. The email renders correctly in Gmail, Apple Mail, and Outlook.

### 4. Store email templates in Firestore for admin editing

Hardcoding templates in JavaScript means every change requires a Cloud Function redeployment. Instead, store your templates in Firestore so your admin can edit them from your app. Create an email_templates collection in Firestore. Each document has a name (String), subject (String), html_body (String — the inner HTML content, without the shell), and last_edited_by (String). In your Cloud Function, fetch the template document by name, inject the dynamic variables using string replacement (replace {{recipientName}} with the actual value), and wrap it in getEmailShell() before sending. Add a Template Editor page in FlutterFlow for your admin, using a multiline TextField bound to the html_body field.

**Expected result:** Admins can edit email template content and subject lines from within the FlutterFlow admin panel without touching any code or redeploying Cloud Functions.

### 5. Trigger notification emails from FlutterFlow actions

In FlutterFlow, create a Custom Action named triggerNotificationEmail. Inside it, call the sendNotificationEmail Cloud Function using FirebaseFunctions.instance.httpsCallable('sendNotificationEmail') with the recipient's email, template name, and any template variables. Add this action to the appropriate trigger points: after a user signs up (welcome email), after an order is completed (order confirmation), or when an admin posts an announcement. For high-volume sends (newsletters to all users), do not loop through users in FlutterFlow — instead create a Cloud Function that queries Firestore for the recipient list and sends in batches of 1,000.

**Expected result:** Completing key actions in your FlutterFlow app (sign-up, purchase, etc.) automatically triggers the corresponding branded notification email to the user.

## Complete code example

File: `email_base_template.html`

```text
<!-- Base email template shell — inline CSS, table layout, 600px max width -->
<!-- Store the content section in Firestore; wrap with this shell in Cloud Function -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>{{subject}}</title>
</head>
<body style="margin:0;padding:0;background-color:#f4f4f5;">
  <!--[if mso]>
  <table width="600" align="center" cellpadding="0" cellspacing="0" border="0">
  <tr><td>
  <![endif]-->
  <table width="100%" cellpadding="0" cellspacing="0" border="0"
         style="background-color:#f4f4f5;min-width:100%;">
    <tr>
      <td align="center" style="padding:24px 16px;">
        <table width="600" cellpadding="0" cellspacing="0" border="0"
               style="max-width:600px;width:100%;background:#ffffff;
                      border-radius:8px;">
          <!-- Logo header -->
          <tr>
            <td style="background-color:#4F46E5;padding:24px 32px;
                       border-radius:8px 8px 0 0;">
              <img src="https://yourapp.com/logo-white.png"
                   alt="YourApp" width="120" height="auto"
                   style="display:block;border:0;">
            </td>
          </tr>
          <!-- Content area - filled from Firestore template -->
          <tr>
            <td style="padding:32px;font-family:Arial,sans-serif;
                       font-size:16px;line-height:1.6;color:#374151;">
              {{html_body}}
            </td>
          </tr>
          <!-- Divider -->
          <tr>
            <td style="height:1px;background:#e5e7eb;"></td>
          </tr>
          <!-- Footer -->
          <tr>
            <td style="padding:20px 32px;background:#f9fafb;
                       border-radius:0 0 8px 8px;">
              <table width="100%" cellpadding="0" cellspacing="0" border="0">
                <tr>
                  <td style="font-family:Arial,sans-serif;font-size:12px;
                             color:#9ca3af;">
                    YourApp Inc., 123 Main St, City, ST 00000<br>
                    <a href="{{unsubscribe_url}}"
                       style="color:#6b7280;text-decoration:underline;">Unsubscribe</a>
                    &nbsp;&middot;&nbsp;
                    <a href="https://yourapp.com/privacy"
                       style="color:#6b7280;text-decoration:underline;">Privacy Policy</a>
                  </td>
                </tr>
              </table>
            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>
  <!--[if mso]></td></tr></table><![endif]-->
</body>
</html>
```

## Common mistakes

- **Using CSS div layouts and external stylesheets in email HTML** — Gmail strips all CSS classes and style tags in the head. Outlook uses Microsoft Word's rendering engine which does not support flexbox, grid, or most modern CSS. A beautiful email built like a webpage will look completely broken in the two most popular email clients. Fix: Use HTML tables for layout and inline styles on every element. Write style='...' directly on each tag. Never use classes, IDs, or link tags to external CSS in email HTML.
- **Hardcoding email templates in Cloud Function code and redeploying for every copy change** — Marketing teams, customer success managers, and product owners need to adjust email copy frequently. Requiring a developer and Cloud Function deployment for every word change slows the team and creates bottlenecks. Fix: Store template HTML in Firestore documents. The Cloud Function fetches the template at send time. Non-technical team members can edit templates from the admin panel without touching code.
- **Sending high-volume emails by looping in a FlutterFlow action** — FlutterFlow actions run on the client. Looping through 1,000 users to send emails from the client would take minutes, exhaust memory, and fail if the user closes the app mid-send. Fix: For bulk email sends, create a Cloud Function that queries the recipient list from Firestore and sends in batches of 1,000 using SendGrid's batch API. Trigger this single function from the app.
- **Not setting up domain authentication (SPF, DKIM, DMARC) before sending transactional emails** — Without DNS authentication records, your emails are flagged as unauthenticated by receiving mail servers and routinely delivered to spam. Users never see your notifications. Fix: In SendGrid, go to Settings → Sender Authentication → Domain Authentication. Follow the step-by-step DNS record setup for your domain. Verify before sending any production emails.

## Best practices

- Always use inline CSS and HTML table layouts — external stylesheets and div layouts break in Gmail and Outlook.
- Store templates in Firestore so non-technical team members can edit copy without code changes or redeployment.
- Set up domain authentication (SPF, DKIM, DMARC) before sending any production emails to avoid the spam folder.
- Include both HTML and plain text versions in every email send — plain text improves deliverability and accessibility.
- Log every email send to an email_logs Firestore collection for debugging and compliance auditing.
- Include an unsubscribe link in every marketing email — this is legally required under CAN-SPAM (US) and GDPR (EU).
- Test templates in at least Gmail, Apple Mail, and Outlook before deploying — use Litmus or Email on Acid for multi-client testing.

## Frequently asked questions

### Why can I not use CSS classes or external stylesheets in email HTML?

Gmail strips all CSS classes and most style tags in the head section of emails. Outlook uses Microsoft Word's HTML renderer which does not support modern CSS. The only CSS that works reliably across all major email clients is inline styles written directly on each HTML element.

### Which email sending service should I use — SendGrid, Mailgun, or Resend?

SendGrid is the most widely used, has a free tier of 100 emails per day, and has the best documentation. Mailgun has competitive pricing for high volume. Resend is a newer option popular with developers for its clean API. All three work with Firebase Cloud Functions — the integration code is nearly identical.

### How do I make email templates editable without redeploying Cloud Functions?

Store the template HTML as a string in a Firestore document. The Cloud Function fetches the template at send time using the template name, replaces placeholders like {{name}} with actual values, wraps it in your HTML shell, and sends. When the template needs updating, edit the Firestore document — no code change required.

### How do I add a logo image to my email template?

Upload your logo to Firebase Storage and get its public download URL. Use that URL in an img tag with explicit width and height attributes and style='display:block;border:0;' to prevent spacing issues in Outlook. Never use base64-encoded images — they bloat the email size and are often blocked.

### What is the maximum email size to avoid spam filters?

Keep emails under 100KB total (HTML + images). Emails over 100KB are clipped by Gmail and shown with a 'Message clipped' warning. Most transactional notification emails should be well under this limit.

### Do I need FlutterFlow Pro to send notification emails?

No. The email sending happens in Firebase Cloud Functions, not in the FlutterFlow app. You need the Firebase Blaze plan for Cloud Functions, but FlutterFlow's Free plan is sufficient to trigger the function via a Custom Action.

### How do I handle email bounces and spam complaints?

SendGrid provides webhooks for bounce and spam complaint events. Create a Firebase Cloud Function HTTP endpoint that receives these webhooks and updates the corresponding user document in Firestore (set emailBounced: true or emailUnsubscribed: true). Check these flags before sending future emails to prevent sending to invalid addresses.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-custom-email-template-for-notifications-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-custom-email-template-for-notifications-in-flutterflow
