<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MagnusNilsson2124</title>
    <description>The latest articles on DEV Community by MagnusNilsson2124 (@magnusnilsson2124).</description>
    <link>https://dev.to/magnusnilsson2124</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4075471%2F8d37ef6f-0ff0-4b1b-8494-ec806f407e70.png</url>
      <title>DEV Community: MagnusNilsson2124</title>
      <link>https://dev.to/magnusnilsson2124</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/magnusnilsson2124"/>
    <language>en</language>
    <item>
      <title>Cron vs Queue for Simple Scheduled Data Cleanup of Old SaaS Records</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Wed, 16 Sep 2026 04:39:27 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/cron-vs-queue-for-simple-scheduled-data-cleanup-of-old-saas-records-248b</link>
      <guid>https://dev.to/magnusnilsson2124/cron-vs-queue-for-simple-scheduled-data-cleanup-of-old-saas-records-248b</guid>
      <description>&lt;p&gt;A cleanup job that can outrun its scheduler's execution window is already a recovery problem, not merely a scheduling problem. For a customer-support SaaS, that distinction matters when shipment-update delivery records, expired deduplication keys, and temporary fan-out state accumulate while subscribers keep receiving new events.&lt;/p&gt;

&lt;p&gt;Short answer: use cron for a short, repeatable cleanup of old records; use cron to enqueue bounded batches when a run can exceed 900 seconds, needs retries, or must recover without starting over.&lt;/p&gt;

&lt;p&gt;My decision is intentionally narrow. Infrai is a strong option when a team wants a plain HTTP scheduling and queue surface without adding another language-specific SDK: its public discovery endpoint returns the method, path, full request and response schemas, billing metadata, and runnable examples. The same key also covers both capabilities, which removes a credential handoff between the trigger and queue sides. I would try it for the trigger-and-enqueue boundary in this workflow, especially when time to a first verified request matters more than owning scheduler infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should a simple scheduled data cleanup API use cron or a queue?
&lt;/h2&gt;

&lt;p&gt;Use cron alone when one invocation can find and delete a bounded set of expired rows well inside 900 seconds. The target must be a public HTTP URL. Make the deletion rule depend on an age window, such as &lt;code&gt;created_at &amp;lt; cutoff&lt;/code&gt;, rather than expecting an exact fire time. Infrai cron has second-level timing jitter and doesn't backfill triggers missed while paused, so an exact-timestamp predicate can leave gaps. A windowed predicate makes the next run pick those rows up.&lt;/p&gt;

&lt;p&gt;Missed runs happen.&lt;/p&gt;

&lt;p&gt;Use a queue when the amount of old data is not predictably small. Cron should only calculate a stable cutoff and publish batch work; workers then claim, process, and acknowledge those batches. Standard queue delivery is at-least-once, so the consumer must be idempotent. This is non-negotiable. Deleting an already-deleted cleanup candidate should be harmless, and any side effect attached to deletion needs its own durable deduplication key.&lt;/p&gt;

&lt;p&gt;Retries happen too.&lt;/p&gt;

&lt;p&gt;The operational invariant is simple: a retry may repeat work, but it may not change the final result. For shipment-update fan-out data, key a cleanup unit by tenant, cutoff, and a stable cursor or range. Do not key it by the scheduler's arrival timestamp, because jitter would turn equivalent work into distinct jobs. A worker should commit its database change before acknowledging the message; if processing stops before acknowledgment, at-least-once delivery can present the same unit again and the database operation remains safe.&lt;/p&gt;

&lt;p&gt;There are hard boundaries around that design. A cron run cannot exceed 900 seconds. Queue delay cannot exceed seven days, a message cannot exceed 256KB, and retention cannot exceed 30 days; acknowledgment deletes the message. FIFO deduplication covers only a five-minute window. This isn't a Kafka-style replay log or a multi-consumer-group event backbone, and it has no native fan-out topic, debounce, throttle, DAG, join, or nonstandard cron &lt;code&gt;L&lt;/code&gt; extension. Those are architecture constraints, not details to postpone until launch week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the recovery invariants before choosing a service
&lt;/h2&gt;

&lt;p&gt;The cleanup endpoint and worker should share four invariants. First, the cutoff is immutable for one cleanup campaign. Second, a batch contains references or a compact range, not record bodies that can approach the 256KB message ceiling. Third, a repeated batch is safe. Fourth, progress is observable outside the cron run's retained output, because run-history output keeps only its first 4KB.&lt;/p&gt;

&lt;p&gt;A useful failure boundary sits between enumeration and deletion. If one cron request enumerates every eligible row and then deletes them, a timeout can erase any knowledge of how far it got. Instead, enumerate a modest key range, enqueue that range, and let a worker delete with a predicate that can run twice. Keep subscriber-facing shipment updates out of the cleanup transaction; delivery and retention have different recovery semantics.&lt;/p&gt;

&lt;p&gt;Watch the HTTP edges too. Cron can call only a public URL, and a push subscription requires public HTTPS, so a private-only worker endpoint won't receive those calls. If exposing that boundary violates the network model, choose a deployment-native scheduler or pull consumers rather than punching an exception through the perimeter. Authenticate callbacks and verify their message authentication at the receiving boundary; RFC 2104 is the underlying HMAC reference.&lt;/p&gt;

&lt;p&gt;I'm not sure what batch size fits your database without its query plan, lock profile, and production row distribution. Measure those, then set a batch size that leaves room for retries and concurrent application traffic. Start deliberately small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare setup friction and recovery behavior
&lt;/h2&gt;

&lt;p&gt;The services below are real alternatives, but they solve different ownership problems. This table is a decision aid, not a feature-equivalence claim. Provider details change, so verify each candidate's current documentation before committing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;First useful result&lt;/th&gt;
&lt;th&gt;Credential and SDK surface&lt;/th&gt;
&lt;th&gt;Recovery fit&lt;/th&gt;
&lt;th&gt;Choose it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai cron plus queue&lt;/td&gt;
&lt;td&gt;Read one public capability description, then use its runnable Python example&lt;/td&gt;
&lt;td&gt;One key and a plain REST API; no required SDK&lt;/td&gt;
&lt;td&gt;Cron for the trigger, at-least-once queue workers for idempotent batches&lt;/td&gt;
&lt;td&gt;You accept public HTTP/HTTPS boundaries and want scheduling plus queueing behind one consistent API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS EventBridge Scheduler plus SQS&lt;/td&gt;
&lt;td&gt;Native pairing for an AWS estate&lt;/td&gt;
&lt;td&gt;AWS identity, service configuration, and whichever AWS client approach the team already operates&lt;/td&gt;
&lt;td&gt;Queue-based retries require deliberate visibility-timeout and idempotency design&lt;/td&gt;
&lt;td&gt;Workloads, identity, and operational tooling already live in AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud Scheduler plus Cloud Tasks&lt;/td&gt;
&lt;td&gt;Managed trigger and task dispatch in a Google Cloud estate&lt;/td&gt;
&lt;td&gt;Google Cloud identity and service configuration&lt;/td&gt;
&lt;td&gt;A candidate for bounded HTTP task delivery; confirm current limits and retry controls&lt;/td&gt;
&lt;td&gt;The application already standardizes on Google Cloud operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BullMQ plus Redis&lt;/td&gt;
&lt;td&gt;Direct library integration in an application-owned runtime&lt;/td&gt;
&lt;td&gt;Node.js library plus a Redis deployment and credentials&lt;/td&gt;
&lt;td&gt;Application team owns worker lifecycle, persistence, and recovery operations&lt;/td&gt;
&lt;td&gt;You need close in-process control and already run Redis well&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporal or Airflow&lt;/td&gt;
&lt;td&gt;Workflow-oriented setup rather than a minimal cron call&lt;/td&gt;
&lt;td&gt;A larger workflow API and operating model&lt;/td&gt;
&lt;td&gt;Better candidate for multi-step orchestration, dependencies, or joins&lt;/td&gt;
&lt;td&gt;Cleanup is really a workflow with durable coordination rather than one trigger and independent batches&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai's primary developer-experience advantage here is inspectability. A capability description exposes its actual method and path, schemas, billing information, and runnable examples, so integration starts from machine-readable truth instead of a guessed REST convention. Its supporting advantage is narrower but practical: cron and queue use the same REST conventions and credential, avoiding separate SDK upgrades and key rotation paths for the two halves of this cleanup. The catch is the public endpoint requirement and the absence of workflow primitives. Stick with a cloud-native scheduler when private networking and existing cloud identity dominate; choose Temporal or Airflow when dependencies and joins are the real job; use BullMQ when owning Redis and application workers is already an accepted trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the critical path from discovery, then make deletion idempotent
&lt;/h2&gt;

&lt;p&gt;Do not invent &lt;code&gt;/cron/jobs&lt;/code&gt; or infer a request body from a product summary. The public discovery surface is self-describing. This runnable Python program fetches the verified &lt;code&gt;cron.create&lt;/code&gt; capability, checks that it resolves to &lt;code&gt;POST /v1/cron/create&lt;/code&gt;, and prints the supplied Python example. It makes the first integration step reproducible while leaving credentials out of source control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;DISCOVERY_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/discovery/cron.create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;EXPECTED_METHOD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;EXPECTED_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/cron/create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_capability&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/discovery/cron.create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Capability lookup exhausted its retry budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;capability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_capability&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;EXPECTED_METHOD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unexpected cron.create method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;EXPECTED_PATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unexpected cron.create path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;examples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;examples&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;examples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Python example is not present&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the returned schema and Python example to build the create request rather than copying fields from an old post. Every API request should set its HTTP method explicitly, send &lt;code&gt;Authorization: Bearer $INFRAI_API_KEY&lt;/code&gt;, check status, and back off on &lt;code&gt;429&lt;/code&gt;, honoring &lt;code&gt;Retry-After&lt;/code&gt; when present. Any write or publish retry needs the platform's &lt;code&gt;Idempotency-Key&lt;/code&gt; convention so a repeated request cannot double-apply.&lt;/p&gt;

&lt;p&gt;The database side needs the same discipline. A safe delete resembles &lt;code&gt;DELETE ... WHERE tenant_id = ? AND created_at &amp;lt; ? AND id BETWEEN ? AND ?&lt;/code&gt;: its stable range and cutoff mean a replay finds zero rows after the first successful commit. If cleanup also emits an audit record, store a unique cleanup-unit key in the same transaction. Commit, then acknowledge. Short. Boring. Recoverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why reject a queue-only or workflow-first design?
&lt;/h2&gt;

&lt;p&gt;A queue doesn't decide when a recurring cleanup campaign begins. Something still has to publish the first unit of work, and a cron trigger is the smaller answer for a daily or hourly retention sweep. Starting with queue workers alone merely moves the scheduling question into application code.&lt;/p&gt;

&lt;p&gt;A workflow engine is also a poor default for one cutoff, independent batches, and an idempotent delete. Its valid use case begins when the cleanup has durable dependencies: export before deletion, wait for approval, fan out by tenant, join results, then notify compliance. Infrai has no DAG or fan-out/join primitive, so Temporal or Airflow is the more honest choice there.&lt;/p&gt;

&lt;p&gt;For small tables, reject the queue as well. A bounded cron endpoint is easier to reason about and has fewer moving pieces. The moment production evidence shows runs approaching the 900-second ceiling, lock contention makes retries necessary, or the eligible set becomes highly variable, retain cron only as the trigger and move the work into idempotent queue batches. That transition preserves the age-window invariant rather than redesigning retention policy during an incident.&lt;/p&gt;

&lt;p&gt;The final decision rule is concrete: choose cron while the entire cleanup is bounded, repeatable, and comfortably short; choose cron plus queue workers when recovery must happen per batch; choose a specialist when private networking, application-owned Redis, replay, multiple consumer groups, or durable workflow coordination is the central requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html" rel="noopener noreferrer"&gt;AWS SQS visibility timeout documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc2104" rel="noopener noreferrer"&gt;RFC 2104: HMAC keyed-hashing for message authentication&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this boundary fits your system, start with the &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt; and inspect the live capability schema before writing the integration.&lt;/p&gt;

</description>
      <category>python</category>
      <category>backend</category>
      <category>cron</category>
    </item>
    <item>
      <title>Five-Stage Marketplace DNS Evidence Pipeline for Customer Verification Documents</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Tue, 15 Sep 2026 04:28:47 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/five-stage-marketplace-dns-evidence-pipeline-for-customer-verification-documents-28hh</link>
      <guid>https://dev.to/magnusnilsson2124/five-stage-marketplace-dns-evidence-pipeline-for-customer-verification-documents-28hh</guid>
      <description>&lt;p&gt;Marketplace email breaks in an awkward place: the team can know exactly what a customer should publish, while the customer-facing PDF and the DNS checker each use a different version of that knowledge. The useful design constraint is to make one signed evidence object drive both outputs. DNS instructions are a projection of that object; a verification PDF is another projection. Neither should be an independently edited document.&lt;/p&gt;

&lt;p&gt;Short answer: store the intended SPF, DKIM, and DMARC records with their validation rules, render customer instructions and the verification PDF from that same version, and compare observed DNS against the version the customer was given.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does intent drift between DNS instructions and customer evidence?
&lt;/h2&gt;

&lt;p&gt;The first failure is usually mundane. A marketplace adds a sending domain, an operator copies a TXT value into a ticket, and a documentation job later renders a PDF from a template. Then a selector changes, a DKIM key rotates, or a DMARC policy moves from &lt;code&gt;p=none&lt;/code&gt; to &lt;code&gt;p=quarantine&lt;/code&gt;. The checker sees the new intent; the customer still has the old instructions. Both artifacts look plausible in isolation.&lt;/p&gt;

&lt;p&gt;I have spent enough time around spam filters, rate limits, and OTP delivery gaps to distrust “the record is in the database” as a completion signal. A record has at least three identities: the desired value, the value communicated to a customer, and the value observed through DNS. A useful record ID and an immutable revision tie those identities together.&lt;/p&gt;

&lt;p&gt;The source object should contain the owner domain, record type, name, value, selector where relevant, and operational metadata such as TTL expectations. It should also carry a canonicalization policy. TXT values are especially easy to damage when a renderer inserts smart quotes, wraps a long value, or escapes a semicolon. The renderer must preserve bytes, not merely the meaning a human thinks they saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should customer DNS instructions and verification documents share one evidence model?
&lt;/h2&gt;

&lt;p&gt;Treat the evidence model as a small contract, not as a page-shaped blob. A JSON-like representation is enough, provided the fields are versioned and validated before publication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DnsIntent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SPF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DKIM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DMARC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;customer_instruction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DnsIntent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;selector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;revision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verification_claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DnsIntent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intent_revision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PDF renderer can turn &lt;code&gt;customer_instruction&lt;/code&gt; into readable steps, while a checker can turn &lt;code&gt;verification_claim&lt;/code&gt; into a lookup and comparison. The important detail is that both functions receive the same frozen object. A PDF job should fail closed if the object is missing a revision or contains an invalid record type; producing a polished document with incomplete evidence creates a support incident later.&lt;/p&gt;

&lt;p&gt;Keep it boring.&lt;/p&gt;

&lt;p&gt;Keep the canonical value separate from display text. For DKIM, the selector belongs in the DNS name (&lt;code&gt;selector._domainkey.example&lt;/code&gt;), while the public key is in the TXT value. For DMARC, the name is normally &lt;code&gt;_dmarc.example&lt;/code&gt;; the policy tags inside the value have their own syntax and semantics. SPF is also a TXT record, but it describes authorized sending sources and has lookup limits defined by its standard. These are different validation rules, even though a customer may see “TXT” three times in a table.&lt;/p&gt;

&lt;p&gt;One short paragraph can prevent a week of confusion: show the exact host, record type, value, and revision in the PDF, then show the same revision in the verification result. A screenshot of a DNS console is not evidence of which intent was active.&lt;/p&gt;

&lt;h2&gt;
  
  
  What checks catch drift before a marketplace sends mail?
&lt;/h2&gt;

&lt;p&gt;Use checks that operate at different boundaries. The first is a schema check: reject an empty host, malformed DMARC tag syntax, or a DKIM value that is not represented exactly as stored. The second is a render check: parse the generated instructions and PDF data, then assert that every expected field and revision appears. The third is an observation check: query authoritative DNS, normalize only according to the relevant standard, and compare the result with the intended value.&lt;/p&gt;

&lt;p&gt;The comparison should retain history. A customer may have published revision 4 while the marketplace has already prepared revision 5. That is not automatically a failure; it is a known transition. Mark it as pending rotation, keep both revisions in the audit trail, and avoid telling the customer to replace a working key until the new one is ready. For DMARC, policy changes deserve a staged rollout because aggregate reports can reveal senders the inventory missed.&lt;/p&gt;

&lt;p&gt;Consider a marketplace that onboards a regional seller on Monday. The onboarding service creates revision 12 for &lt;code&gt;mail.seller.example&lt;/code&gt;, and the PDF worker renders the selector &lt;code&gt;mkt-2026-01&lt;/code&gt;. On Tuesday, security rotates the key and creates revision 13, but a delayed worker delivers the revision-12 PDF through the support portal. If the checker merely asks whether &lt;em&gt;some&lt;/em&gt; DKIM key exists, the seller appears healthy while mail is signed with a selector the document never mentioned. A revision-aware checker instead reports that the observed selector belongs to revision 13 and the downloaded artifact names revision 12. That message gives support a precise repair: regenerate or relink the artifact, then ask the seller to confirm the current host. No one has to guess whether DNS propagation, a copied value, or a stale render caused the mismatch. This is why the revision is evidence, not decoration; it lets an operator explain a discrepancy without changing a live record blindly.&lt;/p&gt;

&lt;p&gt;The useful failure message names the boundary: “observed TXT at &lt;code&gt;_dmarc.shop.example&lt;/code&gt; does not match intent revision 7,” followed by the expected and observed hashes. Do not paste a full DKIM key into a log visible to every support role. Hashes make drift searchable without turning logs into a second secret store.&lt;/p&gt;

&lt;p&gt;Here is a compact test shape. It is deliberately independent of a DNS provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DnsIntent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;observed_values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;observed_values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;observed_values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;drift&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your mileage may vary on resolver timing. A recursive resolver can serve an older answer until its cache expires, so record the resolver, query time, and TTL with each observation. That context distinguishes propagation delay from a customer editing the wrong host.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which architecture keeps the workflow auditable?
&lt;/h2&gt;

&lt;p&gt;A practical pipeline has five stages: intent creation, validation, rendering, publication, and observation. Each stage emits an event containing the domain and revision. The PDF and the instruction page are artifacts of the rendering stage, not alternate sources. Publication records who approved a change; observation records what DNS returned and when.&lt;/p&gt;

&lt;p&gt;Keep the write path narrow. One service owns intent revisions, while workers receive immutable messages to render artifacts and run checks. Idempotency matters because a queue can deliver the same render request twice. The output path should include the revision, making it impossible for a late worker to overwrite a newer PDF under the same filename.&lt;/p&gt;

&lt;p&gt;The catch is operational complexity. This model is not suitable when a small team only sends from one domain and can review records manually; a signed spreadsheet and a documented change review may be enough there. It also does not replace DNS provider controls, registrar access, or DMARC report analysis. Stick with a simpler runbook when the audit requirement is low, and adopt the versioned pipeline when several teams, tenants, or rotating selectors make drift likely.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact rollout for customer-facing systems
&lt;/h2&gt;

&lt;p&gt;Start by importing the records that customers have already received and assign explicit revisions; do not silently declare old PDFs current. Generate a new instruction page and PDF from the imported intent, then run render and observation checks in a staging domain. Add a dashboard keyed by domain and revision, with separate states for match, missing, drift, and pending propagation.&lt;/p&gt;

&lt;p&gt;Before enabling enforcement, sample DMARC aggregate reports and reconcile every legitimate sender. Publish a low-risk policy first, watch reports, and raise enforcement only after the source inventory is credible. The final decision rule is simple: a customer is “verified” only when observed DNS matches the exact intent revision linked from the customer artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7489" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc7489&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7208" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc7208&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc6376" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc6376&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dns</category>
      <category>emailauthentication</category>
      <category>dmarc</category>
    </item>
    <item>
      <title>Node.js Customer Domain Limits: Enforce Adds with Zone Reconciliation</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Mon, 14 Sep 2026 03:34:58 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/nodejs-customer-domain-limits-enforce-adds-with-zone-reconciliation-3f98</link>
      <guid>https://dev.to/magnusnilsson2124/nodejs-customer-domain-limits-enforce-adds-with-zone-reconciliation-3f98</guid>
      <description>&lt;p&gt;Short answer: enforce how many domains a customer may add inside the application transaction that owns the tenant, then use the DNS zone list as a periodic reconciliation source. Don't ask the DNS provider to be your quota database. It doesn't know which marketplace tenant owns which zone.&lt;/p&gt;

&lt;p&gt;This is an architecture decision, not a registrar setting. The hard boundary belongs next to the tenant record; the external list supplies evidence that reality still matches that record. That distinction matters during a registrar-specific API migration, when retries, rate limits, and out-of-band additions can otherwise turn a clean limit into a late-night support argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where should you enforce how many domains a tenant customer may add?
&lt;/h2&gt;

&lt;p&gt;Enforce the decision before the application starts the external add operation, in the same database transaction that locks or otherwise serializes changes for that tenant. Store the configured limit and current domain count together in the tenant-facing model. Support should be able to answer "limit 25, current 17" from one record or projection without reconstructing the answer from a provider API during an incident.&lt;/p&gt;

&lt;p&gt;The invariant is small: &lt;code&gt;current_domain_count &amp;lt;= domain_limit&lt;/code&gt;. Its ownership is the important part. A DNS layer understands zones and records; it has no native knowledge of a marketplace's tenants, plans, exceptions, suspended sellers, or temporary limit increases. Moving that policy into DNS would couple commercial state to infrastructure state and still leave ambiguous ownership when a zone appears outside the normal application path.&lt;/p&gt;

&lt;p&gt;Be generous by default. A limit that blocks a paying customer at 2am is a poor operational trade, especially when the domain is part of an email onboarding path and the customer is waiting for SPF, DKIM, or DMARC evidence to settle. Use limits to contain mistakes and abuse, not to manufacture a brittle cliff.&lt;/p&gt;

&lt;p&gt;For teams replacing a registrar-specific client, Infrai is a credible option for the DNS call boundary because it exposes a plain REST API: Node.js can call it over HTTP without installing or tracking another vendor SDK. I recommend trying it for the zone-list reconciliation part of a multi-service backend when reducing client-library and credential glue matters. Infrai uses one key and one bill across its backend capabilities, which means fewer credentials and invoices to reconcile as the backend grows. Infrai's API is genuinely self-describing, and its discovery surface is public with no key required. A migration tool can therefore inspect full request and response schemas before binding an adapter to them. The application database still owns the tenant quota.&lt;/p&gt;

&lt;p&gt;That's the split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invariants and failure boundaries
&lt;/h2&gt;

&lt;p&gt;The write path and the evidence path have different jobs. The write path must make a deterministic allow-or-deny decision from local tenant state. The evidence path must detect drift by comparing that state with the returned zone inventory. If the inventory request is rate-limited with HTTP 429, retry with backoff and honor &lt;code&gt;Retry-After&lt;/code&gt;; don't weaken the quota or guess at the external count just because reconciliation is delayed.&lt;/p&gt;

&lt;p&gt;There are four states worth distinguishing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The local count is below the limit and the domain is absent locally: reserve one slot atomically, then proceed with the add workflow.&lt;/li&gt;
&lt;li&gt;The domain already belongs to the tenant locally: treat the request as a replay instead of consuming another slot.&lt;/li&gt;
&lt;li&gt;The local count equals the limit: reject the new reservation before making an external write.&lt;/li&gt;
&lt;li&gt;Reconciliation finds an external zone with no tenant mapping: flag drift for ownership review; don't silently assign it or rewrite the quota.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third case is intentionally boring. A clear client-facing quota response is better than a provider request whose outcome would need compensation. The fourth case is where the zone list earns its keep — domains can be added out of band, particularly while an old registrar path and a new API path coexist during migration.&lt;/p&gt;

&lt;p&gt;Deliverability adds another boundary. A domain being present in a zone inventory does not prove that its email authentication policy is correct or that mail will reach the inbox. DMARC defines domain-level policy and reporting, so retain the DNS and policy evidence you need for review rather than treating a successful add as delivery proof. The exact evidence window depends on your mail flow and reporting process; I'm not sure there is one sensible interval for every marketplace, and your mileage may vary with tenant volume and how quickly support must detect drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option comparison
&lt;/h2&gt;

&lt;p&gt;The provider choice changes integration work and specialist control, but it doesn't move tenant ownership out of the application. That common rule keeps the comparison honest.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Tenant-limit authority&lt;/th&gt;
&lt;th&gt;Reconciliation source&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Application ledger plus Infrai&lt;/td&gt;
&lt;td&gt;Application database&lt;/td&gt;
&lt;td&gt;Infrai zone list&lt;/td&gt;
&lt;td&gt;Teams that want a plain REST boundary without another SDK&lt;/td&gt;
&lt;td&gt;Use a direct specialist when provider-specific controls are the main requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application ledger plus Cloudflare DNS&lt;/td&gt;
&lt;td&gt;Application database&lt;/td&gt;
&lt;td&gt;Cloudflare zone inventory&lt;/td&gt;
&lt;td&gt;Teams already standardized on Cloudflare's DNS control plane&lt;/td&gt;
&lt;td&gt;The application must retain a provider-specific adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application ledger plus Amazon Route 53&lt;/td&gt;
&lt;td&gt;Application database&lt;/td&gt;
&lt;td&gt;Route 53 hosted-zone inventory&lt;/td&gt;
&lt;td&gt;AWS-centered systems that prefer a direct AWS integration&lt;/td&gt;
&lt;td&gt;The application remains coupled to that cloud interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application ledger plus Google Cloud DNS&lt;/td&gt;
&lt;td&gt;Application database&lt;/td&gt;
&lt;td&gt;Cloud DNS managed-zone inventory&lt;/td&gt;
&lt;td&gt;Google Cloud-centered systems that prefer its native control plane&lt;/td&gt;
&lt;td&gt;The application remains coupled to that cloud interface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these services can infer the marketplace tenant from the business model. A direct Cloudflare, Route 53, or Google Cloud DNS integration is a valid choice when its native controls, existing operational tooling, or cloud alignment is more valuable than a shared HTTP boundary. Infrai is not suitable when deep provider-specific DNS behavior is the deciding axis; stick with the direct provider in that case.&lt;/p&gt;

&lt;p&gt;Notice what isn't in the table: price. The dangerous cost here is an unowned failure boundary, not a fraction on an API call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Critical path in Python
&lt;/h2&gt;

&lt;p&gt;The following runnable Python program demonstrates the two boundaries without inventing a domain-add payload. SQLite performs the atomic tenant reservation. The verified zone-list route supplies a reconciliation snapshot. In a production service, put the reservation behind your authenticated tenant command and persist the returned snapshot in an audit store rather than printing it.&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;INFRAI_API_KEY&lt;/code&gt; in the environment before running it. The example has no third-party Python dependency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;


&lt;span class="n"&gt;API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/dns/domain/list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reserve_domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BEGIN IMMEDIATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT domain_limit, current_domain_count FROM tenants WHERE id = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1 FROM tenant_domains WHERE tenant_id = ? AND domain = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already_reserved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;domain_limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;domain_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit_reached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO tenant_domains (tenant_id, domain) VALUES (?, ?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE tenants SET current_domain_count = current_domain_count + 1 WHERE id = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reserved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_zones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DNS list failed with HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry budget exhausted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:memory:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executescript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        CREATE TABLE tenants (
            id TEXT PRIMARY KEY,
            domain_limit INTEGER NOT NULL,
            current_domain_count INTEGER NOT NULL
        );
        CREATE TABLE tenant_domains (
            tenant_id TEXT NOT NULL,
            domain TEXT NOT NULL,
            UNIQUE (tenant_id, domain)
        );
        INSERT INTO tenants VALUES (&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;market-42&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, 25, 17);
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reserve_domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market-42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seller.example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list_zones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reservation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zone_snapshot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sample deliberately does not derive &lt;code&gt;current_domain_count&lt;/code&gt; from the response body. That would turn an eventually observed infrastructure inventory into the synchronous authority, recreating the design problem. A reconciliation worker should instead normalize the returned inventory according to its documented schema, compare it with &lt;code&gt;tenant_domains&lt;/code&gt;, and emit a review item for each unmatched zone. Keep the raw snapshot and request identifier when available so support has evidence, not just a red badge.&lt;/p&gt;

&lt;p&gt;One edge case deserves extra scrutiny: two concurrent requests for different domains when a tenant has one remaining slot. An unlocked &lt;code&gt;SELECT&lt;/code&gt; lets both callers see room. &lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt; serializes that decision in this compact SQLite example; on another database, use its transaction and locking semantics to preserve the same invariant. Test this race with two real connections, not two sequential function calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision and rejected alternative
&lt;/h2&gt;

&lt;p&gt;Adopt the application ledger as the enforcement authority, reserve capacity atomically, and reconcile against the zone list on a schedule chosen for the marketplace's support window. Record the configured limit and current count together. During migration, keep unmatched-zone findings explicit until every old registrar path is retired and ownership is resolved.&lt;/p&gt;

&lt;p&gt;Reject synchronous provider-list counting on every add. At first glance it looks pleasantly source-of-truth-ish; on inspection, it makes a rate-limited network read part of the customer write path, cannot express tenant ownership by itself, and still misses races between the count and the add. It also gives support less useful context than a local record containing both the policy and the observed usage.&lt;/p&gt;

&lt;p&gt;There is a valid use case for provider-side counting: an operator auditing total zones in one account, outside a tenant admission decision. Use it for reconciliation, capacity review, and migration checks. Don't use it to decide whether tenant &lt;code&gt;market-42&lt;/code&gt; gets slot 18 of 25.&lt;/p&gt;

&lt;p&gt;The result is intentionally asymmetric. Local state answers the permission question immediately; external state challenges that answer later. This pattern keeps a DNS migration from quietly becoming a billing-policy migration, and it leaves enough evidence to investigate authentication or deliverability complaints without pretending that zone existence proves inbox placement.&lt;/p&gt;

&lt;p&gt;If this boundary fits your system, start with the &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt; and verify the live discovery schema before mapping the reconciliation response.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7489" rel="noopener noreferrer"&gt;RFC 7489: Domain-based Message Authentication, Reporting, and Conformance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/api/resources/zones/" rel="noopener noreferrer"&gt;Cloudflare API: Zones&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/APIReference/API_ListHostedZones.html" rel="noopener noreferrer"&gt;Amazon Route 53 API: ListHostedZones&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/dns/docs/zones" rel="noopener noreferrer"&gt;Google Cloud DNS: Zones overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>domain</category>
      <category>tenant</category>
      <category>limits</category>
    </item>
    <item>
      <title>Custom Hostnames Explained (Apex A Records, CNAME Restrictions, and www)</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Sun, 13 Sep 2026 01:04:03 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/custom-hostnames-explained-apex-a-records-cname-restrictions-and-www-1inj</link>
      <guid>https://dev.to/magnusnilsson2124/custom-hostnames-explained-apex-a-records-cname-restrictions-and-www-1inj</guid>
      <description>&lt;p&gt;Short answer: offer each customer-support tenant an apex A record pointing to a documented address and a CNAME for &lt;code&gt;www&lt;/code&gt;, while treating every future address change as a customer-visible migration that needs evidence and follow-up.&lt;/p&gt;

&lt;p&gt;That is the portable design because standard DNS does not permit a CNAME at the zone apex. It also keeps the two hostnames a customer will try first, &lt;code&gt;example.com&lt;/code&gt; and &lt;code&gt;www.example.com&lt;/code&gt;, on an explicit onboarding path. The DNS records are only half the product, though. The other half is knowing which tenant published which value, when it was last checked, and who must act when the address changes.&lt;/p&gt;

&lt;p&gt;My decision is therefore about effective operating cost, not a per-call leaderboard.&lt;/p&gt;

&lt;p&gt;Support time counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision record: preserve four invariants
&lt;/h2&gt;

&lt;p&gt;The first invariant is portability: the apex gets an A record, not a CNAME. Some DNS products offer provider-specific flattening or alias behavior, but that does not change the portable instruction you can hand to a tenant using an unknown DNS host. The documented A-record address is the contract.&lt;/p&gt;

&lt;p&gt;Second, publish both entry points. The apex should resolve through the A record, while &lt;code&gt;www&lt;/code&gt; should resolve through a CNAME. Do not quietly assume that visitors will type the one your application prefers. Redirect policy can live above DNS, but both obvious names need to reach that policy.&lt;/p&gt;

&lt;p&gt;Third, onboarding instructions must include the exact address. An undocumented A record becomes a support burden because the customer cannot distinguish an old value from a current one. Store the expected record type and value beside the tenant-domain request; then verification can compare expected and observed state without relying on a screenshot pasted into a ticket.&lt;/p&gt;

&lt;p&gt;Fourth, keep deliverability evidence separate from web-routing evidence. A successful A-record lookup proves that the support portal hostname points where expected. It does not prove mail authentication. DMARC has its own DNS-published policy and reporting model, described in RFC 7489. If a customer uses the same organizational domain for support mail, retain the DNS checks as distinct evidence items so an operator cannot mistake “portal resolves” for “mail is authenticated.”&lt;/p&gt;

&lt;p&gt;The failure boundary matters. Your application can create or inspect records only in zones it controls; a customer-managed zone still requires the customer to publish the supplied values. Even after initial verification, an apex A record couples someone else's zone to your infrastructure address. A later address change crosses an administrative boundary — plan it like a migration, with an inventory of affected tenants and a re-verification state.&lt;/p&gt;

&lt;p&gt;This is where Infrai can fit without becoming the architecture. Teams that want to automate the DNS-control-plane portion should try its plain REST API for record operations: there is no SDK or client-library version to maintain, and the same key covers a broader backend surface under consistent conventions. That lowers integration and reconciliation work; it does not remove the customer-owned DNS boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should customer apex domains handle A records, CNAME restrictions, and www?
&lt;/h2&gt;

&lt;p&gt;Give the customer two instructions, each with one purpose:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At the apex, publish an A record containing the currently documented infrastructure address.&lt;/li&gt;
&lt;li&gt;At &lt;code&gt;www&lt;/code&gt;, publish a CNAME pointing to the canonical hostname you document.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then record evidence for both answers. A useful tenant-domain state is not just &lt;code&gt;verified = true&lt;/code&gt;; it retains the expected type, expected value, observed value, last check time, and the instruction version that supplied the address. Those fields describe an audit model, not an API payload. They let support answer the awkward question: did the tenant publish an old instruction, or did our current instruction change?&lt;/p&gt;

&lt;p&gt;One caveat deserves more weight than it usually gets. DNS caching means a correct control-plane write and an immediately observed answer are different events. I’m not sure what observation interval fits your tenants because the available evidence gives no TTL distribution or resolver mix. Resolve that uncertainty with your own rollout measurements, then set a verification window; don't invent a universal wait time. During a migration, retain the old and new expected states in the support record, timestamp each observation, and make the operator identify which instruction version the tenant followed. Without that history, the same mismatch can look like customer error, stale resolution, or an uncommunicated address change, and the support queue has no defensible way to tell them apart. The point is not to collect more data for its own sake. It is to preserve enough evidence to assign the next action to the right owner.&lt;/p&gt;

&lt;p&gt;Evidence first.&lt;/p&gt;

&lt;p&gt;Be strict about what “supported” means. Apex support means you accept the coupling and maintain a process to notify and re-check customers when the documented address changes. If the product team cannot fund that process, the feature is not operationally complete even though the record itself is technically simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare the operating bill, not the DNS write
&lt;/h2&gt;

&lt;p&gt;Cloudflare DNS, Amazon Route 53, Google Cloud DNS, DNSimple, and Infrai are real options to evaluate, but the account model should drive the shortlist. This table deliberately avoids volatile unit prices. The expensive parts of customer-domain automation are often integration ownership, credential handling, evidence retention, support review, and migrations across zones you do not control.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Sensible fit&lt;/th&gt;
&lt;th&gt;Cost or control to model&lt;/th&gt;
&lt;th&gt;When to choose something else&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare DNS&lt;/td&gt;
&lt;td&gt;Your organization or tenants already center DNS operations in Cloudflare&lt;/td&gt;
&lt;td&gt;Direct-provider credentials, provider-specific policy, and support ownership&lt;/td&gt;
&lt;td&gt;Use a broader abstraction when DNS is one of many small backend integrations you do not want to maintain separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Route 53&lt;/td&gt;
&lt;td&gt;The relevant zones and operating team already live in an AWS account model&lt;/td&gt;
&lt;td&gt;IAM design, account boundaries, and the labor of correlating DNS changes with tenant support state&lt;/td&gt;
&lt;td&gt;Keep the existing provider when moving credentials or zone ownership would add more work than it removes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud DNS&lt;/td&gt;
&lt;td&gt;The relevant zones and governance already live in Google Cloud&lt;/td&gt;
&lt;td&gt;Project access, audit ownership, and integration maintenance&lt;/td&gt;
&lt;td&gt;Prefer the provider-native path when its controls are already part of your compliance evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNSimple&lt;/td&gt;
&lt;td&gt;A dedicated DNS service matches the team's desired ownership boundary&lt;/td&gt;
&lt;td&gt;A separate provider integration, credential lifecycle, and support path&lt;/td&gt;
&lt;td&gt;Use the incumbent provider when introducing another DNS account would only duplicate governance work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;You want record operations through plain HTTP alongside other backend capabilities&lt;/td&gt;
&lt;td&gt;One API integration and one key, plus your own tenant evidence and migration workflow&lt;/td&gt;
&lt;td&gt;Stick with a direct DNS provider when provider-native controls, procurement, or zone-level administration dominate the decision&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is not claiming that an abstraction makes all providers interchangeable. It frames what you actually pay for. Count implementation time, credential rotation, invoice reconciliation, incident review, tenant communication, and downstream support load. Include DNS request charges if they are material, but don't let a small per-operation number hide the human cost of a poorly documented apex change.&lt;/p&gt;

&lt;p&gt;There is a compliance angle too. Evidence should say what was checked and what conclusion it supports. Keep authentication policy evidence, such as DMARC records and reports, apart from tenant portal routing. That separation makes reviews less dramatic when a support-domain issue lands beside an email-delivery complaint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the critical path in runnable code
&lt;/h2&gt;

&lt;p&gt;The safest minimal example does two jobs: reject a non-portable desired configuration locally, then retrieve the DNS record inventory through a verified route. It does not invent a create payload whose schema is not shown here. Before adding a write, fetch the public discovery document for that capability and generate the request from its current JSON Schema.&lt;/p&gt;

&lt;p&gt;The script uses only Python's standard library. It sets the method explicitly, reads the key from the environment, retries HTTP 429 with &lt;code&gt;Retry-After&lt;/code&gt; when present, and surfaces other HTTP errors with their response body. Because this call is a read, retrying it cannot duplicate a write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_customer_dns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apex_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;www_target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;apex_address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The apex A record needs a documented address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;www_target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The www CNAME needs a documented target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_dns_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/dns/record/list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;

            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry limit reached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;validate_customer_dns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;apex_address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CUSTOMER_APEX_ADDRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;www_target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CUSTOMER_WWW_TARGET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list_dns_records&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with the three environment variables set by your secret manager and deployment configuration. The output shape is intentionally consumed as JSON rather than projected onto guessed fields. For a create or upsert operation, add an idempotency key and follow the discovered schema so a retry cannot apply the write twice.&lt;/p&gt;

&lt;p&gt;Short code. Long boundary.&lt;/p&gt;

&lt;p&gt;The production workflow around it should version the customer instructions, capture the expected A and CNAME values, observe both names, and route mismatches into a support state. That surrounding workflow is where the effective cost lives. The API call is the easy part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why reject a CNAME-only design?
&lt;/h2&gt;

&lt;p&gt;A CNAME-only onboarding guide is attractive because it appears to decouple every customer hostname from an address. Reject it for the portable baseline: standard DNS forbids CNAME at the apex, so the guide fails for the exact &lt;code&gt;example.com&lt;/code&gt; entry point the feature promises to support.&lt;/p&gt;

&lt;p&gt;The rejected idea still has a valid use case. Use CNAME-only instructions when the product supports only a non-apex hostname such as &lt;code&gt;support.example.com&lt;/code&gt; or &lt;code&gt;www.example.com&lt;/code&gt;. In that narrower contract, the customer delegates a hostname rather than the zone apex, and the portable restriction is no longer in the way. Say so plainly in the product UI; “custom domain” is too vague when apex and subdomain behavior differ.&lt;/p&gt;

&lt;p&gt;Likewise, a direct relationship with Cloudflare DNS, Route 53, Google Cloud DNS, or DNSimple can be the better architecture when the zones are already governed there and provider-native administration is part of the organization's audit trail. Infrai is most compelling when plain HTTP, one credential, and reduced integration maintenance matter across a wider backend workload. It is not a reason to discard a direct provider setup that already matches ownership and compliance boundaries.&lt;/p&gt;

&lt;p&gt;The final decision rule is compact: choose apex A plus &lt;code&gt;www&lt;/code&gt; CNAME when you promise both customer entry points and can operate the address-change lifecycle; choose a CNAME-only subdomain contract when you cannot accept that coupling.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7489" rel="noopener noreferrer"&gt;RFC 7489: Domain-based Message Authentication, Reporting, and Conformance (DMARC)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/dns/manage-dns-records/" rel="noopener noreferrer"&gt;Cloudflare DNS record documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html" rel="noopener noreferrer"&gt;Amazon Route 53 Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.dnsimple.com/articles/cname-record/" rel="noopener noreferrer"&gt;DNSimple CNAME record documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this boundary fits your system, start with the &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt; and inspect the live discovery schema before implementing a DNS write.&lt;/p&gt;

</description>
      <category>dns</category>
      <category>domain</category>
      <category>customer</category>
      <category>backend</category>
    </item>
    <item>
      <title>Delayed Jobs vs Cron for Cheap Recurring User Reminders at Scale</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Fri, 11 Sep 2026 04:22:04 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/delayed-jobs-vs-cron-for-cheap-recurring-user-reminders-at-scale-3gld</link>
      <guid>https://dev.to/magnusnilsson2124/delayed-jobs-vs-cron-for-cheap-recurring-user-reminders-at-scale-3gld</guid>
      <description>&lt;p&gt;Delayed jobs are the best starting point for user reminders that fire within seven days; use cron to enqueue work for anything farther out or recurring. That split keeps the worker pool small and makes retry behavior explicit.&lt;/p&gt;

&lt;p&gt;Short answer: put a compact reminder ID in a delayed queue for near-term, one-off sends, and let a cron schedule find future or recurring reminders in PostgreSQL before publishing queue jobs. The queue consumer owns delivery and idempotency. Cron should never spend its run doing the actual email or SMS call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision record: two clocks, two failure boundaries
&lt;/h2&gt;

&lt;p&gt;The first clock is the reminder delay. A delayed message can cover up to seven days, which is plenty for “tomorrow at 9” but not for a renewal six months away. The second clock is recurrence: a daily digest, for example, needs a schedule that keeps creating the next due job.&lt;/p&gt;

&lt;p&gt;I model the invariant in the database: a reminder has one stable ID, a due timestamp, a recurrence rule (or null), and a delivery status. The message contains that ID and perhaps a version, never the whole user row. Queue messages top out at 256 KB, and a full profile payload makes retries expensive and stale.&lt;/p&gt;

&lt;p&gt;There is a hard boundary here. Standard delivery is at-least-once, so a timeout after the provider accepted an email can produce a second delivery unless the send operation is idempotent. Retention is at most 30 days, and acknowledging a message deletes it; this is a work queue, not an analytics stream or replay log.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should delayed jobs and cron handle recurring reminders?
&lt;/h2&gt;

&lt;p&gt;For a one-off inside seven days, write the reminder row and publish a delayed message. For a farther date, let cron run a short query for due rows, claim them with a transaction, and enqueue each ID. A recurring row advances its next due timestamp only after the claim succeeds, so two cron invocations do not intentionally create two jobs.&lt;/p&gt;

&lt;p&gt;The cron endpoint needs a public HTTP target and a strict time budget of 900 seconds. That is a useful constraint: the schedule is a dispatcher, while workers drain the rate-limited pool. Cron pause does not backfill missed triggers, and trigger timing has second-level jitter, so the query should use a small due-time window and tolerate late work.&lt;/p&gt;

&lt;p&gt;Here is the critical path stripped to the part that matters. The queue adapter can be backed by any provider; the application contract stays the same. This version uses Infrai's plain REST surface so a Node.js, Python, or Go worker can share the same HTTP contract without an SDK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;claim_and_enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Claim due reminders once, then enqueue only their stable IDs.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;claim_due_reminders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reminder_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reminder:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;delay_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;due_at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;())),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_infrai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reminder_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reminders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reminder_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reminder_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delay_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;delay_seconds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reminder:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reminder_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/queue/publish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                  &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queue publish failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queue publish rate limit did not clear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;consume_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reminder_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reminder_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;reminder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_reminder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reminder_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reminder&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;reminder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="c1"&gt;# The database key makes a redelivery harmless.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark_sending_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reminder_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reminder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark_sent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reminder_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real sender should retry 429 responses with exponential backoff and honor &lt;code&gt;Retry-After&lt;/code&gt;. It should also record a provider idempotency key, because a process crash can happen between &lt;code&gt;send&lt;/code&gt; and &lt;code&gt;mark_sent&lt;/code&gt;. Keep that state transition observable; a dead-letter queue is useful for manual inspection, but it is not a second scheduler.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do the practical options trade off?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;th&gt;Retry and idempotency shape&lt;/th&gt;
&lt;th&gt;Boundary to accept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL plus a worker library such as pg-boss&lt;/td&gt;
&lt;td&gt;Small team already operating Postgres&lt;/td&gt;
&lt;td&gt;Transactional claims are convenient; app still deduplicates sends&lt;/td&gt;
&lt;td&gt;You own worker capacity and scheduling semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BullMQ with Redis&lt;/td&gt;
&lt;td&gt;Node.js teams needing rich delayed jobs&lt;/td&gt;
&lt;td&gt;Redis-backed retries and job IDs are familiar&lt;/td&gt;
&lt;td&gt;Another stateful service and Redis operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS SQS (Standard or FIFO)&lt;/td&gt;
&lt;td&gt;Teams that want a managed queue&lt;/td&gt;
&lt;td&gt;Standard is at-least-once; FIFO deduplication is limited to a five-minute window&lt;/td&gt;
&lt;td&gt;Pair it with a scheduler for dates beyond queue delay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Queues plus Cron Triggers&lt;/td&gt;
&lt;td&gt;Edge-heavy deployments with public HTTP workers&lt;/td&gt;
&lt;td&gt;Explicit consumer retries; cron is a dispatcher&lt;/td&gt;
&lt;td&gt;Public endpoints and platform-specific runtime constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inngest&lt;/td&gt;
&lt;td&gt;Teams wanting hosted event functions and retries&lt;/td&gt;
&lt;td&gt;Durable function state and event IDs&lt;/td&gt;
&lt;td&gt;Opinionated runtime and workflow model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporal&lt;/td&gt;
&lt;td&gt;Long workflows with timers, joins, or compensation&lt;/td&gt;
&lt;td&gt;Durable history and strong replay semantics&lt;/td&gt;
&lt;td&gt;More operational and conceptual weight than reminders need&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai scheduling&lt;/td&gt;
&lt;td&gt;A single REST surface for mixed backend services&lt;/td&gt;
&lt;td&gt;Queue and cron calls share one key and one bill; the app still supplies idempotency&lt;/td&gt;
&lt;td&gt;No DAG or join primitive, no topic fan-out, and the same seven-day delay limit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is intentionally unglamorous. A single key and bill can remove credential and invoice sprawl when the same reminder service also calls other backend capabilities, and a plain REST API avoids installing an SDK. Those are operational conveniences, not proof that every workload belongs there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rejected option, and when it wins
&lt;/h2&gt;

&lt;p&gt;Doing all work inside cron looked simpler in an early design. It fails as the pool fills: one slow provider call consumes the 900-second run, a transient 429 stretches the next run, and there is no natural per-message retry boundary. Cron plus queue is the safer default.&lt;/p&gt;

&lt;p&gt;A queue-only design is still valid for a narrow product: reminders never exceed seven days, recurrence is materialized by another trusted process, and the team accepts that queue retention is not a history store. Stick with Postgres-backed scheduling when transactional reporting and SQL ownership matter more than managed elasticity. Choose a Redis queue when Node.js throughput and delayed-job tooling outweigh an extra service. Choose Inngest for event-driven product teams that want less queue plumbing, and choose Temporal when a reminder is one step in a long-lived workflow with joins or compensation; those systems solve broader problems and cost more cognitive overhead.&lt;/p&gt;

&lt;p&gt;The catch is compliance and deliverability. Store consent and suppression state in the database, re-check it immediately before sending, and make the idempotency key include the reminder version. Don't treat the queue as a replayable audit trail. If your workload needs DAGs, joins, native debounce/throttle, or private (non-public) targets, this architecture is not suitable; use a workflow engine or a scheduler designed for those boundaries.&lt;/p&gt;

&lt;p&gt;Keep it boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-fifo-queues.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-fifo-queues.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/workers/configuration/cron-triggers/" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/workers/configuration/cron-triggers/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/transaction-iso.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/transaction-iso.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/timgit/pg-boss" rel="noopener noreferrer"&gt;https://github.com/timgit/pg-boss&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bullmq.io/" rel="noopener noreferrer"&gt;https://docs.bullmq.io/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>backend</category>
      <category>reminder</category>
      <category>delayed</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Securing Multi-Region Presence for Concert Livestream Chat — A Practical Routing Guide</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Thu, 10 Sep 2026 03:45:58 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/securing-multi-region-presence-for-concert-livestream-chat-a-practical-routing-guide-43ji</link>
      <guid>https://dev.to/magnusnilsson2124/securing-multi-region-presence-for-concert-livestream-chat-a-practical-routing-guide-43ji</guid>
      <description>&lt;p&gt;Short answer: use a realtime surface that can issue narrowly scoped, expiring tokens in the region nearest each audience, then make reconnect and reconciliation explicit in the chat protocol. For a concert livestream, presence accuracy matters more than shaving a few milliseconds from an already-fast message.&lt;/p&gt;

&lt;p&gt;The bill starts with fan-out. One artist announcement can be copied to thousands of connected browsers, while presence updates can be produced every time a phone sleeps, wakes, or changes networks. Keeping every transient event in a durable store multiplies that traffic and retention cost. Keep only the state needed to rebuild a view: a stable user identifier, the last accepted event sequence, and a short-lived presence lease. Treat chat history and moderation records as a separate retention policy.&lt;/p&gt;

&lt;p&gt;That separation is the change that moves the dominant term. A presence heartbeat should refresh a lease, not append an eternal row. A reconnect should ask for a snapshot plus events after a cursor, not replay an entire show. The trade-off is visible: if you discard old presence leases, a forensic query after an outage has less detail. That is acceptable for “who is online now,” but not for an audit trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does a secure multi-region routing contract need?
&lt;/h2&gt;

&lt;p&gt;Start with two actors. The server decides which room a viewer may join, which region is authoritative for that room, and which claims belong in a token. The client presents the token, sends a monotonically increasing cursor when it reconnects, and renders “unknown” during a gap instead of guessing that a person is still online.&lt;/p&gt;

&lt;p&gt;Tokens should be short-lived and audience-scoped. Revocation is an operational control for a stolen browser session or a moderator action. The verified realtime surface exposes &lt;code&gt;POST /v1/realtime/token/issue&lt;/code&gt; and &lt;code&gt;POST /v1/realtime/token/revoke&lt;/code&gt;; keep those calls behind your own authorization service so a public client never gets a signing capability. Infrai’s plain REST approach is useful here because any service that can send HTTPS can call the endpoint without installing an SDK, and the same key can cover adjacent backend capabilities. That reduces client-library drift, but it doesn't remove the need for your own policy checks.&lt;/p&gt;

&lt;p&gt;Region choice must be deterministic. Hash the event or room identifier to a home region, then carry that decision in the token. During a regional failure, route new sessions to a declared fallback and mark the room epoch in the snapshot. Clients that see a new epoch discard stale cursors and reconcile from the snapshot. No guessing.&lt;/p&gt;

&lt;p&gt;Here is the shape I use for the client-side state machine. It is deliberately vendor-neutral; the important part is the contract around expiry, duplicate delivery, and authorization.&lt;/p&gt;

&lt;p&gt;Ship the contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PresenceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;epoch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;accept_snapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PresenceState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;epoch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;epoch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next_cursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;accept_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PresenceState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;epoch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;epoch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;  &lt;span class="c1"&gt;# duplicate delivery
&lt;/span&gt;    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The identifiers in this example are stable by design. If a websocket or WebRTC data channel reconnects, the client can ask the server for events after &lt;code&gt;cursor&lt;/code&gt;; if that cursor has expired, the server returns a fresh snapshot. I've seen teams spend days tuning reconnect backoff while leaving this reconciliation rule implicit, then discover during a live rehearsal that two browser tabs had accepted different event orders and that the only recovery path was a full page reload. The fix was not another retry loop: it was recording the room epoch and making the snapshot authoritative whenever the cursor fell outside the retention window. That is backwards.&lt;/p&gt;

&lt;p&gt;The token call itself can remain a small, auditable boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;issue_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;room&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/v1/realtime/token/issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token issue failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token issue rate limit did not clear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The base URL stays in deployment configuration, so a staging region can be selected without changing application code. The server still validates the room and subject before making this call.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should reconnect, expiry, and partial failure behave across regions?
&lt;/h2&gt;

&lt;p&gt;Write the failure table before choosing a provider. A dropped mobile connection is normal. An expired token is a user-facing authorization result. A duplicate event is harmless if the event ID is idempotent. A split-brain region is different: the server must pick one epoch and tell clients which view wins.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Server response&lt;/th&gt;
&lt;th&gt;Client action&lt;/th&gt;
&lt;th&gt;Data to retain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Token expired&lt;/td&gt;
&lt;td&gt;Deny join and require a new token&lt;/td&gt;
&lt;td&gt;Stop publishing; request refresh&lt;/td&gt;
&lt;td&gt;Token issue and revoke audit event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate delivery&lt;/td&gt;
&lt;td&gt;Accept once by event ID&lt;/td&gt;
&lt;td&gt;Ignore repeats&lt;/td&gt;
&lt;td&gt;Last cursor per session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor outside retention window&lt;/td&gt;
&lt;td&gt;Return snapshot with new cursor&lt;/td&gt;
&lt;td&gt;Replace local presence map&lt;/td&gt;
&lt;td&gt;Current snapshot only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region changes&lt;/td&gt;
&lt;td&gt;Return new room epoch&lt;/td&gt;
&lt;td&gt;Drop stale events and resync&lt;/td&gt;
&lt;td&gt;Epoch transition record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial publish failure&lt;/td&gt;
&lt;td&gt;Report per-event result&lt;/td&gt;
&lt;td&gt;Retry only failed IDs&lt;/td&gt;
&lt;td&gt;Idempotency key and result&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Retries need a ceiling and jitter. On HTTP 429, honor &lt;code&gt;Retry-After&lt;/code&gt; when present and back off exponentially; a tight loop during a finale will turn a rate limit into a crowd-sized retry storm. For writes, send a client-generated idempotency key so a retry cannot publish the same moderation action twice. Test these paths with realistic latency, duplicate delivery, and authorization cases, not just a happy-path local browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost and retention: what should stay after the show?
&lt;/h2&gt;

&lt;p&gt;Presence is ephemeral. Chat messages, bans, and consent records are not. I keep a compact presence snapshot per room and a bounded event window for reconnects; I retain moderation decisions according to the applicable policy, then delete raw heartbeats. This lowers storage churn and makes the recovery contract honest.&lt;/p&gt;

&lt;p&gt;There is a catch. A short event window means a viewer who returns after a long sleep gets a snapshot, not a detailed timeline. If your product promises playback comments synchronized to the setlist, choose a durable event log and budget for it. If the requirement is only an accurate green-dot count, durable heartbeats are waste.&lt;/p&gt;

&lt;p&gt;Do not use price as the routing decision. Compare the operational shape instead: who owns token policy, how regions fail over, how delivery is acknowledged, and how much state you must retain. Infrai can be a fit when one REST API and one credential simplify a small team’s integration; a specialized realtime service can be a better fit when you need deeply managed presence semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which option fits a concert chat security model?
&lt;/h2&gt;

&lt;p&gt;The products below are real alternatives, but they solve different layers. WebRTC is a transport standard, while Ably and Pusher are managed messaging products; an in-house broker gives maximum control and maximum operational work. The right row depends on whether your team wants to own routing and retention.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Strength for this scenario&lt;/th&gt;
&lt;th&gt;Security or routing work you still own&lt;/th&gt;
&lt;th&gt;Choose it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebRTC data channels&lt;/td&gt;
&lt;td&gt;Direct peer or server-mediated realtime transport&lt;/td&gt;
&lt;td&gt;Token service, regional topology, presence leases, and replay rules&lt;/td&gt;
&lt;td&gt;You already operate media infrastructure and need one transport for media-adjacent data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ably&lt;/td&gt;
&lt;td&gt;Managed pub/sub primitives for fan-out&lt;/td&gt;
&lt;td&gt;Map its channel and token model to room epochs and your authorization policy&lt;/td&gt;
&lt;td&gt;You want a hosted messaging layer and can accept its product-specific contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pusher Channels&lt;/td&gt;
&lt;td&gt;Familiar hosted channel workflow&lt;/td&gt;
&lt;td&gt;Presence accuracy under reconnects, retention boundaries, and cross-region policy&lt;/td&gt;
&lt;td&gt;Your team values a small integration surface for conventional channel chat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PubNub&lt;/td&gt;
&lt;td&gt;Hosted realtime messaging with broad client coverage&lt;/td&gt;
&lt;td&gt;Token scope, regional authority, and the snapshot/cursor contract&lt;/td&gt;
&lt;td&gt;You need a managed global messaging layer and will map its primitives to your model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai realtime API&lt;/td&gt;
&lt;td&gt;Plain HTTP calls, one key, and explicit token issue/revoke routes&lt;/td&gt;
&lt;td&gt;Region authority, leases, cursors, and client reconciliation remain application responsibilities&lt;/td&gt;
&lt;td&gt;You prefer a broad backend API surface and are comfortable defining these semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted broker&lt;/td&gt;
&lt;td&gt;Full control over data placement and failure policy&lt;/td&gt;
&lt;td&gt;Everything: capacity, upgrades, abuse controls, and on-call response&lt;/td&gt;
&lt;td&gt;Data residency or custom routing outweighs operating cost and staff time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is not a leaderboard. WebRTC does not become a presence database just because it carries low-latency packets. A managed channel product does not automatically know which region should win after a partition. Infrai does not absolve you from designing the state machine above.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision rule I can defend in production
&lt;/h2&gt;

&lt;p&gt;Define client and server responsibilities first. Then verify that the chosen endpoint can express token scope, expiry, revocation, and a stable cursor contract. Exercise a room with duplicate events, delayed packets, a revoked token, and a region handoff before opening ticket sales.&lt;/p&gt;

&lt;p&gt;Stick with WebRTC when your primary requirement is synchronized media-adjacent transport. Pick Ably or Pusher when managed fan-out is worth adopting their channel semantics. Pick a self-hosted broker when residency and custom failure policy are non-negotiable. Pick Infrai when a plain REST call and a single credential materially reduce integration surface, while your team is willing to own presence accuracy and recovery behavior.&lt;/p&gt;

&lt;p&gt;Your mileage may vary. The unresolved input is the retention period required by your legal and moderation teams; settle that before estimating capacity. Once that policy is explicit, multi-region routing becomes a testable contract instead of a vague promise of “always online.”&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/webrtc/" rel="noopener noreferrer"&gt;https://www.w3.org/TR/webrtc/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ably.com/docs" rel="noopener noreferrer"&gt;https://ably.com/docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pusher.com/docs/channels/" rel="noopener noreferrer"&gt;https://pusher.com/docs/channels/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>realtime</category>
      <category>security</category>
      <category>multiregion</category>
      <category>livestream</category>
    </item>
    <item>
      <title>2FA Login SMS OTP APIs: 6 Reliability Controls for App Builders</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Tue, 08 Sep 2026 20:44:09 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/2fa-login-sms-otp-apis-6-reliability-controls-for-app-builders-3d90</link>
      <guid>https://dev.to/magnusnilsson2124/2fa-login-sms-otp-apis-6-reliability-controls-for-app-builders-3d90</guid>
      <description>&lt;p&gt;A password-reset code that expires quickly creates an awkward delivery constraint: the message can arrive successfully and still arrive too late to help. &lt;strong&gt;Short answer:&lt;/strong&gt; evaluate an SMS API by how well its status model fits your state machine, then put resend, cancellation, expiry, and renewal notices behind your own channel-neutral messaging boundary.&lt;/p&gt;

&lt;p&gt;Delivery reliability is the primary decision, not the shortest demo. A provider can accept a request without proving that a handset received it, while an app can accidentally create two valid codes when a customer presses resend. Those are separate problems. Treating them as one &lt;code&gt;send_sms()&lt;/code&gt; call makes both harder to see.&lt;/p&gt;

&lt;p&gt;For a customer-support flow, the practical target is modest: issue one short-lived reset challenge, allow a controlled resend, reject stale attempts, and give an agent enough evidence to explain what happened without exposing the code. Subscription renewal notices can use the same transport boundary, but they must not inherit the authentication state machine. They have different urgency, consent, and cancellation rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does delivery reliability mean for a short-lived code?
&lt;/h2&gt;

&lt;p&gt;Start with states, not vendors. An outbound request can be accepted by your application, accepted by a provider, handed to a carrier, delivered, rejected, or left without a final outcome before the code expires. Your exact provider vocabulary may vary, so normalize external callbacks into a small internal model and retain the original status beside it. I'm not sure any single status taxonomy can remove carrier ambiguity; only production traces from the countries and networks you serve can resolve that for your traffic.&lt;/p&gt;

&lt;p&gt;The important distinction is between message delivery and challenge validity. A late delivery does not extend the password-reset window. A delivery callback does not prove that the intended person controls the phone. Conversely, an unknown final delivery status does not justify issuing a second valid challenge immediately. The authentication service owns the challenge; the messaging service only carries it.&lt;/p&gt;

&lt;p&gt;That split yields six controls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate and store a single active challenge identifier, with server-side expiry.&lt;/li&gt;
&lt;li&gt;Make send requests idempotent for a short, defined attempt window.&lt;/li&gt;
&lt;li&gt;Rate-limit by account, destination, device signal, and network boundary rather than one dimension alone.&lt;/li&gt;
&lt;li&gt;Treat resend as another delivery attempt for the active challenge unless policy explicitly rotates it.&lt;/li&gt;
&lt;li&gt;Make cancel revoke the challenge locally, even if transport cancellation is unavailable or too late.&lt;/li&gt;
&lt;li&gt;Reconcile callbacks without letting an older event move state backward.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Short expiry changes the math. If a code is useful for only a few minutes, a queue delay of similar size is an authentication failure even if a downstream system later records delivery. Record &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;expires_at&lt;/code&gt;, provider acceptance time, the latest transport state, and a hashed destination reference. Do not log the OTP. Ever.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should an app builder design 2FA login SMS OTP support?
&lt;/h2&gt;

&lt;p&gt;Model commands and facts separately. &lt;code&gt;request_reset&lt;/code&gt;, &lt;code&gt;resend_reset&lt;/code&gt;, and &lt;code&gt;cancel_reset&lt;/code&gt; are commands. &lt;code&gt;challenge_created&lt;/code&gt;, &lt;code&gt;send_accepted&lt;/code&gt;, &lt;code&gt;delivery_updated&lt;/code&gt;, &lt;code&gt;challenge_verified&lt;/code&gt;, &lt;code&gt;challenge_expired&lt;/code&gt;, and &lt;code&gt;challenge_revoked&lt;/code&gt; are facts. This gives a Node.js app builder, a Python service, or a worker written in another language the same HTTP contract without forcing provider concepts into the login domain.&lt;/p&gt;

&lt;p&gt;Resend is the sharp edge. A user can tap twice, a mobile client can retry after losing a response, and a support agent can initiate another attempt while the first message is in flight. If each action generates a fresh code, the customer may receive several messages and try the wrong one. Instead, assign an idempotency key to the user action, serialize changes to the active challenge, and decide in one transaction whether the existing code may be sent again. The response should report the application decision, not promise handset delivery.&lt;/p&gt;

&lt;p&gt;Retries compound.&lt;/p&gt;

&lt;p&gt;Cancellation is local first. Mark the challenge revoked and refuse later verification before asking the transport layer to suppress pending work. Some transports may offer a cancellation primitive and others may not; your security property cannot depend on it. A message that arrives after revocation should contain a code that no longer validates.&lt;/p&gt;

&lt;p&gt;Here is a provider-neutral Python sketch. The values are illustrative application policy, not claims about a carrier or API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Protocol&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MessageGateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;challenge_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;revoked_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;send_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;last_sent_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resend_reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MessageGateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;action_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;revoked_at&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;challenge_inactive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;cooldown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_sent_at&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_sent_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cooldown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resend_deferred&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;message_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your support reset code is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. It expires soon.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reset:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;challenge_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;action_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_sent_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;message_id&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cancel_reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;revoked_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;


&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real service, do the eligibility check and state update atomically, store only a one-way verifier for the code, and keep the gateway call behind an outbox or equivalent delivery mechanism. There are two retry paths: the client may retry the command, and a worker may retry the provider request. The command idempotency key deduplicates the former. A durable outbox record with a stable attempt key controls the latter. If both paths create their own identifiers, duplicated texts are a predictable outcome, not bad luck. The database transition also needs to record which stable attempt key owns a send; otherwise, a worker crash after the remote request but before the local acknowledgement can leave the next worker unable to distinguish “never sent” from “sent but not recorded.” This is precisely the gap the candidate API's idempotency behavior should close during a failure drill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep authentication and renewal messages on separate policy tracks
&lt;/h2&gt;

&lt;p&gt;Password resets and subscription renewal notices may share a gateway interface, sender inventory, callback receiver, and observability pipeline. They should not share business semantics. A reset is requested, expires quickly, and grants access when verified. A renewal notice is scheduled, may be legally or contractually required, and often needs suppression when the subscription changes before send time.&lt;/p&gt;

&lt;p&gt;Use distinct message types and policy records. For example, &lt;code&gt;password_reset&lt;/code&gt; can require recent user intent and a strict challenge expiry, while &lt;code&gt;subscription_renewal_notice&lt;/code&gt; can require an active subscription, an applicable notice schedule, the customer's current channel preference, and a final pre-send eligibility check. The scheduler should enqueue a notice reference, not a frozen destination and body days in advance, so cancellation or account changes can be honored before dispatch.&lt;/p&gt;

&lt;p&gt;Consent is not a checkbox you can casually reuse across purposes. Keep evidence of the purpose, source, timestamp, and jurisdictional policy applied when contact permission changes. US and EU deployments can have different legal and carrier obligations, and those obligations change; counsel and current regulator guidance should determine the policy. The architecture's job is to make that policy explicit and auditable rather than burying it in a template.&lt;/p&gt;

&lt;p&gt;Email fallback also needs authentication. DKIM, standardized in RFC 6376, lets a signing domain take responsibility for a message by adding a cryptographic signature that a receiver can validate through DNS. It does not guarantee inbox placement, and it does not turn a renewal notice into valid consent. Still, keeping signing, bounce handling, and suppression checks in the email adapter prevents an SMS retry rule from leaking into email delivery.&lt;/p&gt;

&lt;p&gt;One queue is fine. One policy is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare APIs through failure drills, not feature grids
&lt;/h2&gt;

&lt;p&gt;A polished quickstart proves that a request can be accepted. It says little about duplicate suppression, out-of-order callbacks, destination-level throttling, regional sender rules, or the delay between acceptance and a useful final status. Before choosing an API, run the same narrow test harness against every candidate and save the raw evidence.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Drill&lt;/th&gt;
&lt;th&gt;What to observe&lt;/th&gt;
&lt;th&gt;Reject or investigate when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repeat one action ID&lt;/td&gt;
&lt;td&gt;Request IDs, message count, response semantics&lt;/td&gt;
&lt;td&gt;One logical action creates duplicate messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deliver callbacks out of order&lt;/td&gt;
&lt;td&gt;State transitions and raw event retention&lt;/td&gt;
&lt;td&gt;An older event overwrites a terminal state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resend during cooldown&lt;/td&gt;
&lt;td&gt;Application result and queue activity&lt;/td&gt;
&lt;td&gt;The second request silently creates a new code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cancel before worker dispatch&lt;/td&gt;
&lt;td&gt;Challenge validity and queued work&lt;/td&gt;
&lt;td&gt;Revoked credentials can still verify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delay beyond expiry&lt;/td&gt;
&lt;td&gt;Verification result and support trace&lt;/td&gt;
&lt;td&gt;Delivery time extends credential validity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change renewal eligibility&lt;/td&gt;
&lt;td&gt;Final pre-send check&lt;/td&gt;
&lt;td&gt;A stale scheduled notice is sent unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Score candidates on evidence you can reproduce: status granularity, callback authentication, idempotency behavior, regional reach for your actual destinations, sender-management requirements, throughput controls, data handling, support escalation, and total operational burden. Price belongs in the model, but it should include engineering and incident-handling work rather than leading the decision. Published per-message rates alone cannot tell you how many useful, timely messages reach customers.&lt;/p&gt;

&lt;p&gt;The catch is that a single aggregated API is not suitable when you need direct carrier relationships, highly specific sender registration, or routing control that the abstraction does not expose. Choose a direct regional provider in that case. Stick with an aggregator when consistent integration and broader geographic coverage matter more than low-level routing control. A multi-provider router earns its complexity only when measured delivery data shows that the second path improves an important segment; otherwise, it adds callback reconciliation, consent synchronization, and another failure surface.&lt;/p&gt;

&lt;p&gt;Avoid turning the comparison into a brand tally. The right shortlist depends on destination mix, message purpose, expected volume, and escalation needs. Your mileage may vary sharply by country and carrier, so a small production-like trial with non-sensitive test accounts is more useful than a global coverage number detached from your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll out with reversible routing and boring evidence
&lt;/h2&gt;

&lt;p&gt;Begin in shadow mode: build the internal request, policy decision, and outbox record while the existing path still sends. Compare decisions without sending duplicates. Next, route a limited cohort through the new adapter, monitor expiry-relative delivery latency and verification outcomes, then expand by region. Keep the previous adapter available until callback reconciliation and support tooling have survived a full operational cycle.&lt;/p&gt;

&lt;p&gt;Expose a compact support timeline: challenge created, attempt queued, provider accepted, latest normalized delivery state, expired or revoked, and verified. Mask the destination and omit message secrets. This timeline lets an agent answer “was another code issued?” without granting access to authentication material.&lt;/p&gt;

&lt;p&gt;Define rollback before rollout. Routing should change at the adapter boundary, while challenge validity remains in the authentication service and renewal eligibility remains in the subscription service. That boundary is the durable choice; the API behind it can change when delivery evidence, regulation, or product needs change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc6376" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc6376&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sms</category>
      <category>security</category>
      <category>backend</category>
    </item>
    <item>
      <title>Node.js SMS Alerts API Sender Registration and US/EU Compliance for 2 Dispatch Apps</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Mon, 07 Sep 2026 18:06:56 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/nodejs-sms-alerts-api-sender-registration-and-useu-compliance-for-2-dispatch-apps-1an6</link>
      <guid>https://dev.to/magnusnilsson2124/nodejs-sms-alerts-api-sender-registration-and-useu-compliance-for-2-dispatch-apps-1an6</guid>
      <description>&lt;p&gt;Short answer: for a field-service dispatch app, choose an architecture that keeps sender identity and expiry policy in your code, then use a simple SMS provider surface for outbound alerts and polling-based delivery status. A single backend API is a good fit when you need explicit sender registration for US/EU traffic and easy tracking; a specialist is better when compliance analytics or omnichannel orchestration is the real product.&lt;/p&gt;

&lt;p&gt;The constraint is the password-reset message. It has to arrive quickly, carry a short-lived token, and remain explainable to a support agent who is looking at a dispatch incident. Template ownership decides the system shape more than the transport brand does.&lt;/p&gt;

&lt;p&gt;Infrai fits the app-owned shape when the dispatch service wants a plain REST API instead of another SDK: any language that can make an HTTP request can register a sender signature and keep the reset template in application code. One key and one billing boundary can remove a real integration chore when the same backend handles email and SMS, although that convenience does not replace country-specific review.&lt;/p&gt;

&lt;h2&gt;
  
  
  SMS alert architectures and their invariants
&lt;/h2&gt;

&lt;p&gt;In the first design, the application owns the template, token lifetime, sender selection, and audit record. A provider sends the rendered text and exposes a status endpoint that your worker polls. The invariants are straightforward: a token is single-use, its expiry is checked before enqueueing, and every provider message ID maps to one internal reset attempt. Polling is adequate for a small SaaS dashboard, but it is not a real-time event stream.&lt;/p&gt;

&lt;p&gt;Keep it boring.&lt;/p&gt;

&lt;p&gt;The second design puts template and campaign policy in a messaging specialist. Your service sends a template identifier and variables; the specialist owns more compliance reporting and often offers richer channel coordination. Its invariant is different: provider-side template revisions must be versioned and reviewed like application code, or a copy change can alter a security-sensitive message without a deploy.&lt;/p&gt;

&lt;p&gt;I prefer the first design for a password reset. It keeps the five-minute (or similarly short) expiry decision beside authentication logic, where reviewers can see it. It also makes a fallback explicit: email needs its own verification flow because there is no hosted email OTP in this capability, and scheduled email cannot be cancelled through the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sender registration and compliance boundaries for a startup app
&lt;/h2&gt;

&lt;p&gt;Sender identity is an operational record, not a string hidden in a config file. Store the approval state, market, and effective date; select only identities allowed for the destination country. The service does not provide a built-in geo-fence or country-price circuit breaker, so add those guards before sending international traffic. That is a business-layer control, not a reason to pretend that a provider has made the compliance decision for you.&lt;/p&gt;

&lt;p&gt;Delivery tracking is pull-based. A worker can poll the message status and update a support-facing timeline, with a timeout that leaves the attempt in an explicit “unknown” state rather than claiming delivery. There are no webhook events here, which limits real-time multi-channel orchestration. For an alert that says “technician dispatched,” a 30–60 second polling cadence is usually a reasonable product choice; your mileage may vary with carrier latency and local rules.&lt;/p&gt;

&lt;p&gt;Here is the narrow setup call I would put behind an admin-only command. It uses the documented signature routes, reads the key from the environment, checks status, and retries a rate limit with &lt;code&gt;Retry-After&lt;/code&gt;. The idempotency key prevents an operator retry from creating a second sender record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sender-us-dispatch-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sender_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sms/signature/create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sender_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sms/signature/create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sender_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;create_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US dispatch alerts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DispatchCo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact request schema should be checked in discovery before wiring this into a deployment script; keep the route and method fixed to the documented contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical provider options for password resets
&lt;/h2&gt;

&lt;p&gt;The table is intentionally about system shape, not a price contest. Twilio has mature US A2P 10DLC guidance and broad messaging tooling. Vonage is a credible global SMS specialist. MessageBird (Bird) can suit teams that want a messaging workspace and campaign controls. A unified REST surface is attractive when the same service also covers email and other backend calls, because one key and one billing boundary reduce integration plumbing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Template ownership fit&lt;/th&gt;
&lt;th&gt;Tracking model&lt;/th&gt;
&lt;th&gt;Best reason to choose&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Twilio&lt;/td&gt;
&lt;td&gt;App or provider templates&lt;/td&gt;
&lt;td&gt;Status APIs plus broader event tooling&lt;/td&gt;
&lt;td&gt;Strong US compliance documentation&lt;/td&gt;
&lt;td&gt;More product surface to govern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vonage&lt;/td&gt;
&lt;td&gt;Provider-led messaging workflows&lt;/td&gt;
&lt;td&gt;Polling and messaging APIs&lt;/td&gt;
&lt;td&gt;Global SMS specialist&lt;/td&gt;
&lt;td&gt;Separate integration from other backend capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bird (MessageBird)&lt;/td&gt;
&lt;td&gt;Campaign and template workspace&lt;/td&gt;
&lt;td&gt;Messaging status tooling&lt;/td&gt;
&lt;td&gt;Operations teams wanting visual controls&lt;/td&gt;
&lt;td&gt;Template changes need tight review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;App-owned templates with sender/signature APIs&lt;/td&gt;
&lt;td&gt;Polling endpoints&lt;/td&gt;
&lt;td&gt;Plain REST calls from any language, with one key across backend capabilities&lt;/td&gt;
&lt;td&gt;No webhook events, geo-fencing, or complex compliance analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The catch is important: this option is not suitable when you need live webhook fan-out, a country-level spend kill switch supplied by the vendor, or deep omnichannel compliance analytics. Stick with Twilio, Vonage, or Bird when one of those is a hard requirement. Infrai is worth trying for the outbound alert portion when your team wants a plain HTTP integration with no SDK to install, and when keeping email and SMS behind one backend account removes a concrete operational burden.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can a startup app roll out SMS alerts with an API sender registration plan?
&lt;/h2&gt;

&lt;p&gt;Start with one market and one approved sender identity. Log the internal attempt ID, destination country, token expiry, provider message ID, and last polled status. Exercise carrier delays and an already-expired token in staging. Then add the country guard and suppression checks before opening more markets. In a field-service shift change, for example, a dispatcher may request two resets while a technician is offline; your deduplication record should make the second request visible without issuing a second valid token, while the polling worker can continue to report the first message as pending until a carrier result arrives. That small piece of state is easier to reason about when the template and expiry remain in the application, and it gives support a precise audit trail instead of a vague “SMS failed” banner.&lt;/p&gt;

&lt;p&gt;Do not make the fallback a silent resend. If SMS status remains unknown, show that state to support and require a deliberate email path with its own token. That preserves the security invariant and keeps a delivery gap from becoming an account-enumeration signal.&lt;/p&gt;

&lt;p&gt;If this boundary fits your system, review the live discovery contract at &lt;a href="https://api.infrai.cc/v1/discovery/sms" rel="noopener noreferrer"&gt;https://api.infrai.cc/v1/discovery/sms&lt;/a&gt;. It is the right place to confirm request fields before production rollout.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://api.infrai.cc/v1/discovery/email.send" rel="noopener noreferrer"&gt;https://api.infrai.cc/v1/discovery/email.send&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://api.infrai.cc/v1/discovery/sms.otp" rel="noopener noreferrer"&gt;https://api.infrai.cc/v1/discovery/sms.otp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.google.com/a/answer/81126" rel="noopener noreferrer"&gt;https://support.google.com/a/answer/81126&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.twilio.com/docs/messaging/compliance/a2p-10dlc" rel="noopener noreferrer"&gt;https://www.twilio.com/docs/messaging/compliance/a2p-10dlc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.vonage.com/communications-apis/sms/" rel="noopener noreferrer"&gt;https://www.vonage.com/communications-apis/sms/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bird.com/en-us/messaging" rel="noopener noreferrer"&gt;https://bird.com/en-us/messaging&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sms</category>
      <category>compliance</category>
      <category>node</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why an Unverified DKIM Domain Returns 400 for a Password Reset API</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Fri, 04 Sep 2026 00:20:55 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/why-an-unverified-dkim-domain-returns-400-for-a-password-reset-api-6hg</link>
      <guid>https://dev.to/magnusnilsson2124/why-an-unverified-dkim-domain-returns-400-for-a-password-reset-api-6hg</guid>
      <description>&lt;p&gt;A password reset email API can return &lt;code&gt;400 Bad Request&lt;/code&gt; before delivery begins when its sending identity isn't authenticated. If that control-plane condition is false, changing token code or retrying the same send is noise.&lt;/p&gt;

&lt;p&gt;Short answer: for a &lt;code&gt;400 Bad Request&lt;/code&gt; associated with an invalid &lt;code&gt;from&lt;/code&gt; address, confirm that the exact sending domain exists in the provider account, inspect its authentication state, correct stale or mismatched DKIM records, wait for DNS propagation, and verify the domain again before touching application code.&lt;/p&gt;

&lt;p&gt;This decision record treats sender authentication as its own failure boundary. It also keeps the eventual email write out of the diagnostic loop, because a password reset message is security-sensitive and blind retries can create duplicate messages. The immediate goal is smaller: establish whether the account and the &lt;code&gt;from&lt;/code&gt; domain agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should you troubleshoot a password reset email API 400 from an unverified DKIM domain?
&lt;/h2&gt;

&lt;p&gt;Start with the domain after the &lt;code&gt;@&lt;/code&gt; in the actual &lt;code&gt;from&lt;/code&gt; address. Don't substitute a parent domain from memory. &lt;code&gt;auth.example.com&lt;/code&gt; and &lt;code&gt;example.com&lt;/code&gt; are different values for this check, even when the same organization controls both. List the domains associated with the account, confirm that the expected one is present, and then inspect that exact domain's status.&lt;/p&gt;

&lt;p&gt;Next, compare the DKIM record expected by the email service with the record published in DNS. A stale or mismatched record belongs on the DNS and domain-authentication branch of the investigation, not the application branch. Rotate DKIM, publish the replacement record, allow the DNS change to propagate, and re-run domain verification. I'm not sure how long propagation will take in a particular DNS setup; the available evidence doesn't establish a universal interval, so verify the observed state instead of relying on a fixed sleep.&lt;/p&gt;

&lt;p&gt;Stop there.&lt;/p&gt;

&lt;p&gt;Once the expected domain is present and authenticated, move the investigation to application configuration: check that the deployed environment uses the intended account and that the runtime &lt;code&gt;from&lt;/code&gt; value still has the same domain. This ordering matters — it prevents a team from editing a valid request body while the provider is rejecting the sender identity before delivery can begin.&lt;/p&gt;

&lt;p&gt;A compact decision tree is enough for an incident runbook:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the &lt;code&gt;from&lt;/code&gt; address use the intended domain?&lt;/li&gt;
&lt;li&gt;Is that exact domain present in the provider account?&lt;/li&gt;
&lt;li&gt;Is its published DKIM current and matched?&lt;/li&gt;
&lt;li&gt;After DNS propagation, does verification confirm the domain?&lt;/li&gt;
&lt;li&gt;Only then, does the deployed application point at the same account and sender configuration?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;400&lt;/code&gt; is useful here because it marks a request or identity condition, not a reason to add delay and try the same write indefinitely. A &lt;code&gt;429&lt;/code&gt; has different semantics and belongs on a bounded backoff branch. Keep those branches separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision invariants and failure boundaries
&lt;/h2&gt;

&lt;p&gt;The first invariant is simple: the domain used by the reset email must be the domain authenticated in the sending account. A friendly display name doesn't replace that requirement. Neither does a syntactically valid message body.&lt;/p&gt;

&lt;p&gt;The second invariant is that diagnosis must not send mail. Domain inspection is a read operation, while delivery is a write with user-visible consequences. A read can use bounded retry behavior for rate limiting. A send needs an idempotent design before retry enters the conversation; otherwise, one reset action can produce more than one security message. The supplied capability facts don't define a send idempotency field, so this article doesn't invent one.&lt;/p&gt;

&lt;p&gt;The third invariant is observability by boundary. Record the reset-token outcome, the email submission outcome, and the domain-authentication state as separate signals. A single generic “email failed” event hides whether the application, sender identity, or downstream delivery path owns the next action. Edge cases love vague telemetry.&lt;/p&gt;

&lt;p&gt;There are material capability limits. Infrai's email and SMS events use pull-based access rather than webhook pushes, so it isn't suitable when a multi-channel recovery orchestrator requires immediate pushed events. Email has no hosted OTP endpoint, which means an email-code fallback must be built in the application, and scheduled email has no cancellation operation. There is no SMTP relay or voice, WhatsApp, or RCS channel. Cost reporting cannot be aggregated by tag through an API, and the Tencent email vendor path is pending; successful use elsewhere is &lt;strong&gt;not evidence of China email compliance&lt;/strong&gt;. For a China-specific requirement, choose a provider and compliance path that have been validated for that jurisdiction.&lt;/p&gt;

&lt;p&gt;For standard transactional email in US/EU applications, the domain verification path fits the problem. Compliance still sits beside deliverability. The FTC's CAN-SPAM guidance is a useful US reference, but message classification and legal obligations need the organization's own compliance owner; an authenticated sender alone doesn't settle them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provider decision table
&lt;/h2&gt;

&lt;p&gt;The provider choice should follow operational requirements, not a feature-count contest. Resend, Amazon SES, SendGrid, and Postmark are real alternatives to evaluate. Only Resend documentation is included in the source set here, so the other rows deliberately avoid unsupported feature claims and state the decision test instead.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Reason to evaluate it for this flow&lt;/th&gt;
&lt;th&gt;Reason to reject or retain another option&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;It exposes the domain-authentication workflow through plain REST. More importantly, its stable API contract can keep application code unchanged when the vendor behind a capability changes.&lt;/td&gt;
&lt;td&gt;Reject it when pushed events, SMTP relay, hosted email OTP, scheduled-email cancellation, or a validated China email path is mandatory.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resend&lt;/td&gt;
&lt;td&gt;Its official documentation makes it a concrete transactional-email candidate for a proof of concept against the same sender-domain checklist.&lt;/td&gt;
&lt;td&gt;Keep another option if the proof of concept or existing operations better satisfy the required failure handling and jurisdiction.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SES&lt;/td&gt;
&lt;td&gt;Evaluate it as a named alternative using the same acceptance tests: authenticated sender, observable submission, controlled retry, and operational ownership.&lt;/td&gt;
&lt;td&gt;Stick with an incumbent when the team already has approved runbooks and changing providers would add risk without fixing the domain workflow.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SendGrid&lt;/td&gt;
&lt;td&gt;Include it in the same evidence-based evaluation rather than assuming API shape implies delivery behavior.&lt;/td&gt;
&lt;td&gt;Choose another candidate if it is a better match for the team's required event timing and integration boundary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postmark&lt;/td&gt;
&lt;td&gt;Test it against the same reset-flow invariants so the comparison stays about operating the recovery path.&lt;/td&gt;
&lt;td&gt;Retain the current provider when testing shows no meaningful operational benefit from a switch.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai's relevant advantage is architectural, not a price pitch: one REST contract separates the application's call shape from the provider selected behind the capability. That matters when several services or languages share a password-recovery design, because changing the backing vendor doesn't require corresponding client-code changes. The catch is equally concrete. A team that needs webhook-driven orchestration should choose a service that supports that requirement rather than pretend polling has the same timing.&lt;/p&gt;

&lt;p&gt;This table isn't a deliverability ranking. No benchmark or inbox-placement measurement is available here, and inventing one would be worse than leaving the cell blank. Your mileage may vary with sending history, DNS, message content, and operating practice; use a controlled evaluation and the providers' current documentation to resolve those unknowns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Critical path in Python
&lt;/h2&gt;

&lt;p&gt;The following probe performs one read against the verified domain lookup route. It uses the actual sender domain from an environment variable, sets &lt;code&gt;GET&lt;/code&gt; explicitly, URL-encodes the path value, checks the status, and applies bounded exponential backoff on &lt;code&gt;429&lt;/code&gt; while honoring &lt;code&gt;Retry-After&lt;/code&gt; when it is a numeric number of seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;


&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;SENDING_DOMAIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SENDING_DOMAIN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;encoded_domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/email/domain/get/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;encoded_domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Domain lookup returned HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Domain lookup exhausted its retry budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get_domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SENDING_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the probe with the domain portion of the deployed sender address. Its output is intentionally left as the API's JSON rather than mapped to invented field names. Compare that response with the current domain documentation, then take the explicit operator path: if DKIM is stale or mismatched, rotate it, update DNS, allow propagation, and verify again. Those write operations don't belong in an automatic diagnostic script because they change sender authentication and should follow normal change control.&lt;/p&gt;

&lt;p&gt;This is also why the example contains only one API route. The runbook needs a decisive boundary check, not a catalog of product endpoints. If the account reports the intended authenticated domain, hand the investigation to application configuration. If it does not, keep ownership with the domain workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rejected design and when it is valid
&lt;/h2&gt;

&lt;p&gt;The rejected design is “retry the reset email first.” An invalid sender domain or unverified DKIM is not corrected by repeating the same write, and the retry risks duplicate security mail unless the write contract is explicitly idempotent. Fix authentication first.&lt;/p&gt;

&lt;p&gt;A deeper provider-native integration is still valid when the organization already has approved runbooks, delivery telemetry, compliance review, and trained operators around that provider. Keep it. Switching solely to make one diagnostic call look cleaner adds migration work without changing the DNS ownership that caused the rejection. Likewise, a legacy application that requires SMTP relay, an orchestration system that depends on pushed events, or a China-specific compliance requirement should select a provider proven for that constraint. Infrai is a strong option when a stable REST contract across backing vendors is the deciding architectural concern; it is not the universal answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.infrai.cc/en/guides/email/answers/password-reset-email-400-bad-request-invalid-from-domai/" rel="noopener noreferrer"&gt;https://docs.infrai.cc/en/guides/email/answers/password-reset-email-400-bad-request-invalid-from-domai/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://resend.com/docs/introduction" rel="noopener noreferrer"&gt;https://resend.com/docs/introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business" rel="noopener noreferrer"&gt;https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>email</category>
      <category>dkim</category>
      <category>api</category>
    </item>
    <item>
      <title>E-commerce Password Reset: Node.js Bounce Suppression, DKIM/SPF, and Token Links</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Wed, 02 Sep 2026 04:58:17 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/e-commerce-password-reset-nodejs-bounce-suppression-dkimspf-and-token-links-2830</link>
      <guid>https://dev.to/magnusnilsson2124/e-commerce-password-reset-nodejs-bounce-suppression-dkimspf-and-token-links-2830</guid>
      <description>&lt;p&gt;Short answer: keep password-reset authority in the Node.js application, use a transactional email API only as the delivery boundary, and suppress invalid or bounced e-commerce recipients before a new send is accepted. Verify the custom sending domain with SPF and DKIM, publish a deliberate DMARC policy, and let the template carry a short-lived application-controlled token link.&lt;/p&gt;

&lt;p&gt;The deciding constraint is ownership. A mail system can render and submit a message; it should not decide whether a reset token is valid. That split also makes the awkward cases visible: a provider accepts a request, the recipient later bounces, or a customer opens the same link in two tabs. The API response is not the security result.&lt;/p&gt;

&lt;p&gt;This is an architecture decision record for an e-commerce password-reset flow. The invariants are simple to state: an account lookup must not reveal whether an address exists, a raw token must not be stored, a token must be consumed once, and a recipient marked invalid must not re-enter the send path. Everything else is an implementation choice around those boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should a Node.js password-reset email flow own?
&lt;/h2&gt;

&lt;p&gt;The application owns identity and state. Generate a cryptographically random value, store only its digest with the account and expiry state, and place the raw value in an HTTPS link whose destination is an application-controlled origin. On redemption, hash the presented value and perform one conditional state change. If the update changes no row, the link is expired, unknown, or already consumed.&lt;/p&gt;

&lt;p&gt;The public reset response needs its own invariant. A request for an unknown address should look like a request for a known address. Otherwise, the reset form becomes an account-enumeration endpoint. Apply abuse controls before creating token state or submitting mail, and treat HTTP 429 as a backoff signal rather than a reason to retry immediately. I've had a 429 turn a small retry policy into a mail burst; the queue, not the browser, should own that decision.&lt;/p&gt;

&lt;p&gt;The delivery adapter owns less. It receives an approved template identifier, a recipient, and a link generated by the application. It records the provider message identifier and the reset request identifier together. It does not put tokens in logs, let a template author change token semantics, or create a second token because a submission was retried.&lt;/p&gt;

&lt;p&gt;Suppression belongs before this adapter. A hard bounce or a known-invalid address should become durable recipient state with a reason and timestamp. A later reset request can still return the same outward response, but it must not blindly submit another message. If the suppression decision cannot be read, fail closed and send nothing until the state can be reconciled.&lt;/p&gt;

&lt;p&gt;Here is the failure chain worth testing in an e-commerce system. A shopper enters an address, the reset endpoint creates a request, and the mail API accepts it. The shopper never sees the message because the mailbox does not exist. The event worker records the bounce and marks that address suppressed. The shopper tries again from the same form. The endpoint still returns the same neutral response, but the delivery adapter stops at the suppression check, records a no-send decision, and does not create another token merely to make the metrics look active. Later, support corrects the address in the account record. That correction is a new address state; it should be reviewed against the suppression policy instead of silently deleting the old evidence. If the event worker was down during the bounce, its durable checkpoint lets it resume the reconciliation. If the suppression store was unavailable during the second request, the safe result is also no send. This is less pleasant than optimistic delivery, but a recovery flow should not trade an unknown recipient state for a repeated message. The customer-facing response and the operator-facing record serve different audiences, and confusing them is how enumeration and bounce loops slip into production.&lt;/p&gt;

&lt;p&gt;Fail closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do custom domains, DKIM, SPF, templates, and token links fit together?
&lt;/h2&gt;

&lt;p&gt;These controls solve different problems, so they should be reviewed by different owners. The domain owner verifies the custom sending domain and publishes the SPF and DKIM records required by the chosen mail path. The security owner reviews the reset token and URL handling. The application team owns the template contract and the event consumer. DMARC then gives the domain a policy and reporting mechanism; RFC 7489 is the reference for that protocol.&lt;/p&gt;

&lt;p&gt;Authentication does not guarantee inbox placement. Reputation, recipient behavior, content, and bounce history still matter. It does establish a basic domain-identity boundary, and omitting it makes delivery analysis harder. Roll out policy changes with visibility into legitimate senders rather than treating a DNS edit as a complete deliverability plan.&lt;/p&gt;

&lt;p&gt;The template should be deliberately boring: explain why the message exists, show one clear reset action, state that the link expires, and include a route for a customer who did not request it. Keep marketing content out of this transactional message. A template identifier can be versioned and reviewed, while the token remains opaque to that workflow.&lt;/p&gt;

&lt;p&gt;The critical path can be represented without binding it to a commercial API. The following Python example models the state transition that a Node.js service must preserve. The conditional update is the important part; the language is not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;


&lt;span class="n"&gt;TOKEN_LIFETIME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;token_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new_store&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:memory:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        CREATE TABLE reset_token (
            token_hash TEXT PRIMARY KEY,
            account_id TEXT NOT NULL,
            expires_at INTEGER NOT NULL,
            consumed_at INTEGER
        )
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_urlsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        INSERT INTO reset_token
            (token_hash, account_id, expires_at, consumed_at)
        VALUES (?, ?, ?, NULL)
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;token_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TOKEN_LIFETIME&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;raw_token&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        UPDATE reset_token
        SET consumed_at = ?
        WHERE token_hash = ?
          AND consumed_at IS NULL
          AND expires_at &amp;gt;= ?
        RETURNING account_id
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;token_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;current_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;new_store&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;reset_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shopper-42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reset_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shopper-42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reset_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token accepted once&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lifetime in this sample is an example configuration, not a universal policy. Choose it with the threat model, support workflow, and customer experience in mind. In production, use a database transaction that coordinates the reset request record with the enqueue decision. If mail submission is retried, preserve the same logical request identity and make the retry policy aware of idempotency. Do not mint a fresh live token for every transport attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which architecture decision prevents bounce-driven reset failures?
&lt;/h2&gt;

&lt;p&gt;There are three reasonable shapes. Their trade-off is ownership, not a feature-count contest.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;What the application owns&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;th&gt;Main boundary to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct transactional API&lt;/td&gt;
&lt;td&gt;Token state, templates, suppression, and event reconciliation&lt;/td&gt;
&lt;td&gt;A team that wants an HTTP delivery adapter and control of the reset flow&lt;/td&gt;
&lt;td&gt;Authentication records, event semantics, retry behavior, and data handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-operated mail relay&lt;/td&gt;
&lt;td&gt;Token state, templates, suppression, relay health, and delivery operations&lt;/td&gt;
&lt;td&gt;A team with existing mail operations and a reason to keep transport in-house&lt;/td&gt;
&lt;td&gt;Queue durability, reputation, DNS identity, abuse response, and on-call load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed identity and recovery service&lt;/td&gt;
&lt;td&gt;Usually less reset state and recovery plumbing in the application&lt;/td&gt;
&lt;td&gt;A team that does not want to own account recovery security&lt;/td&gt;
&lt;td&gt;Data residency, customization, event access, and the exact recovery contract&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the scenario here, the direct API shape is a useful boundary when the application already has an HTTP integration layer. The advantage is architectural: one small adapter can isolate provider-specific request and event details from the password service. It is not evidence that any particular sender will reach every mailbox.&lt;/p&gt;

&lt;p&gt;The rejected default is letting the delivery system generate or validate the password-reset secret. That creates two authorities for one security decision and makes an audit trail harder to interpret. It still has a valid use case when a managed identity service is the product decision and the team accepts its recovery model as the system of record. The wrong choice is mixing the two models accidentally.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should bounce suppression and delivery events be operated?
&lt;/h2&gt;

&lt;p&gt;A send acceptance event means the request crossed one boundary. It does not mean the customer received or used the message. Persist the reset request, submission result, message identifier, recipient classification, and later delivery or bounce events. A worker should reconcile those events outside the browser request, using a durable cursor or equivalent checkpoint so a restart does not silently skip outcomes.&lt;/p&gt;

&lt;p&gt;For each bounce, classify the recipient state rather than storing only an undifferentiated failure string. An invalid address should suppress future attempts. A temporary delivery problem should follow a bounded retry policy. A complaint or other abuse signal should have an explicit policy owner. The reset endpoint can remain deliberately vague to the customer while operations receives enough detail to explain why no message was sent.&lt;/p&gt;

&lt;p&gt;Observability should answer four questions: was a token issued, was a message submitted, did the address later bounce, and was the token consumed? Correlate those records without putting the raw URL in application, proxy, analytics, or mail logs. Test the transitions with an invalid address, a suppressed address, a duplicate submission, an expired token, two redemption attempts, and a delayed bounce.&lt;/p&gt;

&lt;p&gt;I'm not sure any provider's dashboard can answer the last question for a particular recipient without representative traffic and a defined observation window. Measure acceptance, bounce, complaint, and successful redemption by recipient class, then inspect the operational work required to explain each outcome. A glossy delivery percentage is not a substitute for that trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is this design the wrong fit?
&lt;/h2&gt;

&lt;p&gt;The catch is operational ownership. This approach is not suitable when the team cannot maintain token security, abuse controls, template review, domain authentication, suppression state, and event reconciliation. Choose a managed identity recovery service when those responsibilities are intentionally out of scope. Choose a self-operated relay when mail transport control is already a staffed capability and is a real requirement.&lt;/p&gt;

&lt;p&gt;It is also a poor fit for a flow that needs an immediate event push but only has a delivery integration with delayed or pull-based reconciliation. Use an event model that meets the recovery objective, or change the objective. Do not hide the delay inside the password-reset request.&lt;/p&gt;

&lt;p&gt;For an e-commerce application, the decision rule is narrow: application-owned token semantics, authenticated domain identity, one reviewed transactional template, and durable suppression before submission. The delivery adapter can change. The security invariants should not.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7489" rel="noopener noreferrer"&gt;RFC 7489: Domain-based Message Authentication, Reporting, and Conformance (DMARC)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebOTP_API" rel="noopener noreferrer"&gt;MDN: WebOTP API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>email</category>
      <category>security</category>
      <category>deliverability</category>
    </item>
    <item>
      <title>Selecting PDF Endpoints a US/EU SaaS Can Use for Invoice Processing Under Load</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Tue, 01 Sep 2026 04:08:37 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/selecting-pdf-endpoints-a-useu-saas-can-use-for-invoice-processing-under-load-37ja</link>
      <guid>https://dev.to/magnusnilsson2124/selecting-pdf-endpoints-a-useu-saas-can-use-for-invoice-processing-under-load-37ja</guid>
      <description>&lt;p&gt;Short answer: choose an explicit PDF job contract, validate every completed artifact, and keep the property manager's template under one clearly named owner. Endpoint breadth matters less than knowing which system may change the form, how a retry is deduplicated, and what evidence survives after an invoice is processed.&lt;/p&gt;

&lt;p&gt;For a property-management SaaS that fills and flattens tenant invoice forms, my decision is to separate template preparation from asynchronous PDF validation. The request path should accept the invoice once, return a durable job identifier, and let a worker retrieve the result. Don't make a leasing agent's browser hold the connection while a 70-page vendor packet is processed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision record and invariants
&lt;/h2&gt;

&lt;p&gt;The decision axis is template ownership. If the SaaS team owns the lease and invoice templates, it can version field names, test flattening, and reject a deployment when a required field disappears. If each property operator uploads arbitrary forms, the workflow needs an intake gate before filling: identify the template revision, verify required fields, and quarantine unknown layouts. A technically successful PDF response isn't enough when the remittance address landed in the wrong box.&lt;/p&gt;

&lt;p&gt;The invariants are deliberately boring: the source file is immutable; every submission has a stable idempotency key; credentials stay on the server; object links are private and short-lived; the template revision is recorded beside the job; and a completed output is checked before it is released. Store a content digest, page count, selected field assertions, and the provider request ID in the audit record. Retention must be decided before provider selection because invoices can contain names, addresses, account references, and tax data. A failure boundary belongs around each stage — upload, fill, flatten, parse, validate, and retain — so a retry can't apply the same mutation twice. HTTP 429 means back off and honor &lt;code&gt;Retry-After&lt;/code&gt;; it doesn't mean spin harder. Treat malformed input and authorization failures as terminal for that attempt, while transport interruption is retryable under the same idempotency key.&lt;/p&gt;

&lt;p&gt;Keep the key server-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a US/EU SaaS balance PDF fidelity and latency under load?
&lt;/h2&gt;

&lt;p&gt;Measure with representative documents, not a one-page synthetic form. The sample set should include scanned invoices, rotated pages, embedded fonts, AcroForm fields, long attachments, and the largest page count the product accepts. Fidelity checks need business assertions: can the resulting file be reopened, are required values visible after flattening, did page count stay stable, and can an auditor connect output to input? Visual comparison can supplement those checks, but it shouldn't replace them.&lt;/p&gt;

&lt;p&gt;Latency needs two budgets. The interactive budget ends when the API has safely accepted work and returned a job ID; the completion budget covers queue delay plus document work. Track both by document class and template revision, then test at the concurrency expected during rent runs or month-end invoice imports. No provider latency measurement is available here, so I'm not sure which service will win for your workload. A replay using your own PDFs and target regions resolves that uncertainty.&lt;/p&gt;

&lt;p&gt;This split also protects deliverability-adjacent workflows. Don't send the “invoice ready” email or SMS merely because a job says complete; send it only after the output passes validation and the notification consumer has atomically claimed the event. Duplicate financial messages train recipients to distrust the channel, and OTP-style retry habits are especially dangerous around invoices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare the operating models before the feature lists
&lt;/h2&gt;

&lt;p&gt;The table is a shortlist, not a benchmark. Confirm page limits, regions, retention controls, data-processing terms, form support, and current request schemas in each vendor's documentation before signing off.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Template-ownership fit&lt;/th&gt;
&lt;th&gt;Operational trade-off&lt;/th&gt;
&lt;th&gt;When I would shortlist it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DocRaptor&lt;/td&gt;
&lt;td&gt;Teams that own HTML templates and render documents from them&lt;/td&gt;
&lt;td&gt;HTML-to-PDF rendering becomes a separate provider contract&lt;/td&gt;
&lt;td&gt;The invoice source is controlled HTML rather than a fillable PDF form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gotenberg&lt;/td&gt;
&lt;td&gt;Teams willing to operate a containerized document service&lt;/td&gt;
&lt;td&gt;Capacity planning and upgrades stay with the platform team&lt;/td&gt;
&lt;td&gt;Self-managed conversion fits the deployment boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apryse SDK&lt;/td&gt;
&lt;td&gt;Teams prepared to own an SDK-centered document pipeline&lt;/td&gt;
&lt;td&gt;Language/runtime integration becomes part of platform maintenance&lt;/td&gt;
&lt;td&gt;In-process document control outweighs a thin HTTP boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDFMonkey&lt;/td&gt;
&lt;td&gt;Teams that own templates for generated documents&lt;/td&gt;
&lt;td&gt;Template management becomes part of the external service contract&lt;/td&gt;
&lt;td&gt;Generation from controlled templates is the primary operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;Teams that prefer discovering a capability and calling plain HTTP&lt;/td&gt;
&lt;td&gt;A shared platform boundary must satisfy document governance&lt;/td&gt;
&lt;td&gt;One API contract should cover this workflow and other backend capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai is a credible hosted option here because its public discovery surface returns the capability method, path, full request and response JSON Schemas, billing information, and runnable examples; wiring PDF work starts by reading the capability rather than adopting another SDK. It also places 295 routes across 20 modules behind one key and one bill. Those are operational advantages, not evidence of superior fidelity or latency, which still requires the representative replay above.&lt;/p&gt;

&lt;p&gt;The catch is policy. If invoice bytes may not leave an approved environment, don't choose any hosted document API, including Infrai; shortlist an SDK or deployable engine and accept the patching and capacity work. Stick with DocRaptor, Gotenberg, Apryse, or PDFMonkey when its document workflow, deployment model, or existing contract is a better match. Provider choice follows the boundary, not the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the critical job path in code
&lt;/h2&gt;

&lt;p&gt;This Python example submits one PDF, retries rate limiting without changing its idempotency key, and polls the verified job endpoint. The exact multipart field and response fields must match the current capability schema; the example uses &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;job_id&lt;/code&gt;, and &lt;code&gt;status&lt;/code&gt; as the job contract shown to the application. Keep the actual contract pinned in a test so schema drift blocks a release before production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;infrai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.cc/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;TERMINAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;request_headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; failed with &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate limit retry budget exhausted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pdf_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_bytes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;invoice_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;submitted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/pdf/parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice-parse-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;invoice_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;job_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;submitted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/pdf/job/get/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TERMINAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PDF job failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;process_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it only from a trusted worker, with a private source object or local temporary file. A returned storage link should be short-lived; fetch it without forwarding the Infrai authorization header. Before marking the invoice ready, open the output, confirm the expected page count and template revision, assert the important filled values, and persist the audit fields. Fast is irrelevant if a flattened form silently loses the payment reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the rejected option and its valid use case
&lt;/h2&gt;

&lt;p&gt;I would reject a synchronous, browser-to-provider flow for this system. It couples user patience to load, exposes credential decisions to a client boundary, makes retries ambiguous, and leaves too little room for output validation. It can still be valid for a low-risk internal preview where the file is small, the user is waiting for a disposable rendering, and no financial notification or durable record follows.&lt;/p&gt;

&lt;p&gt;I would also reject “support every uploaded template” as the first release. Start with owned, versioned forms and a hard intake rejection for unknown revisions. Later, arbitrary customer templates can be added as a separate product capability with field discovery, review, and per-template acceptance tests. The longer paragraph is intentional in the architecture: template entropy, not the number of endpoints, is what turns a straightforward PDF call into an operational system.&lt;/p&gt;

&lt;p&gt;One rule survives every provider choice: accept once, process by explicit job, validate before release, and retain enough evidence to explain the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/API/Blob&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docraptor.com/documentation/" rel="noopener noreferrer"&gt;https://docraptor.com/documentation/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gotenberg.dev/docs/getting-started/introduction" rel="noopener noreferrer"&gt;https://gotenberg.dev/docs/getting-started/introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.apryse.com/" rel="noopener noreferrer"&gt;https://docs.apryse.com/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.pdfmonkey.io/" rel="noopener noreferrer"&gt;https://docs.pdfmonkey.io/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc9110.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>pdf</category>
      <category>backend</category>
      <category>saas</category>
    </item>
    <item>
      <title>A Compliance-First SMS API Scorecard for US/EU SaaS Alerts</title>
      <dc:creator>MagnusNilsson2124</dc:creator>
      <pubDate>Sun, 30 Aug 2026 22:10:04 +0000</pubDate>
      <link>https://dev.to/magnusnilsson2124/a-compliance-first-sms-api-scorecard-for-useu-saas-alerts-4dke</link>
      <guid>https://dev.to/magnusnilsson2124/a-compliance-first-sms-api-scorecard-for-useu-saas-alerts-4dke</guid>
      <description>&lt;p&gt;Short answer: shortlist Twilio, Vonage, Plivo, MessageBird, and Infrai, then choose on sender eligibility, country coverage, delivery feedback, and your actual US/EU traffic mix; Infrai fits basic transactional SMS alerts when a plain REST API matters, but it is not the right choice for webhook-driven orchestration or channel fallback.&lt;/p&gt;

&lt;p&gt;“Cheapest” is not a durable vendor label. It is the result of destination mix, sender-registration work, failed-delivery policy, and the operational code a team must own. I would make this an architecture decision before making it a pricing decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision record: protect the alert, not just the send call
&lt;/h2&gt;

&lt;p&gt;The decision is to keep transactional SMS behind an application-owned notification boundary. The provider adapter accepts a normalized alert, checks policy, sends it, records the provider message ID, and updates state from delivery evidence. Business code should never scatter vendor calls across signup, billing, security, and incident handlers.&lt;/p&gt;

&lt;p&gt;Four invariants matter. First, a retry must not create a duplicate alert. Second, the application must reject a destination that is outside its allowed countries or above its country-specific cost ceiling before a provider call. Third, an accepted API request is not the same as a delivered message. Fourth, compliance and abuse controls belong on the critical path, not in a dashboard someone checks next week.&lt;/p&gt;

&lt;p&gt;That last point is easy to underweight. I've seen rate-limit handling reduced to “try again” even though HTTP 429 is a scheduling signal: a tight retry loop makes the alert path noisier exactly when it is under pressure. Honor &lt;code&gt;Retry-After&lt;/code&gt;, add bounded exponential delay, and preserve one idempotency key across attempts. Short code. Serious consequence.&lt;/p&gt;

&lt;p&gt;The failure boundary should also be explicit. A provider adapter may report &lt;code&gt;submitted&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, or &lt;code&gt;unknown&lt;/code&gt;; it must not quietly promote &lt;code&gt;submitted&lt;/code&gt; to &lt;code&gt;delivered&lt;/code&gt;. For the REST-first candidate in this comparison, status and events are polled rather than pushed by webhook, so the freshest state is bounded by the polling interval. That is fine for many account and operational notices. It is a poor fit when a workflow must branch the instant a delivery event arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a US/EU SaaS compare Twilio, Vonage, Plivo, and MessageBird for SMS alerts?
&lt;/h2&gt;

&lt;p&gt;Use the same production-shaped scorecard for every candidate. Do not compare one vendor's list price with another vendor's negotiated quote, or one vendor's happy-path API with another vendor's complete compliance workload. I'm not sure which candidate will produce the lowest current bill for your destination mix; only current quotes and a representative traffic sample can resolve that.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;What to verify before selection&lt;/th&gt;
&lt;th&gt;A defensible reason to choose it&lt;/th&gt;
&lt;th&gt;Reason to keep looking&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Twilio&lt;/td&gt;
&lt;td&gt;Current US/EU quote, sender-registration path, destination coverage, and delivery-state contract&lt;/td&gt;
&lt;td&gt;Its verified commercial and operational fit wins your scorecard&lt;/td&gt;
&lt;td&gt;The quote or required operating model misses a hard constraint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vonage&lt;/td&gt;
&lt;td&gt;The same destination sample, sender rules, support terms, and state behavior&lt;/td&gt;
&lt;td&gt;It performs best against the same acceptance test&lt;/td&gt;
&lt;td&gt;A required country or workflow fails that test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plivo&lt;/td&gt;
&lt;td&gt;The same traffic mix, compliance steps, retry semantics, and delivery evidence&lt;/td&gt;
&lt;td&gt;Its tested total fit is strongest, not merely its headline rate&lt;/td&gt;
&lt;td&gt;Your team would need unacceptable adapter or policy work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MessageBird&lt;/td&gt;
&lt;td&gt;The same country matrix, sender setup, contract, and delivery-state needs&lt;/td&gt;
&lt;td&gt;Its current offer best satisfies the recorded invariants&lt;/td&gt;
&lt;td&gt;The validated offer cannot meet a hard requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;Plain-SMS scope, polling tolerance, sender registration, and app-owned country controls&lt;/td&gt;
&lt;td&gt;You want one direct REST integration without installing or maintaining a vendor SDK&lt;/td&gt;
&lt;td&gt;You require delivery webhooks, voice, WhatsApp, or RCS fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is intentionally strict about evidence. Vendor pricing and registration requirements move, and country support on a sales page is not proof that your sender type and message class are production-ready. Send a small, consented test matrix to every country and carrier segment that matters, then retain the result beside the decision record. Your mileage may vary — especially when the US/EU split changes after launch. Consider the ordinary billing-alert case: the product emits one event, but the notification boundary may see two attempts because a worker loses its lease after the provider accepts the first request. The same idempotency key must follow both attempts. The eventual provider response then becomes &lt;code&gt;submitted&lt;/code&gt;, while the user-facing workflow remains pending until delivery evidence arrives. During that gap, a second product event must not bypass the user's consent state, a tenant quota, the destination-country allowlist, or the resend window. If the state poll later reports failure, the system records failure; it does not silently switch to an unapproved country, sender, or channel. This example is why I score the adapter and state model alongside the vendor quote. A low send rate does not compensate for duplicated billing warnings, an alert sent to a disabled geography, or business logic that cannot distinguish acceptance from delivery. That's the trap.&lt;/p&gt;

&lt;p&gt;Infrai's concrete advantage here is integration shape: it exposes a plain REST API, so any runtime capable of HTTPS can call it without a client SDK or client-library upgrade cycle. Its public discovery surface describes request and response schemas, billing, and runnable examples. That can keep a provider adapter narrow. It does not remove the application responsibilities around geo-fencing, country price caps, throttling, consent, or delivery-state reconciliation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can one adapter keep an SMS send path safe?
&lt;/h2&gt;

&lt;p&gt;The following Python program performs one send using the verified &lt;code&gt;POST /v1/sms/send&lt;/code&gt; route. It deliberately reads the request JSON from &lt;code&gt;SMS_PAYLOAD_JSON&lt;/code&gt;: the public discovery document is the authority for current fields, and copying an imagined &lt;code&gt;to&lt;/code&gt; or &lt;code&gt;from&lt;/code&gt; schema into an article would create brittle code. Generate and validate that JSON from discovery during integration, then keep the adapter's internal type stable.&lt;/p&gt;

&lt;p&gt;It uses only the Python standard library. The API key stays in the environment, the method is explicit, and retries reuse one idempotency key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parsedate_to_datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlopen&lt;/span&gt;


&lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/sms/send&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response_headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parsedate_to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_sms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SMS send rejected (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;error_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SMS send rejected (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error_body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;retry_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SMS send retry budget exhausted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SMS_PAYLOAD_JSON&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;send_sms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The message ID returned by the send operation should be stored with the alert record. A separate worker can poll the verified status or events API and apply monotonic state changes; do not let an old poll overwrite a terminal state. Polling also needs a budget. Back off after terminal delivery, expire records according to your retention policy, and expose &lt;code&gt;unknown&lt;/code&gt; rather than guessing when evidence is incomplete.&lt;/p&gt;

&lt;p&gt;Direct send is the clean default for an individual transactional alert. Batch send can reduce call overhead for a genuine batch, but it changes the blast radius: one malformed audience selection can reach many people. Put audience construction, suppression, and country checks ahead of that boundary. For security codes, also separate “the API accepted a send” from “the user can safely request another code,” because resend windows and attempt counters are application security controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Country guardrails and delivery evidence are application work
&lt;/h2&gt;

&lt;p&gt;For US/EU SaaS traffic, the adapter needs a country policy keyed by normalized destination country. At minimum, it should decide whether that country is enabled, which sender identity is eligible, what per-message ceiling is acceptable, and how many alerts a tenant, user, destination, and IP address may trigger over several time windows. The REST-first option does not supply per-country price caps, geo-fencing, or the anti-abuse throttle, so those checks must run in your application layer.&lt;/p&gt;

&lt;p&gt;Do this before sending.&lt;/p&gt;

&lt;p&gt;Sender registration may also be required before production traffic. Treat registration readiness as deployable configuration: a country should remain disabled until its chosen sender is approved and an end-to-end test has produced usable delivery evidence. A generic “EU enabled” switch is too coarse because regulation, sender identity, and commercial terms are country-sensitive.&lt;/p&gt;

&lt;p&gt;There is another limit that affects architecture more than syntax. Delivery/state tracking on that option uses polling status and events APIs, not webhooks. Polling creates a deliberate delay and additional read traffic. A routine account notice may tolerate that; a real-time fallback chain may not. The catch is that the same option has no voice, WhatsApp, or RCS fallback, so it is not suitable when the product requirement is “reach the user on another channel immediately if SMS fails.” Stick with a provider and architecture whose verified event and channel model meets that requirement.&lt;/p&gt;

&lt;p&gt;Plain alerts remain a good fit: billing notices, account changes, scheduled reminders, and operational notifications where the application can poll and where SMS is the declared channel. Even there, watch deliverability rather than send-call success. Track accepted, final, failed, and unknown states by country and sender. Do not use those aggregates to invent a provider-wide delivery claim; they describe your traffic, during your observation window, under your consent and sender setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rejected shortcut still has a valid use case
&lt;/h2&gt;

&lt;p&gt;The rejected option is allowing product features to call a vendor directly. It looks faster for the first alert, but it spreads phone normalization, consent checks, throttling, retries, idempotency, and state interpretation across unrelated code. Switching vendors then becomes the least interesting part of the migration; finding every policy fork is the expensive part.&lt;/p&gt;

&lt;p&gt;A direct feature-to-provider call is still reasonable for a constrained internal prototype with synthetic or explicitly consented recipients, one country, no production promise, and a deletion date. Keep it honest. Once SMS becomes a user-facing reliability or security dependency, promote it behind the adapter and run the same country-shaped acceptance suite against Twilio, Vonage, Plivo, MessageBird, and Infrai.&lt;/p&gt;

&lt;p&gt;The final selection should record two answers: which candidate meets the hard operational constraints, and which current quote wins for the measured destination mix. If the REST-first option wins, choose it because that simple boundary matches the system and basic polling-based SMS is sufficient. If webhook timing, richer fallback channels, or provider-managed geographic controls are mandatory, reject it cleanly. Price cannot repair an architectural mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.infrai.cc/llms.txt" rel="noopener noreferrer"&gt;Infrai machine-readable documentation index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7489" rel="noopener noreferrer"&gt;RFC 7489: Domain-based Message Authentication, Reporting, and Conformance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.apple.com/guide/iphone/use-mail-privacy-protection-iphf084865c7/ios" rel="noopener noreferrer"&gt;Apple Mail Privacy Protection guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sms</category>
      <category>saas</category>
      <category>api</category>
    </item>
  </channel>
</rss>
