Blog
GUIDEENGINEERINGUSER EXPERIENCE

Cross-Channel Notification State: Why Read Receipts Are Harder Than They Look

Kyle Seyler

February 03, 2026

state management

Table of contents

TLDR

The state problem nobody talks about

Why users care (more than you think)

Three approaches to state management

What states need to sync?

Implementation patterns

What not to sync (and why)

The "good enough" implementation

Testing state sync

When to invest in state sync

The bottom line

TLDR

Most notification systems treat each channel as separate. Email has state. Push has state. In-app has state. They don't share it. Users get the same notification marked "unread" in three places and have to dismiss it three times. The fix is deciding on a source of truth: either each channel owns its state (simple but frustrating), or a central system tracks state and channels read from it (harder but consistent). The goal isn't perfect sync. It's predictable sync that users can understand.


The state problem nobody talks about

User reads your email. Opens the app. The notification bell shows 1 unread. They tap it. It's the same notification they just read in email.

custom buttons

This happens constantly. Most products have this bug. Few realize it's a bug.

Why it happens:

Copied!

Email System ────────────► Email state (read: yes)
(no connection)
Push System ─────────────► Push state (read: no)
(no connection)
In-App System ───────────► In-app state (read: no)

Each channel is its own system. Each system tracks its own state. When you send the same notification to multiple channels, each one creates its own record.

The user sees one notification. Your systems see three.


Why users care (more than you think)

State inconsistency creates small frustrations that compound:

  1. Wasted attention. User dismisses the same thing multiple times. Each dismissal costs a tiny bit of trust.

  2. Notification fatigue. That red badge showing "3 unread" when there's really 1 thing to see trains users to ignore badges.

  3. Confusion. "Did I already deal with this?" Users lose confidence that your notifications mean anything.

  4. Broken mental models. Users assume your product is one system. When notifications behave like three separate systems, the product feels janky.

For products where notifications are core to the experience (collaboration tools, marketplaces, communication platforms), state consistency directly impacts retention.


Three approaches to state management

Approach 1: Channel-first (each channel owns its state)

Copied!

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Email │ │ Push │ │ In-App │
│ (state A) │ │ (state B) │ │ (state C) │
└─────────────┘ └─────────────┘ └─────────────┘
↑ ↑ ↑
Separate Separate Separate

How it works: Each channel tracks state independently. Read email? Email is marked read. Push and in-app remain unread.

Pros:

  • Simple to implement
  • Each provider/integration handles its own state
  • No central coordination required

Cons:

  • Users mark the same notification read multiple times
  • Badge counts are inaccurate
  • No unified view of what users have seen

When to use it: Early-stage products where simplicity matters more than polish. Or when channels are genuinely independent (marketing email vs. transactional push, where users wouldn't expect them to sync).

Approach 2: Central-first (one system owns all state)

Copied!

┌────────────────────┐
│ Central State │
│ (source of truth)│
└────────────────────┘
↑ ↑ ↑
┌─────────┼────┼────┼─────────┐
│ │ │ │ │
┌────┴───┐ ┌───┴──┐ │ ┌──┴───┐ ┌───┴────┐
│ Email │ │ Push │ │ │In-App│ │ SMS │
│(reads) │ │(reads)│ │ │(reads)│ │(reads) │
└────────┘ └──────┘ │ └──────┘ └────────┘

How it works: One central system tracks notification state. Channels query this system to know if something is read. When any channel marks something read, it updates the central state, and other channels reflect that.

Pros:

  • Consistent state across all channels
  • Accurate badge counts
  • Unified analytics on what users have seen

Cons:

  • Requires building or buying a central state system
  • All channels must integrate with it
  • Adds latency (channels must check central state)
  • Sync failures can cause inconsistency

When to use it: Products where notifications are core to the experience. Collaboration tools, messaging platforms, anything where users interact with notifications frequently across multiple surfaces.

Approach 3: Event-first (state changes emit events, channels subscribe)

Copied!

┌─────────┐ ┌─────────┐ ┌─────────┐
│ Email │ │ Push │ │ In-App │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────┐
│ Event Bus │
│ (notification.read, etc.) │
└─────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Email │ │ Push │ │ In-App │
│(updates)│ │(updates)│ │(updates)│
└─────────┘ └─────────┘ └─────────┘

How it works: Every state change (read, dismissed, actioned) emits an event. Channels subscribe to relevant events and update their local state. Each channel has its own state, but they stay in sync through the event bus.

Pros:

  • Most flexible architecture
  • Channels can choose which events to react to
  • Scales well with many channels
  • Supports eventual consistency patterns

Cons:

  • Most complex to implement
  • Eventual consistency means temporary inconsistency
  • Event ordering and replay handling required
  • Debugging is harder

When to use it: Large-scale systems with many notification channels, high volume, and teams that can manage event-driven architecture.


What states need to sync?

"Read" isn't the only notification state. Before implementing sync, decide which states matter:

StateDefinitionShould It Sync?
DeliveredSystem confirmed deliveryRarely (channel-specific)
Read/SeenUser viewed the notificationUsually yes
ClickedUser clicked a link/buttonSometimes
DismissedUser explicitly closed itUsually yes
ActionedUser took the intended actionUsually yes
ArchivedUser archived for laterDepends
SnoozedUser deferred until laterUsually yes

The minimum viable sync: Most products start by syncing "read" state. If a user sees a notification anywhere, it should be marked as seen everywhere.

The trap: Trying to sync everything perfectly. Some states are inherently channel-specific (email open tracking is imprecise, push delivery confirmation varies by OS). Accept that some state will always be approximate.


Implementation patterns

Pattern 1: Read sync via notification ID

Every notification gets a unique ID that's consistent across channels.

Copied!

Notification ID: ntf_abc123
├── Email: sent to user@example.com
├── Push: sent to device_xyz
└── In-App: shown in notification center

When any channel marks ntf_abc123 as read, all channels check and update.

The challenge: Email providers don't always support custom IDs. You may need to map your internal ID to provider-specific identifiers.

Pattern 2: Webhook-based state updates

Channels send webhooks when state changes. A central handler processes them and updates other channels.

Copied!

1. User opens email
2. Email provider sends open webhook
3. Handler receives: {notification_id: "abc123", event: "opened"}
4. Handler updates central state
5. Handler notifies in-app to update UI

The challenge: Email open tracking is unreliable (pixel blocking, preview panes). Don't depend on email opens for critical state logic.

Pattern 3: Client-side sync

When users interact with notifications in your app, the app updates both local state and server state. Server broadcasts changes to other clients.

Copied!

// User marks notification read
async function markRead(notificationId) {
// Update local state immediately
localState.markRead(notificationId);
// Update server (which syncs to other channels)
await api.notifications.markRead(notificationId);
}

The challenge: Requires your app to be the coordination point. Works for in-app, harder for email/SMS.


What not to sync (and why)

Some state shouldn't sync across channels:

Delivery state: "Was this email delivered?" and "Was this push delivered?" are different questions with different answers. Don't conflate them.

Channel-specific interactions: If users can reply to a notification in Slack, that reply doesn't need to appear in email. The action was channel-specific.

Timing metadata: "Opened at 3pm" vs "Opened at 3:05pm" across channels. Syncing precise timing creates confusion. Sync the fact that it was opened, not the exact moment.


The "good enough" implementation

For most products, here's a practical approach:

  1. Assign every notification a unique ID before sending to any channel.

  2. Store notification state centrally with at least: created, sent_channels, read_at, actioned_at.

  3. Update central state from in-app interactions (these are the most reliable signals).

  4. Optionally update from email opens (treat as soft signal due to tracking limitations).

  5. Don't depend on push delivery confirmation (varies too much by platform).

  6. Expose state to all clients via API. When your app loads, fetch current state from the central source.

This gives you consistent unread counts and prevents users from seeing the same notification as "new" in multiple places, without requiring perfect real-time sync across all channels.


Testing state sync

State bugs are subtle. Test these scenarios:

ScenarioExpected Behavior
User opens email, then opens appIn-app shows notification as read
User dismisses in-app, checks emailEmail is still there (can't un-send email)
User clicks push, completes actionAll channels show as actioned
User has app open on two devicesBoth devices stay in sync
User marks all read on one channelOther channels reflect this

The edge cases matter. Test with slow networks, offline states, and race conditions (user interacts on two channels simultaneously).


When to invest in state sync

High priority if:

  • Notifications are core to your product experience
  • Users interact with notifications across multiple channels
  • Badge counts matter for engagement
  • You're seeing user complaints about redundant notifications

Lower priority if:

  • Each channel serves a genuinely different purpose
  • Notifications are low-frequency
  • Users typically use only one channel
  • You're early-stage and need to ship other things first

The question isn't whether state sync is valuable. It's whether it's more valuable than whatever else you could build with that engineering time.


The bottom line

Users don't think in channels. They think in messages. When they've seen something, they've seen it.

State management is boring infrastructure work that users never notice when it's done right. They only notice when it's wrong.

The goal isn't perfect sync. It's predictable behavior. If email read state doesn't sync to your app, that's fine as long as it's consistent. What users can't handle is inconsistency they don't understand.

Pick an approach that matches your product's complexity. Implement it consistently. Test the edge cases. And remember: the red badge that says "3 unread" when there's 1 new thing is a small lie that compounds into distrust.


This post is part of The Ping, a series about building notifications that don't get ignored.

Similar resources

courier and expo push notifications
GuideEngineering

Expo Push Notifications: The Complete Implementation Guide (SDK 52+)

Expo push notifications are alerts sent from a server to a user's phone, even when the app isn't open. To set them up, install the expo-notifications library, ask the user for permission, and get a unique push token for their device. Your server sends a message to Expo's push service with that token, and Expo delivers it through Apple or Google. Push notifications only work on real phones, not simulators. Local notifications are different — they're scheduled by the app itself for things like reminders. You can also route Expo push through services like Courier to add email, SMS, and Slack fallbacks.

By Kyle Seyler

February 24, 2026

email infrastructure providers
AIGuideEngineering

Best Email API Providers for Developers in 2026: SendGrid vs Postmark vs Mailgun vs SES vs Resend

Your email provider sticks with you longer than most technical decisions. Courier handles notification infrastructure for thousands of teams, so we went deep on the six email providers that show up most: SendGrid, Postmark, Mailgun, Amazon SES, Resend, and SMTP. This guide covers real API primitives, actual code from each provider's docs, Courier integration examples with provider overrides, and an honest read on where each developer experience holds up and where it breaks down. We also asked Claude to review every API and tell us which one it would wire up first. The answer surprised us.

By Kyle Seyler

February 23, 2026

notification infrastructure for regulated industries
Notifications LandscapeGuide

A Resilient Notification Strategy for Regulated Industries

Notification compliance isn't a legal checklist—it's an infrastructure problem. In 2026, Reg E deadlines, HIPAA content rules, and TCPA consent requirements dictate your system architecture. This guide breaks down the engineering constraints of regulated notifications for fintech, healthcare, and insurance. Learn why hard-coded deadlines fail, how "alert without disclosing" works in practice, and why the smart escalation pattern (Push → SMS → Email) is the only way to satisfy both user urgency and regulatory documentation. Build systems that absorb complexity, not application code that breaks every time a state law changes.

By Kyle Seyler

February 11, 2026

Multichannel Notifications Platform for SaaS

Products

Platform

Integrations

Customers

Blog

API Status

Subprocessors


© 2026 Courier. All rights reserved.