Push Notifications
Push notifications keep users engaged by delivering timely messages even when the app is closed. In this course, you will learn how push notifications work end-to-end — from registering device tokens to sending notifications from your Go API and handling them in the app.
What are Push Notifications?
Push notifications are different from in-app messages. An in-app message only shows when the user has the app open. A push notification reaches the user regardless — their phone buzzes, the message appears on the lock screen, and tapping it opens your app.
The flow involves three parties:
- 1. Your app — registers for notifications and gets a push token
- 2. Your Go API — stores the token and sends notifications when events occur
- 3. Apple/Google — delivers the notification to the specific device
Challenge: Understand the Flow
Explain the push notification flow in your own words. Why can't your server send a notification directly to a phone? Why do Apple and Google sit in the middle?
Expo Push Notifications
Expo simplifies push notifications significantly. Instead of configuring APNS and FCM separately, you use Expo's unified push service. Your server sends a notification to Expo, and Expo handles routing it through APNS or FCM depending on the device.
The complete flow looks like this:
1. App launches → requests notification permissions
2. App gets Expo push token (e.g., "ExponentPushToken[xxxxxx]")
3. App sends token to your API: POST /api/devices { push_token: token }
4. API stores token in the database (linked to the user)
5. When an event occurs, API sends notification via Expo push service
6. Expo routes it through APNS (iOS) or FCM (Android)
7. Device receives the notificationChallenge: Request Permissions
What permissions does the app need to request for push notifications? On iOS, the user must explicitly grant permission. On Android 13+, notification permission is also required. What happens if the user denies permission?
Getting the Push Token
Use Expo's Notifications API to get the device's push token:
import * as Notifications from 'expo-notifications'
import * as Device from 'expo-device'
import { Platform } from 'react-native'
async function registerForPushNotifications() {
// Push notifications only work on physical devices
if (!Device.isDevice) {
console.log('Push notifications require a physical device')
return null
}
// Request permission
const { status: existingStatus } = await Notifications.getPermissionsAsync()
let finalStatus = existingStatus
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync()
finalStatus = status
}
if (finalStatus !== 'granted') {
console.log('Permission not granted for push notifications')
return null
}
// Get the Expo push token
const token = (await Notifications.getExpoPushTokenAsync()).data
console.log('Push token:', token)
// Android needs a notification channel
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
})
}
return token
}After getting the token, send it to your API so the server can store it:
// Send token to your API
const token = await registerForPushNotifications()
if (token) {
await api.post('/api/devices', { push_token: token })
}Device.isDevice first.Challenge: Register a Push Token
On a physical device, run the push token registration code. What does the token look like? It should start with "ExponentPushToken[". Send it to your API and verify it's stored in the database.
The grit-notifications Plugin
Grit provides a notifications plugin that handles device token storage, sending notifications to one or many devices, and keeping a notification history.
go get github.com/MUKE-coder/grit-plugins/grit-notificationsThe plugin provides:
- • Device model — stores push tokens linked to user accounts
- • Send to one — send a notification to a single device
- • Send to many — broadcast to all devices of a user, or all users
- • Notification history — log of all sent notifications for debugging
- • Token cleanup — automatically removes invalid tokens
Challenge: Explore the Plugin
Look at the grit-notifications plugin documentation. What functions does it expose? What database tables does it create?
Sending a Notification
From your Go API, send a notification using the plugin:
import notify "github.com/MUKE-coder/grit-plugins/grit-notifications"
// Send a notification to a specific device
err := notify.Send(notify.PushMessage{
To: pushToken,
Title: "New Workout Added",
Body: "Your trainer added a new workout for today.",
Data: map[string]string{"screen": "workout", "id": "42"},
})
// Send to all devices of a specific user
err = notify.SendToUser(db, userID, notify.PushMessage{
Title: "Weekly Summary",
Body: "You completed 5 workouts this week!",
Data: map[string]string{"screen": "stats"},
})The Data field is a key-value map that is delivered to the app but not shown to the user. Use it to pass information the app needs to handle the notification — like which screen to navigate to and what ID to load.
screen key in the Data field. This tells the app which screen to navigate to when the user taps the notification.Challenge: Design a Notification
If you were building an order confirmation notification for an e-commerce app, what would the Title, Body, and Data fields contain? Write out the complete PushMessage struct.
Handling Notifications
Notifications can arrive in three different states, and you need to handle each one:
- • Foreground — app is open and visible. Show an in-app banner or toast.
- • Background — app is in the background. The OS shows the notification in the tray.
- • Killed — app is completely closed. The OS shows the notification. Tapping it opens the app.
import * as Notifications from 'expo-notifications'
import { useRouter } from 'expo-router'
export function useNotificationListeners() {
const router = useRouter()
useEffect(() => {
// Foreground: notification received while app is open
const receivedSub = Notifications.addNotificationReceivedListener(
(notification) => {
const data = notification.request.content.data
console.log('Received in foreground:', data)
// Show an in-app toast or banner
}
)
// User tapped: notification was tapped (from any state)
const responseSub = Notifications.addNotificationResponseReceivedListener(
(response) => {
const data = response.notification.request.content.data
// Navigate to the relevant screen
if (data.screen === 'workout' && data.id) {
router.push('/workouts/' + data.id)
}
}
)
return () => {
receivedSub.remove()
responseSub.remove()
}
}, [])
}Challenge: Handle Notification Tap
What should happen when a user taps a notification about a new workout? The app should open, navigate to the workout detail screen, and show the specific workout. Write the handler code.
Challenge: Foreground Behavior
When a notification arrives while the app is open (foreground), the default behavior on iOS is to not show it. How would you configure Expo to show a banner even in the foreground? (Hint: look at setNotificationHandler)
Local Notifications
Use Expo's scheduleNotificationAsync to schedule a local notification:
import * as Notifications from 'expo-notifications'
// Schedule a notification for 1 hour from now
await Notifications.scheduleNotificationAsync({
content: {
title: "Reminder",
body: "Time to work out!",
data: { screen: "workouts" },
},
trigger: {
seconds: 3600, // 1 hour
},
})
// Schedule a daily notification at 9:00 AM
await Notifications.scheduleNotificationAsync({
content: {
title: "Good morning!",
body: "Ready for today's workout?",
},
trigger: {
hour: 9,
minute: 0,
repeats: true,
},
})Local notifications are perfect for features like:
- • Workout reminders ("Time to exercise!")
- • Timer completions ("Rest period over — next set!")
- • Daily streaks ("Don't break your 7-day streak!")
- • Meal reminders ("Time to log your lunch")
Challenge: Schedule a Local Notification
Schedule a local notification that fires 10 seconds from now with a title and body of your choice. Does it appear on your device? What happens if you close the app before it fires?
Summary
Here's what you learned in this course:
- Push notifications are delivered through Apple (APNS) or Google (FCM)
- Expo simplifies push by providing a unified API for both platforms
- Push tokens uniquely identify a device and must be stored on your server
- Handle three states: foreground, background, and killed
- Local notifications work offline and are great for reminders and timers
Challenge: Design a Notification System
When should your fitness app send push notifications? List 5 scenarios. For each, specify whether it should be a push notification (from the server) or a local notification (from the app).
Challenge: Write the Payloads
For each of your 5 scenarios from the previous challenge, write the complete notification payload: Title, Body, and Data (with screen and ID). Make the copy concise and actionable.
Enjoying the course?
Help us grow — star us on GitHub, subscribe on YouTube, and follow on LinkedIn.