Riley Napier
June 30, 2021

Put simply, building and maintaining a completely custom notification system in-house is a pain. It requires a lot of human effort in the beginning and will undoubtedly need to scale at some point. Maintaining a system like this takes away development time from core tasks and business needs.
To make sure teams don’t need to build an in-house solution for a notification systems problem, we adapted our offering. We created a lightweight solution using React that has a global state and runs independently in the background — so teams can render our components regardless of their tech stack.
While React is a popular library, we recognize not everyone uses it, and it might not be as widely used in the future as competing front-end architectures emerge. This is why we wanted to find a way to create custom components that can work in any front end setup with any user interface.
To solve this we decided to make custom Courier components in React that take inspiration from Web Components. The idea behind Web Components is that they allow developers to build custom, reusable elements where the functionality lives independently from other parts of the codebase.
This modular setup is what allows for a custom solution that can be implemented anywhere, with any specific user interface, and with any front-end library or framework. Because the logic can live outside the context of your other code, our components can run independently in the background.
The initial setup is straightforward. You place two script tags in the body (the order of the tags is important). The first script tag holds a small amount of code where you identify configurations like your user with a userId and your Courier clientKey. The second script tag downloads the Courier components.
Copied!
<body><section><h1>Hello World</h1><courier-toast></courier-toast><courier-inbox></courier-inbox></section><script type="text/javascript">window.courierConfig = {clientKey: "{{CLIENT_KEY}}",userId: "{{USER_ID}}"};</script><script src="https://courier-components-xvdza5.s3.amazonaws.com/latest.js"></script></body>
Additional configuration options let you defer the initialization of Courier components, as well as map the configuration for each component you load on the page. The two components you can currently load are toast and inbox.
Our SDK is exposed on window.courier and is loaded asynchronously. Calling window.courierAsyncInit will let you know Courier has successfully loaded.
Copied!
<script type="text/javascript">window.courierAsyncInit = () => {console.log("Courier is Ready!");};</script>
If you’d prefer to separate the logic for each component (the toast and inbox components), you can also choose to set window.courierAsyncInit to an array.
After initialization, window.courier is ready, and you can listen for actions inside the Courier SDK. A small amount of code lets you init the toast component.
Copied!
<script>window.courierAsyncInit = () => {window.courier.on("toast/init", () => {window.courier.toast({title: "Hello",body: "World",});};};</script>
You can configure the components in two ways:
Copied!
//inline<courier-toast auto-close="false"></courier-toast>
window.courierConfigCopied!
window.courierConfig = {components: {toast: {autoClose: false,}}};
If you need to use multiple configuration options with a component, window.courierConfig gives you that ability without having to add too many attributes to your HTML element.
If you do choose to use the inline configuration, you’ll need to make sure you’re always formatting in kebab case since HTML attributes are not case sensitive.
It’s pretty easy to get up and running with the components. But one hurdle we needed to overcome was making sure the data you need from us is accessible to every Courier React component. And this needs to happen anywhere in your project, regardless of component hierarchy. We make use of React Context and React Portals to inject components anywhere in your DOM.
If you’re unfamiliar with React Context and React Portals, here’s a quick rundown.
Context allows you to pass props between components without explicitly having to deal with tree structure. This allows for easy access to data regardless of UI requirements. The result is global data accessible by child components that live outside the nesting levels of parent components that contain necessary data.
The use of a portal allows you to inject a child anywhere into the DOM, retaining the context of the parent node even though it’s outside the standard nesting structure. Even though the portal can be placed randomly in the DOM tree, the portal still retains its context in the React tree. This means events like bubbling will still function normally.
After the initialization of Courier, we analyze the HTML and find components to dynamically import, making sure not to download any extra components you aren't using. We identify them by HTML tags and then render them inside the context of the Courier SDK. This allows us to then render them wherever you need in the DOM with the Courier context they need.
So through a combination of React Context and React Portals, we preserve the global state our Courier components rely on. Our toast and inbox components render into a portal, and the portal allows for those components to act as children out of the hierarchy order of the parent. This allows you to render our Courier components into anything that's not in the official React DOM tree.
We’re not here to add code bloat. We purposefully found solutions that guarantee we keep our integration as small as possible.
We currently have two components you can render, the toast message and the inbox. We're cognizant that library size matters, and while some might see a need to integrate both components, others might only want to integrate one. We also have plans to add more components in the future, so it's important to dynamically load what's needed, not everything.
By providing a small amount of code for you to implement that handles the automatic download of desired components, we make sure your project remains as small and lightweight as possible. When you load our code, we analyze your HTML to see what components you’ve identified that you need. These components are loaded dynamically and are then cached. This ensures that subsequent renders aren’t refreshing the code.
We do this withReact Suspense, which does exactly what it says. It suspends the rendering of React components until a condition is met. In the example below, the portal we’ve created is waiting to see if the toast component has a configuration set up. If it does, we will load it.
Copied!
import React, { lazy, Suspense } from "react";const toastElement = document.querySelector("courier-toast") ?? undefined;const toastConfig = {...componentConfigs?.toast,...getAttrsAsJson(toastElement)};<CourierSdkactiveComponents={{toast: Boolean(toastElement)}}>{toastElement &&ReactDOM.createPortal(<Suspense fallback={<div />}><Toast config={toastConfig} /></Suspense>,toastElement)}</CourierSdk>;
When a component does need to render, it can do so asynchronously. This implementation method also allows us to scale in the future by adding new components that can be dynamically imported.
In addition to dynamically imported components, we also keep the bundle small by using Preact. Preact uses the same ES6 API as React, but Preact is more lightweight and able to load a faster, thinner virtual DOM. We’ve carefully built this implementation so Preact can fully replace all instances of React.
You can check out the repo here.
Courier enables developers to deliver the right message to the right user at the right time. To find out more about Courier’s full offering and see how it can integrate into your stack, check out our docs and our API.

How Top Notification Platforms Handle Quiet Hours & Delivery Windows in 2026
No platform offers per-template delivery windows in 2026—it's either per-workflow (Customer.io, Knock), per-campaign (Braze), or global settings. This comparison shows exactly how six platforms handle quiet hours and send time controls based on their documentation and API specs. Braze leads on AI timing (23% open rate lift from Intelligent Timing across their customer base). Novu is the only platform letting subscribers set their own delivery windows. Customer.io and Knock require manual workflow configuration. OneSignal's strength is push-specific optimization across 300K+ apps. Courier combines per-node flexibility with API control. Includes feature matrix, timezone handling, and frequency capping differences.
By Kyle Seyler
January 16, 2026

Notification Observability: How to Monitor Delivery, Engagement, and Provider Health
Notification observability is the practice of monitoring notification delivery, engagement, and provider health using the same tools and discipline you apply to the rest of your application infrastructure. It means tracking whether messages are delivered, opened, and acted on across email, SMS, push, and in-app channels, then surfacing that data in dashboards alongside your other application metrics. Key metrics include delivery rate by channel, bounce and failure rates, provider latency, open rate trends, and click-through rates by template. Teams can build notification observability through DIY webhook handlers that pipe provider events to Datadog or Prometheus, log aggregation from application send logs, or notification platforms with built-in observability integrations. This matters most for multi-channel systems, business-critical notifications like password resets and payment confirmations, and teams using multiple providers with fallback routing.
By Kyle Seyler
January 15, 2026

SMS Opt-Out Rules in 2026
TCPA consent rules changed in April 2025. Consumers can now revoke consent using any reasonable method, including keywords like "stop," "quit," "end," "revoke," "opt out," "cancel," or "unsubscribe." Businesses must honor opt-out requests within 10 business days, down from 30. The controversial "revoke all" provision, which would require opt-outs to apply across all automated messaging channels, has been delayed until January 2027 and may be eliminated entirely. SMS providers like Twilio handle delivery infrastructure and STOP keyword responses at the number level. They don't sync opt-outs to your email provider, push notification service, or in-app messaging. That cross-channel gap is your responsibility. Courier provides unified preference management that enforces user choices across SMS, email, push, and chat automatically.
By Kyle Seyler
January 13, 2026
© 2026 Courier. All rights reserved.