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
Create the Firestore schema for email templates and user preferences
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.
Build the user email preferences page with Switch toggles
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.
Create the Cloud Function that sends personalized emails via SendGrid
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).
1const sgMail = require('@sendgrid/mail');2const functions = require('firebase-functions');3const admin = require('firebase-admin');4admin.initializeApp();56sgMail.setApiKey(functions.config().sendgrid.key);78exports.sendOrderEmail = functions.firestore9 .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();1516 if (!user.emailPreferences?.orders) return;1718 const templateSnap = await admin.firestore()19 .collection('email_templates')20 .where('triggerEvent', '==', 'order_confirmed')21 .where('isActive', '==', true)22 .limit(1).get();2324 if (templateSnap.empty) return;25 const template = templateSnap.docs[0].data();2627 const subject = template.subject28 .replace('{{orderNumber}}', order.orderNumber);29 const html = template.bodyHtml30 .replace(/{{userName}}/g, user.displayName)31 .replace(/{{orderNumber}}/g, order.orderNumber)32 .replace(/{{orderTotal}}/g, `$${order.total.toFixed(2)}`);3334 await sgMail.send({35 to: user.email,36 from: 'noreply@yourapp.com',37 subject,38 html,39 });4041 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.
Build the admin email template management page
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.
Add an email log page for delivery tracking
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
1// Cloud Function: Send personalized email on order creation2// Deploy: firebase deploy --only functions34const sgMail = require('@sendgrid/mail');5const functions = require('firebase-functions');6const admin = require('firebase-admin');7admin.initializeApp();89sgMail.setApiKey(functions.config().sendgrid.key);1011// Generic email sender helper12async function sendTemplatedEmail(userId, triggerEvent, data) {13 const userDoc = await admin.firestore()14 .collection('users').doc(userId).get();15 const user = userDoc.data();1617 // Check user preferences18 const prefCategory = triggerEvent.split('_')[0]; // e.g. 'order' from 'order_confirmed'19 if (!user.emailPreferences?.[prefCategory + 's']) return;2021 // Fetch active template22 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();2930 // Replace all placeholders31 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 }3940 // Send via SendGrid41 await sgMail.send({42 to: user.email,43 from: 'noreply@yourapp.com',44 subject,45 html,46 });4748 // Log the email49 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}5758// Trigger: new order created59exports.onOrderCreated = functions.firestore60 .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 });6869// Trigger: order status updated70exports.onOrderUpdated = functions.firestore71 .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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation