Software & practice

Use push notifications selectively and clearly

Plan ordinary task notices around clear value, suitable timing and a meaningful next action.

Symbolic everyday scene in AZ colours: a man in a saturated red overshirt looks at a normally held phone in a café. A neutral German New task notice directs him to details in the app. An ivory cup sits on charcoal stone.

A push notification interrupts another activity

A push notification is useful when its occasion matters to its recipient and the next action is understandable. It often reaches someone outside the currently open application. Being technically able to transmit every new event is therefore insufficient. Design must explain why this particular notice deserves an interruption and which meaningful action actually follows from seeing or opening it.

Our original fictional case concerns ordinary tasks and reviews in a work application. It is not emergency alerting or guaranteed reachability. We develop an executed decision function for topic selection, permission, validity, spacing and quiet hours. The code prepares a possible submission; it sends no notification and provides no evidence of a message appearing on an actual lock screen.

These distinctions matter in practice. A relevant event may still be skipped because permission is missing. A prepared notice may later become stale. Acceptance by a sending service is not proof that a person read it. Dependable products name these states separately and do not treat an intermediate technical success as completion of the underlying work or the recipient’s next action.

Explain value before requesting system permission

A useful invitation appears at a relevant moment in the task. Someone following a particular item can understand why changes would matter. A generic request on first opening instead demands a decision without context. Before the system prompt, explain the occasion and expected scope so people can judge whether this kind of interruption fits how they intend to use the product.

Product topic preferences and operating-system permission are different layers. An enabled category cannot override denied system permission. Conversely, granted permission does not mean every imaginable notice is useful or wanted. Our example requires both and models the web-style permission states default, denied and granted. Only the last can allow any submission preparation, subject to the remaining application rules.

When permission is missing, the decision function does not repeatedly request it. It returns a reason. The application should remain usable and display its actual tasks inside its own interface. A denied notification is no reason to hide important content or create repeated request loops. A system permission prompt belongs in a relevant interaction explicitly initiated by the person using the application.

web.dev: Permission UX · MDN: Notifications API

Separate topics and bound ordinary notices

This fictional product has task and review topics. A person can choose them separately or disable both. The small set describes real work occasions rather than exposing a long list of internal technical event names. A single switch mixing useful task notices with arbitrary marketing and minor repeated updates would likewise fail to communicate what someone is actually choosing.

On the web, permission, an active subscription and product topic preferences coexist. Android adds its platform-specific settings and notification channels. These mechanisms do not have identical meanings or interfaces everywhere. Our example uses an explicitly simplified shared business decision state. It does not claim to implement and test the entire Android or browser integration on every possible device merely by checking those input values.

An arbitrary urgency claim must not secretly bypass the agreed preference. This code does not treat an extra urgent property as special authority. Quiet hours remain respected too. A product genuinely requiring separate critical alerts needs its purpose, platform rules and appropriate agreement developed independently. Ordinary task reminders do not become alarm infrastructure simply because a caller labels an event urgent.

Android: Notification Permission · Notification Channels

Model the event, timing and relevance lifetime explicitly

An event has a fixed identifier, topic, creation time and expiry time. Times are supplied as concrete UTC instants. An event supposedly created in the future is not processed, nor is one already expired. This prevents an old task change from becoming a current interruption simply because connectivity or another processing opportunity became available much later than the original occasion.

Relevance lifetime depends on the occasion. A short coordination request may become pointless within minutes, while a general review can remain relevant longer. This business expiry differs from the retention period technically allowed by a sending service. An actual integration should coordinate them. Opening a notice must also handle a task that has already been completed or removed since the notification was prepared.

The function checks time types and creation-before-expiry ordering. Its caller supplies the current instant, making boundaries reproducible without waiting. Fixed timestamps in the checks are deliberately selected test inputs, not observations of real delivery delay. Actual transmission would need additional evidence involving the chosen service and real devices. A correct planning result alone cannot establish what appeared on someone’s phone.

Evaluate quiet hours in the intended local time

The example deliberately uses the fixed Europe/Berlin time zone and whole-hour quiet boundaries. Its ordinary setting runs from 22:00 to 07:00. Across midnight, an hour is quiet when it lies after the start or before the end. A same-day window uses the corresponding bounded interval. Identical start and end hours are rejected as ambiguous rather than guessed to mean either all day or no restriction.

The implementation does not assume one permanent offset from UTC. Intl.DateTimeFormat obtains the Berlin hour for a specific instant. The executed logic therefore follows the time-zone transition. Searching for the next allowed minute advances through real UTC instants, so the local end of quiet hours can be a different duration away on the two clock-change nights.

The search is bounded to 36 hours. Whole-hour windows keep this chosen contract manageable. It is not a complete library for arbitrary worldwide calendars, holidays or travelling devices. Multiple zones or minute-specific personal windows require an extended design. The tests establish the implemented Berlin rule, including both 2026 clock changes, without implying universal support for every scheduling requirement.

MDN: Intl.DateTimeFormat

Combine spacing and quiet hours in the right order

The example requires at least fifteen minutes between ordinary notices previously accepted by the sending service. That is a chosen product rule, not a universal recommendation for every application. The latest known acceptance time is supplied as external state and is not stored by the function. If there has been no such acceptance, it introduces no additional waiting interval.

First calculate the earliest possible instant from now and the spacing rule. Then evaluate quiet hours for that candidate. This catches a meaningful boundary case: a notice arriving shortly before quiet hours could be pushed inside them by the spacing requirement. Checking only the original instant would overlook the combination. An executed test explicitly exercises that route and its next morning result.

If the next allowed window begins at or after event expiry, the notice is skipped. Deferral must not turn a stale occasion into a fresh one. Later reevaluation needs to check permission, preference and expiry again. A returned future instant is not permanent sending authority that may ignore subsequent changes to the person’s settings or the event’s actual relevance.

Read the complete decision module

The following JavaScript implements the executed rule. Its possible results are Skip, Defer and Ready to submit. The last includes a neutral title, general body and fixed route to the notification inbox. It does not copy confidential free text from the event. After opening, the actual application can load details under the user’s current authorisation rather than putting them on a potentially visible notification surface.

Context also contains identifiers with known prior acceptance. Such an identifier suppresses another preparation in this decision. That checks supplied state; it is not an atomic queue. Two simultaneous calls with the same old context could both prepare the same notice. A production writing integration must solve that separate concurrency problem explicitly. Naming this boundary prevents a small planning function from appearing to guarantee exactly-once delivery.

const TOPICS = new Set(['tasks','reviews']);
const MINUTE = 60_000;
const COOLDOWN = 15 * MINUTE;
const clock = new Intl.DateTimeFormat('de-DE', {
  timeZone:'Europe/Berlin', hour:'2-digit', hourCycle:'h23'
});
const validTime = value => Number.isSafeInteger(value) && value >= 0 && value <= 8_000_000_000_000_000;
function isQuiet(at, quiet) {
  if (!quiet.enabled) return false;
  const hour = Number(clock.formatToParts(new Date(at)).find(p=>p.type==='hour').value);
  return quiet.start < quiet.end ? hour >= quiet.start && hour < quiet.end
    : hour >= quiet.start || hour < quiet.end;
}
function nextAllowed(at, quiet) {
  if (!isQuiet(at,quiet)) return at;
  let candidate = Math.floor(at/MINUTE)*MINUTE + MINUTE;
  // UTC instants advance, while the local hour follows Berlin's time-zone rules.
  for (let i=0;i<36*60;i++,candidate+=MINUTE) {
    if (!isQuiet(candidate,quiet)) return candidate;
  }
  throw new Error('Kein zulässiges Zeitfenster im begrenzten Suchraum.');
}
export function decidePush(event, prefs, context) {
  const invalid = {status:'skip',reason:'invalid_input'};
  if (!event || !prefs || !context || !prefs.quiet) return invalid;
  const q=prefs.quiet;
  if (!/^DEMO-[0-9]{3}$/.test(event.id) || !TOPICS.has(event.topic)
    || !validTime(event.createdAt) || !validTime(event.expiresAt)
    || event.expiresAt <= event.createdAt || !validTime(context.now)
    || !Array.isArray(prefs.enabledTopics) || prefs.enabledTopics.some(t=>!TOPICS.has(t))
    || typeof q.enabled !== 'boolean' || !Number.isInteger(q.start) || !Number.isInteger(q.end)
    || q.start < 0 || q.start > 23 || q.end < 0 || q.end > 23 || q.start === q.end
    || !['default','denied','granted'].includes(context.permission)
    || typeof context.hasSubscription !== 'boolean' || !(context.acceptedEventIds instanceof Set)
    || (context.lastAcceptedAt !== null && (!validTime(context.lastAcceptedAt)
      || context.lastAcceptedAt > context.now))) return invalid;
  if (event.createdAt > context.now) return {status:'skip',reason:'not_yet_created'};
  if (event.expiresAt <= context.now) return {status:'skip',reason:'expired'};
  if (!prefs.enabledTopics.includes(event.topic)) return {status:'skip',reason:'topic_disabled'};
  if (context.permission !== 'granted') return {status:'skip',reason:'permission_missing'};
  if (!context.hasSubscription) return {status:'skip',reason:'subscription_missing'};
  if (context.acceptedEventIds.has(event.id)) return {status:'skip',reason:'already_accepted'};
  const earliest = context.lastAcceptedAt === null ? context.now
    : Math.max(context.now,context.lastAcceptedAt + COOLDOWN);
  const at = nextAllowed(earliest,q);
  if (at >= event.expiresAt) return {status:'skip',reason:'expires_before_window'};
  if (at > context.now) return {status:'defer',until:at};
  // Preparation only: no subscription endpoint, provider call or delivery claim.
  return {status:'ready_to_submit',eventId:event.id,path:'/mitteilungen',
    title:event.topic==='tasks'?'Neue Aufgabe':'Prüfung erforderlich',
    body:'Weitere Angaben finden Sie in der App.'};
}

Write useful text without unnecessary detail

The example titles mean New task and Review required. The body directs someone to further details in the app. It communicates the general occasion without exposing customer names, contract terms or internal details on a surface others might see. Neutral wording is not itself a complete privacy mechanism, because actual device settings also influence what appears and where it can be read.

Within available space, a useful notice explains what happened and which next step makes sense. It need not repeat every detail. Opening should reveal the current authorised state. A removed or completed item needs a comprehensible response. An old notification should not send someone into an unrelated record or an unexplained failure screen simply because its original destination no longer applies.

A fixed inbox route keeps the small example straightforward. Production software may use a precise item destination, but must check authorisation again when opening it. An identifier in the message does not replace that check. Even an apparently internal link grants no additional rights and should not expose confidential information merely because someone possesses it or opens a historical notification.

Keep preparation, acceptance and reading separate

The function finishes before actual submission. It calls no browser service, Android service or push provider. Its ready_to_submit result therefore means only that the displayed product rules currently permit preparation. A later integration can attempt transmission and must store or interpret its response according to the meaning that response actually has for the chosen platform and service.

Service acceptance may matter for retry rules but is not a read receipt. A device can be offline later or alter presentation. A person can ignore the notice. Sending or opening therefore must not automatically complete the business task. The application owns the actual work state, and its normal interface should make that state available independently of whether someone noticed an interruption.

A lost response after service acceptance creates further retry questions. This planning module does not solve them. A real queue needs operation identifiers, atomic reservation and traceable transitions around submission. Document the confirmation actually provided by the service. An unconditional claim of guaranteed once-only delivery would exceed anything established by the pure function or its controlled local test set.

Actual winter, spring-transition and autumn-transition quiet-hour outcomes all end at seven in Berlin but at different UTC instants. Additional cards distinguish preparation, missing permission and expiry. Explanatory labels are German.
Actual returns from the displayed decision module. All three quiet windows end at seven Berlin local time. The 51 checks evaluate rules and boundaries, not sent or received push messages. Explanatory labels are German.

Examine 51 executed checks including both clock changes

The module ran through 51 conditions in Node 24.22.1. They cover missing permission, missing subscription, disabled topics, future and expired events, prior acceptances and invalid input types. They also check the exact fifteen-minute boundary, one remaining minute and spacing that crosses into quiet hours. None of these checks sends a real notification or measures a device’s delivery behaviour.

Berlin quiet hours are tested on an ordinary winter evening and the spring and autumn 2026 clock-change nights. Morning consistently begins at 07:00 Berlin time, while the corresponding UTC instant differs. Exact 22:00 start, exact 07:00 end and a daytime quiet interval are included too. The illustration draws on the actual returned results rather than an invented scheduling measurement.

A separate case shows that preparation does not record an identifier as accepted. Repeating with unchanged state can consequently prepare another proposal. This exposes the absent atomic submission protection rather than hiding it. Further cases establish that confidential free-form event text is not copied into the neutral message and that an arbitrary urgency marker cannot override preferences or quiet hours.

Review a real integration on actual devices

Complete integration needs the chosen platform. Web implementation includes a secure context, service worker, subscription and its real lifecycle. Android has requirements associated with the intended platform version, permissions and channel settings. These are not interchangeable switches. Implement them from current official documentation and review them on devices the product actually supports instead of assuming a shared planning object proves integration.

Acceptance follows real tasks: grant permission, deny it, change it later, disable a topic, temporarily lose connectivity and open an old notice. Check whether a task change has made the notice obsolete. Executing this decision function does not answer those integration questions. It provides the explicit product rules that the connection to the actual platform must preserve during all those transitions.

Operating systems can handle background activity and presentation differently. A useful integration record therefore identifies device conditions and observed outcomes. Do not claim delivery percentages or second-level guarantees without relevant measurements. Ordinary work notifications especially benefit from a dependable in-app overview, so the underlying task does not depend on someone happening to perceive a brief interruption at the right time.

Assess usefulness through tasks rather than clicks alone

A frequently opened notice is not automatically helpful. Its text might be so unclear that everyone has to inspect the app. Ask whether the interruption supported a relevant action and arrived at an appropriate time. Examine complaints, unwanted repetition and people disabling topics. Such observations help refine occasion, wording and frequency rather than treating any increase in notification interaction as success.

Appropriate restrained data can support that evaluation. Transmission attempts should not silently become a comprehensive behaviour profile. What measurement is necessary and permissible needs separate consideration for the real application. This example collects no usage data and offers no legal approval of a notification concept. It demonstrates only the traceable execution of the stated decision rules and their bounded consequences.

A small well-named topic set and clear quiet-hours contract can be more useful than many hard-to-explain exception levels. Explain the user problem each extra exception solves. If nobody can describe the difference between two categories, the additional controls may create administration without value. The meaningful unit remains the actual occasion and its effect on the person receiving the interruption.

Maintain event rules and message templates together

Notifications are part of product maintenance. A task-state change can obsolete an earlier trigger. New topics require coordinated preferences, defaults and explanation. Revised templates must not start copying confidential details from newly available fields. This example’s neutral template exposes that boundary clearly and makes it easy to verify. A template change deserves review of its real output rather than only a source-code inspection.

Document what every state means. Skipped is not a sending-service failure. Deferred is not an accepted submission. Accepted is not a completed business task. These distinctions help support and maintenance investigate the right layer. Without them, a missing notice can be mistaken for a lost task, or an unprocessed task can incorrectly appear finished because a transmission request was technically successful.

Good push notifications complement an understandable application. They have a specific occasion, respect selection and timing and lead to a suitable next action. The complete decision module offers a bounded verifiable foundation. Separately reviewing its connection to platform, queue and the actual work process is what turns that foundation into a dependable notification feature for the people using the product.

Interpret states accurately

OutcomeMeaning in this example
SkipFor example expired, disabled or without permission
DeferFind next candidate instant and reevaluate later
Ready to submitProduct rules pass, no submission attempted yet
Accepted by serviceExternal spacing/retry state, not proof of reading
Task completedIndependent business state owned by the application

Checklist for understandable notices

  • Name a relevant occasion and next action.
  • Explain value before requesting system permission.
  • Separate topic selection from platform permission.
  • Respect denial and retain the in-app task route.
  • Define creation, expiry and quiet hours explicitly.
  • Apply spacing before evaluating the quiet window.
  • Recheck preferences and relevance before later submission.
  • Keep confidential details out of the neutral template.
  • Distinguish acceptance, presentation, reading and task completion.
  • Separately verify the queue and real platform on actual devices.

Sources and review scope

Primary sources reviewed on 9 November 2026. Original fictional decision module with 51 executed Node conditions. No push message sent, no real platform receipt measured and no emergency or delivery guarantee claimed.

Let’s move forward

What would you like to change?

An idea, a specific challenge or your next step with AI. We’re listening.

Let’s talk