DEV Community

daxharrington5274
daxharrington5274

Posted on

One Provider Beats Separate Email and SMS Vendors When Reset Evidence Must Match

TL;DR: Use one email-and-SMS provider for a startup password-reset flow when it can export the same compliance evidence for both channels. Choose separate specialists only when a required region, retention rule, or evidence field is missing. The decisive benchmark is not the number of SDK calls. It is the time required to reconstruct one reset attempt without joining incompatible logs.

This is a narrow choice on purpose. A reset token with a short expiry creates an awkward record: the system must prove what it tried, where it sent the message, and what the provider accepted, while keeping the token itself out of durable logs. A unified provider makes that evidence model easier to keep consistent. Two specialists can give better channel coverage, but they also create two event vocabularies, two retention settings, and more glue at exactly the boundary an auditor will inspect.

My default is consolidated delivery. The boundary is strict: if its evidence cannot satisfy the deployment regions and retention policy, split the channels and normalize the records before launch.

Should one provider handle the event notification stack?

A quick integration benchmark usually stops at time-to-first-call. That is too early. For password resets, I would run a second stopwatch: start with an internal event ID and find the corresponding request, provider receipt, terminal status, and policy version for both email and SMS. The architecture with fewer lines of send code can still lose this test.

The useful comparison is concrete:

Test One provider Separate specialists
Correlation One external namespace may be enough An internal ID must bridge both namespaces
Evidence schema Usually one adapter shape Two adapters must converge on one shape
Retention control One policy surface to verify Each channel needs an independent check
Channel boundary Coupled operational dependency Failures and credentials can be isolated
Regional fit Must cover every required destination Each channel can use its required operator

"May" matters in that table. A shared logo or SDK does not prove that email and SMS receipts expose matching fields. Verify the exported records. Config pages do not count.

Email adds one standards-shaped check. DKIM defines a domain-level signing framework in which a signer computes a signature over selected message headers and the body, and a verifier retrieves a public key through DNS. That can support authentication evidence, but it does not prove that a recipient read a reset message. Keep the claim small. RFC 6376 is precise about the mechanism and its scope.

SMS needs the same skepticism. A provider status should be stored as an attributed observation, not silently translated into "the user received it." The article's evidence model therefore records provider status verbatim alongside a normalized state. That separation prevents a convenient label from becoming a stronger compliance claim than the source event supports.

The limitation of one provider is concentration. Email and SMS can share credentials, operational controls, and an outage boundary; its regional coverage may also satisfy one channel but not the other. Separate vendors are a better fit when isolation or a channel-specific regional requirement matters more than a shared evidence surface. The trade-off is extra normalization, credential rotation, contract testing, and investigation time. Do not hide it behind one wrapper and call the systems equivalent.

That is the trade-off.

The constraint that changed the build

The reset expires quickly, so retry behavior cannot be separated from evidence design. A retry after expiry is noise. A retry before expiry can become a duplicate unless every attempt carries stable identity and the dispatcher knows whether it is repeating an accepted request or creating a new one.

I would persist a notification intent before contacting any provider. It contains an opaque reset ID, channel, destination region, expiry, policy version, and an idempotency key. It does not contain the reset secret. The URL handed to the channel adapter can contain the secret, but the durable evidence object must not.

This is the least glamorous part of the stack. It is also the part I benchmark. Give an engineer only the incident ID and measure whether they can answer four questions: which policy selected the channel, when the request left, which external ID came back, and whether a later callback matched the original attempt. If the answer requires opening two dashboards and comparing timestamps by eye, the integration is unfinished.

No magic. Just IDs.

Measure the reconstruction.

The smallest working implementation

The provider boundary should be boring TypeScript. Both channel adapters accept the same intent and return the same receipt. Provider-specific payloads stay inside adapters, and raw callback bodies go to access-controlled evidence storage only when the retention policy permits them.

type Channel = "email" | "sms";
type DeliveryState = "accepted" | "rejected" | "unknown";

type ResetIntent = {
  eventId: string;
  channel: Channel;
  destination: string;
  region: "US" | "EU";
  expiresAt: string;
  policyVersion: string;
  idempotencyKey: string;
};

type DeliveryReceipt = {
  eventId: string;
  channel: Channel;
  providerMessageId: string;
  normalizedState: DeliveryState;
  providerStatus: string;
  observedAt: string;
};

interface ChannelAdapter {
  sendReset(intent: ResetIntent, resetUrl: URL): Promise<DeliveryReceipt>;
}

async function dispatchReset(
  intent: ResetIntent,
  resetUrl: URL,
  adapters: Record<Channel, ChannelAdapter>,
  now = new Date(),
): Promise<DeliveryReceipt> {
  if (now.getTime() >= Date.parse(intent.expiresAt)) {
    throw new Error("Reset intent expired before dispatch");
  }

  return adapters[intent.channel].sendReset(intent, resetUrl);
}
Enter fullscreen mode Exit fullscreen mode

The interface is deliberately smaller than a provider SDK. It gives the application one auditable contract without pretending channel semantics are identical. The adapter owns status mapping. The evidence record retains the original status so a mapping change can be reviewed later.

For an agent or automated workflow that can trigger sends, define the tool just as tightly. Anthropic's tool-use guide describes tools through a name, description, and input schema, and recommends detailed descriptions. The same principle applies even if no model is involved: constrain the callable surface, validate it, and keep policy selection outside the send primitive. An automation should request an approved intent, not invent a destination or expiry.

const sendResetTool = {
  name: "send_password_reset",
  description: "Dispatch an already-approved password-reset intent before its expiry.",
  input_schema: {
    type: "object",
    properties: {
      eventId: { type: "string" },
      channel: { type: "string", enum: ["email", "sms"] },
    },
    required: ["eventId", "channel"],
    additionalProperties: false,
  },
} as const;
Enter fullscreen mode Exit fullscreen mode

Notice what is absent: the reset token, arbitrary message text, and provider selection. Smaller input means fewer policy decisions leak into callers. I hate config bloat most when it disguises authority.

What I would change at scale

First, I would separate dispatch records from callback observations. They have different trust boundaries and may arrive out of order. The reducer that derives current state must tolerate duplicate observations and preserve the original sequence rather than overwriting history.

Then I would test the evidence path as a product feature. Contract tests should feed each adapter accepted, rejected, duplicate, late, and unknown statuses. A deployment test should start from an event ID and retrieve the full chain under the same access controls an investigator will use. Synthetic resets should use controlled destinations and tokens that cannot affect customer accounts.

At higher volume, separate specialists become reasonable when the operational benefit is measurable: one channel needs regional reach the combined provider lacks, evidence retention differs by channel, or isolation is worth the extra adapter and on-call surface. Price can enter the decision, but it should not lead it. A cheap send with irreconcilable evidence is an expensive incident.

The split model also needs a routing policy with versioned decisions. Do not let application code scatter region checks across handlers. One policy should select the adapter; its version goes into every intent. This adds configuration, so earn it. Two vendors without a tested failover or regulatory reason are just two credentials to rotate.

A decision rule that survives procurement

Pick the consolidated model if one provider passes all required US and EU destination tests, exposes exportable request and status evidence for both channels, supports the required retention controls, and lets the team correlate a reset attempt without manual timestamp matching. That is the easiest integration because the operational proof is easy, not because the hello-world call is short.

Pick separate specialists when one of those requirements fails and a channel-specific service closes the exact gap. Before signing, require both adapters to pass the same evidence contract and rehearse reconstruction from the internal event ID. The added integration work is justified only by a named requirement.

This yields a clear default for an early e-commerce reset flow: consolidate first, keep the adapter boundary, and split only on evidence or coverage. Do not optimize for a hypothetical migration. Do preserve the IDs and raw meanings that make a real migration possible.

Further reading

Top comments (0)