AI & Development

AI-Powered Push Notifications: Personalization at Scale (2026)

Use AI to generate personalized push notification copy, predict optimal send times and lift engagement, with practical Firebase and Cloud Functions examples.

The average mobile app sends the same push notification to every user and wonders why open rates are under 5%. The apps that achieve 25%+ open rates are doing something different: they are sending messages that feel personally relevant - the right content, at the right moment, written in a way that reflects what the specific user cares about. AI makes this achievable for apps that do not have a team of dedicated growth engineers. A few Cloud Functions and an LLM call per notification can move the needle significantly, and this guide shows the architecture that makes it work.

What personalisation actually means in push notifications

Personalisation is not just inserting the user's name in the notification. The dimensions that actually affect open rates are:

  • Content relevance - Is the notification about something the user has demonstrated interest in? A user who has only used feature A should not receive a notification about feature B.
  • Timing - Is it sent when the user is likely to be receptive? A notification at 2am local time gets ignored or disabled notifications globally. Send time optimisation is the highest-leverage personalisation you can apply.
  • Copy tone - Does the language match how the user engages with the app? A user who uses formal language in support tickets may disengage from a notification that sounds like a teenage influencer.
  • Value clarity - Does the notification immediately communicate what the user gains by tapping? "Check this out!" is not a value proposition. "Your weekly progress: 3 levels above last week" is.

The architecture: event-driven AI copy generation

The cleanest architecture for AI-personalised notifications is event-driven: something happens in your app or backend that triggers a notification-worthy event, a Cloud Function receives the event, calls an LLM to generate personalised copy based on the user's profile, and sends the notification via FCM.

// Cloud Function triggered by a Firestore event


export const onLevelCompleted = onDocumentCreated(
  'users/{userId}/completions/{levelId}',
  async (event) => {
    const userId = event.params.userId;
    const completionData = event.data?.data();

    // Fetch user profile for personalisation context
    const userProfile = await getUserProfile(userId);

    // Generate personalised notification copy
    const { title, body } = await generateNotificationCopy(userProfile, completionData);

    // Send via FCM
    if (userProfile.fcmToken) {
      await getMessaging().send({
        token: userProfile.fcmToken,
        notification: { title, body },
        data: { type: 'level_complete', levelId: event.params.levelId },
        apns: { payload: { aps: { badge: 1 } } },
      });
    }
  }
);

Generating notification copy with an LLM

The LLM call for notification copy needs to be fast (Haiku, not Sonnet) and constrained (short, specific output). The key is giving the model enough user context to personalise without overwhelming it:

async function generateNotificationCopy(
  userProfile: UserProfile,
  event: CompletionEvent
): Promise<{ title: string; body: string }> {
  const response = await anthropic.messages.create({
    model: 'claude-haiku-4-5-20251001',
    max_tokens: 150,
    system: `You write push notification copy for a mobile app.
Return ONLY a JSON object: {"title": "...", "body": "..."}.
Title: max 50 chars. Body: max 100 chars.
Tone: ${userProfile.preferredTone ?? 'friendly and encouraging'}.
Never use exclamation points more than once. No emojis unless the user uses them.`,
    messages: [{
      role: 'user',
      content: `User profile:
- Name: ${userProfile.displayName}
- Streak: ${userProfile.currentStreak} days
- Level completed: ${event.levelName} (difficulty: ${event.difficulty})
- Time taken: ${event.completionTimeMinutes} minutes
- Personal best: ${event.isPersonalBest ? 'yes' : 'no'}

Write a push notification celebrating this completion.`,
    }],
  });

  const raw = response.content[0].text;
  return JSON.parse(raw);
}

The system prompt is doing most of the work: constraining length, setting tone, defining format, and prohibiting specific patterns (multiple exclamation points, emojis unless contextually appropriate). The user message provides the specific personalisation context.

Send time optimisation without ML infrastructure

True ML-based send time optimisation requires training data and model infrastructure. A simpler approach that captures 80% of the benefit: track each user's app opens by hour of day in Firestore, and send at their historically most active hour.

// Record app opens by hour
async function recordAppOpen(userId: string) {
  const hour = new Date().getHours(); // 0-23 local time
  const ref = db.doc(`users/${userId}/activity/hourly`);
  await ref.set({ [`hour_${hour}`]: FieldValue.increment(1) }, { merge: true });
}

// Find optimal send hour
async function getOptimalSendHour(userId: string): Promise<number> {
  const doc = await db.doc(`users/${userId}/activity/hourly`).get();
  const data = doc.data() ?? {};

  const hours = Array.from({ length: 24 }, (_, i) => ({
    hour: i,
    opens: data[`hour_${i}`] ?? 0,
  }));

  // Return the hour with most opens (in 9am-9pm window to avoid late-night sends)
  const validHours = hours.filter(h => h.hour >= 9 && h.hour <= 21);
  return validHours.sort((a, b) => b.opens - a.opens)[0].hour;
}

Use Firebase Cloud Scheduler to run a batch notification job that calculates each user's optimal send time and enqueues their notification accordingly, rather than sending all at once.

Frequency caps and fatigue prevention

AI-generated personalised notifications are still notifications - users will disable them if they receive too many, even if each one is well-written. Frequency capping is mandatory:

async function canSendNotification(userId: string): Promise<boolean> {
  const ref = db.doc(`users/${userId}/notificationLimits/daily`);
  const today = new Date().toISOString().slice(0, 10);

  const doc = await ref.get();
  const data = doc.data();

  if (data?.date === today && data?.count >= 2) {
    return false; // Max 2 notifications per day
  }

  await ref.set({
    date: today,
    count: (data?.date === today ? data.count : 0) + 1,
  });

  return true;
}

A maximum of 2 notifications per user per day is a conservative starting point. Adjust based on your engagement data - but err on the side of fewer, more targeted notifications. An app that sends 1 excellent notification per week retains users better than one that sends 3 mediocre ones per day.

Measuring what works

Track notification performance at the individual copy level, not just the campaign level. Store the generated title, body, user segment, and send time with each notification record. After 48 hours, update the record with whether it was opened. This creates a dataset you can analyse to understand which copy patterns, tones, and timing strategies are working for which user segments - and feed that back into your prompt engineering.

The shift from broadcast notifications to AI-personalised ones is not a minor optimisation - it changes the relationship between your app and its users. A notification that acknowledges the specific thing the user did today feels like a message from a product that is paying attention. That feeling, more than any feature, is what drives long-term retention.