BowlerKit
Features

Notifications

1/22/2026

Documentation for Push Notifications, In-App Inbox, and Notification Management in Expo.

The boilerplate provides a sophisticated notification system using Expo Notifications for push and local alerts, integrated with Firebase Cloud Messaging (FCM) for remote delivery, and a dedicated Notification History for an in-app inbox experience.

Overview

Notifications in the Expo project are structured into several layers:

  1. Push Notifications (FCM): Remote delivery using Firebase Cloud Messaging via Expo's push service.
  2. Foreground Notifications: UI alerts shown when the app is active.
  3. Notification History: Backend-stored notification logs accessible via API.
  4. Preferences Management: User settings for specific notification types, persisted locally and synced to the server.

🏗 Architecture

Initialization

The notification system is initialized in app/_layout.tsx using the usePushNotifications hook. This ensures listeners for incoming notifications and user interactions are active as soon as the app starts.

// app/_layout.tsx
export default function RootLayoutContent() {
  // ...
  // Initialize push notifications (listeners)
  usePushNotifications();
  // ...
}

1. Push Notifications (FCM)

Uses the expo-notifications library. The logic is encapsulated in the usePushNotifications hook located at hooks/use-push-notifications.ts.

  • Token Management: The hook handles requesting permissions and retrieving the FCM token. It automatically saves the token to the preferences-store and syncs it to the backend when the user is authenticated.
  • Handling States:
    • Foreground: Notifications.setNotificationHandler defines how notifications appear when the app is open (alerts, sounds, badges).
    • Taps/Interactions: addNotificationResponseReceivedListener catches when a user taps a notification, allowing for deep link navigation.
  • Permission Flow: Managed within the hook. Permissions are typically requested during onboarding or when the user toggles notifications in settings.

2. Notification History (In-App Inbox)

Unlike ephemeral push notifications, the notification history is a persistent log of alerts stored on the backend.

  • Fetching: Handled by the useNotifications query in queries/use-notifications.ts. It uses TanStack Query for efficient caching and state management.
  • Actions: Marking notifications as read or deleting them is handled by mutations in mutations/use-notification-mutations.ts.
  • UI: The app/notifications.tsx screen provides a rich interface for browsing, reading, and managing these notifications.

3. Settings & Preferences

Managed via the usePreferencesStore (Zustand).

  • Persistence: Settings like "Push Enabled", "Promotions", and "Vibration" are saved to device storage using expo-secure-store or AsyncStorage.
  • Server Sync: When a user changes a preference, the store automatically debounces and syncs the new state to the backend using api.put(ENDPOINTS.NOTIFICATION_PREFERENCES).
  • Offline Support: If sync fails (e.g., no internet), the change is marked as pending and retried once the connection is restored.

🛠 Configuration

Expo & Firebase Setup

  1. app.json: Configure the expo-notifications plugin:
    {
      "expo": {
        "plugins": [
          [
            "expo-notifications",
            {
              "icon": "./assets/images/notification-icon.png",
              "color": "#ffffff",
              "defaultChannel": "default"
            }
          ]
        ]
      }
    }
  2. Credentials:
    • Place google-services.json in the root (for Android).
    • Place GoogleService-Info.plist in the root (for iOS).
  3. EAS Build: Ensure push notifications are enabled in your Expo dashboard and proper credentials (FCM Server Key) are uploaded.

🚀 Usage

Using the Push Notification Hook

To manually request permissions or get the push token:

import { usePushNotifications } from '@/hooks/use-push-notifications';

const MyComponent = () => {
  const { registerForPushNotificationsAsync, expoPushToken } = usePushNotifications();

  const handleEnable = async () => {
    const token = await registerForPushNotificationsAsync();
    console.log('Registered with token:', token);
  };
};

Accessing Notification Settings

Use the preferences store to read or update settings:

import { usePreferencesStore } from '@/store/preferences-store';

const { pushEnabled, setPushEnabled, setPromotions } = usePreferencesStore();

// Update a setting
await setPromotions(true); // This automatically syncs to backend

Fetching & Managing History

Interact with the in-app inbox using TanStack Query hooks:

import { useNotifications } from '@/queries/use-notifications';
import { useMarkNotificationReadMutation } from '@/mutations/use-notification-mutations';

const NotificationsList = () => {
  const { data: notifications } = useNotifications();
  const markRead = useMarkNotificationReadMutation();

  const handleSelect = (id: string) => {
    markRead.mutate(id);
  };
  
  // Render your list...
};

🔗 Deep Linking

When a notification is tapped, the responseListener in use-push-notifications.ts is triggered. You can extend this logic to navigate to specific routes based on the data payload:

// Inside use-push-notifications.ts
Notifications.addNotificationResponseReceivedListener(response => {
  const data = response.notification.request.content.data;
  if (data.url) {
    router.push(data.url);
  }
});