BlueskyNSE#
BlueskyNSE is an iOS Notification Service Extension that processes push notifications before they are displayed to the user. NSE stands for "Notification Service Extension", a native iOS app extension type.
What It Does#
This extension intercepts incoming push notifications and performs processing before displaying them:
- Manages badge counts for app icon
- Applies custom notification sounds based on user preferences
- Enables notification customization without requiring the main app to be running
How It Works#
When a push notification arrives on iOS, the system can invoke this extension to modify the notification content before displaying it. The extension runs in a separate process from the main app and has strict time limits (approximately 30 seconds) to complete its work.
Architecture#
The extension uses shared UserDefaults (via App Groups) to access preferences set by the main app:
- App Group:
group.app.bskyallows data sharing between the main app and the extension - Shared Preferences: Stored in UserDefaults suite accessible by both processes
- Thread Safety: Uses a dedicated serial DispatchQueue (
NSEPrefsQueue) to prevent race conditions when multiple notifications arrive simultaneously
Notification Processing Flow#
- System receives push notification
NotificationService.didReceive()is called- Extension creates mutable copy of notification content
- Based on notification type (determined by
reasonfield):- Chat messages (
reason == "chat-message"): Applies custom DM sound if user preferenceplaySoundChatis enabled - Other notifications: Increments and applies badge count
- Chat messages (
- Extension delivers modified notification to system via
contentHandler
Badge Count Management#
Badge counts are managed centrally by the extension:
- Each non-chat notification increments the badge count
- Count is synchronized across notification instances using the serial queue
- Main app can reset the count via the
expo-background-notification-handlermodule
Notification Sounds#
Two sound types are supported:
- Default system sound: Standard iOS notification sound
- DM sound: Custom
dm.aiffsound file for chat messages
DM sound only plays if the user has enabled the playSoundChat preference in the main app's chat settings.
Key Files#
| File | Purpose |
|---|---|
NotificationService.swift |
Main service extension implementation |
BlueskyNSE.entitlements |
iOS entitlements configuration for App Group access |
Info.plist |
Extension metadata and configuration |
NotificationService.swift#
Contains two main classes:
NotificationService: The main extension class that implements UNNotificationServiceExtension
didReceive(_:withContentHandler:): Processes incoming notificationsserviceExtensionTimeWillExpire(): Handles timeout scenarios- Mutation methods for modifying notification content
NSEUtil: Singleton utility class for shared state management
- Provides shared
UserDefaultsinstance for the App Group - Manages serial queue for thread-safe preference access
- Helper methods for notification content manipulation
Configuration#
App Group Setup#
The extension requires the group.app.bsky App Group to be configured in:
- Main app target capabilities
- Extension target capabilities (defined in
BlueskyNSE.entitlements)
Shared Preferences#
The following preferences are shared between the main app and extension:
| Preference Key | Type | Purpose |
|---|---|---|
badgeCount |
Int | Current badge count for app icon |
playSoundChat |
Bool | Whether to play sound for chat notifications |
These are managed by the expo-background-notification-handler module in the main app.
Sound Files#
The custom DM sound file (dm.aiff) must be included in the extension's bundle. The iOS project configuration handles copying this resource during the build.
Platform Support#
- iOS: Fully supported (primary platform for this extension)
- Android: Not applicable (Android uses different notification handling mechanisms)
- Web: Not applicable (web notifications are handled by browser APIs)
Integration with Main App#
The extension coordinates with the main app through:
- expo-background-notification-handler module: Provides JavaScript API for managing shared preferences
- App Group shared storage: Enables data synchronization between processes
- Push notification payload: Must include
reasonfield to determine notification type
Setting User Preferences#
Users can control notification sounds via the Chat Settings screen (src/screens/Messages/Settings.tsx):
import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
const {preferences, setPref} = useBackgroundNotificationPreferences()
setPref('playSoundChat', true) // Enable DM sounds
Limitations#
- Time constraints: Extension must complete processing within ~30 seconds or the system will terminate it
- Process isolation: Runs in separate process with limited memory and resources
- iOS only: Notification Service Extensions are an iOS-specific feature
- Concurrent processing: Multiple notifications may arrive simultaneously, requiring careful state management
Best Practices#
When modifying this extension:
- Keep processing fast and synchronous when possible
- Use the shared serial queue for any UserDefaults mutations
- Avoid network requests that could cause timeouts
- Always call
contentHandlerwith modified content, even on errors - Test with multiple concurrent notifications to verify thread safety