Preferences sit at the bottom of the stack for push notifications. Everything else depends on what the user has stated is their communication preference.

As such, notification preferences are a crucial component of a well-designed push notification system. They empower users to control their experience while helping you deliver the right message at the right time. These settings can be all or nothing, or set granularly depending on the product or service.

Implementing preferences requires navigating the complex interplay between your application's preference system and the device's native controls.

Setting up your preference architecture

Your preference schema needs to capture both user intent and device reality. The structure below separates concerns cleanly: global controls, category-specific settings, delivery channels, and device states. Notice how we store both whether a device is enabled in our system AND its actual permission status, as these can differ when users change system settings outside your app.

const userPreferenceSchema = {
  userId: String,
  globalEnabled: Boolean,
  categories: {
    transactional: {
      enabled: Boolean,
      channels: { push: Boolean, email: Boolean, sms: Boolean },
    },
    marketing: {
      enabled: Boolean,
      channels: { push: Boolean, email: Boolean, sms: Boolean },
      frequency: { maxPerDay: Number, maxPerWeek: Number },
    },
  },
  quietHours: {
    enabled: Boolean,
    startTime: String, // "22:00"
    endTime: String, // "08:00"
    timezone: String,
    exceptions: [String], // ["transactional"]
  },
  devices: [
    {
      deviceId: String,
      enabled: Boolean,
      permissionStatus: String, // "granted", "denied", "provisional"
    },
  ],
};

This schema supports common use cases like "send me emails but not push notifications for marketing" or "notify me about everything except during my sleep hours." The frequency caps on marketing notifications prevent notification fatigue, a leading cause of users disabling notifications entirely, so you’ll want to heed the users requests like this.

How to check user preferences

When deciding whether to send a notification, you’ll also need a clear, ordered checklist that respects both user preferences and system constraints. This algorithm runs for every notification, so optimize for the common case (early returns) while maintaining correctness. The order matters—checking expensive operations like database counts should come after cheap boolean checks.

async function shouldSendNotification(userId, deviceId, category, channel) {
  const prefs = await getPreferences(userId);
 
  // 1. Global toggle
  if (!prefs.globalEnabled) return false;
 
  // 2. Category enabled
  if (!prefs.categories[category]?.enabled) return false;
 
  // 3. Channel enabled
  if (!prefs.categories[category]?.channels[channel]) return false;
 
  // 4. Device-specific checks for push
  if (channel === "push") {
    const device = prefs.devices.find((d) => d.deviceId === deviceId);
    if (!device?.enabled || device.permissionStatus !== "granted") {
      return false;
    }
  }
 
  // 5. Quiet hours
  if (
    prefs.quietHours?.enabled &&
    !prefs.quietHours.exceptions.includes(category)
  ) {
    if (isCurrentlyQuietHours(prefs.quietHours)) return false;
  }
 
  // 6. Frequency caps
  const frequency = prefs.categories[category]?.frequency;
  if (frequency?.maxPerDay) {
    const sent = await getTodayCount(userId, category);
    if (sent >= frequency.maxPerDay) return false;
  }
 
  return true;
}

Each check in this function maps to a promise you're making to users: respect their master switch, honor category preferences, use only approved channels, verify device permissions, observe quiet hours, and limit notification volume. Cache the preference lookup per user session to avoid database hits, but ensure the cache invalidates when users update their settings. For quiet hours, always calculate in the user's timezone. Sending a "good morning" notification at 10 PM destroys trust immediately.

Platform-specific considerations

Different platforms have different user workflows and expectations, so you’ll want to consider differences when it comes to notification preferences across key platforms:

How to sync device permissions

The trickiest part is keeping device permissions in sync with your backend. Users can change notification permissions in their device settings at any time, and your app needs to detect and respond to these changes. This sync function should run on every app launch and ideally when the app returns to the foreground.

// Check device permission on app launch
async function syncDevicePermission() {
  const { status } = await getNotificationPermissions();
 
  await api.updateDevice({
    deviceId: getDeviceId(),
    permissionStatus: status,
    enabled: status === "granted",
  });
 
  // If permission was revoked, disable push for this device
  if (status !== "granted") {
    await api.updatePreferences({
      [`devices.${deviceId}.enabled`]: false,
    });
  }
}

When push notification permissions are revoked, always update your backend immediately so you don't attempt to send push notifications that will fail, wasting API calls and potentially hitting rate limits.

From there, your best bet to drive user engagement is to fall back to other notification channels like email or SMS to get users back in your app.

Key implementation tips

  1. Make preferences real-time: Changes should take effect immediately, not require an app restart.
  2. Handle permission changes: Users can change device settings anytime, so detect and sync these changes.
  3. Default to respectful: Start with conservative defaults and let users opt into more notifications.
  4. Test edge cases: Time zones for quiet hours, permission revocation mid-session, and frequency cap resets.
  5. Provide clear UI feedback: Show when device permissions are blocking notifications, even if app preferences allow them.

The goal is to give users control while ensuring your system respects both their choices and platform limitations. A well-built preference system reduces unsubscribes and builds trust with your users.