| name | push-topics-segments |
| description | Design topic- and segment-based push targeting with unsubscribe management. Use when planning broadcast vs targeted notification delivery for a mobile app. |
Push Topics and Segments
Instructions
Direct-token sends do not scale to millions of users. Use topics for opt-in broadcast channels and segments for ad-hoc targeted sends. Keep user control explicit.
1. Topics
A topic is a channel a device subscribes to at the provider level. FCM supports native topics; APNs does not (emulate with your own fan-out, or use FCM for iOS).
Good topic design:
- Coarse-grained and stable (
news_en, weather_nyc, promo_black_friday).
- Avoid per-user topics -- that defeats the purpose; use direct tokens or segments instead.
- Hierarchical naming helps:
sports.nba, sports.nfl.
FCM topic subscribe (server-initiated, recommended over client-side for auditability):
import { getMessaging } from "firebase-admin/messaging";
await getMessaging().subscribeToTopic(deviceTokens, "sports.nba");
await getMessaging().unsubscribeFromTopic(deviceTokens, "sports.nba");
Server-initiated subscription lets you persist the (user_id, topic, subscribed_at) truth in your DB, regardless of provider state drift.
2. Segments
A segment is a query over your user database resolved at send time: "US users who opened the app in the last 7 days and opted into promos". Segments are computed by the backend, not the provider.
Pipeline:
- Marketer/ops defines a segment (SQL-backed DSL, or UI).
- Campaign service materializes the device-token list (streaming, chunked).
- Push sender enqueues chunks (e.g., 500 tokens per batch for FCM multicast).
func SendSegment(ctx context.Context, seg Segment, msg Message) error {
tokens, err := segments.Resolve(ctx, seg)
if err != nil { return err }
return StreamInChunks(tokens, 500, func(chunk []string) error {
return fcm.SendMulticast(ctx, chunk, msg)
})
}
3. User Preferences
Every user has a preferences row that is the source of truth:
{
"user_id": "u_01HX7...",
"channels": {
"marketing": { "push": true, "email": false },
"security": { "push": true, "email": true },
"social": { "push": false, "email": false }
},
"quiet_hours": { "start": "22:00", "end": "08:00", "tz": "America/New_York" },
"locale": "en-US",
"updated_at": "2026-04-10T09:00:00Z"
}
- Every push carries a category (
marketing, security, social, transactional). The send service must check the user's preference for that category before dispatching.
- Quiet hours delay non-urgent categories to the end window.
- Transactional pushes (order shipped, 2FA) ignore marketing toggles but respect hard unsubscribe.
4. Unsubscribe
- One-tap unsubscribe from push lands in a deep-linked settings screen with the category pre-selected.
- Unsubscribing updates the preferences row immediately and purges topic subscriptions for that category.
- Offer per-category and "all marketing" master switches.
- Keep an audit trail of unsubscribe events; marketers must not be able to resubscribe silently.
5. Frequency Capping
Hard cap per user per day per category (e.g., 3 marketing sends/day). Implement in the scheduler:
async def can_send(user_id: str, category: str) -> bool:
key = f"pushcap:{user_id}:{category}:{today()}"
n = await redis.incr(key)
if n == 1: await redis.expire(key, 60 * 60 * 26)
return n <= CATEGORY_LIMITS[category]
6. Localization and Rendering
- Pick the locale from user prefs (falling back to the device locale recorded at last login).
- Render title/body on the backend; never ship provider-side templating tokens to untrusted flows.
- A/B testing: include a
campaign_id and variant in data for client-side analytics.
7. Targeting Correctness
- Dry-run mode: resolve a segment and show counts + a 100-row preview before launching.
- Exclusions: always exclude users who unsubscribed from the category, even if the segment includes them.
- Suppression lists (bounced emails, churned users) applied at the final step.
8. Client Consumption
Android:
FirebaseMessaging.getInstance().subscribeToTopic("sports.nba")
.addOnCompleteListener { Log.d("push", "subscribe: ${it.isSuccessful}") }
But prefer server-side subscribe via an API call after user opt-in; the client merely reports the toggle:
api.updatePreferences(category = "sports.nba", enabled = true)
iOS: no native topics; your notification-service segments by user prefs and sends direct.
Checklist