Building Notification Systems: Email, SMS, and Push for Web Apps
Every web application needs to communicate with its users. An order is shipped. An appointment is approaching. A document needs a signature. A payment is overdue. Someone left a comment. A report is ready.
Without notifications, users have to manually check the application to see if anything happened. With well-designed notifications, the application reaches out to them at the right time, through the right channel, with the right message. We go deeper on all of this in a plain English guide to custom web applications.
We build notification systems for custom web applications, and they’re one of those features that clients initially treat as an afterthought (“oh, and it should send emails”), but quickly realize are central to how their application functions. A great notification system makes your app feel alive and responsive. A bad one fills inboxes with noise, annoys users, and gets ignored.
Here’s how to build notification systems that people actually appreciate receiving.
Multi-Channel: Why One Channel Isn’t Enough
Different messages need different channels. An appointment reminder works well as a text message. A detailed invoice is better as an email. A real-time alert belongs in a push notification.
And different users have different preferences. Some people live in their email inbox. Others never check email but respond to texts within minutes. Some love push notifications. Others have disabled them entirely.
A well-designed notification system supports multiple channels and lets users choose how they want to be reached.
Best for: Detailed content, documents, receipts, reports, anything the user might want to reference later.
Advantages:
- Universal: everyone has an email address
- Rich formatting: HTML templates, images, attachments
- Persistent: sits in the inbox until addressed
- Searchable: users can find old notifications
Disadvantages:
- Slow response times: average email open time is hours, not minutes
- Deliverability challenges: emails can land in spam
- Inbox fatigue: people are overwhelmed with email
SMS/Text Messages
Best for: Time-sensitive alerts, appointment reminders, verification codes, short status updates.
Advantages:
- Near-instant delivery and read rates (95%+ open rate)
- Works on any phone, no app required
- Feels personal and immediate
- High action rates for simple requests
Disadvantages:
- Limited to 160 characters (or segmented into multiple messages)
- Costs per message (typically $0.01-0.05 per SMS)
- Regulatory requirements (opt-in, opt-out compliance)
- Can feel intrusive if overused
Push Notifications (Web and Mobile)
Best for: Real-time alerts, status changes, interactive prompts when the user isn’t actively using the app.
Advantages:
- Instant delivery
- Highly visible on the user’s device
- Can include action buttons
- No per-message cost after implementation
Disadvantages:
- Requires user permission (and many people decline)
- Web push support varies across browsers and platforms
- Easy to abuse: too many notifications and users revoke permission
- Limited content: short messages only
In-App Notifications
Best for: Non-urgent updates, activity feeds, system messages visible when the user is actively using the app.
Advantages:
- No external service dependencies
- Fully controlled formatting and behavior
- Can include rich content and interactive elements
- No deliverability concerns
Disadvantages:
- Only visible when the user is in the app
- Not suitable for time-sensitive communications
- Must be built into the application UI
Choosing the Right Channel for Each Notification
We use a simple framework when designing notification systems:
| Notification Type | Primary Channel | Secondary Channel |
|---|---|---|
| Appointment reminder | SMS | |
| Invoice/receipt | In-app | |
| Real-time status change | Push | In-app |
| Document ready for signature | Email + SMS | Push |
| Payment overdue | SMS | |
| Welcome/onboarding | In-app | |
| System maintenance | In-app | |
| Security alert (login, password) | Email + SMS | Push |
| Daily/weekly digest | – | |
| Comment or reply | Push |
The primary channel is the default. The secondary channel is a fallback or supplement. For critical notifications (security alerts, payment issues), we often send through two channels simultaneously.
Building with Email: SendGrid and Beyond
For transactional email (messages triggered by user actions, not marketing blasts), we primarily use SendGrid, though there are several solid options.
Transactional Email Providers
Email Architecture Best Practices
Template system. Don’t construct emails in code. Use a template system where:
- A base layout defines the header, footer, and overall structure
- Individual notification templates extend the base with specific content
- Dynamic variables populate personalized data
- Templates can be updated without code changes
Responsive design. Email HTML is notoriously difficult to style. We use a tested base template (built on frameworks like MJML or Maizzle) that renders consistently across Gmail, Outlook, Apple Mail, and mobile clients.
Plain text fallback. Every HTML email should include a plain text version. Some email clients prefer it, and it helps with spam filtering.
Unsubscribe handling. Every email should include an unsubscribe mechanism. For transactional emails, this might be a preference center rather than a full unsubscribe: “stop sending me invoice reminders” rather than “stop all email.”
Delivery tracking. Track:
- Delivery rate: did the email reach the server?
- Bounce rate: hard bounces (invalid address) vs. soft bounces (full inbox)
- Open rate: was the email opened? (Note: Apple Mail Privacy Protection affects this metric)
- Click rate: did the user click any links?
- Spam complaint rate: are users marking your emails as spam? This should be near zero for transactional email.
Building with SMS: Twilio and Compliance
SMS notifications require more care than email, both technically and legally.
SMS Providers
SMS Compliance Requirements
SMS is regulated, and non-compliance can result in fines and having your number blocked.
Opt-in is mandatory. You must have explicit consent before sending SMS messages. This means:
- A clear opt-in mechanism (checkbox, text-to-subscribe, form field)
- Documentation of when and how consent was given
- Disclosure of what types of messages will be sent
- Disclosure of message frequency
Opt-out must be immediate. Users must be able to text STOP at any time, and your system must immediately cease sending messages to that number.
10DLC registration. In the US, businesses sending application-to-person (A2P) SMS must register their brand and campaigns with The Campaign Registry. Unregistered traffic faces higher filtering rates and potential blocking.
Message content requirements:
- Identify your business in every message
- Include opt-out instructions (e.g., “Reply STOP to unsubscribe”)
- Don’t send messages outside reasonable hours (typically 8am-9pm in the recipient’s time zone)
Implementation Patterns
A typical SMS notification implementation:
// Send an appointment reminder
async function sendAppointmentReminder(appointment) {
const message = await twilioClient.messages.create({
body: `Reminder: Your appointment with ${appointment.providerName} is tomorrow at ${appointment.time}. Reply C to confirm or R to reschedule.`,
from: process.env.TWILIO_PHONE_NUMBER,
to: appointment.customerPhone,
statusCallback: `${process.env.APP_URL}/webhooks/sms-status`
});
// Log the notification
await NotificationLog.create({
type: 'sms',
recipient: appointment.customerId,
messageId: message.sid,
status: 'sent',
content: message.body
});
}The statusCallback URL receives delivery status updates from Twilio, letting you track whether each message was delivered, failed, or triggered a reply.
Building with Push Notifications
Web push notifications let your application reach users even when they’re not on your site.
How Web Push Works
- User grants permission. The browser shows a permission prompt. If the user allows it, you receive a subscription object.
- Store the subscription. Save the subscription details (endpoint URL and encryption keys) on your server.
- Send notifications. When an event occurs, your server sends a push message through the browser’s push service.
- Service worker handles receipt. A service worker running in the background receives the push message and displays a notification to the user.
Push API Implementation
// Client: Request permission and subscribe
async function subscribeToPush() {
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VAPID_PUBLIC_KEY
});
// Send subscription to your server
await fetch('/api/push-subscriptions', {
method: 'POST',
body: JSON.stringify(subscription),
headers: { 'Content-Type': 'application/json' }
});
}// Service Worker: Handle incoming push
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icon-192.png',
badge: '/badge-72.png',
data: { url: data.url }
})
);
});
// Handle notification click
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});Push Notification Best Practices
- Ask for permission at the right time. Don’t show the permission prompt immediately on page load. Wait until the user has engaged with your app and there’s a clear reason to subscribe. “Would you like to be notified when your order ships?” is better than a generic prompt on the homepage.
- Be selective about what triggers a push. Reserve push for genuinely important, time-sensitive notifications. If you push too often, users will revoke permission.
- Include an action URL. When the user taps the notification, take them directly to the relevant content, not the homepage.
- Handle expired subscriptions. Push subscriptions can expire. Handle 410 (Gone) responses from the push service by removing the subscription from your database.
User Notification Preferences
A well-designed notification system puts users in control. Here’s what a notification preference center should include:
Channel Preferences
Let users choose their preferred channel for each notification category:
- Appointment reminders: Email / SMS / Push / All / None
- Billing notifications: Email / SMS / Push / All / None
- Status updates: Email / SMS / Push / All / None
- Marketing: Email / None
Frequency Controls
For non-urgent notifications, let users choose frequency:
- Real-time: get every notification as it happens
- Daily digest: a summary once per day
- Weekly digest: a summary once per week
Quiet Hours
Let users set times when they don’t want to receive notifications (except for critical alerts like security events).
Global Controls
An easy “pause all notifications” toggle and a “notification level” setting (all, important only, critical only) give users a quick way to manage volume.
Delivery Tracking and Reliability
For business-critical notifications, you need to know whether they were delivered.
Notification Logging
Every notification should be logged with:
- Timestamp: when it was sent
- Recipient: who it was sent to
- Channel: email, SMS, push, in-app
- Content: what was sent (or a reference to the template and data)
- Status: queued, sent, delivered, failed, bounced, opened, clicked
- Error details: if it failed, why
Retry Logic
For failed notifications:
- Transient failures (network timeout, service unavailable): retry with exponential backoff (1 min, 5 min, 30 min)
- Permanent failures (invalid email, disconnected phone): don’t retry. Flag the contact record for review.
- Rate limit failures: queue and retry after the rate limit window expires
Fallback Channels
For critical notifications, implement automatic fallback:
- Attempt primary channel (e.g., SMS)
- If delivery fails or is unconfirmed after a threshold (e.g., 30 minutes), send through secondary channel (e.g., email)
- Log both attempts
Monitoring and Alerting
Set up monitoring for:
- Delivery success rate dropping below a threshold
- Bounce rates increasing (might indicate a sender reputation issue)
- Queue depth growing (might indicate a processing bottleneck)
- Provider outages (switch to backup provider if available)
Architecture Considerations
Queue-Based Processing
Don’t send notifications synchronously in your application code. If sending an email takes two seconds and your endpoint needs to send five notifications, that’s ten seconds of blocked processing.
Instead, use a message queue:
- Application publishes a notification event to the queue
- A worker process picks up the event and handles delivery
- The application continues processing without waiting
Popular queuing options: Redis + Bull, RabbitMQ, Amazon SQS, or even a database-backed queue for simpler applications.
Template Management
Keep notification templates separate from application code:
- Store templates in a database or file system
- Support variables for personalization
- Allow non-developers to edit template content
- Version templates so changes can be rolled back
Rate Limiting
Protect your users and your sender reputation:
- Maximum notifications per user per hour/day
- Maximum total sends per minute (respect provider rate limits)
- Duplicate suppression (don’t send the same notification twice)
- Throttling during high-volume events
What It Costs
For a typical small business web application sending a few thousand notifications per month:
The infrastructure costs are modest. The development cost is the main investment, and it pays off every day the system is running.