Chapter 4
What building a notification center really costs, when it's worth it, and how Courier lets you keep the UI you want while it runs the Inbox backend: real-time delivery, read state, storage, cross-device sync, and preferences.

Last updated: November 2026
Your notification center should look and behave like your product. That's the part worth owning. The infrastructure behind the bell isn't.
Here's what you sign up to run for as long as your product exists:
None of that is your product. Every month your team spends on it is a month the same engineers aren't shipping the features your customers pay for.
Every option below runs on the same Courier-hosted feed. What changes is how much of the interface is yours.
| What you build | What Courier runs | Best for | |
|---|---|---|---|
| Prebuilt inbox | Drop in the component and set colors, fonts, and light and dark themes | The whole UI: list, tabs, unread badge, actions, and pagination, plus the backend | Shipping fast |
| Custom components (web) | Your own header, list items, or empty, loading, and error states | Everything you don't replace, plus fetching, paging, and real-time updates | Matching your design system |
| Custom UI (headless) | The entire interface, from the SDK's messages and actions | Messages, unread counts, read and archive actions, and real-time sync | Full control |
Start with the Inbox component. On the web, render it inline or as a popup menu from a bell. Theme it to match your app, and it handles unread counts, Unread and Archived views, in-message actions, and pagination out of the box.
When the default inbox is close and one piece isn't, swap that piece. The web SDKs (React, Vue, Angular, and Web Components) take your own renderer for any of these slots:
Slots you leave out keep the built-in element, and the inbox still manages the list, sync, and pagination. Render slots are web-only. On iOS, Android, React Native, and Flutter, theme the prebuilt inbox or build your own UI.
If your app already has a list component you want to keep, skip CourierInbox entirely and render the SDK's data yourself. On the web, nothing loads automatically. You register the feeds, open the real-time connection, then load:
import { useEffect } from "react";
import { useCourier, defaultFeeds } from "@trycourier/courier-react";
function CustomInbox() {
const { inbox } = useCourier();
useEffect(() => {
inbox.registerFeeds(defaultFeeds());
inbox.listenForUpdates();
inbox.load();
}, []);
const messages = inbox.feeds["all_messages"]?.messages ?? [];
return (
<ul>
{messages.map((message) => (
<li
key={message.messageId}
onClick={() =>
message.read ? inbox.unreadMessage(message) : inbox.readMessage(message)
}
>
{message.title}
</li>
))}
</ul>
);
}On mobile, adding an inbox listener after sign-in loads the first page for you. Unread counts, archive actions, and pagination come with the SDK either way.
expiresAt, and it drops from the feed when that time passes. Without one, it persists.Authentication runs through a short-lived JWT. Your backend keeps the workspace API key and calls Create a JWT with a scope bound to one user:
{
"scope": "user_id:user_123 inbox:read:messages inbox:write:events",
"expires_in": "2 days"
}The client passes the token to the SDK's signIn, and that one sign-in covers the inbox, toasts, and (with the preference scopes added) the preference center. Always set expires_in, and mint a fresh token before it expires, since the SDKs don't refresh tokens on their own. The authentication guide has the full scope list.
Your app keeps ownership of who the user is, who gets each notification, and whether they're allowed to take the action a message links to. If a row offers an approval, your backend checks permission when the user clicks it.
The Inbox covers users while they're in your product. Email and push reach them when they aren't. With Courier, the Inbox is one more channel on the same send, not a separate system. This Send API request delivers a report-ready notification to the Inbox and email at once:
{
"message": {
"to": { "user_id": "user_123" },
"template": "REPORT_READY_TEMPLATE_ID",
"data": { "report_id": "report-482" },
"routing": {
"method": "all",
"channels": ["inbox", "email"]
}
}
}all sends on every listed channel. single tries channels in order and stops at the first that delivers, which is how you set up fallback, for example push first and email if push can't deliver. The routing docs cover both, plus nested routes like "always Inbox, plus push or email." For time-based logic, such as a reminder when a report sits unopened for a day, build a journey.
The Developer plan is free for 10,000 sends a month. Business is $0.005 per send, with no per-seat or per-channel fees, and Enterprise adds custom volume pricing and uptime and support SLAs (pricing).
A send is one template to one recipient. The template above delivers Inbox and email to one person and counts once, and the Inbox has no fee of its own. In a journey, each Send node delivers one channel, so a journey that sends email and Inbox counts two sends.
Twilio embedded Courier's Web Inbox in the Twilio Console, replacing a patchwork of alerts with one real-time feed for onboarding guidance, new features, and account updates. Twilio considered several options and picked Courier because it fit into existing workflows with no rewrites. Twilio sells communications APIs to more than 10 million developers, and its in-app feed runs on Courier.
Build your own backend when owning it is the requirement itself: notifications are your core product, or you need control over storage, protocol, or deployment that a managed service can't give you. A custom design isn't one of those reasons. Courier's headless SDK gives you any UI you can draw on top of a backend you don't have to run.
Sign up at app.courier.com, mint a JWT for a test user, and render the inbox. Chapter 2 has the full setup for React, iOS, Android, and React Native. Send yourself a notification, watch it land live, mark it read on one device, and check it on another.
Then theme it, swap in your own components, or go fully headless when your design calls for it.
Comparing platforms? See Knock alternatives, Courier vs Knock, and the real cost of notifications at scale.
The first version is the cheap part. The lasting cost is owning a WebSocket backend, read state across devices, storage and pagination, preferences, and an on-call rotation, plus the servers that run it. With Courier, the Developer plan is free for 10,000 sends a month, Business is $0.005 per send, and there's no Inbox infrastructure for you to run.
Yes. Theme the prebuilt inbox, replace the header, list items, or loading, empty, and error states with your own components on the web, or build an entirely custom UI from the SDK's messages, unread counts, and actions on any platform. Courier runs the backend in every case.
Yes. Courier has Inbox SDKs for React, Vue, Angular, and Web Components on the web, plus iOS, Android, React Native, and Flutter. Read, opened, and archived states sync across every device a user is signed in on, so a message read in your web app shows as read in your mobile app.
Until the user archives them or they reach their expiration. A message sent with an expiresAt drops from the feed when that time passes. Without one, it stays.
Not through the Send API. One template to one recipient is one send, however many channels it reaches, and the Inbox has no fee of its own. In a journey, each Send node delivers one channel, so email plus Inbox counts as two sends.
Previous chapter
Notification Center Best Practices
How to design notifications users actually read: content, format, batching, preferences, performance, security, and the different constraints on web and mobile apps.
Next chapter
Notification Center Use Cases
How product teams use in-app notification centers: SaaS approvals, DevOps and incident alerts, healthcare care coordination, and marketplace transaction tracking.