| name | push-notifications |
| description | Add push notifications to my app — set up FCM, APNs, or OneSignal for Flutter, React Native, Expo, or web with permission flows, foreground and background handlers, notification channels, topic subscriptions, deep link routing, and device token management |
| version | 2.0.0 |
| category | integration |
| platforms | ["CLAUDE_CODE"] |
You are in AUTONOMOUS MODE. Do NOT ask questions. Execute the full pipeline below
without pausing for user input. Make reasonable decisions using sensible defaults.
PURPOSE:
Set up a complete push notification system for mobile and/or web platforms. This includes
provider SDK installation, credential configuration, a notification service layer,
foreground and background notification handling, topic/channel management, deep link
routing, and verification that notifications deliver correctly.
INPUT:
$ARGUMENTS
The user may specify:
- Provider: "fcm", "apns", "onesignal" (default: auto-detect based on platform)
- Platform: "flutter", "react-native", "web", "ios", "android" (default: auto-detect)
- Features: "topics", "deep-links", "rich" (rich media notifications)
If no arguments, auto-detect the platform and use FCM as the default provider.
=== PHASE 1: PLATFORM DETECTION ===
Step 1.1 — Detect Client Platform
Scan for project files to determine the platform:
| File / Pattern | Platform | Default Provider |
|---|
| pubspec.yaml | Flutter (iOS + Android + Web) | FCM via firebase_messaging |
| package.json with "react-native" | React Native (iOS + Android) | FCM via @react-native-firebase/messaging |
| package.json with "expo" | Expo (React Native) | Expo Notifications (wraps FCM/APNs) |
| package.json with "next" or "react" | Web | FCM via firebase/messaging or Web Push API |
| Podfile / *.xcodeproj | Native iOS | APNs directly or FCM |
| build.gradle with android | Native Android | FCM |
Record: PLATFORM, CLIENT_ROOT, PROVIDER
Step 1.2 — Detect Backend
Scan for a backend server (notifications need a server component):
- package.json with server framework → Node.js backend
- requirements.txt / pyproject.toml with server framework → Python backend
- If no backend detected, note that server-side sending will need to be set up separately
Record: BACKEND_FRAMEWORK, BACKEND_ROOT
Step 1.3 — Check Existing Push Setup
Search for existing push notification code:
- Packages: firebase_messaging, @react-native-firebase/messaging, onesignal, web-push,
expo-notifications, firebase-admin
- Service worker files: firebase-messaging-sw.js
- Env vars: FCM_SERVER_KEY, ONESIGNAL_APP_ID, VAPID_KEY
- Notification permission request code
- Existing notification handlers
If complete push notification system exists, report it and exit.
If partial, identify gaps and extend.
=== PHASE 2: CLIENT SDK INSTALLATION ===
Step 2.1 — Install Client SDK
Based on platform and provider:
Flutter + FCM:
flutter pub add firebase_messaging firebase_core
flutter pub add flutter_local_notifications # for foreground display
Ensure firebase_core is initialized in main.dart.
Run flutterfire configure if Firebase is not yet configured.
React Native + FCM:
npm install @react-native-firebase/app @react-native-firebase/messaging
cd ios && pod install
Expo + Expo Notifications:
npx expo install expo-notifications expo-device expo-constants
Web + FCM:
npm install firebase
Create public/firebase-messaging-sw.js service worker.
Native iOS + APNs:
- Enable Push Notifications capability in Xcode
- Enable Background Modes > Remote notifications
- No SDK install needed — uses UserNotifications framework
OneSignal (any platform):
- Flutter:
flutter pub add onesignal_flutter
- React Native:
npm install react-native-onesignal
- Web: Include OneSignal SDK script
Step 2.2 — Install Server SDK
On the backend (for sending notifications):
FCM (via firebase-admin):
- Node.js:
npm install firebase-admin
- Python:
pip install firebase-admin
APNs (direct):
- Node.js:
npm install apn or npm install @parse/node-apn
- Python:
pip install apns2
OneSignal:
- Node.js:
npm install @onesignal/node-onesignal
- Python:
pip install onesignal-sdk
=== PHASE 3: CREDENTIAL CONFIGURATION ===
Step 3.1 — Configure Provider Credentials
FCM:
APNs:
OneSignal:
Add all variables to .env.example with descriptions.
Step 3.2 — Platform-Specific Configuration
Flutter / Android:
Flutter / iOS:
- Verify GoogleService-Info.plist exists in ios/Runner/
- Verify push notification capability is enabled in Xcode signing
- Add APNs capability in the Runner.entitlements file
React Native / Android:
- Verify google-services.json in android/app/
- Verify build.gradle applies google-services plugin
React Native / iOS:
- Verify GoogleService-Info.plist in ios/ project
- Verify push notification capability in Xcode
Web:
- Create firebase-messaging-sw.js in public/ directory:
importScripts('https://www.gstatic.com/firebasejs/10.x/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.x/firebase-messaging-compat.js');
firebase.initializeApp({ });
const messaging = firebase.messaging();
messaging.onBackgroundMessage((payload) => {
self.registration.showNotification(payload.notification.title, {
body: payload.notification.body,
icon: '/icon-192x192.png',
});
});
=== PHASE 4: NOTIFICATION SERVICE (SERVER) ===
Step 4.1 — Create Notification Service
Create a server-side notification service:
class NotificationService {
async sendToDevice(params: {
token: string;
title: string;
body: string;
data?: Record<string, string>;
imageUrl?: string;
badge?: number;
sound?: string;
channelId?: string;
}): Promise<{ success: boolean; messageId?: string }>
async sendToTopic(params: {
topic: string;
title: string;
body: string;
data?: Record<string, string>;
}): Promise<{ success: boolean; messageId?: string }>
async sendToMultiple(params: {
tokens: string[];
title: string;
body: string;
data?: Record<string, string>;
}): Promise<{ successCount: number; failureCount: number; failedTokens: string[] }>
async subscribeToTopic(params: {
tokens: string[];
topic: string;
}): Promise<{ success: boolean }>
async unsubscribeFromTopic(params: {
tokens: string[];
topic: string;
}): Promise<{ success: boolean }>
}
The service MUST:
- Initialize the provider SDK with credentials from environment
- Wrap all calls in try/catch with meaningful error logging
- Handle expired/invalid tokens by returning them for cleanup
- Support both notification messages (displayed by OS) and data messages (handled by app)
- Support platform-specific payload customization (android vs apns vs web sections)
- Implement retry logic for transient failures (provider rate limits, network errors)
Step 4.2 — Create Device Token Management
Create an API endpoint and storage for device tokens:
Route: POST /api/notifications/register-device
- Accept: { token, platform, userId }
- Store the device token linked to the user
- Handle token updates (user re-installs app, token changes)
- Deduplicate tokens per user
Route: DELETE /api/notifications/unregister-device
- Accept: { token }
- Remove the device token
Create a device token model/table:
DeviceToken {
id: string
userId: string
token: string (unique)
platform: "ios" | "android" | "web"
createdAt: DateTime
lastUsedAt: DateTime
}
Step 4.3 — Create Notification API Endpoints
Route: POST /api/notifications/send
- Accept: { userId, title, body, data?, type? }
- Look up user's device tokens
- Send to all user devices
- Require admin or system-level auth
Route: POST /api/notifications/send-topic
- Accept: { topic, title, body, data? }
- Send to all subscribers of a topic
- Require admin auth
=== PHASE 5: CLIENT-SIDE HANDLING ===
Step 5.1 — Permission Request
Create a permission request flow:
Flutter:
final messaging = FirebaseMessaging.instance;
final settings = await messaging.requestPermission(
alert: true, badge: true, sound: true,
provisional: false, // set true for quiet notifications on iOS
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
final token = await messaging.getToken();
// Send token to backend
}
React Native:
const authStatus = await messaging().requestPermission();
if (authStatus === messaging.AuthorizationStatus.AUTHORIZED) {
const token = await messaging().getToken();
}
Web:
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const token = await getToken(messaging, { vapidKey: VAPID_KEY });
}
The permission flow MUST:
- Request at an appropriate time (not on app launch — after user action or onboarding)
- Handle denied permissions gracefully (show settings prompt, allow skip)
- Send the device token to the backend registration endpoint
- Listen for token refresh events and re-register
Step 5.2 — Foreground Notification Handling
When the app is in the foreground, notifications are NOT displayed by default on most platforms.
Create a foreground handler:
Flutter:
- Use flutter_local_notifications to display a local notification
- Create a notification channel for Android (id, name, description, importance)
- Show the notification with the payload data
- Handle tap events on the foreground notification
React Native:
- Use @react-native-firebase/messaging onMessage listener
- Display using a custom in-app notification component or local notification library
- Handle tap to navigate
Web:
- Use the onMessage callback from firebase/messaging
- Display using the Notification API or a toast component
- Handle click events
Step 5.3 — Background Notification Handling
Configure background message handling:
Flutter:
- Register a top-level background handler: FirebaseMessaging.onBackgroundMessage(handler)
- The handler MUST be a top-level function (not a method, not a closure)
- Handle data-only messages that need processing
- Do NOT perform heavy work — background handlers have limited execution time
React Native:
- Register setBackgroundMessageHandler
- Handle data processing in background
- Use headless JS task if needed for heavy processing
Web:
- Handle in firebase-messaging-sw.js (service worker)
- Use self.registration.showNotification for custom display
Step 5.4 — Notification Tap / Open Handling
Handle what happens when a user taps a notification:
Flutter:
- FirebaseMessaging.instance.getInitialMessage() — for app opened from terminated state
- FirebaseMessaging.onMessageOpenedApp — for app opened from background
- Extract the data payload and navigate accordingly
React Native:
- messaging().getInitialNotification() — for terminated state
- messaging().onNotificationOpenedApp() — for background state
- Navigate based on data payload
Web:
- Handle notificationclick event in service worker
- Use clients.openWindow() to navigate to the correct URL
=== PHASE 6: TOPICS AND CHANNELS ===
Step 6.1 — Create Topic Management
Define standard topics for the application:
TOPICS = {
GENERAL: 'general', // All users
ANNOUNCEMENTS: 'announcements', // Product announcements
MARKETING: 'marketing', // Promotional content (opt-in)
}
Create a notification preferences screen/page:
- List available topics with toggle switches
- Save preferences locally and sync with backend
- Subscribe/unsubscribe from FCM topics based on toggles
Step 6.2 — Create Android Notification Channels
For Android (required for Android 8.0+):
Channels:
- default: General notifications (importance: high)
- messages: Chat messages (importance: high, vibration: true)
- updates: App updates (importance: default)
- marketing: Promotional (importance: low)
Create channels during app initialization, before any notification is received.
Step 6.3 — Deep Link Configuration
Set up deep linking so notification taps navigate to the correct screen:
Flutter:
React Native:
- Configure React Navigation deep linking
- Map notification data to navigation actions
Web:
- Map notification data.url to page routes
- Use window.location or router.push
The deep link handler MUST:
- Validate the route before navigating (prevent open redirect)
- Handle the case where the target screen requires authentication
- Queue the navigation if the app is still initializing
- Fall back to the home screen for unknown routes
=== PHASE 7: VERIFICATION ===
Step 7.1 — Static Verification
Run the project's linter and type checker:
- Flutter:
flutter analyze
- React Native:
tsc --noEmit and npx eslint .
- Web: framework-specific linting
- Backend: type check and lint
Fix all errors introduced by the push notification integration.
Step 7.2 — Integration Checklist
Verify and report:
=== OUTPUT ===
Print the following summary:
Push Notification Integration Complete
Platform: [detected platform]
Provider: [selected provider]
Backend: [detected backend framework]
Files Created/Modified
| File | Purpose |
|---|
| [path] | [description] |
Environment Variables Required
| Variable | Purpose | Where to get it |
|---|
| [KEY] | [purpose] | [provider console URL] |
Notification Endpoints
| Method | Path | Purpose |
|---|
| POST | /api/notifications/register-device | Register device token |
| DELETE | /api/notifications/unregister-device | Remove device token |
| POST | /api/notifications/send | Send to specific user |
| POST | /api/notifications/send-topic | Send to topic subscribers |
Topics Configured
| Topic | Description | Default |
|---|
| general | All users | Subscribed |
| announcements | Product updates | Subscribed |
| marketing | Promotional | Opt-in |
Testing
- FCM: Use Firebase Console > Cloud Messaging > Send test message
- FCM CLI:
firebase messaging:send --json '{"token":"...","notification":{"title":"Test"}}'
- Backend: Call POST /api/notifications/send with a valid user ID
- Verify foreground: Send while app is open — should show in-app notification
- Verify background: Send while app is backgrounded — should show system notification
- Verify tap: Tap the notification — should navigate to correct screen
=== NEXT STEPS ===
After push notification integration:
- "Run
/email to add email as a fallback notification channel."
- "Run
/analytics-tracking to track notification open rates and engagement."
- "Run
/auth-provider to link notifications to authenticated users."
- "Run
/integrate audit to check overall integration health."
=== DO NOT ===
- Do NOT request notification permission on first app launch — wait for user context.
- Do NOT store device tokens without linking them to a user — orphaned tokens waste quota.
- Do NOT send to stale tokens — clean up tokens that return NotRegistered/InvalidRegistration.
- Do NOT put sensitive data in notification payloads — they may be visible on lock screens.
- Do NOT use legacy FCM HTTP API — use the v1 API (projects.messages.send).
- Do NOT skip background handler registration — unhandled background messages cause crashes on some platforms.
- Do NOT perform heavy computation in background handlers — they have strict execution time limits.
- Do NOT hardcode FCM server keys or APNs certificates in source code — use environment variables.
- Do NOT send marketing notifications without opt-in — this violates platform policies and law.
- Do NOT create Android notification channels after sending notifications — channels must exist first.
- Do NOT ignore token refresh events — tokens change periodically and must be re-registered.
- Do NOT use alert/badge/sound in data-only messages — they are silently ignored on iOS.
============================================================
SELF-HEALING VALIDATION (max 3 iterations)
After completing the integration, validate:
- Run the project's test suite to verify the integration works end-to-end.
- Run build/compile to confirm no breakage.
- Verify the integration responds correctly (health checks, test calls, smoke tests).
- If failures occur, diagnose from error output and apply minimal fixes.
- Repeat up to 3 iterations.
IF STILL FAILING after 3 iterations:
- Document the integration state and what's blocking
- Include error output and attempted fixes
============================================================
SELF-EVOLUTION TELEMETRY
After producing output, record execution metadata for the /evolve pipeline.
Check if a project memory directory exists:
- Look for the project path in
~/.claude/projects/
- If found, append to
skill-telemetry.md in that memory directory
Entry format:
### /push-notifications — {{YYYY-MM-DD}}
- Outcome: {{SUCCESS | PARTIAL | FAILED}}
- Self-healed: {{yes — what was healed | no}}
- Iterations used: {{N}} / {{N max}}
- Bottleneck: {{phase that struggled or "none"}}
- Suggestion: {{one-line improvement idea for /evolve, or "none"}}
Only log if the memory directory exists. Skip silently if not found.
Keep entries concise — /evolve will parse these for skill improvement signals.