Directly calling a push provider from every feature starts simple. Then each feature implements preferences, quiet hours, retries, templates, invalid tokens, analytics, and duplicate prevention differently.
I prefer one notification capability. Features request an intent; the notification system decides how and when to deliver it.
type NotificationIntent = {
id: string;
userId: string;
kind: "lesson_reminder" | "milestone_earned";
data: Record<string, string>;
requestedAt: string;
};
The intent describes meaning. It does not contain Firebase-specific fields or a fully rendered device message.
Separate policy from delivery#
The pipeline can perform distinct steps:
- load user preferences and timezone;
- decide whether the intent is eligible;
- select channels and schedule time;
- render a versioned template;
- deliver through a provider adapter;
- record the outcome.
Separating these steps makes behavior explainable. “Skipped because reminders are disabled” is different from “push provider timed out.”
interface PushGateway {
send(message: PushMessage): Promise<DeliveryResult>;
}
Firebase, APNs, or another provider stays behind the gateway. Provider error codes are translated into outcomes such as retryable failure, invalid token, or permanent rejection.
Give every delivery an identity#
Use a stable key such as {intentId}:{channel} and enforce uniqueness. A queue retry should not send the same milestone notification twice.
Scheduled notifications also need cancellation and replacement. If a user's reminder time changes, update the pending schedule instead of adding another message.
Treat tokens as changing infrastructure#
Device tokens expire and rotate. Store multiple devices per user, update last-seen information, and disable a token when the provider identifies it as invalid. Token cleanup is part of delivery correctness.
Track operational outcomes such as queued, delivered-to-provider, retrying, skipped, and failed. “Accepted by provider” is not the same as “read by user,” so name metrics honestly.
A notification platform costs more than one provider call, but it prevents every product feature from becoming a partial notification system. Centralized policy produces consistent user behavior and far better operational control.