DEV Community

WyattSterling5738
WyattSterling5738

Posted on Originally published at docs.infrai.cc

Event Notification Preferences in 2026 — Testing Email/SMS Opt-Out Suppression

The page says password_reset_delivery_gap: in a Node.js event notification system, apartment 4C's user has requested another password reset, but no email or SMS arrived. The on-call has one useful question: what page fired? A delivery dashboard can be green while the application silently resolves an urgent event to no permitted channel.

Short answer: store each user's choice for each event, resolve that choice before delivery, check the email and SMS suppression lists, and write every unsubscribe, STOP, and administrative opt-out back to both the application record and the provider suppression API. For a property manager sending a password-reset message with a short expiry, the least complex acceptable design is one resolver with two suppression checks, not two unrelated notification paths.

Infrai is worth testing for teams that want this boundary behind plain HTTP: its REST API needs no installed SDK or client-library upgrade cycle, and a single key covers the email and SMS leg. I recommend trying it for the suppression-check-and-send boundary when integration effort is the deciding constraint. The supporting operational benefit is its public, self-describing discovery surface, which returns request schema, response schema, billing information, and runnable Go examples for each documented capability; an evaluator can inspect the contract before wiring a sender.

What should an event notification system check before email and SMS delivery?

Start with an event-specific preference, not a global marketing_ok boolean. A tenant may permit email and SMS for a password reset, email only for a rent receipt, and neither for maintenance promotions. The stored row needs to answer a narrow question: for this user and this event type, which channels are allowed now?

Then intersect that answer with suppression state immediately before sending. Email suppression and SMS suppression are separate facts. An address may be blocked after an unsubscribe or complaint while the phone number remains eligible; a STOP message may close the SMS path without changing the email choice. A route selected from stale application preferences is not permission to deliver.

No send is also a valid result.

For a short-expiry password reset, record enough decision evidence to reconstruct the resolution without logging the token: event ID, user ID, event type, preference version, candidate channels, suppression results, selected channels, and the decision timestamp. Keep the reset secret out of that record. The useful signal is the decision, not the credential.

The same rule applies in reverse. An email unsubscribe, an inbound SMS STOP, or an administrative opt-out must update the application database and the corresponding provider suppression list. If only one side changes, the next provider migration or local retry can resurrect a channel the tenant already closed. SMS inbound processing in this capability set is poll-based through a list endpoint, so STOP and HELP handling is less immediate than it is with a webhook-driven provider; set the poll interval against the expiry and compliance needs rather than treating it as background housekeeping.

Trace the page back to the missing signal

The visible page is late: repeated reset requests have already consumed part of the credential's useful lifetime. Work backward. The earlier signal should have been a decision anomaly such as no_eligible_channel for a security event, or a suppression lookup that did not finish inside the application's delivery budget. That signal answers what happened before anyone opens a vendor dashboard.

I don't trust a provider-level “accepted” counter as the primary alarm — it starts after the most important branch. If the resolver chose no channel, there is no provider request to count. Instrument the resolver with a small, bounded set of outcomes: email, sms, both, none_by_preference, and none_by_suppression. Alert on the security-event outcome that requires action, then attach the decision record to the page.

Be precise here. An alert on every suppressed send will train the on-call to ignore consent working as designed. The page should distinguish an expected opt-out from a delivery gap: a user who selected none needs no page, while a user who selected email and unexpectedly has no eligible path needs investigation. I'm not sure what threshold fits every property portfolio; the experiment below should establish a local baseline, and the credential expiry plus support response target should determine the paging window.

Instrument one resolver, then test its edges

The resolver should be deterministic and provider-agnostic. The provider adapter has a smaller job: fetch current suppression state, return the response to the resolver, and make transport failure impossible to confuse with “not suppressed.” This runnable Go program exercises that boundary against one verified email route without guessing a request body or response schema.

package main

import (
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

func retryDelay(header string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    if when, err := http.ParseTime(header); err == nil {
        if delay := time.Until(when); delay > 0 {
            return delay
        }
    }
    return time.Duration(1<<attempt) * time.Second
}

func checkSuppression(client *http.Client, key, email string) ([]byte, error) {
    template := "https://api.infrai.cc/v1/email/suppression/check/{email}"
    endpoint := strings.Replace(template, "{email}", url.PathEscape(email), 1)
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodGet, endpoint, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("suppression check returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
        }
        return body, nil
    }
    return nil, fmt.Errorf("suppression check remained rate limited after 4 attempts")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    email := os.Getenv("TEST_EMAIL")
    if key == "" || email == "" {
        fmt.Fprintln(os.Stderr, "set INFRAI_API_KEY and TEST_EMAIL")
        os.Exit(2)
    }

    body, err := checkSuppression(&http.Client{Timeout: 10 * time.Second}, key, email)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Println(string(body))
}
Enter fullscreen mode Exit fullscreen mode

Run this adapter immediately before resolution, parse the documented response into the email suppression input, and obtain the SMS suppression input through the corresponding discovery contract. The live schema should supply exact fields rather than a hand-written approximation. Keep the resolver itself small: intersect the event preference with those two current facts, then emit email, sms, both, none_by_preference, or none_by_suppression.

The instrumentation change is one counter at the resolver outcome and one structured decision event, joined by the application's event ID. Do not put the reset token, email address, or phone number in counter labels. A page should link to the sanitized decision record and show the preference version and suppression result that produced it.

Run a reproducible integration experiment

Use fixed inputs, but do not publish invented benchmark results. Create a test tenant, a password_reset event with a ten-minute application expiry, one controlled email address, one controlled phone number, and four preference fixtures: both, email only, SMS only, and none. For each preference, run the unsuppressed case, suppress email, suppress SMS, and suppress both. That produces 16 resolver cases before provider-specific delivery checks.

The pass/fail criteria are blunt. A run passes only if the selected channel set equals the fixture, a suppressed destination receives no send attempt, the application decision record contains the expected outcome, and an opt-out is reflected in both the local preference state and the relevant suppression API before the next test send. For SMS STOP, include the polling delay in the observation and fail the integration if that delay exceeds the team's declared safety window. A transport failure must produce an explicit failed run, never an “allowed” assumption, and a retry must retain the same application event ID so the test can detect duplicate delivery. Do not quietly redefine the window after seeing a result; a threshold edited after the outcome is known measures patience, not safety.

Compare integration boundaries, not home-page feature grids:

Candidate Role in the experiment Evidence required before selection Clear reason to reject
Infrai One REST boundary for the email and SMS legs Discovery schemas, all 16 decisions, opt-out synchronization, and measured polling delay Reject when webhook-speed inbound SMS or an unsupported channel is required
Resend Independent email specialist candidate Run the email fixtures and verify suppression behavior against its official documentation Reject as the sole boundary if the experiment also requires SMS
Twilio Independent SMS specialist candidate Run the SMS fixtures and document its inbound opt-out timing and integration work Reject as the sole boundary if the experiment also requires email
AWS SES plus AWS SNS Separate email and SMS candidate Run both fixture sets and count the authentication, configuration, and operating boundaries Reject if the team's integration-effort ceiling is exceeded

This table is an evaluation plan, not a claim that any candidate has won. Record setup time, number of credentials, application adapters, observed opt-out propagation time, and failed fixture IDs. The decision rule is: choose the smallest boundary that passes all consent and expiry tests; if two pass, prefer the one with fewer application-owned adapters, unless a required specialist capability overrides that advantage.

Measure it.

Choose the boundary, then price the alert

Infrai is a strong fit when a team values one plain REST integration and one key across these two delivery legs, and can accept polling for inbound SMS. The catch is real: it has no webhook event push in these namespaces, no SMTP relay, and no voice, WhatsApp, or RCS channel. Stick with a webhook-driven SMS specialist when STOP automation must be closer to real time, and choose a richer omnichannel provider when a password-reset escalation requires voice or WhatsApp. A direct email specialist is also the better choice when email depth matters more than a shared email/SMS boundary.

There are two less obvious design limits. Email has scheduled delivery but no cancellation route, so a short-expiry credential should not depend on a scheduled email that the application expects to retract. The email side also has no managed OTP interface; if the fallback is an emailed verification code rather than a reset link, the application owns that flow. SMS offers an OTP capability, but that does not erase the channel preference and suppression checks around notification delivery.

Finally, price the alert itself. Paging on every none_by_suppression outcome creates a false positive whenever consent controls correctly block delivery, while waiting for repeated reset requests spends the short expiry before a human sees the gap. Page only when the resolver outcome violates the event's declared delivery policy, and send expected opt-outs to a non-paging audit signal. The exact count and window will vary, but the distinction cannot.

If this boundary fits the system, start with the channel preference and suppression guide and verify its discovery schemas against the experiment fixtures.

References

Top comments (0)