How FCM Messages Are Structured
Firebase Cloud Messaging (FCM) splits pushes into two delivery modes: notification messages and data messages. Notification messages are handled automatically by the browser — the UI displays them without any client-side code. Data messages, on the other hand, require manual handling in the application, which gives you full control over how the payload is rendered or processed.
An FCM setup needs two main parts: a trusted server environment for building and sending messages (Express backend, Cloud Functions, etc.) and a client app for receiving them through the platform's transport layer. For this project, the Express server handles the sending side, while a React app receives and processes the messages as data payloads.
Configuring Firebase Project Credentials
Working with FCM requires two separate credential sets — one for the frontend and one for the backend. To prepare these, start by creating a Firebase project in the Firebase console with your Google account. After the project spins up, you'll need to register a web app and capture its configuration object for the React side.
For the backend, click the gear icon next to Project Overview to create a service account. Follow the console prompts to download a JSON key file containing your credentials. Keep this file secure and store it in a protected location on your server in production.
Running The Starter Project
You can use the Fireact repository to follow along. Fork it and check out the 01-get-started branch. The repository bundles both the client and server folders so you can run them side by side:
- Open the
client/directory and runyarn install, thenyarn start. - For the server, create a
.envfile. Add aCONNECTION_STRINGpointing to a PostgreSQL database, plus aPORTvalue — use3001since React already occupies3000. - Run
yarn installin theserver/directory, then runyarn runQueryto create the database andyarn startdevto launch the API. - Verify the setup by visiting https://localhost:3001/v1/messages — you should see JSON-formatted message records.
Sending Messages From The Express Server
On the backend, initialize the Firebase Admin SDK. Install it inside the server/ folder, then add an environment variable to point to your downloaded service account file — for example, GOOGLE_APPLICATION_CREDENTIALS=path/to/file.json. Export this path in server/src/settings.js so it can be referenced safely across the project.
Create a server/src/firebaseInit.js file that imports the admin module, initializes the app with your credentials file, and exports the messaging instance:
The initialization code loads the certificate from the path stored in your environment variables. Passing sensitive file paths directly into code is less secure, so always read them through configuration.
To confirm the setup works, add a temporary import of the messaging instance into server/src/app.js and log it to the console. Once you see the messaging object printed, remove those test lines.
Creating The Notification Helper
FCM data messages don't have required fields — you decide which key-value pairs to include. For sending, the only requirement is at least one target device token. Create a helper file server/src/notify.js that takes an array of tokens and a data object:
The function calls sendMulticast on the messaging instance, which returns a promise. On resolution, you get a response array from which you can count successful and failed deliveries. The per-platform settings (like android or apns) are optional extensions you can add for platform-specific behavior.
Wire this helper into the message creation flow. Update the addMessage function in server/src/controllers/message.js to invoke the notification helper after a message is successfully inserted into the database. The tokens array remains empty for now — you'll plug in the device token generated by your frontend in the next step.
Client-Side Setup for Firebase Messaging
The React frontend is a standard App.js component styled with react-bootstrap, featuring a toast component for displaying notifications. The main logic lives inside the Messaging component.
# library imports
import { Messaging } from './Messaging';
axios.defaults.baseURL = 'https://localhost:3001/v1';
const App = () => {
return (
<Fragment>
<ToastContainer autoClose={2000} position="top-center" />
<Navbar bg="primary" variant="dark">
<Navbar.Brand href="#home">Firebase notifictations with React and Express</Navbar.Brand>
</Navbar>
<Container className="center-column">
<Row>
<Col>
<Messaging />
</Col>
</Row>
</Container>
</Fragment>
);
};
export default App;
The Messaging component manages two state variables: messages, which holds the list of messages fetched from the database, and requesting, which toggles the loader state. A React.useEffect hook fetches data from the /messages endpoint and populates the state.
The component renders messages by mapping over the list and displaying the name and message fields, alongside a form for creating new entries. Formik manages the form logic, providing handleChange and handleSubmit functions, with isSubmitting controlling the submit button state.
export const Messaging = () => {
const [messages, setMessages] = React.useState([]);
const [requesting, setRequesting] = React.useState(false);
React.useEffect(() => {
setRequesting(true);
axios.get("/messages").then((resp) => {
setMessages(resp.data.messages);
setRequesting(false);
});
}, []);
return (
<Container>
{/* form goes here */}
<div className="message-list">
<h3>Messages</h3>
{requesting ? (
<Spinner animation="border" role="status">
<span className="sr-only">Loading...</span>
</Spinner>
) : (
<>
{messages.map((m, index) => {
const { name, message } = m;
return (
<div key={index}>
{name}: {message}
</div>
);
})}
</>
)}
</div>
</Container>
);
};
<Formik
initialValues={{
name: "",
message: "",
}}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
toast.success("Submitted succesfully");
}, 1000);
}}
>
{(prop) => {
const { handleSubmit, handleChange, isSubmitting } = prop;
return (
<>
<InputGroup className="mb-3">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon1">Name</InputGroup.Text>
</InputGroup.Prepend>
<FormControl
placeholder="Enter your name"
onChange={handleChange("name")}
/>
</InputGroup>
<InputGroup className="mb-3">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon1">Message</InputGroup.Text>
</InputGroup.Prepend>
<FormControl
onChange={handleChange("message")}
placeholder="Enter a message"
/>
</InputGroup>
{isSubmitting ? (
<Button variant="primary" disabled>
<Spinner
as="span"
size="sm"
role="status"
animation="grow"
aria-hidden="true"
/>
Loading...
</Button>
) : (
<Button variant="primary" onClick={() => handleSubmit()}>
Submit
</Button>
)}
</>
);
}}
</Formik>
Requesting Permission and Generating a Token
To begin using Firebase on the frontend, install the Firebase JavaScript client library — a different package from the firebase-admin SDK used on the server.
# install firebase client library
yarn add firebase
Create client/src/firebaseInit.js and initialize Firebase with your project config. The Firebase docs note that the full client library includes support for Authentication, Realtime Database, Storage, and Cloud Messaging, so we import only the messaging feature here.
import firebase from 'firebase/app';
import 'firebase/messaging';
const config = {
apiKey: "API-KEY",
authDomain: "AUTH-DOMAIN",
databaseURL: "DATABASE-URL",
projectId: "PROJECT-ID",
storageBucket: "STORAGE-BUCKET",
messagingSenderId: "MESSAGING-SENDER-ID",
appId: "APP-ID"
};
firebase.initializeApp(config);
const messaging = firebase.messaging();
// next block of code goes here
Now add the functions that request permission and listen for incoming messages:
export const requestFirebaseNotificationPermission = () =>
new Promise((resolve, reject) => {
messaging
.requestPermission()
.then(() => messaging.getToken())
.then((firebaseToken) => {
resolve(firebaseToken);
})
.catch((err) => {
reject(err);
});
});
export const onMessageListener = () =>
new Promise((resolve) => {
messaging.onMessage((payload) => {
resolve(payload);
});
});
requestFirebaseNotificationPermission prompts the browser for notification permission and resolves with a unique token that FCM uses to target the browser. onMessageListener handles messages received while the browser is open in the foreground. For a production app, the token should be stored server-side, but for this tutorial we'll copy it from the console.
Import requestFirebaseNotificationPermission into App.js and call it before the component's return statement:
import { requestFirebaseNotificationPermission } from './firebaseInit'
requestFirebaseNotificationPermission()
.then((firebaseToken) => {
// eslint-disable-next-line no-console
console.log(firebaseToken);
})
.catch((err) => {
return err;
});
Launching the app triggers the permission prompt. After you allow it, the token appears in the console. If you refresh the page, the prompt won't reappear (since permission is already granted), but the token will still be logged. Note that Firefox 75 requires a user-generated action (like a click) to show the permission request.
Handling Message Creation and Foreground Notifications
Once the permission flow works, complete the form submission logic to save messages. Replace the onSubmit handler with a POST request to the /messages endpoint. On success, prepend the returned data to the message list and show a confirmation toast.
onSubmit={(values, actions) => {
axios
.post("/messages", values)
.then((resp) => {
setMessages(resp.data.messages.concat(messages));
actions.setSubmitting(false);
toast.success("Submitted succesfully");
})
.catch((err) => {
console.log(err);
toast.error("There was an error saving the message");
});
}}
Before testing the POST request, open server/src/controllers/messages.js and comment out the notification-sending line. Run both servers, add a message, and verify it works; then uncomment that line and continue.
Copy the generated token into the tokens array on the backend. It's a very long string.
const tokens = [
'eEa1Yr4Hknqzjxu3P1G3Ox:APA91bF_DF5aSneGdvxXeyL6BIQy8wd1f600oKE100lzqYq2zROn50wuRe9nB-wWryyJeBmiPVutYogKDV2m36PoEbKK9MOpJPyI-UXqMdYiWLEae8MiuXB4mVz9bXD0IwP7bappnLqg',
];
Next, import onMessageListener in Messaging.js and call it before the return statement. The listener returns a promise that resolves with the notification payload, which we render as a toast title and body. This handles notifications only when the app is in the foreground.
import { onMessageListener } from './firebaseInit';
React.useEffect(() => {
...
}, []);
onMessageListener()
.then((payload) => {
const { title, body } = payload.data;
toast.info(`${title}; ${body}`);
})
.catch((err) => {
toast.error(JSON.stringify(err));
});
Displaying Background Notifications with a Service Worker
Since we're sending data messages (not notification objects), background notifications need a service worker to handle display behavior.
Create client/public/firebase-messaging-sw.js with the service worker script:
importScripts('https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/7.14.2/firebase-messaging.js');
const config = {
apiKey: "API-KEY",
authDomain: "AUTH-DOMAIN",
databaseURL: "DATABASE-URL",
projectId: "PROJECT-ID",
storageBucket: "STORAGE-BUCKET",
messagingSenderId: "MESSAGING-SENDER-ID",
appId: "APP-ID"
};
firebase.initializeApp(config);
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function(payload) {
console.log('[firebase-messaging-sw.js] Received background message ', payload);
const notificationTitle = payload.data.title;
const notificationOptions = {
body: payload.data.body,
icon: '/firebase-logo.png'
};
return self.registration.showNotification(notificationTitle,
notificationOptions);
});
self.addEventListener('notificationclick', event => {
console.log(event)
return event;
});
This file imports only the firebase-app and firebase-messaging libraries. Keep the versions in sync with those in your package.json. After initializing Firebase, setBackgroundMessageHandler determines how the browser shows notifications, including optional icons. The notificationclick event handler controls click behavior.
Now register the service worker. Create client/src/serviceWorker.js with a function that first checks for serviceWorker support in the navigator object, then registers the file we just made:
export const registerServiceWorker = () => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('firebase-messaging-sw.js')
.then(function (registration) {
// eslint-disable-next-line no-console
console.log('[SW]: SCOPE: ', registration.scope);
return registration.scope;
})
.catch(function (err) {
return err;
});
}
};
Finally, open client/src/index.js, import this function, and call it. The service worker's scope should appear in the console. To test, open the app in a second browser at https://localhost:3000/messaging, create a message, and verify the notification appears in the first browser.
# other imports
import { registerServiceWorker } from './serviceWorker'
ReactDOM.render(
...
);
registerServiceWorker()
With both foreground and background scenarios covered, you now have a complete implementation—from token generation to message listening—using Firebase Cloud Messaging in a React app.
For further exploration, consult the official FCM documentation and the related resources linked below:
- Firebase Cloud Messaging, Firebase Docs
- Fireact, Orji Chidi Matthew, GitHub
- Firebase Admin Node.js SDK
- Service Worker Cookbook, Mozilla



