Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Set Up a Personalized Email Notification System in FlutterFlow

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.

What you'll learn

  • How to store and manage email templates in Firestore with placeholder syntax
  • How to build user email preference toggles that control which emails they receive
  • How to create Cloud Functions that send personalized emails via SendGrid
  • How to log sent emails and build an admin email management dashboard
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner7 min read20-25 minFlutterFlow Free+March 2026RapidDev Engineering Team
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.

Prerequisites

  • 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).

functions/index.js
1const sgMail = require('@sendgrid/mail');
2const functions = require('firebase-functions');
3const admin = require('firebase-admin');
4admin.initializeApp();
5
6sgMail.setApiKey(functions.config().sendgrid.key);
7
8exports.sendOrderEmail = functions.firestore
9 .document('orders/{orderId}')
10 .onCreate(async (snap, context) => {
11 const order = snap.data();
12 const userDoc = await admin.firestore()
13 .collection('users').doc(order.userId).get();
14 const user = userDoc.data();
15
16 if (!user.emailPreferences?.orders) return;
17
18 const templateSnap = await admin.firestore()
19 .collection('email_templates')
20 .where('triggerEvent', '==', 'order_confirmed')
21 .where('isActive', '==', true)
22 .limit(1).get();
23
24 if (templateSnap.empty) return;
25 const template = templateSnap.docs[0].data();
26
27 const subject = template.subject
28 .replace('{{orderNumber}}', order.orderNumber);
29 const html = template.bodyHtml
30 .replace(/{{userName}}/g, user.displayName)
31 .replace(/{{orderNumber}}/g, order.orderNumber)
32 .replace(/{{orderTotal}}/g, `$${order.total.toFixed(2)}`);
33
34 await sgMail.send({
35 to: user.email,
36 from: 'noreply@yourapp.com',
37 subject,
38 html,
39 });
40
41 await admin.firestore().collection('email_log').add({
42 userId: order.userId,
43 templateName: template.name,
44 recipientEmail: user.email,
45 sentAt: admin.firestore.FieldValue.serverTimestamp(),
46 status: 'sent',
47 });
48 });

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 working example

functions/index.js — SendGrid Email Cloud Function
1// Cloud Function: Send personalized email on order creation
2// Deploy: firebase deploy --only functions
3
4const sgMail = require('@sendgrid/mail');
5const functions = require('firebase-functions');
6const admin = require('firebase-admin');
7admin.initializeApp();
8
9sgMail.setApiKey(functions.config().sendgrid.key);
10
11// Generic email sender helper
12async function sendTemplatedEmail(userId, triggerEvent, data) {
13 const userDoc = await admin.firestore()
14 .collection('users').doc(userId).get();
15 const user = userDoc.data();
16
17 // Check user preferences
18 const prefCategory = triggerEvent.split('_')[0]; // e.g. 'order' from 'order_confirmed'
19 if (!user.emailPreferences?.[prefCategory + 's']) return;
20
21 // Fetch active template
22 const templateSnap = await admin.firestore()
23 .collection('email_templates')
24 .where('triggerEvent', '==', triggerEvent)
25 .where('isActive', '==', true)
26 .limit(1).get();
27 if (templateSnap.empty) return;
28 const template = templateSnap.docs[0].data();
29
30 // Replace all placeholders
31 let subject = template.subject;
32 let html = template.bodyHtml;
33 const replacements = { userName: user.displayName, ...data };
34 for (const [key, value] of Object.entries(replacements)) {
35 const regex = new RegExp(`{{${key}}}`, 'g');
36 subject = subject.replace(regex, String(value));
37 html = html.replace(regex, String(value));
38 }
39
40 // Send via SendGrid
41 await sgMail.send({
42 to: user.email,
43 from: 'noreply@yourapp.com',
44 subject,
45 html,
46 });
47
48 // Log the email
49 await admin.firestore().collection('email_log').add({
50 userId,
51 templateName: template.name,
52 recipientEmail: user.email,
53 sentAt: admin.firestore.FieldValue.serverTimestamp(),
54 status: 'sent',
55 });
56}
57
58// Trigger: new order created
59exports.onOrderCreated = functions.firestore
60 .document('orders/{orderId}')
61 .onCreate(async (snap) => {
62 const order = snap.data();
63 await sendTemplatedEmail(order.userId, 'order_confirmed', {
64 orderNumber: order.orderNumber,
65 orderTotal: `$${order.total.toFixed(2)}`,
66 });
67 });
68
69// Trigger: order status updated
70exports.onOrderUpdated = functions.firestore
71 .document('orders/{orderId}')
72 .onUpdate(async (change) => {
73 const before = change.before.data();
74 const after = change.after.data();
75 if (before.status !== after.status) {
76 await sendTemplatedEmail(after.userId, `order_${after.status}`, {
77 orderNumber: after.orderNumber,
78 statusText: after.status,
79 });
80 }
81 });

Common mistakes

Why it's a problem: Hardcoding email content directly in the Cloud Function

How to avoid: 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.

Why it's a problem: Ignoring user email preferences before sending

How to avoid: Always check the user's emailPreferences map field before sending. Skip the email if the relevant category is set to false.

Why it's a problem: Storing the SendGrid API key in the Cloud Function source code

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a personalized email notification system in FlutterFlow with Firestore and Cloud Functions. I need Firestore-stored email templates with {{placeholder}} syntax, user email preference toggles, a Cloud Function that sends emails via SendGrid on Firestore events, and an email log. Show me the schema, Cloud Function code, and FlutterFlow page layout.

FlutterFlow Prompt

Create a settings page section with three Switch toggles for email notification preferences (Order Updates, Marketing, App Updates). Bind each Switch to the current user's emailPreferences map field in Firestore and update on toggle.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.