Guides/How to Add a Notification Center to Your App/Build vs Buy a Notification Center

Chapter 4

Build vs Buy a Notification Center

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.

How to Build a Notification Center

Last updated: November 2026

Key takeaways

  • You don't have to choose between your own design and a managed backend. Keep the UI you want, and let Courier run the Inbox behind it: real-time delivery, read state, storage, cross-device sync, and preferences.
  • Pick how much UI you own: the prebuilt inbox themed to your brand, your own components in the web SDK's render slots, or a fully custom UI built on the SDK's data and actions.
  • Building one means owning a WebSocket backend, read state, storage, preferences, and an on-call rotation. Writing the code is the cheap part.
  • The same send that reaches the Inbox can reach email, push, SMS, Slack, and Teams. One template to one recipient through the Send API counts as one send.
  • Developer is free for 10,000 sends a month. Business is $0.005 per send.

Your notification center should look and behave like your product. That's the part worth owning. The infrastructure behind the bell isn't.

What building it actually costs

Here's what you sign up to run for as long as your product exists:

  • A real-time backend. A WebSocket service that streams new notifications and state changes to every open client, handles reconnects, and stays healthy as traffic grows.
  • Read state you can trust. Per-user read, unread, opened, and archived state, plus an accurate unread count, kept consistent across every device a user is signed in on.
  • Storage and pagination. A datastore that holds each user's history and pages through it quickly when the feed is thousands of items deep.
  • Preferences. Per-topic and per-channel opt-outs, enforced everywhere a notification can go, not only in the feed.
  • On-call. Scaling the socket layer, uptime, upgrades, security patches, and the pager, plus the server bill under it all.

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.

Keep the UI you want

Every option below runs on the same Courier-hosted feed. What changes is how much of the interface is yours.

What you buildWhat Courier runsBest for
Prebuilt inboxDrop in the component and set colors, fonts, and light and dark themesThe whole UI: list, tabs, unread badge, actions, and pagination, plus the backendShipping fast
Custom components (web)Your own header, list items, or empty, loading, and error statesEverything you don't replace, plus fetching, paging, and real-time updatesMatching your design system
Custom UI (headless)The entire interface, from the SDK's messages and actionsMessages, unread counts, read and archive actions, and real-time syncFull control

Prebuilt inbox

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.

Custom components in render slots

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:

  • Header
  • List item
  • Empty state
  • Loading state
  • Error state
  • Pagination item
  • Popup menu button

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.

Headless: 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:

JSX
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.

What Courier runs behind your UI

  • Real-time delivery. Once a user signs in, the inbox opens a real-time connection and new messages appear live. The connection uses the same authenticated session, so there's no separate socket auth to build.
  • Cross-device sync. Read, opened, and archived states sync across every device where that user is signed in. Read it on the web, and it's read on their phone.
  • Storage and retention. A message stays in the feed until the user archives it or it expires. Send it with an expiresAt, and it drops from the feed when that time passes. Without one, it persists.
  • Cross-channel read state. When a template sends to both email and the Inbox, opening the email marks the Inbox message read. This uses the email's open tracking, so keep open tracking on.
  • Preferences. Users pick the topics they receive and how each is delivered in the embeddable preference center, and Courier applies those choices on every channel.

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:

JSON
{
  "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.

One send reaches the Inbox and every other channel

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:

JSON
{
  "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.

What it costs

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 built its Console inbox on Courier

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.

When building makes sense

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.

Start free

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.

Frequently asked questions

How much does it cost to build a notification center in-house?

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.

Can I use my own UI with Courier?

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.

Does Courier support both web and mobile notification centers?

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.

How long do messages stay in a Courier Inbox?

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.

Does sending to the Inbox and email cost two sends?

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.