DEV Community

eliasfischer8351
eliasfischer8351

Posted on

Marketplace Email Deliverability Strategy: Auditable SMS Alert Fallback for Support Routing

Marketplace contact forms have a deceptively hard failure mode: a message can be accepted by your API, rejected by a recipient domain, and still look successful to the seller who submitted it. Short answer: treat bounce handling as a durable state transition, poll provider events with a bounded cursor, and escalate only verified hard bounces to an SMS queue; keep the routing template owned by your application so a regional policy change does not silently change the audit trail.

The decision is about evidence, not speed. A text sent for every transient deferral becomes noise, while a text sent after an unobserved hard bounce hides a real support outage. In a marketplace, the queue destination is part of the business record: buyer disputes, seller verification, and payout questions may have different owners even when their forms share one endpoint.

Model delivery as an auditable state machine

Store the contact request, the rendered template version, and the outbound message identifier in one append-only record before attempting delivery. A useful minimum state set is accepted, queued, delivered, deferred, soft_bounced, hard_bounced, sms_queued, and closed. Event names differ between mail systems, so normalize them at the boundary and retain the original event payload for investigation.

The state transition must be monotonic for a given message. If a poll sees delivered after an earlier deferred, that is a valid forward transition; if an old retry later reports deferred, it must not move the record backwards. This is an exactly-once mindset applied to effects that are usually at-least-once. The database can enforce it with a version column or a compare-and-swap update, and an idempotency key can protect the SMS enqueue operation from duplicate polls.

Consider two events emitted in different regions with equal second-level timestamps: one marks a deferral and the other marks delivery. If the poller orders only by time, a retry can overwrite the terminal delivery state, enqueue an unnecessary SMS, and leave reconciliation with two plausible histories. Order by the provider cursor when its contract guarantees ordering, retain the provider event ID for deduplication, and use the message ID plus the normalized transition rank as a deterministic tie-breaker inside one page. If the contract does not guarantee global order, application state must decide whether a transition is admissible; wall-clock time cannot do that job. The audit row should capture the rejected stale event as well as the accepted one, because absence of a state change is still evidence during a dispute review.

Don't guess.

How should Node.js polling handle email events and SMS alerts without webhooks?

A polling worker needs three controls: a durable cursor, a lease, and a replay window. The cursor records the last fully processed page, the lease prevents two workers from claiming the same interval, and the replay window lets a restarted worker re-read recent events without losing a page at a boundary. Set the next poll from the provider's documented pagination token; do not manufacture page numbers when the API exposes a cursor.

The worker below is intentionally provider-neutral. listEvents represents a documented event listing call, and the adapter maps provider payloads to the small internal shape. The application owns the support template and queue mapping; the mail adapter owns transport details.

type DeliveryEvent struct {
    Cursor    string
    MessageID string
    Kind      string
    Occurred  time.Time
}

func processPage(ctx context.Context, events []DeliveryEvent, store Store, sms SMSQueue) error {
    for _, event := range events {
        if err := store.ApplyDeliveryEvent(ctx, event); err != nil {
            return err
        }
        if event.Kind != "hard_bounced" {
            continue
        }
        request, err := store.ContactForMessage(ctx, event.MessageID)
        if err != nil {
            return err
        }
        key := "sms-fallback:" + event.MessageID
        if err := sms.Enqueue(ctx, key, request.Queue, request.Phone); err != nil {
            return err
        }
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

ApplyDeliveryEvent should reject stale transitions and record an audit row even when the resulting state is unchanged. Enqueue should be idempotent on the message key, with a separate delivery status for the SMS provider. That separation matters for reconciliation: an SMS accepted by its provider is not proof that a human read it.

Which bounce evidence belongs in a marketplace support queue?

Use the strongest evidence available. A hard bounce caused by a permanent address failure is a reasonable escalation trigger; a temporary 4xx deferral should remain in retry policy until its retry horizon expires. Authentication failures, policy blocks, and mailbox-full events need classification because they can be transient or domain-specific. Never infer permanence from a single HTTP response from your own submission endpoint.

Routing should be deterministic and explainable. Persist the rule ID, template version, locale, and region used to choose the queue. For US and EU tenants, keep consent and quiet-hour checks in the SMS policy layer, and redact message content from operational logs. The queue receives a compact alert containing the contact ID, bounce class, and a link to an access-controlled case; it does not receive the full customer message by default.

The catch is that SMS is not suitable when the recipient has not granted the required consent, when the alert contains regulated content, or when the queue has no staffed owner. In those cases, stick with an in-product task or a verified secondary email and record the reason. A fallback that violates policy is a second incident.

Stop there.

Template ownership is a control, not a branding preference

Keep a versioned template manifest in the application repository, while allowing the transport adapter to supply provider-specific substitutions. A release should carry the template hash into the delivery record. If a provider changes a default footer or a regional sending option, your audit still shows which business wording was approved.

Test this contract with fixtures for duplicate events, cursor expiry, clock skew, a hard bounce followed by delivery, and a restart halfway through a page. Property-based tests are useful for the invariant that no event can move a message from a terminal state back to a retryable one. Operationally, alert on cursor age, lease contention, and the ratio of sms_queued to hard_bounced; those signals expose a stalled poller and an over-eager classifier.

A measured rollout for polling-based fallback

Start with shadow mode: classify events and write audit rows without sending SMS. Compare classifications with support decisions for a full business cycle, then enable one queue and one region. Add a kill switch that stops new SMS enqueues while allowing already accepted messages to reconcile.

Before expanding, verify retention, access controls, and deletion behavior for both email events and phone numbers. I am not sure any provider's event vocabulary will remain identical across regions, so keep the adapter contract small and monitor unknown event kinds rather than silently treating them as failures. Your mileage may vary with polling limits; the cursor, lease, and replay measurements should drive the interval.

A sound fallback is therefore a controlled accounting process: every escalation has a cause, an owner, and a reversible operational decision. The transport can change later; the evidence and template ownership should not.

References

Top comments (0)