Guides/How to Build a Notification Center/How to Build a Notification Center

Chapter 2

How to Build a Notification Center

A technical guide to notification center architecture covering backend, delivery, and frontend layers. Includes implementation code for React, iOS, Android, and React Native, plus multi-channel orchestration, real-time delivery, and customization.

How to Build a Notification Center

Architecture overview

Building a notification center means coordinating three distinct layers that need to work together, and when I say "work together" I mean they need to work together smoothly enough that users never think about the complexity underneath. Let's walk through what each layer actually does.

product notifications

The backend layer is where everything starts. Your application triggers notification events whenever something happens that users might care about. Someone comments on a document, an order ships, a payment fails, whatever it is that constitutes an event in your system. The notification API receives these events and processes them, which means figuring out who should receive the notification, what it should say, and where it should go. Message routing determines which channels to use based on your rules and the user's preferences. User preferences filter what actually gets sent because not everyone wants every notification. Think of this layer as mission control. It's handling the logic and decision-making before anything reaches a user.

The delivery layer is all infrastructure. This is the plumbing that gets messages from your servers to your users' devices. WebSocket connections handle real-time delivery because HTTP polling is too slow and inefficient for modern expectations. Message persistence ensures nothing gets lost if a user is offline when the notification fires. State synchronization keeps everything consistent across devices, so marking something read on your phone also marks it read on your laptop. Integration with push notification services handles mobile notifications through APNs and FCM. This layer doesn't make decisions, it just reliably executes on them.

The frontend layer is what users actually see and interact with. UI components display the inbox, toasts, and badges in a way that hopefully doesn't look terrible. State management tracks which messages are read, unread, or archived and keeps that state consistent with what's happening on the backend. User authentication keeps everything secure so people only see their own notifications. Real-time updates make new messages appear instantly without the user doing anything. This is the interface, the part users judge you on, and it needs to work flawlessly because users don't care that the backend is complicated.

The path from event to notification looks straightforward on paper but involves a lot of coordination. Your application fires an event, the notification API receives it, the message routing logic checks the user's preferences and decides which channels to use, then it delivers via WebSocket to any open sessions while also storing it for later retrieval. When the user marks the notification as read, that state update syncs back through the system and propagates to all their devices. In practice this means your backend, delivery layer, and frontend all need to speak the same language about state, handle failures gracefully, and stay synchronized even when connectivity is spotty.

Here's the thing that makes this complicated. Every layer has failure modes. WebSockets drop. Databases go down. Users lose connectivity. Mobile apps get suspended by the operating system. Your code has bugs. The infrastructure has outages. A robust notification center needs to handle all of these failure modes gracefully, which means retry logic, queuing, fallbacks, and probably more monitoring than you initially thought necessary. This is why most teams who start building notification centers in-house end up either abandoning the project or shipping something that technically works but doesn't feel reliable in practice.

Core capabilities

Real-time message delivery is non-negotiable for a modern notification center, and when we talk about real-time we mean WebSocket connections, not HTTP polling. Polling might have been acceptable in 2010 but users expect messages to appear immediately now. The moment someone comments on their post or approves their request, that notification should show up without any action from the user. No page refresh, no pull-to-refresh gesture, it just appears.

The technical challenge with WebSockets is that they're stateful connections, which means they can break in ways that HTTP requests don't. Users lose connectivity constantly. They walk into elevators, go through tunnels, switch from WiFi to cellular. Your app gets backgrounded and iOS or Android eventually kills the WebSocket to save battery. The user's phone goes to sleep. All of these scenarios require connection recovery, which means automatically reconnecting, reestablishing authentication, and syncing any messages that arrived while disconnected. The connection health monitoring becomes its own job because you need heartbeats to detect silent failures, timeouts to catch hung connections, and exponential backoff on reconnection attempts to avoid hammering the server when things are struggling.

what is a notification center good for?

Cross-channel state management sounds simple until you try to implement it, then you realize it's one of those problems that has tentacles everywhere. Here's the basic scenario that needs to work perfectly. You send a notification to someone via inbox and email simultaneously because you want to cover all the bases. They're at their desk and they see the email first. They open it, read it, maybe click a link. Now they stand up, grab their phone, open your mobile app. What should they see? If the inbox notification is still sitting there marked as unread, you've failed. The system should know they already dealt with this notification via email. The inbox message should be marked as read, or maybe it should be archived entirely depending on your logic.

This same cross-channel sync needs to work for SMS and push notifications too. The user clicks a link in an SMS, the inbox message updates. They dismiss a push notification, the inbox message archives. They take action on an inbox message, any related push notifications vanish. The goal is making the notification system feel like one coherent thing instead of three or four separate systems that happen to be sending similar messages.

Courier handles this synchronization automatically, which is one of those features that's easy to take for granted until you try building it yourself. When a notification goes out through multiple channels, the backend tracks interaction events from all of them. Email opened, SMS link clicked, push notification dismissed, inbox message read, Slack message responded to. These events flow back to a central state store that maintains the canonical state for each message. Any time the state changes, it propagates to all channels and all devices. The result is that users experience one notification, not five copies of it.

Multi-device synchronization works on the same principles but across a different axis. Users switch between web, iOS, and Android constantly throughout the day. Your notification center needs to stay in sync across all of them without the user thinking about it. Read something on your phone during your commute, it's marked read on your laptop when you get to the office. Archive something on your laptop, it vanishes from your phone. This isn't optional functionality anymore, it's baseline expectation.

The technical challenge is maintaining a single source of truth for message state while handling the reality that devices aren't always connected, clocks aren't perfectly synchronized, and users might take actions on multiple devices in quick succession. You need conflict resolution logic for when someone marks the same message as read on two devices at nearly the same time. You need offline queuing for actions taken while disconnected that should sync once connectivity returns. You need efficient sync protocols so you're not pulling down the entire message history every time the app opens.

customer journey notification center

Automation and customer journeys are where notification centers get really powerful, though it's often overlooked in basic implementations. The simplest notification system just sends messages immediately when events happen. A better system lets you build logic around those notifications, because immediate sending isn't always the right strategy.

Courier's Journeys feature handles this through a visual workflow builder that doesn't require code. You can schedule notifications to send at optimal times instead of immediately. Maybe you want to send that onboarding email at 9am in the user's timezone instead of at 3am when they signed up. You can add delays between messages to avoid overwhelming users. Send the welcome email immediately, wait 24 hours, then send the feature tour, wait another 48 hours, then send the tips and tricks guide. You can build conditional logic like "if user hasn't verified their email within 24 hours, send a reminder" or "if user has completed their profile, send the next onboarding step, otherwise send the profile completion prompt."

You can also chain notifications across channels based on behavior. Try sending via push first, wait an hour, check if they opened it, if not then send via email. Fetch data from your APIs on the fly to personalize messages based on current state instead of state at the time the event fired. Update user profiles based on notification interactions so someone who clicks through from an onboarding email gets tagged as an engaged user.

The visual customer journey builder means non-technical team members can create and modify these workflows. Product managers can test different messaging strategies. Marketing can iterate on campaign timing. Support can adjust onboarding sequences based on what they're seeing in tickets. All without filing requests to engineering and waiting for the next deployment. This matters because notification strategy evolves faster than your release cycle. You want to experiment, measure results, and iterate quickly. Automations makes that possible.

User preference management is essential and yet somehow many systems get it wrong or half-implement it. Users need control over what notifications they receive and how they receive them. Some people want every notification. Others want almost none. Most people want something in between, and their preferences are nuanced in ways that a simple on/off toggle can't capture.

A proper preference system lets users choose notification types they care about. Maybe they want mentions and direct messages but not routine status updates. They can select preferred channels per notification type. Approvals might go to inbox and email, but routine updates only to inbox. They can set global preferences like "no notifications after 8pm" or "only urgent notifications on weekends." They can manage subscription topics, opting into billing notifications and product updates while opting out of marketing emails and feature announcements.

preference management

Courier's preference management is hosted, which means you don't build and maintain the preference UI yourself. The preference center is a pre-built component that handles all the complexity of nested preferences, channel selections, and subscription topics. The backend API enforces these preferences automatically when you send notifications. The system handles GDPR and CCPA compliance requirements around data access, export, and deletion. This sounds like a small thing until you've actually tried to build preference management that works reliably, stores everything correctly, and stays in sync across your application and all the places where notifications originate.

Technical implementation

Let's look at what it actually takes to get a notification center running in a real application, starting with React and then covering mobile platforms.

React setup

Install the package with npm or your preferred package manager:

Copied!

npm install @trycourier/courier-react

Now add the inbox component where you want it to appear in your app:

Copied!

import { useCourier, CourierInbox } from "@trycourier/courier-react";
import { useEffect } from "react";
export default function NotificationCenter() {
const courier = useCourier();
useEffect(() => {
courier.shared.signIn({
userId: 'user_123',
jwt: 'your_jwt_token'
});
}, []);
return (
<div>
<h1>Notifications</h1>
<CourierInbox />
</div>
);
}

That's it. You now have a functional notification center with real-time delivery, cross-device sync, tied in with your multi-channel routing logic. The inbox component handles real-time updates, message rendering, state updates, and user interactions.

You only need to sign in with Courier once in your app, whether you're using Inbox, Toast, or accessing specific attributes the SDK exposes. For example, you could call signIn once when the page loads and then show the unread message count in your navigation bar, a full Inbox popup, and render toasts when new notifications arrive. The SDK provides the UI building blocks and handles state management and backend communication.

Web Components for vanilla JavaScript

If you're not using React, the Web Components SDK works with vanilla JavaScript or any framework. Web Components is a standard for reusable, custom elements, which means they work in Angular, Vue, Svelte, or just plain HTML and JavaScript:

Copied!

npm install @trycourier/courier-ui-inbox

or

Copied!

npm install @trycourier/courier-ui-toast

Copied!

<body>
<courier-inbox></courier-inbox>
<script type="module">
import { Courier } from '@trycourier/courier-js';
Courier.shared.signIn({
userId: 'user_123',
jwt: 'your_jwt_token'
});
</script>
</body>

The authentication and connection management works the same way under the hood, you're just using custom HTML elements instead of React components.

iOS setup

Mobile implementations follow similar patterns but with platform-specific considerations. iOS development these days means supporting both UIKit and SwiftUI, and the Courier SDK handles both.

Add the package via Swift Package Manager:

Copied!

dependencies: [
.package(
url: "https://github.com/trycourier/courier-ios",
from: "5.0.0"
)
]

Then initialize and display the inbox:

Copied!

import Courier_iOS
// Authenticate
Courier.shared.signIn(
userId: "user_123",
jwt: "your_jwt_token"
)
// UIKit
let inbox = CourierInbox()
view.addSubview(inbox)
// Or in SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Notifications")
CourierInboxView()
}
}
}

The SDK handles platform-specific details like lifecycle management when the app backgrounds, push notification registration, and state restoration.

The iOS SDK has been getting regular updates that make it progressively more capable. Recent releases added long press gesture support with haptic feedback, pull-to-refresh even when the inbox is empty, proper brand color integration throughout the UI, improved WebSocket handling that prevents disconnections after 10 minutes of foreground inactivity, and better background/foreground transition logic. There's also improved concurrency safety since the shared Courier instance is now an actor instead of a plain singleton. These might sound like minor improvements but they're the kind of polish that users notice even if they can't articulate what's better.

Android setup

Android requires adding the Courier SDK to your gradle dependencies and making sure your repository list includes JitPack:

Copied!

repositories {
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
dependencies {
implementation 'com.github.trycourier:courier-android:latest'
}

Then initialize and add the inbox to your layout:

Copied!

import com.courier.android.Courier
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Authenticate
Courier.shared.signIn(
userId = "user_123",
jwt = "your_jwt_token"
)
// Add inbox to layout
setContent {
CourierInbox()
}
}
}

The SDK supports both Jetpack Compose and traditional Android views. Recent updates improved logo rendering, fixed edge cases around state restoration, and enhanced the documentation with better code samples.

React Native setup

Flutter and React Native implementations work across both iOS and Android from a single codebase, which is the whole point of using these frameworks. The setup is slightly more involved because you're dealing with native dependencies on both platforms, but the Courier SDKs handle most of the complexity.

For React Native, install the package and native dependencies:

Copied!

npm install @trycourier/courier-react-native
cd ios && pod install

Update your Android MainActivity:

Copied!

import com.courierreactnative.CourierReactNativeActivity
class MainActivity : CourierReactNativeActivity() {
// Your activity code
}

And make sure your iOS Podfile specifies the minimum version:

Copied!

platform :ios, '15.0' # Courier requires iOS 15+

Then use it in your components:

Copied!

import { Courier, CourierInbox } from '@trycourier/courier-react-native';
import { useEffect } from 'react';
export default function NotificationScreen() {
useEffect(() => {
Courier.shared.signIn({
userId: 'user_123',
jwt: 'your_jwt_token'
});
}, []);
return <CourierInbox />;
}

Sending notifications to the inbox

Having the inbox component running in your app is only half of the equation. You need to actually send messages to it, and this is where Courier's API comes into play.

The simplest possible notification is just an API call with a recipient, content, and routing information:

Copied!

const { requestId } = await courier.send({
message: {
to: {
user_id: "user_123"
},
content: {
title: "New Comment",
body: "Sarah commented on your post"
},
routing: {
method: "all",
channels: ["inbox"]
}
}
});

You specify who gets it by user ID, what it says with a title and body, and where it goes with the routing configuration. This sends to just the inbox, and the user sees it appear in real-time if they're currently looking at the app.

Multi-channel orchestration

Multi-channel orchestration is where things get interesting and where Courier's architecture really shows its advantage. Instead of writing separate code to send an email, trigger a push notification, update the inbox, send an SMS, and post to Slack, you make one API call that targets all five channels:

Copied!

const { requestId } = await courier.send({
message: {
to: {
user_id: "user_123",
email: "[email protected]",
phone_number: "+1234567890",
slack: {
access_token: "xoxb-your-token",
channel: "C1234567890"
}
},
content: {
title: "Order Shipped",
body: "Your order #12345 has shipped and will arrive tomorrow"
},
routing: {
method: "all",
channels: ["inbox", "email", "push", "sms", "chat"]
}
}
});

The same notification content automatically adapts to each channel's requirements and constraints. The inbox message includes full rich text and action buttons. The email renders as proper HTML with responsive design. The push notification condenses to a title and body suitable for a lock screen. The SMS becomes plain text with a shortened link, automatically staying under character limits. The Slack message formats as a rich message card with interactive elements.

Beyond inbox, email, push, and SMS, Courier's chat channel supports sending notifications to collaboration platforms where your users actually work. This includes Slack for team workspaces, Microsoft Teams for enterprise environments, and WhatsApp Business for customer communication. These chat integrations render notifications as native message formats with interactive elements, threaded conversations, and proper formatting for each platform.

The routing engine gives you control over how channels interact. You can send to all channels simultaneously with the "all" method like above. Or you can try channels in sequence with fallbacks using the "single" method:

Copied!

routing: {
method: "single",
channels: ["push", "sms", "email"]
}

Courier tries the push channel first. If push is unavailable (ex. because there's no device token or the user opted out), the system tries to send via SMS, followed by email. The routing engine handles the logic and tracks which channel succeeded. You can also designate certain channels as "always send" regardless of what else happens, so maybe inbox always gets the message while email and push are conditional based on user preferences or business logic.

Using notification templates

When you're sending the same type of notification repeatedly, using templates makes way more sense than hardcoding content in your API calls:

Copied!

const { requestId } = await courier.send({
message: {
to: {
user_id: "user_123"
},
template: "comment-notification",
data: {
commenter: "Sarah",
post_title: "Q4 Planning",
comment_preview: "Great insights on the roadmap..."
}
}
});

Templates live in Courier's Designer interface, which means non-technical team members can edit content, adjust formatting, or A/B test different versions without touching code. You just reference the template by ID in your API call and pass in the dynamic data that populates it.

Templates automatically adapt across channels. Write your notification once and the template system renders it appropriately for inbox, email, push, SMS, Slack, Microsoft Teams, WhatsApp Business, or whatever other channels you're using. This isn't just a time-saver, it's a consistency win. The same message doesn't accidentally drift in tone or content across different channels because someone copied and pasted incorrectly.

Toast notifications

While the inbox handles persistent messages, toast notifications are perfect for brief, temporary alerts that appear and disappear. They're particularly useful for confirmation messages and status updates:

Copied!

import { CourierToast } from "@trycourier/courier-react";
function SuccessMessage() {
return (
<CourierToast
title="Payment Successful"
body="Your card has been charged $49.99"
duration={5000}
/>
);
}

Toasts work alongside the inbox seamlessly. The toast gives immediate feedback for user actions while the inbox maintains the permanent record. You can configure toast position, duration, styling, and whether they should automatically dismiss or require user interaction.

Customization options

While the default Inbox' UI is designed to support a wide variety of production apps, you may want to match your app's design. Courier makes this possible without requiring you to rebuild everything from scratch with customizable theming.

Theming works through configuration objects where you specify colors, fonts, border radius, spacing, and other style attributes. The Inbox component takes these theme configurations as React props or HTML attributes and applies them throughout the UI.

Twilio branded inbox

Courier also provides brand settings in the dashboard where you can configure your design system once and apply it across all notifications. This is particularly useful if you're using multiple Courier components or sending notifications through multiple channels, because the branding stays consistent everywhere without duplicating configuration in code. Dark mode support is built into the components, they automatically adapt based on system settings or can be manually controlled.

The built-in views handle most common needs. Unread shows messages the user hasn't seen yet. All Messages shows everything. Archived shows the messages users have cleaned up but not deleted. For more specific needs, you can create custom filtered views that display subsets of messages based on tags, categories, dates, or any other attribute in the message metadata. A customer support dashboard might have views for different ticket priorities. A project management tool might filter by project or phase. The filtering logic is flexible enough to support whatever organizational scheme makes sense for your users.

custom buttons

Action buttons transform notifications from passive information displays into interactive workflows. Instead of just telling the user something happened, you give them options for what to do about it. An approval notification includes approve and reject buttons. A meeting invitation includes accept and decline. A friend request includes confirm and ignore. These actions can deep link to specific screens in your app, or they can trigger custom handlers that update state without navigation.

The inbox component fire events when users interact with actions, so your application can pick up the user's decision and respond appropriately. Maybe clicking approve sends an API request to your backend. Maybe it opens a modal for additional input. Maybe it just updates local state and marks the notification as complete. The SDK gives you the hooks to build whatever interaction pattern fits your product.

Batch and digest notifications help manage notification volume for power users or particularly active periods. Instead of sending 15 individual notifications about comments on a thread, you send one notification that says "15 new comments on Q4 Planning." Instead of notifying about every minor event throughout the day, you send a daily digest at 9am with everything that happened since yesterday.

Courier's Digests feature can handle this automatically:

  • Group similar notifications by time window
  • Combine them into a single digest message
  • Send the digest at optimal times (when users typically check)
  • Skip the digest if the user has already seen the updates

You can configure these rules through Courier's customer journey builder, where the system automatically combines notifications with templates that handle the aggregation formatting. This respects user attention while still keeping them informed. The difference between a useful notification system and an annoying one often comes down to how well you manage volume and timing.


Previous chapter

Introduction to Notification Centers

Learn what notification centers are and why they're essential for modern apps. Covers use cases from DroneDeploy and LaunchDarkly, plus key components like cross-channel sync and action buttons that separate good implementations from great ones.

Next chapter

Best Practices for Notification Centers

Best practices for notification design covering content writing, format selection, and user experience principles. Includes guidance on batching, preference management, common pitfalls to avoid, plus performance optimization and security considerations.

Multichannel Notifications Platform for SaaS

Products

Platform

Integrations

Customers

Blog

API Status

Subprocessors


© 2025 Courier. All rights reserved.