Skip to main content
Two files get a Courier Inbox working in React: a server route that mints a token, and a component that renders the feed. A Vite app is all browser, and your API key cannot go there. That is the only reason this needs a server at all.

courier-react-quickstart

The finished app from this guide, ready to clone and run.

Prerequisites

  • , from any environment. Every one ships with the Courier Inbox provider already configured.
  • A React app on Vite, and Node 20.9 or newer
  • A backend you can add one route to

Clone the finished app

Skip the steps below if you would rather read working code. This repo is this guide, already assembled.
Open localhost:5173, then run npm run send in a second terminal. The message arrives in the open page without a refresh.

Add an inbox to your app

1

Install the packages

The React package renders the inbox. The server package mints tokens.
Put your key in .env. Your server reads it, and the browser never does.
.env
2

Add a route that mints the token

Your API key signs the JWT, so this runs on your backend and never in the browser.
server.js
Take the user id from your session, never from the request body or a query parameter. A caller who can name any user can read that user’s inbox.The route calls , and the two inbox: scopes are the minimum for a working feed. lists what to add for preferences and push.
3

Point Vite at your server

Proxying keeps the fetch same-origin, so there is no CORS to configure.
vite.config.ts
4

Render the inbox

Fetch the token, call signIn, then mount CourierInbox.
src/CourierInbox.tsx
StrictMode mounts every component twice in development. A second signIn signs the first user out again, so guard it with a ref.
5

Give the inbox a height

CourierInbox fills its container’s width and takes its height from the parent, so size the parent.
src/App.tsx
On React 17, install @trycourier/courier-react-17 instead. The API is identical.
6

Send a message to the inbox

inbox is a channel like email or sms. Including it in routing.channels is what puts the message in the feed.
scripts/send.ts
content here is the shorthand form, which takes a title and a body. For multi-element layouts and per-channel variants, use a instead and reference it by id.

Where the token route belongs

The sample ships a small server.js so it runs on its own. In your app, that route belongs on the backend you already have, next to the endpoint that returns the current user. Any framework works, because the route does one thing: read the session, mint a JWT, return it. Express, Fastify, Hono, a Lambda, or a Rails controller are all fine.

Keep the session alive

The SDKs do not refresh tokens. Before the current one expires, mint a new JWT and call signIn again with it. Match expires_in to your own session length. A short-lived token can expire while a tab stays open, which quietly empties the inbox.
expires_in is optional, and omitting it mints a token that never expires. Always set it. Rotating the API key that signed it is the only way to revoke one.

Verify

1

Load the page

The inbox renders, signed in and empty.
2

Send a message

Run the send against the same user_id your token was scoped to.
3

Watch it arrive

The message appears in the open page in real time, and the unread count moves.
If the inbox renders but stays empty, suspect the token before anything else. An expired or mis-scoped JWT signs in silently and returns no messages. walks the four causes in order.

FAQ

The key can send to anyone and read any user’s data. It is a server credential, and a JWT scoped to one user is what the browser is meant to hold.
Drop the Vite proxy, fetch the absolute URL, and allow that origin with CORS. The token route itself does not change.
The JWT is built for the browser. It is scoped to one user and it expires. Your API key, which signs it, stays on the server.
The feed is filtered by tenant. Signing in with a tenantId hides messages sent without one. See .