# How to Set Up a Personalized Email Notification System in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 20-25 min
- Compatibility: FlutterFlow Free+
- Last updated: March 2026

## TL;DR

Set up a personalized email notification pipeline using Firestore email templates with placeholder syntax, user preference toggles, and Cloud Functions that send emails via SendGrid. Store templates in a Firestore collection so non-technical team members can edit content without redeploying. Trigger emails on events like order status changes, respecting each user's notification preferences.

## Building a Personalized Email Notification System in FlutterFlow

Automated emails keep users informed about orders, updates, and important events. This tutorial builds a complete email pipeline: Firestore-stored templates with placeholders, user preference toggles, Cloud Functions that merge data and send via SendGrid, and an email log for tracking delivery. Perfect for e-commerce, SaaS, and community apps.

## Before you start

- A FlutterFlow project with Firebase authentication enabled
- A SendGrid account with an API key (free tier supports 100 emails/day)
- Firestore database configured in your Firebase project
- Cloud Functions enabled on the Blaze plan

## Step-by-step guide

### 1. Create the Firestore schema for email templates and user preferences

In Firestore, create an `email_templates` collection with fields: name (String, e.g. 'order_confirmed'), subject (String with placeholders like 'Order {{orderNumber}} Confirmed'), bodyHtml (String with HTML + placeholders like '{{userName}}', '{{orderTotal}}'), triggerEvent (String matching the event name), isActive (Boolean). On your existing users collection, add an emailPreferences map field with keys like orders, marketing, and updates, each set to true by default. In FlutterFlow's Data section, register both collections so they appear in queries and forms.

**Expected result:** The email_templates collection has at least one template document, and user documents include an emailPreferences map.

### 2. Build the user email preferences page with Switch toggles

Create a new page or section on your existing Settings page. Add a Column with a heading Text 'Email Notifications'. Below it, add three Row widgets, each containing a Text label (Order Updates, Marketing Emails, App Updates) and a Switch widget. Bind each Switch's initial value to the corresponding field in the current user's emailPreferences map. On each Switch toggle, update the user document's emailPreferences map field with the new boolean value. Add a brief description Text below each toggle explaining what emails that category includes.

**Expected result:** Users see three toggles for email categories. Flipping a toggle immediately updates their Firestore preferences, controlling which emails they receive.

### 3. Create the Cloud Function that sends personalized emails via SendGrid

In your Firebase project's Cloud Functions directory, create a function triggered by Firestore events. For example, when a document is created in the `orders` collection with status 'confirmed', the function reads the user's emailPreferences to check if orders is true. If yes, it fetches the matching email_template by triggerEvent, replaces all placeholders in the subject and bodyHtml with actual values from the order and user documents, then sends the email via the SendGrid API using the @sendgrid/mail npm package. After sending, create a document in the `email_log` collection with userId, templateName, recipientEmail, sentAt timestamp, and status (sent/failed).

```
const sgMail = require('@sendgrid/mail');
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

sgMail.setApiKey(functions.config().sendgrid.key);

exports.sendOrderEmail = functions.firestore
  .document('orders/{orderId}')
  .onCreate(async (snap, context) => {
    const order = snap.data();
    const userDoc = await admin.firestore()
      .collection('users').doc(order.userId).get();
    const user = userDoc.data();

    if (!user.emailPreferences?.orders) return;

    const templateSnap = await admin.firestore()
      .collection('email_templates')
      .where('triggerEvent', '==', 'order_confirmed')
      .where('isActive', '==', true)
      .limit(1).get();

    if (templateSnap.empty) return;
    const template = templateSnap.docs[0].data();

    const subject = template.subject
      .replace('{{orderNumber}}', order.orderNumber);
    const html = template.bodyHtml
      .replace(/{{userName}}/g, user.displayName)
      .replace(/{{orderNumber}}/g, order.orderNumber)
      .replace(/{{orderTotal}}/g, `$${order.total.toFixed(2)}`);

    await sgMail.send({
      to: user.email,
      from: 'noreply@yourapp.com',
      subject,
      html,
    });

    await admin.firestore().collection('email_log').add({
      userId: order.userId,
      templateName: template.name,
      recipientEmail: user.email,
      sentAt: admin.firestore.FieldValue.serverTimestamp(),
      status: 'sent',
    });
  });
```

**Expected result:** When a new order is created, the Cloud Function checks user preferences, merges template placeholders with real data, sends the email via SendGrid, and logs the result.

### 4. Build the admin email template management page

Create an admin-only page called EmailTemplates. Add a ListView bound to a Backend Query on the email_templates collection. Each list item shows the template name, subject line, trigger event, and an isActive toggle Switch. Add an Edit button on each item that navigates to an EmailTemplateEditor page. On that editor page, add TextFields for name, subject (with placeholder hint text showing {{userName}} syntax), a multiline TextField for bodyHtml, a DropDown for triggerEvent, and an isActive Switch. On save, update the Firestore document. Add a Preview button that replaces placeholders with sample data and displays the result in a Container with rendered HTML.

**Expected result:** Admins can create, edit, and preview email templates directly in the app without touching Cloud Function code.

### 5. Add an email log page for delivery tracking

Create an EmailLog admin page with a ListView bound to the email_log collection ordered by sentAt descending. Each row displays the recipient email, template name, sent timestamp, and a color-coded status badge (green for sent, red for failed). Add a ChoiceChips filter at the top for status (All/Sent/Failed) and a DatePicker range filter. Add a Text widget at the top showing total emails sent today (aggregate query or count from filtered results). This gives your team visibility into email delivery performance.

**Expected result:** The admin email log page shows all sent emails with delivery status, filterable by date range and status.

## Complete code example

File: `functions/index.js — SendGrid Email Cloud Function`

```dart
// Cloud Function: Send personalized email on order creation
// Deploy: firebase deploy --only functions

const sgMail = require('@sendgrid/mail');
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

sgMail.setApiKey(functions.config().sendgrid.key);

// Generic email sender helper
async function sendTemplatedEmail(userId, triggerEvent, data) {
  const userDoc = await admin.firestore()
    .collection('users').doc(userId).get();
  const user = userDoc.data();

  // Check user preferences
  const prefCategory = triggerEvent.split('_')[0]; // e.g. 'order' from 'order_confirmed'
  if (!user.emailPreferences?.[prefCategory + 's']) return;

  // Fetch active template
  const templateSnap = await admin.firestore()
    .collection('email_templates')
    .where('triggerEvent', '==', triggerEvent)
    .where('isActive', '==', true)
    .limit(1).get();
  if (templateSnap.empty) return;
  const template = templateSnap.docs[0].data();

  // Replace all placeholders
  let subject = template.subject;
  let html = template.bodyHtml;
  const replacements = { userName: user.displayName, ...data };
  for (const [key, value] of Object.entries(replacements)) {
    const regex = new RegExp(`{{${key}}}`, 'g');
    subject = subject.replace(regex, String(value));
    html = html.replace(regex, String(value));
  }

  // Send via SendGrid
  await sgMail.send({
    to: user.email,
    from: 'noreply@yourapp.com',
    subject,
    html,
  });

  // Log the email
  await admin.firestore().collection('email_log').add({
    userId,
    templateName: template.name,
    recipientEmail: user.email,
    sentAt: admin.firestore.FieldValue.serverTimestamp(),
    status: 'sent',
  });
}

// Trigger: new order created
exports.onOrderCreated = functions.firestore
  .document('orders/{orderId}')
  .onCreate(async (snap) => {
    const order = snap.data();
    await sendTemplatedEmail(order.userId, 'order_confirmed', {
      orderNumber: order.orderNumber,
      orderTotal: `$${order.total.toFixed(2)}`,
    });
  });

// Trigger: order status updated
exports.onOrderUpdated = functions.firestore
  .document('orders/{orderId}')
  .onUpdate(async (change) => {
    const before = change.before.data();
    const after = change.after.data();
    if (before.status !== after.status) {
      await sendTemplatedEmail(after.userId, `order_${after.status}`, {
        orderNumber: after.orderNumber,
        statusText: after.status,
      });
    }
  });
```

## Common mistakes

- **Hardcoding email content directly in the Cloud Function** — Every text change — even fixing a typo — requires editing the function code and redeploying. Non-technical team members cannot update email copy. Fix: Store all email templates in a Firestore collection. The Cloud Function reads the template at send time, so edits in Firestore take effect immediately without redeployment.
- **Ignoring user email preferences before sending** — Sending emails to users who opted out violates their preferences and can lead to spam complaints that damage your SendGrid sender reputation. Fix: Always check the user's emailPreferences map field before sending. Skip the email if the relevant category is set to false.
- **Storing the SendGrid API key in the Cloud Function source code** — API keys in source code get committed to version control and are visible to anyone with repository access. They can be abused to send spam from your account. Fix: Use firebase functions:config:set sendgrid.key=YOUR_KEY and access it via functions.config().sendgrid.key in the function.

## Best practices

- Use Firestore-stored templates so non-technical team members can edit email copy without code changes
- Always check user email preferences before sending to respect opt-out choices
- Log every sent email to the email_log collection for delivery tracking and debugging
- Include an unsubscribe link in every marketing email to comply with CAN-SPAM regulations
- Use a verified sender domain in SendGrid to improve email deliverability
- Test templates with a Send Test Email button before activating them for production triggers
- Add error handling in the Cloud Function to log failed sends with the error message

## Frequently asked questions

### Can I use Mailgun instead of SendGrid for sending emails?

Yes. Replace the @sendgrid/mail package with the mailgun.js package in your Cloud Function and update the send call to use Mailgun's API format. The Firestore template and preference logic stays the same.

### How do I add an unsubscribe link to emails?

Include a placeholder like {{unsubscribeUrl}} in your templates. In the Cloud Function, generate a URL that links to your app's email preferences page with a token parameter. When clicked, the user can disable that email category.

### What happens if the SendGrid API call fails?

Wrap the send call in a try-catch block. On failure, log the error to the email_log collection with status 'failed' and the error message. Consider implementing a retry mechanism with exponential backoff.

### Can I send emails with attachments like invoices?

Yes. Generate the attachment (e.g., a PDF invoice) in the Cloud Function, base64-encode it, and pass it in the SendGrid send call's attachments array with filename and content type.

### How do I preview an email template before sending?

On the template editor page, add a Preview button that replaces all placeholders with sample data and renders the resulting HTML in a WebView or flutter_html Custom Widget.

### Can RapidDev help build a transactional email system?

Yes. RapidDev can implement a production email pipeline with template versioning, A/B testing, analytics tracking, bounce handling, and multi-provider failover for maximum deliverability.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-set-up-a-personalized-email-notification-system-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-set-up-a-personalized-email-notification-system-in-flutterflow
