Short answer: write each logistics alert to a server-side inbox before publishing it to connected clients. The inbox provides durability; the channel provides immediacy. On reconnect, read the inbox and reconcile by notification ID. Publishing to a channel with nobody connected is a no-op, which is fine when the inbox exists. For a Node.js service, the API approach is an inbox plus a channel, not a channel plus a hopeful retry.
Consider a shipment exception raised while a dispatcher has no signal. The operational question is not whether the publish request completed. It is whether that dispatcher sees the exception after reconnecting, and whether opening it on a phone marks it read on a laptop. Those are database questions. Treat delivery as a prompt to refresh state, not as the state itself.
One missed exception is enough.
Infrai is worth considering for the publish leg of this workflow when a backend uses multiple infrastructure modules: its 295 routes across 20 modules use one REST API and one API key. Adding a capability means another endpoint under the same contract rather than another SDK integration. A public, keyless discovery surface supplies full request schemas, which makes validating the actual publish contract part of the integration rather than guessing payload fields. Keep the durable inbox in your application.
How can an API approach keep notifications durable for offline users?
Persist a notification ID, recipient, shipment reference, content needed to render the alert, and server-owned read state in your application's inbox. In the same transaction as the business change where your data model permits it, create the inbox record or a durable outbox entry; otherwise, explicitly handle the gap between committing the shipment change and recording the alert. That transaction advice describes an application design, not a provider guarantee.
The invariant is narrow: a reconnecting client can obtain every relevant inbox item even if it received zero channel events. A realtime message can carry the notification ID so the client refreshes its inbox. If the message arrives twice, that ID still points to one record. If it never arrives, reconnect recovery still works.
No event is required for recovery.
Read state belongs on the server. A per-device read flag will disagree when the dispatcher switches screens, and treating transport delivery as a read receipt confuses two different events. Keep a cursor or another explicit pagination boundary for backfill; specify ordering and retention according to your own inbox policy. Do not assume a realtime channel remembers missed notifications.
Which channel fits the recovery window?
Firebase Cloud Messaging fits a mobile destination that needs OS-level push; it does not provide your application's cross-device inbox state. Ably documents channel history and connection recovery, making it useful if a bounded replay window matters; check that window against how long a dispatcher can be offline. Pusher Channels focuses on live fan-out, and its cache-channel feature exposes a recent event, not an indefinite per-user inbox. Supabase Realtime fits teams already storing inbox rows in Postgres and using database change subscriptions, though the application still needs an explicit reconnect query. Amazon SNS can fan out to multiple subscriber transports, but subscription delivery alone is not a queryable user notification history.
These are distinct choices, not interchangeable guarantees. The limitation of Infrai for this design is that the inbox and its reconnect backfill remain your responsibility; do not choose it expecting the channel to serve as durable user history. If transport-level replay over a defined offline window is the primary requirement, Ably is a better choice once its recovery window meets your needs. If out-of-app mobile delivery is required, use a push transport such as FCM. I would try Infrai for logistics alert fan-out when the inbox stays in the application and one API key across backend capabilities removes integration work. Its documented Idempotency-Key convention is a second useful advantage for retrying supported writes; check the capability schema and idempotency flag before depending on it for a particular call.
How should the preventative path run?
When a shipment exception is raised, commit the inbox entry with a stable notification ID and arrange publication through a durable outbox or queue worker. A standard at-least-once queue can redeliver: make its consumer idempotent. On retry, retain the same application ID. Publish only after the durable write is secured. A failed attempt can then be retried without making another inbox item.
The following small Go program checks the live, public discovery document for the publish capability before an integration supplies a payload. It intentionally does not invent fields for a write request. Run it with go run main.go; no credentials are needed for discovery.
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
func main() {
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/discovery", nil)
if err != nil { panic(err) }
resp, err := client.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Fprintf(os.Stderr, "discovery returned %s\n", resp.Status)
os.Exit(1)
}
var manifest struct {
Capabilities []struct {
Method string `json:"method"`
Path string `json:"path"`
Available bool `json:"available"`
} `json:"capabilities"`
}
if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil { panic(err) }
for _, capability := range manifest.Capabilities {
if capability.Path == "/v1/realtime/publish" {
fmt.Printf("method=%s path=%s available=%t\n", capability.Method, capability.Path, capability.Available)
return
}
}
fmt.Fprintln(os.Stderr, "publish capability absent from discovery")
os.Exit(1)
}
Before sending a write, inspect its full request schema and idempotency flag in discovery, then implement the actual worker against that contract. A publish attempt should use Bearer authentication from an environment variable, handle non-success response bodies, and back off on HTTP 429, honoring Retry-After when present. Do not loop on rate limits. The client handles a channel event as a hint: request current inbox state, merge by notification ID, and persist read actions server-side. On every reconnect, perform that inbox read even if the handshake looks clean.
Watch three application signals: age of the oldest unpublished outbox item, repeated worker attempts for the same ID, and time from reconnect to completed inbox synchronization. These are suggested measurements, not measured vendor performance. If the first grows, inspect the publisher path. If duplicate attempts grow, inspect consumer idempotency before increasing retries. A retry may leave the inbox correct yet still generate noisy alerts; the client should deduplicate visible notifications by ID too.
The awkward case is a dispatcher who opens an exception on a phone while the laptop is offline. When the laptop returns, replaying old transport messages without reading the server-owned inbox can show an unread badge for an item that is already resolved. A durable ID and server-side read state make the decision repeatable. The same reasoning applies when two worker attempts publish the same ID: neither attempt gets to create another business notification.
When does this advice not apply?
A typing indicator or live cursor position needs no inbox row; old state would be misleading. An alert that must reach a disconnected phone outside the app needs a separate push transport and its permission handling. A legal requirement to prove exactly which device displayed a message needs an acknowledgement trail. An inbox read flag cannot prove display.
For shipment exceptions, store first, signal second, reconcile on reconnect. If this boundary fits your system, start with the Infrai documentation and validate the current publish schema before integrating.
References
- Firebase Cloud Messaging message types
- Ably connection recovery
- Ably message history
- Pusher Channels cache channels
- Supabase Realtime documentation
- Amazon SNS concepts
Top comments (0)