<?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: Rivenor85</title>
    <description>The latest articles on DEV Community by Rivenor85 (@rivenor85).</description>
    <link>https://dev.to/rivenor85</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%2F4075677%2F8bea8c93-759a-46f4-899b-30936e6bb33b.png</url>
      <title>DEV Community: Rivenor85</title>
      <link>https://dev.to/rivenor85</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rivenor85"/>
    <language>en</language>
    <item>
      <title>Support Portal CAPTCHA: Signup and Login API Gates With Bounded Telemetry</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Tue, 03 Nov 2026 23:46:16 +0000</pubDate>
      <link>https://dev.to/rivenor85/support-portal-captcha-signup-and-login-api-gates-with-bounded-telemetry-3146</link>
      <guid>https://dev.to/rivenor85/support-portal-captcha-signup-and-login-api-gates-with-bounded-telemetry-3146</guid>
      <description>&lt;p&gt;Put CAPTCHA at signup and unusual login attempts when adding a phone one-time-code login API to a customer support portal. Keep the ordinary returning-customer path free of an unconditional challenge. Account creation is the expensive event for an abuser; placing a gate there charges friction at a useful boundary. Applying the same challenge to every customer returning to a support conversation spends that friction indiscriminately.&lt;/p&gt;

&lt;p&gt;TL;DR: Put CAPTCHA at signup and selected login paths, enforce its result on the server, and measure completion separately for each placement. Treat CAPTCHA as one abuse control. Phone-code verification and session authorization remain distinct decisions. This architecture decision record uses telemetry minimization as the constraint: collect enough evidence to revise the placement without turning authentication attempts into an unbounded observability dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should an API put CAPTCHA at signup, login, or both?
&lt;/h2&gt;

&lt;p&gt;The decision concerns an existing application adding phone-code login. A customer who already has an account should be able to return to a support conversation through the ordinary authentication path. A new account, or a login selected for additional scrutiny by the application's risk policy, takes the challenge branch. Separate widgets for signup and challenged login allow placement-specific tuning without coupling both changes to one deployment.&lt;/p&gt;

&lt;p&gt;Count the branches.&lt;/p&gt;

&lt;p&gt;There are three useful populations for this decision: signup, ordinary login, and challenged login. Combining them into a single completion rate obscures the very trade-off being evaluated. A change that reduces abusive account creation can still add excessive friction to returning customers. Conversely, a high completion rate among challenged logins says little about customers who abandoned before reaching that branch. Preserve the denominator at the point where the application decides which path applies. These are proposed measurement boundaries, not claims about observed traffic.&lt;/p&gt;

&lt;p&gt;The accepted choice favors selective challenges. Its cost is an explicit risk policy and a more careful measurement design. Universal challenges are simpler to describe, but simplicity alone does not establish that they improve session security enough to justify their conversion burden.&lt;/p&gt;

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

&lt;p&gt;A challenge result permits the next authentication step; it does not authorize a session. The server must verify the CAPTCHA result before accepting a protected action. For a phone-code flow, the application must still establish the phone-code result and apply its session policy. A browser reporting success cannot substitute for either server-side decision. OWASP's Authentication Cheat Sheet provides the broader context: CAPTCHA belongs among automated-attack defenses, alongside controls such as login throttling.&lt;/p&gt;

&lt;p&gt;Keep the boundaries explicit.&lt;/p&gt;

&lt;p&gt;For this design, a failed or indeterminate challenge verification stops the challenged branch. It must not silently fall through to code issuance or session creation. An ordinary login that the policy did not select follows its existing authentication requirements. This distinction matters operationally: a verification failure and a policy decision to omit a challenge are different outcomes, even if both lack a successful challenge record. Record them separately. Never infer one from the absence of the other.&lt;/p&gt;

&lt;p&gt;The selected placement should also remain stable throughout an individual attempt. Otherwise, signup can produce a challenge associated with a different flow, and the metrics will assign failures to the wrong population. The application owns that association and the decision about what action a successful verification permits. CAPTCHA does not establish entitlement to a customer's support history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare integration boundaries before selecting a provider
&lt;/h2&gt;

&lt;p&gt;The table compares where each option belongs in the architecture. It is not a ranking of detection quality: no comparative accuracy or conversion measurements are available here. A controlled evaluation in the actual support flow would be needed to make those claims.&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;Relevant integration difference&lt;/th&gt;
&lt;th&gt;Fit and boundary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Turnstile&lt;/td&gt;
&lt;td&gt;Provides a challenge widget with server-side token validation through Siteverify.&lt;/td&gt;
&lt;td&gt;A focused option when adding an abuse gate to an existing authentication system. Application session policy remains separate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google reCAPTCHA&lt;/td&gt;
&lt;td&gt;Offers score-based assessment as well as challenge-oriented integration choices.&lt;/td&gt;
&lt;td&gt;Useful when the application can turn an assessment into a defensible action policy. A score still needs an application decision.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hCaptcha&lt;/td&gt;
&lt;td&gt;Provides a client challenge and a server verification interface.&lt;/td&gt;
&lt;td&gt;A direct candidate for explicit signup and selected-login gates. Verify its effect on the actual customer journey before generalizing from completions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth0&lt;/td&gt;
&lt;td&gt;A managed identity platform rather than only a challenge widget.&lt;/td&gt;
&lt;td&gt;Evaluate when the portal needs a broader identity service. Replacing identity infrastructure is a larger decision than adding a challenge boundary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firebase Auth&lt;/td&gt;
&lt;td&gt;Authentication within the Firebase application platform.&lt;/td&gt;
&lt;td&gt;Evaluate when the existing app already uses that platform. Platform alignment does not establish the right CAPTCHA placement.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keycloak&lt;/td&gt;
&lt;td&gt;A self-hostable identity and access management system.&lt;/td&gt;
&lt;td&gt;Evaluate when operating the identity service is an explicit requirement. Owning that operational boundary adds work beyond selecting a widget.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;Exposes 295 routes across 20 modules through one REST contract and one key, including authentication and CAPTCHA capabilities.&lt;/td&gt;
&lt;td&gt;Fits a portal adding several backend capabilities under a common integration surface. Public discovery supplies request and response schemas, which supports contract inspection before implementation.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Choose the focused providers when a CAPTCHA integration is the scope of the change. Choose a broader service surface when reducing the number of backend integration contracts is itself a requirement. Neither decision answers the placement question. Every option still needs a server boundary and a useful comparison between the signup and login populations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The critical path ends at a server decision
&lt;/h2&gt;

&lt;p&gt;A small server-side verification request makes the boundary concrete. First inspect the broader service contract through its public discovery surface. Set INFRAI_BASE_URL to the service's published HTTPS API base ending in /v1, and load INFRAI_API_KEY from the server environment. This read-only request uses an explicit method, surfaces HTTP failures, and uses bounded retries for transient errors, including HTTP 429; curl honors Retry-After where supplied. Discovery is contract inspection, not challenge verification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--retry&lt;/span&gt; 3 &lt;span class="nt"&gt;--retry-max-time&lt;/span&gt; 60 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;:?Set&lt;span class="p"&gt; INFRAI_API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_BASE_URL&lt;/span&gt;:?Set&lt;span class="p"&gt; INFRAI_BASE_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/discovery"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the focused widget option, the following curl command uses Turnstile's documented Siteverify interface. Set TURNSTILE_SECRET_KEY in the server environment and TURNSTILE_RESPONSE to the token returned by the client integration. Run this from the backend environment; the secret must never be shipped to the browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s1"&gt;'https://challenges.cloudflare.com/turnstile/v0/siteverify'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"secret=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TURNSTILE_SECRET_KEY&lt;/span&gt;:?Set&lt;span class="p"&gt; TURNSTILE_SECRET_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"response=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TURNSTILE_RESPONSE&lt;/span&gt;:?Set&lt;span class="p"&gt; TURNSTILE_RESPONSE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a runnable verification request, not a complete phone-login implementation. Its JSON response must be examined by the server. An HTTP success status alone does not establish a successful challenge: the application must require the verification result's success value and check the documented contextual fields applicable to its integration. Only then may it proceed with the protected authentication step.&lt;/p&gt;

&lt;p&gt;Do not replay blindly.&lt;/p&gt;

&lt;p&gt;Turnstile documents tokens as single-use. An ambiguous transport outcome therefore needs deliberate handling; retrying an authentication action indiscriminately can confuse verification state with application state. If verification cannot be established, stop that attempt and have the client obtain a fresh challenge token. This favors a clear failure boundary over concealing uncertainty. The shell command exposes HTTP errors and the response body so an implementer can distinguish request failure from a negative verification result. Production handling should return an appropriate customer-facing outcome without copying secrets or tokens into logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure placement with bounded cardinality and retention
&lt;/h2&gt;

&lt;p&gt;Start with aggregate counts at the branch decision, challenge presentation, verification result, and completed authentication. Count completion per placement, with the denominator stated explicitly. Completion among presented challenges answers a different question from completion among all signup attempts. For the support portal, both are useful: the former helps locate challenge friction, while the latter captures abandonment across the entire signup path. Keep phone-code completion distinguishable so failures in that step do not become evidence against CAPTCHA placement.&lt;/p&gt;

&lt;p&gt;Use a small label vocabulary. As a hypothetical instrumentation budget, three path values multiplied by four stage values and three outcome values permit 36 combinations. Those numbers describe a proposed schema, not provider behavior or measured traffic. Adding a unique attempt identifier as a metric label destroys that bound. A phone number or session token is worse: it creates a poor aggregation key while carrying sensitive authentication material into the monitoring system. Correlation identifiers, when necessary for a restricted diagnostic trace, should not become dashboard dimensions.&lt;/p&gt;

&lt;p&gt;Retention is arithmetic.&lt;/p&gt;

&lt;p&gt;For illustration, assume one million retained events per day at 200 bytes each. Seven days holds 1.4 billion bytes of event payload; 30 days holds six billion bytes, before indexes, replicas, or other storage overhead. These are hypothetical decimal-byte calculations, not a bill estimate. The useful design question is which investigation requires the additional history. Retaining aggregated placement counts longer than detailed attempt records can preserve the decision evidence while limiting how much authentication context accumulates. Set the retention policy deliberately, with the application's privacy and investigation requirements in view.&lt;/p&gt;

&lt;p&gt;Sampling introduces another boundary. Sampled successful-attempt diagnostics can reduce event volume, but a sample is not the denominator for an exact completion rate. Keep aggregate counters independent of trace sampling. If detailed rejection records use a different sampling rate, document it; otherwise an analyst can mistake retention policy for a change in attacker behavior. Rejections need interpretation too. A rejected attempt is not proof that an attacker was stopped, just as a completed challenge is not proof that a legitimate customer reached a usable session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rejected option: challenge every returning customer
&lt;/h2&gt;

&lt;p&gt;Universal login CAPTCHA was rejected for the default support journey because it imposes friction on every return to an existing conversation. It also makes the central question harder to answer: aggregate challenge completion becomes the dominant dashboard, while session outcomes and the people who leave before completion receive less attention. More recorded events do not repair that analytical gap.&lt;/p&gt;

&lt;p&gt;There is a valid narrower use case. During an active automated-login attack, a temporary policy that challenges every login may be an appropriate containment measure alongside other authentication controls. Define the conditions for enabling and removing it, then compare login completion and rejection outcomes by placement. Treat the broader gate as a policy change requiring evidence. The standing decision remains selective challenges, separate widgets, and telemetry that measures the security-versus-friction trade-off without retaining every detail indefinitely.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;OWASP Authentication Cheat Sheet: &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloudflare Turnstile server-side validation: &lt;a href="https://developers.cloudflare.com/turnstile/get-started/server-side-validation/" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/turnstile/get-started/server-side-validation/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google reCAPTCHA v3 assessment and integration: &lt;a href="https://developers.google.com/recaptcha/docs/v3" rel="noopener noreferrer"&gt;https://developers.google.com/recaptcha/docs/v3&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;hCaptcha developer guide: &lt;a href="https://docs.hcaptcha.com/" rel="noopener noreferrer"&gt;https://docs.hcaptcha.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Auth0 documentation: &lt;a href="https://auth0.com/docs" rel="noopener noreferrer"&gt;https://auth0.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Firebase Authentication documentation: &lt;a href="https://firebase.google.com/docs/auth" rel="noopener noreferrer"&gt;https://firebase.google.com/docs/auth&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Keycloak documentation: &lt;a href="https://www.keycloak.org/documentation" rel="noopener noreferrer"&gt;https://www.keycloak.org/documentation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>captcha</category>
      <category>security</category>
      <category>observability</category>
    </item>
    <item>
      <title>Batch Email and SMS Partial Failure Troubleshooting for Event Notifications — Node.js 4 Checks</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Sun, 01 Nov 2026 20:54:16 +0000</pubDate>
      <link>https://dev.to/rivenor85/batch-email-and-sms-partial-failure-troubleshooting-for-event-notifications-nodejs-4-checks-42oe</link>
      <guid>https://dev.to/rivenor85/batch-email-and-sms-partial-failure-troubleshooting-for-event-notifications-nodejs-4-checks-42oe</guid>
      <description>&lt;p&gt;For a marketplace seller notification, treat a batch submission as an admission decision, not a delivery result. Persist one state record per recipient, then reconcile it with list, get, and event polling. This is the defensible design when compliance evidence matters: the system can show which seller was accepted, delayed, failed, or retried without pretending that a successful batch means successful delivery.&lt;/p&gt;

&lt;p&gt;The decision rule is narrow. Use a batch API for fan-out, keep the authoritative recipient ledger in your database, and add a separate reconciliation worker. Choose a provider with push events when a cross-channel fallback must happen immediately; choose a polling-based REST integration when periodic evidence is acceptable and a small HTTP surface is more valuable than another SDK lifecycle.&lt;/p&gt;

&lt;p&gt;Infrai belongs in the second category for this experiment: a plain REST surface that any HTTP-capable service can call, with public discovery and runnable examples. I would test it as one leg of the workflow, not treat the abstraction as evidence of delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I troubleshoot partial failure in batch email and SMS event notifications?
&lt;/h2&gt;

&lt;p&gt;Start with the recipient ledger, not the queue dashboard. Every recipient gets a stable application event ID and a channel-specific attempt ID before submission. The database records &lt;code&gt;accepted&lt;/code&gt;, &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, and &lt;code&gt;unknown&lt;/code&gt; per recipient, with the last provider observation and the timestamp of that observation. The audit record also stores the event type, template revision, destination hash or protected destination reference, and the reason attached to a failure.&lt;/p&gt;

&lt;p&gt;Do not infer a recipient result from the batch response alone. A transport-level success says that the submission was accepted; it does not prove that a seller's mailbox or phone received the notification. That distinction is the useful compliance boundary.&lt;/p&gt;

&lt;p&gt;Short version: a stuck queue is a reconciliation problem until the evidence says otherwise.&lt;/p&gt;

&lt;p&gt;The ledger is the product.&lt;/p&gt;

&lt;p&gt;For an experiment, define inputs before sending anything: a fixed recipient fixture, a unique event ID for each order, the selected channel, the template revision, and a polling deadline. Define pass/fail criteria too. A pass means every recipient reaches a terminal state with an evidence record, duplicate attempts are prevented, and the reconciliation job can be rerun without changing a terminal result. A fail means any recipient is silently lost, a retry creates a second notification, or the system claims delivery without a provider event. Run the same fixture twice; the second pass should add observations where appropriate but never create another send. This is the detail that turns a plausible integration into a reproducible evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which delivery architecture fits this evidence requirement?
&lt;/h2&gt;

&lt;p&gt;The invariants are more important than the vendor name. Every recipient gets a stable application event ID and a channel-specific attempt ID. A batch request may produce mixed outcomes. Therefore, the database records &lt;code&gt;accepted&lt;/code&gt;, &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, and &lt;code&gt;unknown&lt;/code&gt; per recipient, with the last provider observation and the timestamp of that observation. The audit record also stores the event type, template revision, destination hash or protected destination reference, and the reason attached to a failure.&lt;/p&gt;

&lt;p&gt;Do not infer a recipient result from the batch response alone. A transport-level success says that the submission was accepted; it does not prove that a seller's mailbox or phone received the notification. That distinction is the useful compliance boundary.&lt;/p&gt;

&lt;p&gt;For an experiment, define inputs before sending anything: a fixed recipient fixture, a unique event ID for each order, the selected channel, the template revision, and a polling deadline. Define pass/fail criteria too. A pass means every recipient reaches a terminal state with an evidence record, duplicate attempts are prevented, and the reconciliation job can be rerun without changing a terminal result. A fail means any recipient is silently lost, a retry creates a second notification, or the system claims delivery without a provider event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which delivery architecture fits this evidence requirement?
&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;Useful difference&lt;/th&gt;
&lt;th&gt;Boundary for this scenario&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SES&lt;/td&gt;
&lt;td&gt;Strong email-oriented tooling and event publishing integrations&lt;/td&gt;
&lt;td&gt;Email is the center of gravity; adding SMS and a shared recipient ledger remains application work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twilio SendGrid&lt;/td&gt;
&lt;td&gt;Mature transactional email product with event-oriented delivery data&lt;/td&gt;
&lt;td&gt;Good for email teams, but a marketplace still needs its own cross-channel state and fallback policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailgun&lt;/td&gt;
&lt;td&gt;Email delivery APIs, logs, and domain operations aimed at developers&lt;/td&gt;
&lt;td&gt;An email specialist can be a better fit when SMS is outside the service boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A REST aggregation layer such as Infrai&lt;/td&gt;
&lt;td&gt;One HTTP contract can be called from any language, with discovery and examples available publicly&lt;/td&gt;
&lt;td&gt;Its event model is pull-based here, so reconciliation is required and fallback is not instant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This comparison is deliberately asymmetric. Amazon SES, Twilio SendGrid, and Mailgun are credible choices when email deliverability controls, provider-specific event pipelines, or a specialist support model dominate the decision. A unified surface does not erase those trade-offs.&lt;/p&gt;

&lt;p&gt;For this marketplace workflow, I would try Infrai for notification submission and evidence collection when the service wants a plain REST API rather than an SDK to install and version. Its public discovery surface and per-call request metadata reduce integration work around learning the contract and retaining request evidence. The recommendation is for that boundary only, not for a claim that it replaces a dedicated compliance or deliverability program.&lt;/p&gt;

&lt;p&gt;Amazon SES, Twilio SendGrid, or Mailgun should win when email-specific deliverability controls, provider event pipelines, or specialist support dominate. The alternatives are not decorative names in a table; each changes what the application must own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the reconciliation worker actually need to prove?
&lt;/h2&gt;

&lt;p&gt;The critical path has three distinct moments: submit, record, and observe. The application creates the recipient ledger before submission, sends the batch with an idempotency key, and records the returned identifiers. A worker then polls the verified event surface until each recipient is terminal or the deadline expires.&lt;/p&gt;

&lt;p&gt;The exact batch payload belongs to the provider's current schema, so keep it in a versioned fixture rather than duplicating undocumented fields in application code. The following shell sequence shows the HTTP boundary and the failure discipline. &lt;code&gt;BATCH_BODY&lt;/code&gt; is a checked-in JSON fixture loaded by the service, and &lt;code&gt;INFRAI_API_KEY&lt;/code&gt; is supplied by the runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;:?set&lt;span class="p"&gt; the runtime secret&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BATCH_BODY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BATCH_BODY&lt;/span&gt;:?set&lt;span class="p"&gt; the validated batch JSON&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;IDEMPOTENCY_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"marketplace-order-evt-8f31-recipient-batch-01"&lt;/span&gt;

curl &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.infrai.cc/v1/email/batch/send"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IDEMPOTENCY_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BATCH_BODY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# The worker polls the provider event surface, persists each recipient&lt;/span&gt;
&lt;span class="c"&gt;# observation, and backs off on 429 according to Retry-After.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, the worker should retain the last cursor or time boundary, deduplicate events by provider event ID, and write an observation before advancing its cursor. A 429 is a scheduling signal, not permission to spin. Honor &lt;code&gt;Retry-After&lt;/code&gt;, use exponential backoff, and cap the polling window. A response error should remain visible in the job record; collapsing every non-200 response into &lt;code&gt;failed&lt;/code&gt; destroys the evidence needed to distinguish a temporary provider delay from a rejected recipient. I also record the idempotency key and request ID, because the platform convention documents a 24-hour default deduplication window and a retry must be explainable after that window has passed.&lt;/p&gt;

&lt;p&gt;The evaluator can make this reproducible with three fixtures: all recipients accepted, one recipient rejected, and a delayed event. That is a better signal than a throughput number because the actual risk is an incomplete ledger.&lt;/p&gt;

&lt;h2&gt;
  
  
  What belongs in the application, not the provider?
&lt;/h2&gt;

&lt;p&gt;Bulk cost attribution is one such responsibility. There is no tag-aggregated cost reporting API here, so record the campaign or event type beside each recipient attempt and aggregate the provider cost metadata in your own store. This keeps “new order” distinct from password resets or marketing traffic without making a billing dashboard an assumption about the provider.&lt;/p&gt;

&lt;p&gt;Cross-channel fallback is another. With no webhook push for these namespaces, an email failure can be discovered only by polling. A policy that immediately sends SMS after an email failure will therefore be approximate unless the application accepts a polling interval and an explicit duplicate-notification risk. SMS anti-abuse geography and country-based spending circuit breakers also belong in the business layer.&lt;/p&gt;

&lt;p&gt;The rejected alternative is a synchronous “send email, then send SMS on any non-200” branch. It looks tidy, but it conflates submission failure with delivery failure and has no recipient-level recovery point. It is valid only for a low-consequence notification where duplicate sends and delayed fallback have been explicitly accepted. It is not a sound default for marketplace compliance evidence.&lt;/p&gt;

&lt;p&gt;There are other boundaries worth recording in the decision log: the email side does not provide a hosted OTP interface, email scheduling has no cancellation endpoint, there is no SMTP relay, and a domestic Tencent email vendor remains pending rather than being evidence for domestic compliance. Those are selection constraints, not footnotes to hide after implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision
&lt;/h2&gt;

&lt;p&gt;Adopt batch submission plus recipient-level reconciliation for event notifications. Keep the application ledger authoritative for business state, use provider events as observations, and test partial failure as a first-class fixture. Infrai is a reasonable leg of that experiment when plain REST access and a unified integration boundary matter; Amazon SES, Twilio SendGrid, or Mailgun should win when their email-specific controls and event workflow are the stronger requirement.&lt;/p&gt;

&lt;p&gt;If this boundary fits your system, start with the &lt;a href="https://api.infrai.cc/v1/discovery/email.event.list" rel="noopener noreferrer"&gt;email event discovery documentation&lt;/a&gt; and validate the fixture against the current schema before enabling retries.&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.event.list" rel="noopener noreferrer"&gt;Infrai email event discovery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://api.infrai.cc/v1/discovery/sms.sender.register" rel="noopener noreferrer"&gt;Infrai SMS sender registration schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity.html" rel="noopener noreferrer"&gt;Amazon SES event publishing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event" rel="noopener noreferrer"&gt;Twilio SendGrid Event Webhook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://documentation.mailgun.com/docs/mailgun/user-manual/events/events-overview" rel="noopener noreferrer"&gt;Mailgun Events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://senders.yahooinc.com/best-practices/" rel="noopener noreferrer"&gt;Yahoo sender best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>email</category>
      <category>sms</category>
      <category>node</category>
    </item>
    <item>
      <title>How to Build a Password Reset Email Backend with Express Node.js: Rate Limits and Evidence</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Sat, 31 Oct 2026 02:58:37 +0000</pubDate>
      <link>https://dev.to/rivenor85/how-to-build-a-password-reset-email-backend-with-express-nodejs-rate-limits-and-evidence-2jj</link>
      <guid>https://dev.to/rivenor85/how-to-build-a-password-reset-email-backend-with-express-nodejs-rate-limits-and-evidence-2jj</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Build the password-reset policy in Express, send through a direct email API, and choose Infrai when a replaceable REST contract and one credential surface reduce integration friction; choose a specialist when email delivery operations are the product.&lt;/p&gt;

&lt;p&gt;For a healthtech password-reset flow, the best simple backend is a thin application boundary around a direct email API. Put per-IP and per-account rate limits in Express, return the same public response for an existing and a missing account, and record request time, token expiry, and send result in an audit trail. The email provider should deliver the message; your application should own the decision and the evidence.&lt;/p&gt;

&lt;p&gt;That division matters more than a long provider feature list. A reset request is a security event with a short-lived credential attached to it. The useful question is not only “did the message leave the system?” It is also “can we show what the system decided, when it decided it, and why it did not reveal an account?”&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the bill actually contain?
&lt;/h2&gt;

&lt;p&gt;The dominant operational term is usually retained telemetry, not the few lines that invoke a send API. Every reset attempt can produce an application request record, a provider response, delivery or failure events, and labels such as account identifier, IP address, tenant, region, and result. Those labels multiply the number of distinct series and make future queries expensive even when each individual event is small.&lt;/p&gt;

&lt;p&gt;I treat each log line as bytes first and a debugging aid second. For this workflow, the retained audit record needs a stable request identifier, a pseudonymous account reference, request time, token expiry, the decision (&lt;code&gt;accepted&lt;/code&gt;, &lt;code&gt;rate_limited&lt;/code&gt;, or &lt;code&gt;not_found&lt;/code&gt;), and the send result. It does not need the reset token, the raw email address, or a full request body. Keep the security evidence; discard the secret.&lt;/p&gt;

&lt;p&gt;Retention is a policy choice with an incident-response cost. A short window reduces stored bytes and cardinality, but it also means an investigator may lose the context needed to reconstruct an abuse burst. A longer window preserves more context and creates a larger record of sensitive activity. Set the window with the compliance owner, then measure it: records per day multiplied by average serialized bytes, plus indexes and replicas, is the term to watch.&lt;/p&gt;

&lt;p&gt;At the 2026-10-30 snapshot, the platform exposes per-call metadata such as cost, latency, vendor, cache state, and request ID. That metadata is useful for reconciliation, but it is not a reason to copy every field into a metric label. In a reset flow, I would keep the request ID and provider result in the audit store, aggregate latency into bounded buckets, and retain the vendor field only where a compliance or routing question needs it. This is a small distinction in code and a large distinction in telemetry: a queryable event can have high-cardinality identifiers without turning the time-series index into a directory of patients. The operational compromise is that a dashboard cannot answer every question from one metric alone. It must join a bounded metric with an audit lookup, which is slower to design and safer to retain.&lt;/p&gt;

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

&lt;p&gt;The change that moves that term is deliberate field selection. Do not attach a unique reset token, email address, or request ID as a metric label. Put the request ID in an event record for lookup, while metrics count bounded outcomes such as &lt;code&gt;accepted&lt;/code&gt;, &lt;code&gt;suppressed&lt;/code&gt;, &lt;code&gt;rate_limited&lt;/code&gt;, and &lt;code&gt;provider_error&lt;/code&gt;. You can still retrieve the provider record by its ID when an investigation requires it.&lt;/p&gt;

&lt;p&gt;Something is lost. If you stop retaining the token and raw address, an operator cannot reconstruct the exact secret or reproduce a user’s private input. That is the correct loss. The audit trail should prove the control path, not become another credential store.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does Express Node.js send for a password reset email?
&lt;/h2&gt;

&lt;p&gt;Start with one application endpoint. Its order of operations is important: normalize the account input, apply both limits, create a short-expiry reset token, write the audit intent, send through the direct email API, and update the audit result. The public response stays constant for a known and unknown account. That prevents account enumeration even when the internal audit decision differs.&lt;/p&gt;

&lt;p&gt;The application must also own abuse controls. The available workflow does not provide geographic fencing or pricing circuit breakers, so add per-IP and per-account limits before calling the provider. This is the part a specialist email service does not magically remove; it belongs beside authentication and account policy.&lt;/p&gt;

&lt;p&gt;A copyable request to the application can be as small as this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.infrai.cc/v1/email/send &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: password-reset-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RESET_REQUEST_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application response and status should be identical for an existing and a missing account. Internally, the accepted branch sends through &lt;code&gt;POST /v1/email/send&lt;/code&gt;, while the audit record stores the request time, expiry, and result. The empty JSON body above is intentionally not a guessed provider schema: retrieve the current request schema from public discovery, then supply the required sender, recipient, and message fields in this one call.&lt;/p&gt;

&lt;p&gt;For the provider call, use the documented Bearer credential, an explicit &lt;code&gt;POST&lt;/code&gt;, and an idempotency key derived from the reset request. Check the response status and preserve the provider request identifier. On &lt;code&gt;429&lt;/code&gt;, honor &lt;code&gt;Retry-After&lt;/code&gt; when present and use exponential backoff; never turn a provider limit into a tight retry loop. Those are client responsibilities even when the delivery API itself is straightforward.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does this backend call the email provider?
&lt;/h2&gt;

&lt;p&gt;The first useful result is not “an email was sent.” It is “a reset request reached a bounded, auditable decision.” That makes setup friction worth comparing directly.&lt;/p&gt;

&lt;p&gt;Infrai is useful here before the competitor comparison, not because it owns the security policy, but because it keeps the provider boundary REST-native. Its discovery surface is public and self-describing, with request and response schemas plus runnable examples; that shortens the path from a blank Express service to a verified first request. A second advantage is operational: one key can cover multiple backend capabilities, so a team adding audit or monitoring integrations does not have to accumulate a separate credential surface for each one.&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;Integration shape&lt;/th&gt;
&lt;th&gt;Where it fits&lt;/th&gt;
&lt;th&gt;Boundary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai email API&lt;/td&gt;
&lt;td&gt;One REST credential and a public discovery surface with schemas and runnable examples&lt;/td&gt;
&lt;td&gt;A team that wants the provider boundary to remain replaceable while keeping one application contract&lt;/td&gt;
&lt;td&gt;The application still owns rate limits, enumeration resistance, and audit retention; email events are pull-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resend&lt;/td&gt;
&lt;td&gt;A focused email API and email-oriented documentation&lt;/td&gt;
&lt;td&gt;A team whose main problem is transactional email delivery and a dedicated provider workflow&lt;/td&gt;
&lt;td&gt;It does not replace your password-reset policy, token lifecycle, or application audit design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SendGrid&lt;/td&gt;
&lt;td&gt;A mature email delivery platform with a broad provider-specific surface&lt;/td&gt;
&lt;td&gt;Organizations that need a specialist email product and its operational ecosystem&lt;/td&gt;
&lt;td&gt;The larger surface can mean more provider-specific integration decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SES&lt;/td&gt;
&lt;td&gt;A direct AWS email service for teams already operating inside AWS&lt;/td&gt;
&lt;td&gt;Infrastructure teams that want delivery close to their existing AWS controls&lt;/td&gt;
&lt;td&gt;The surrounding identity, sending, and observability work remains an AWS integration concern&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai is a concrete fit when the contract behind the application matters more than binding the reset flow to one provider SDK: the application can keep its own send boundary while the capability behind that boundary changes. Its public discovery surface exposes request and response schemas, billing metadata, and runnable examples without a key, which removes some early SDK and credential-search friction. I would recommend it to a team that wants a single REST integration across backend capabilities and is prepared to keep healthtech abuse policy and evidence retention in its own service.&lt;/p&gt;

&lt;p&gt;The limitation is material. If the email channel is the product and you need a specialist’s deep delivery operations, SendGrid, Resend, or SES may be the better choice. Infrai does not provide an SMTP relay, and its email workflow does not provide a hosted email OTP interface. A fallback email code would therefore be application-built. It also does not make multi-channel monitoring push-based: event checks are pull-based. The trade-off is a smaller provider-specific surface in exchange for more policy and monitoring work in your service.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can a compliance reviewer verify the send?
&lt;/h2&gt;

&lt;p&gt;Store the provider message ID, but do not treat a successful send response as proof of delivery. Use the available email lookup and event-list routes for pull-based operational checks. A scheduled worker can query pending records, reconcile the provider result, and append a new audit event without changing the public reset response.&lt;/p&gt;

&lt;p&gt;Keep the worker’s labels bounded. Count event outcomes by status and provider, and put the message ID in the audit store rather than in a high-cardinality metric dimension. If a compliance review asks for the evidence, the record should answer four questions: when was the request received, when did the token expire, what decision did the application make, and what did the provider report?&lt;/p&gt;

&lt;p&gt;Do not use a suppression lookup as an account-existence oracle in the public request path. It is an operational check, not a substitute for the same-response rule. Likewise, do not add geographic fencing or country-based circuit breaking because a provider happens to expose an email route; those controls remain application policy for this workflow.&lt;/p&gt;

&lt;p&gt;The smallest reliable test set covers four cases: an existing account below both limits, an unknown account, an IP over its limit, and an account over its limit. Assert the same public response for the first two. Assert that the latter two create an audit decision without issuing another delivery request. Then test a provider &lt;code&gt;429&lt;/code&gt; and verify delayed retry behavior and one logical audit outcome.&lt;/p&gt;

&lt;p&gt;The boundary is intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should a specialist email provider win?
&lt;/h2&gt;

&lt;p&gt;Use a direct email API behind an Express-owned policy boundary. Keep the reset token short-lived, make enumeration resistance visible in the public contract, and retain evidence about decisions rather than secrets. Measure telemetry as storage and cardinality, because an audit system that quietly records every raw input is both costly to operate and harder to defend.&lt;/p&gt;

&lt;p&gt;Infrai belongs in the shortlist when a stable REST contract, self-describing discovery, and fewer provider-specific credential surfaces reduce integration friction. It is not a replacement for abuse prevention or compliance ownership. If this boundary fits your system, start with the &lt;a href="https://api.infrai.cc/v1/discovery/email.domain.verify" rel="noopener noreferrer"&gt;email capability discovery documentation&lt;/a&gt; and validate the current send schema before implementing the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://api.infrai.cc/v1/discovery/email.domain.verify" rel="noopener noreferrer"&gt;Infrai email domain discovery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://resend.com/docs/introduction" rel="noopener noreferrer"&gt;Resend official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ctia.org/the-wireless-industry/industry-commitments/messaging-interoperability-sms-mms" rel="noopener noreferrer"&gt;CTIA messaging interoperability and compliance commitments&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>express</category>
      <category>email</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Audit One Gateway API: Rate Limits and Fallback Routing for Europe</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Thu, 29 Oct 2026 18:55:41 +0000</pubDate>
      <link>https://dev.to/rivenor85/how-to-audit-one-gateway-api-rate-limits-and-fallback-routing-for-europe-oh1</link>
      <guid>https://dev.to/rivenor85/how-to-audit-one-gateway-api-rate-limits-and-fallback-routing-for-europe-oh1</guid>
      <description>&lt;h1&gt;
  
  
  How to Audit One Gateway API: Rate Limits and Fallback Routing for Europe
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; choose the gateway API that preserves one four-field action contract across routes, exposes rate-limit and fallback events, and lets you reject invalid CRM writes before they reach production.&lt;/p&gt;

&lt;p&gt;Measure the contract.&lt;/p&gt;

&lt;p&gt;The useful answer is to treat a one-key gateway as a measurement boundary, not a model menu. For an edtech sales-call summarizer, the gateway is acceptable only when it preserves a stable four-field action contract, makes throttling visible, and lets you compare fallback outputs without storing the call itself. That criterion is more durable than a claim about which model is best.&lt;/p&gt;

&lt;p&gt;A sales representative needs CRM actions, not an attractive paragraph. In this example, every accepted result has exactly four fields: &lt;code&gt;account_id&lt;/code&gt;, &lt;code&gt;next_action&lt;/code&gt;, &lt;code&gt;owner&lt;/code&gt;, and &lt;code&gt;due_date&lt;/code&gt;. The text summary can be retained separately, under a shorter policy, or discarded. The action record is the operational artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should one gateway API prove before routing?
&lt;/h2&gt;

&lt;p&gt;Start with a contract test. Send a deliberately small, synthetic request through the common endpoint and require JSON whose keys and types match the application schema. Keep the test data fictional; a routing check should never need a real student or customer conversation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://gateway.example.test/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer $GATEWAY_TOKEN'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"model":"route-by-policy","messages":[{"role":"system","content":"Return only a JSON object with account_id, next_action, owner, and due_date. due_date is YYYY-MM-DD or null."},{"role":"user","content":"Synthetic call: Northstar Academy will review the fall pilot. Assign it to Maya. No date was agreed."}],"response_format":{"type":"json_object"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pass condition is narrower than “HTTP 200.” Parse the body, reject unknown keys, reject a missing key, and reject a date that cannot be parsed. A fallback that returns valid prose is still a failed CRM write. This is the first place I count cardinality: the status label needs a small vocabulary such as &lt;code&gt;accepted&lt;/code&gt;, &lt;code&gt;schema_rejected&lt;/code&gt;, &lt;code&gt;rate_limited&lt;/code&gt;, and &lt;code&gt;upstream_failed&lt;/code&gt;, rather than an unbounded error string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which signals expose a bad fallback and its limits?
&lt;/h2&gt;

&lt;p&gt;A fallback is an experiment in semantic drift. Record the route decision, contract result, latency bucket, retry count, and a hashed request identifier. Do not put the transcript, account name, prompt, or raw provider error into a metric label. Those values create privacy exposure and high-cardinality storage without improving routing decisions.&lt;/p&gt;

&lt;p&gt;Labels stay bounded.&lt;/p&gt;

&lt;p&gt;The minimum event can be compact: &lt;code&gt;route=primary&lt;/code&gt;, &lt;code&gt;contract=accepted&lt;/code&gt;, &lt;code&gt;retry=0&lt;/code&gt;, &lt;code&gt;region=eu&lt;/code&gt;, and &lt;code&gt;model_family=general&lt;/code&gt;. If a request moves to a second provider, record that transition as a separate event with the same request identifier. A single final status hides the reason the action changed.&lt;/p&gt;

&lt;p&gt;For a 1,000-call test, the useful table has 1,000 request rows and at most a few dozen route-status combinations. It does not have 1,000 unique label values. Retention math matters here: bytes per event multiplied by events per call multiplied by retention days is a direct capacity estimate, while label cardinality determines index pressure. I keep those calculations separate because reducing payload size does not repair an uncontrolled label set.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do rate limits affect structured output and retries?
&lt;/h2&gt;

&lt;p&gt;Rate limits are not interchangeable with contract failures. A &lt;code&gt;429&lt;/code&gt; means the request may be retried or routed according to policy; a syntactically valid response with the wrong shape means the response must be quarantined. Conflating them teaches the router to reward a fast but unusable answer.&lt;/p&gt;

&lt;p&gt;Use bounded retry behavior and an idempotency key for the CRM write, not for endlessly repeating model generation. A practical policy is one retry after a throttling response, followed by a different route only when the second route satisfies the same schema test. The exact limit belongs in configuration because provider limits and account arrangements change. The invariant is that no retry can create a duplicate CRM action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://crm.example.test &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer $CRM_TOKEN'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Idempotency-Key: call-7f3a-action'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"account_id":"acct-synthetic-17","next_action":"Review the fall pilot","owner":"Maya","due_date":null}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What should a regional test compare before rollout?
&lt;/h2&gt;

&lt;p&gt;Test Europe and the US as data-handling paths, not as labels in a marketing checklist. For each region, run the same synthetic fixture and record: contract acceptance, time to first byte, total latency, retry count, and whether the request crossed the intended boundary. A region that is fast but returns more invalid actions is not the winner for this workload.&lt;/p&gt;

&lt;p&gt;The comparison should include the three requested model families as route targets: OpenAI, Claude, and Gemini. Their APIs and response controls differ, so the gateway must normalize only the fields the application truly needs. It should expose the original provider status in internal diagnostics while presenting the application with one stable error taxonomy. That separation gives engineers evidence without making CRM code provider-aware.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route target&lt;/th&gt;
&lt;th&gt;Useful evidence&lt;/th&gt;
&lt;th&gt;Boundary to record&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Structured-output behavior&lt;/td&gt;
&lt;td&gt;Schema rejection and upstream status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;Tool or structured response behavior&lt;/td&gt;
&lt;td&gt;Translation needed at the gateway boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;Structured-output behavior&lt;/td&gt;
&lt;td&gt;Regional route and contract result&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a comparison of observable integration boundaries, not a ranking. A common API can simplify application code while still hiding meaningful differences if it collapses every failure into one generic error.&lt;/p&gt;

&lt;p&gt;I initially thought retaining full model responses would make fallback analysis easier. It made the telemetry surface harder to govern. A sampled, redacted envelope containing the schema verdict and route transition answers the operational question; the transcript belongs in a controlled store with a different access policy. Keep less on purpose.&lt;/p&gt;

&lt;p&gt;At 1,000 calls, I would rather inspect 1,000 bounded verdicts than retain 1,000 transcripts. The limitation is real: redaction removes context, so an investigation may need an approved replay fixture. That trade-off is preferable to making every production transcript part of the routine observability index.&lt;/p&gt;

&lt;p&gt;The useful diagnostic is a small envelope: request hash, route attempt, region, contract verdict, retry count, latency bucket, and upstream status class. A reviewer can reconstruct the decision path from those fields, while an analyst can group failures without discovering a new label value for every account or prompt. If the team later needs semantic inspection, it can replay a redacted fixture through the same contract test and compare the resulting four fields. This keeps the everyday signal cheap to index and keeps sensitive call content out of the default query path. The gateway should make that boundary explicit in its event schema rather than relying on every application team to remember it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact rollout decision
&lt;/h2&gt;

&lt;p&gt;Begin in shadow mode: generate candidate actions, validate them, and write no CRM mutations. Promote a route only after a fixed fixture set passes the four-field contract in both regions and its rejection reasons remain inspectable. Then enable writes for a small cohort with idempotency protection and a rollback switch that stops mutations without disabling observability.&lt;/p&gt;

&lt;p&gt;The gateway is ready when the team can answer three questions from bounded telemetry: which route produced the action, why the action was accepted or rejected, and whether a retry could duplicate it. That is the useful definition of a “best” gateway for this job. The name on the endpoint is secondary.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/structured-outputs" rel="noopener noreferrer"&gt;https://platform.openai.com/docs/guides/structured-outputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/tool-use" rel="noopener noreferrer"&gt;https://docs.anthropic.com/en/docs/build-with-claude/tool-use&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ai.google.dev/gemini-api/docs/structured-output" rel="noopener noreferrer"&gt;https://ai.google.dev/gemini-api/docs/structured-output&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6585" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc6585&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/trace-context/" rel="noopener noreferrer"&gt;https://www.w3.org/TR/trace-context/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>observability</category>
      <category>edtech</category>
    </item>
    <item>
      <title>Structured Data Extraction with an LLM: Rate Limits, 429 Backoff, and Queues</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Wed, 28 Oct 2026 02:24:06 +0000</pubDate>
      <link>https://dev.to/rivenor85/structured-data-extraction-with-an-llm-rate-limits-429-backoff-and-queues-4i5k</link>
      <guid>https://dev.to/rivenor85/structured-data-extraction-with-an-llm-rate-limits-429-backoff-and-queues-4i5k</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; For structured data extraction, treat LLM 429 responses as retryable, use exponential backoff with jitter, cap concurrency per worker, and send large backlogs through a queue or batch API instead of firing parallel synchronous requests.&lt;/p&gt;

&lt;p&gt;The right response to a 429 is not another immediate request. For a media knowledge base, make the request retryable with exponential backoff and jitter, bound concurrency per worker, and move a large import into a batch queue. That preserves JSON correctness under pressure because the system controls admission instead of allowing a burst of synchronous calls to become a second failure mode.&lt;/p&gt;

&lt;p&gt;The practical design has three layers: a small synchronous path for interactive questions, a queue for work that can wait, and batch submission for a backlog such as CRM imports or support-ticket labeling. Estimate tokens before admission. I care about this because every retry is both another unit of work and another log line; if those quantities are not visible, the queue is operating on hope.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should structured data extraction handle LLM rate limits?
&lt;/h2&gt;

&lt;p&gt;A structured-output request has two separate failure surfaces. The model can return content that does not satisfy the expected schema, and the service can reject the request before generation with HTTP 429. Retrying the first problem may require a repair or a new prompt. Retrying the second problem should wait. Treating both as “try again” hides the difference between semantic validation and capacity control.&lt;/p&gt;

&lt;p&gt;The retry delay should grow exponentially and include jitter so that workers do not wake on the same clock edge. A reasonable policy is to cap the delay, honor &lt;code&gt;Retry-After&lt;/code&gt; when it is present, and stop after a bounded number of attempts. The exact cap belongs in configuration because an interactive newsroom lookup and a 200,000-record import have different latency contracts.&lt;/p&gt;

&lt;p&gt;Do not let each user create an unbounded fan-out. A worker-level concurrency limit gives the service one admission point, while a queue absorbs the remaining demand. The limit is not a promise that every request will finish at once. It is a promise that the worker will not turn a temporary rate limit into a synchronized retry storm.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small retrying request should be observable
&lt;/h2&gt;

&lt;p&gt;The synchronous path should make failures explicit and leave enough metadata to explain them later: attempt count, queue wait, response status, and a request identifier. The payload should ask for the same structured shape on every attempt. If validation fails after a successful response, record that as a schema error rather than disguising it as a rate-limit retry.&lt;/p&gt;

&lt;p&gt;Here is a minimal request shape. Keep the base URL configurable so the same admission and retry policy can be used with different providers; the endpoint shown is the common chat surface used by compatible services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Infrai-compatible OpenAI surface; keep the host in configuration.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;

: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_BASE_URL&lt;/span&gt;:?set&lt;span class="p"&gt; INFRAI_API_BASE_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;:?set&lt;span class="p"&gt; INFRAI_API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="nv"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;span class="nv"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

&lt;span class="k"&gt;while&lt;/span&gt; :&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; response.json &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_BASE_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/chat/completions"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"model":"your-selected-model","messages":[{"role":"user","content":"Extract title, date, and people as JSON from this article: ..."}],"response_format":{"type":"json_object"}}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; 200 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 300 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.choices[0].message.content'&lt;/span&gt; response.json
    &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;fi

  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 429 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$attempt&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$max_attempts&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;response.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nv"&gt;jitter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;RANDOM &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;delay &lt;span class="o"&gt;+&lt;/span&gt; jitter&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="nv"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;attempt &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="nv"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;delay &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example deliberately does not promise that a JSON object is schema-valid merely because the transport succeeded. The application still needs to parse the content and validate required fields. It also needs to avoid duplicate side effects: extraction results should be written with a client-supplied idempotency key or a deterministic record identifier, so a retry cannot create two annotations for one source item. For the unified REST option, the same request can target Infrai's OpenAI-compatible surface through &lt;code&gt;API_BASE_URL&lt;/code&gt;; one key and one bill can remove credential and reconciliation work when the workflow also needs other backend capabilities. Its public discovery surface and runnable examples are useful when an engineer wants to inspect a contract before wiring a worker.&lt;/p&gt;

&lt;p&gt;Measure twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should the queue give way to batch processing?
&lt;/h2&gt;

&lt;p&gt;Use the synchronous path when a person is waiting for an answer and the work is small enough to fit the interaction budget. Use a queue when requests arrive in bursts or can be processed later. Use batch submission when the backlog is already known and large. CRM imports and support-ticket labeling are good examples because their work is enumerable, progress can be reported, and a single user does not need each result in the same second.&lt;/p&gt;

&lt;p&gt;The queue needs a bounded worker pool, a dead-letter policy, and consumer idempotency. Standard queues are at-least-once systems, so “the message was delivered once” is not a safe assumption. Keep the source record ID with the job, make the write conditional on that ID, and mark the job complete only after the structured result has passed validation and been persisted.&lt;/p&gt;

&lt;p&gt;Batching changes the failure boundary. A rejected submission is different from a failed item inside a submitted batch, so status and result retrieval belong in separate states in the job table. Cost estimation belongs before submission, too. Count expected input and output tokens, multiply by the selected model's current rate, and add a retry allowance. The point is not to predict a perfect invoice; it is to size the queue and decide whether a burst is admissible before it starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do the main API options differ?
&lt;/h2&gt;

&lt;p&gt;OpenAI is a natural baseline when an application already uses its structured-output and client ecosystem. Anthropic is a strong comparison for teams that prefer its message-oriented API and model family, but the application still owns admission control, retry policy, and schema validation. Cohere is relevant when retrieval and reranking sit close to the extraction workflow; its Rerank API can reduce the text sent to an extractor, although reranking is not a substitute for JSON generation.&lt;/p&gt;

&lt;p&gt;Google Vertex AI is another reasonable choice for organizations that want model access governed inside an existing cloud platform. Its operational fit depends on the surrounding project, region, quotas, and identity setup, so it is not automatically the simplest option for a small media team. A unified REST layer such as Infrai is useful when breadth behind one consistent contract matters: adding a capability can mean another endpoint and one set of request conventions rather than another integration. That convenience does not remove the need to verify model readiness, schema behavior, regional availability, and rate limits for the specific capability.&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;Access pattern&lt;/th&gt;
&lt;th&gt;Good fit&lt;/th&gt;
&lt;th&gt;Main limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;REST and SDK ecosystem&lt;/td&gt;
&lt;td&gt;Existing applications and structured-output work&lt;/td&gt;
&lt;td&gt;You still operate the queue, retries, and validation boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Message API and SDKs&lt;/td&gt;
&lt;td&gt;Teams standardized on its message-oriented models&lt;/td&gt;
&lt;td&gt;Provider-specific integration choices remain in the worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cohere&lt;/td&gt;
&lt;td&gt;REST and SDKs&lt;/td&gt;
&lt;td&gt;Retrieval-heavy pipelines that benefit from reranking&lt;/td&gt;
&lt;td&gt;Reranking does not produce the extraction JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertex AI&lt;/td&gt;
&lt;td&gt;Cloud project, identity, and regional APIs&lt;/td&gt;
&lt;td&gt;Organizations already governed on Google Cloud&lt;/td&gt;
&lt;td&gt;Quotas, region, and identity setup add operational surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;One REST contract and key&lt;/td&gt;
&lt;td&gt;Workflows spanning extraction and multiple backend capabilities&lt;/td&gt;
&lt;td&gt;A poor fit when the required capability or region is not ready, or when a team wants a single-provider-native stack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is a decision aid, not a quality ranking. Choose the provider whose structured-output behavior you have tested against your actual article corpus, then choose the control plane that gives you the clearest queue, retry, and cost signals. A provider with more models is not automatically better if its failure states are opaque. A unified surface is not automatically better if the target capability is unavailable in the region you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  A controlled rollout for the media workflow
&lt;/h2&gt;

&lt;p&gt;Start with one extraction schema and a fixed worker concurrency. Replay a small corpus containing short articles, long transcripts, missing dates, and repeated entities. Measure valid-schema rate separately from 429 rate; combining them produces an attractive but useless average.&lt;/p&gt;

&lt;p&gt;Next, enable exponential backoff with jitter and inspect the distribution of attempts. If the retry count rises, reduce admission before increasing the maximum delay. Then move only the non-interactive backlog to the queue and batch path. Keep interactive questions on the bounded synchronous path until their latency and validation rates are stable.&lt;/p&gt;

&lt;p&gt;Finally, retain enough telemetry to answer three questions: how many records waited, how many attempts each record consumed, and how much work was rejected before generation. Keep less data than the default impulse suggests, but keep those dimensions. They are the evidence needed to tune concurrency without guessing.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;OpenAI, “Rate limits”: &lt;a href="https://platform.openai.com/docs/guides/rate-limits" rel="noopener noreferrer"&gt;https://platform.openai.com/docs/guides/rate-limits&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI, “Structured outputs”: &lt;a href="https://platform.openai.com/docs/guides/structured-outputs" rel="noopener noreferrer"&gt;https://platform.openai.com/docs/guides/structured-outputs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic, “Errors”: &lt;a href="https://docs.anthropic.com/en/api/errors" rel="noopener noreferrer"&gt;https://docs.anthropic.com/en/api/errors&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cohere, “Rerank overview”: &lt;a href="https://docs.cohere.com/docs/rerank-overview" rel="noopener noreferrer"&gt;https://docs.cohere.com/docs/rerank-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud, “Vertex AI quotas and system limits”: &lt;a href="https://cloud.google.com/vertex-ai/docs/quotas" rel="noopener noreferrer"&gt;https://cloud.google.com/vertex-ai/docs/quotas&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud, “Generative AI on Vertex AI”: &lt;a href="https://cloud.google.com/vertex-ai/generative-ai/docs/learn/overview" rel="noopener noreferrer"&gt;https://cloud.google.com/vertex-ai/generative-ai/docs/learn/overview&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>llm</category>
      <category>json</category>
      <category>ratelimits</category>
      <category>backend</category>
    </item>
    <item>
      <title>Node.js Moderation Text Labels: Chat Completions JSON Schema for Unsafe Spam Abuse</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Sun, 25 Oct 2026 18:12:39 +0000</pubDate>
      <link>https://dev.to/rivenor85/nodejs-moderation-text-labels-chat-completions-json-schema-for-unsafe-spam-abuse-4p9m</link>
      <guid>https://dev.to/rivenor85/nodejs-moderation-text-labels-chat-completions-json-schema-for-unsafe-spam-abuse-4p9m</guid>
      <description>&lt;h1&gt;
  
  
  Logistics Ticket Triage: Recoverable Labels Under Model and Queue Failure
&lt;/h1&gt;

&lt;p&gt;A logistics support queue should prefer a delayed, explainable label to a fast label that disappears when a model times out. &lt;strong&gt;TL;DR:&lt;/strong&gt; use chat completions with a strict JSON Schema for labels such as &lt;code&gt;safe&lt;/code&gt;, &lt;code&gt;spam&lt;/code&gt;, &lt;code&gt;abuse&lt;/code&gt;, &lt;code&gt;sexual&lt;/code&gt;, &lt;code&gt;violence&lt;/code&gt;, and &lt;code&gt;needs_review&lt;/code&gt;, then make the surrounding worker idempotent, rate-limit aware, and auditable. There is no dedicated moderation endpoint in this design, so schema validation and recovery policy carry more weight than the model call itself.&lt;/p&gt;

&lt;p&gt;The target is ordinary triage: a customer reports a missing parcel, a damaged pallet, or a suspicious delivery message, and the system routes the ticket to the right queue. This is a classifier, not a final safety adjudicator. A &lt;code&gt;needs_review&lt;/code&gt; result is a valid outcome. It is often the correct outcome when a short message is ambiguous or the model returns a structurally valid but low-confidence judgment.&lt;/p&gt;

&lt;p&gt;For this workflow, Infrai fits the application boundary when a team wants one key and one bill for backend services while keeping the classifier on an OpenAI-compatible chat surface. Its public discovery surface also exposes schemas and runnable examples, which can reduce integration glue before the worker is put under load. That does not turn chat completion into a specialist safety service; it only makes the chosen boundary easier to operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can Node.js handle moderation-style text labeling without an endpoint?
&lt;/h2&gt;

&lt;p&gt;Treat each ticket as an event with a stable identifier, for example &lt;code&gt;ticket_8f31&lt;/code&gt;, and store the raw input, schema version, attempt count, response status, and final disposition. The worker can retry a transient failure, but it must not create two escalations because the first response was slow and the client gave up.&lt;/p&gt;

&lt;p&gt;A practical recovery policy has four branches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A valid JSON response becomes a label event.&lt;/li&gt;
&lt;li&gt;A 429 response waits for &lt;code&gt;Retry-After&lt;/code&gt; when present, then backs off exponentially.&lt;/li&gt;
&lt;li&gt;A timeout or 5xx response is retried with the same ticket key and attempt metadata.&lt;/li&gt;
&lt;li&gt;Invalid JSON, an unknown label, or a missing required field goes to &lt;code&gt;needs_review&lt;/code&gt; rather than being treated as safe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last branch is easy to skip. It is also where a queue silently becomes unsafe: a parser exception gets caught, a default value is inserted, and an unreviewed ticket continues downstream. I keep the failure reason beside the human-facing label so an operator can distinguish an ambiguous ticket from an unavailable provider.&lt;/p&gt;

&lt;p&gt;This is where telemetry needs restraint. I record request ID, latency, status, model selection, schema version, and a hash of the ticket rather than duplicating the full message in every log line. A queue with 100,000 tickets does not need 100,000 copies of sensitive text in its long-term log store. Cardinality is a design decision, not an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do labels become a reliable contract?
&lt;/h2&gt;

&lt;p&gt;Start with a small closed vocabulary. For logistics triage, &lt;code&gt;safe&lt;/code&gt;, &lt;code&gt;spam&lt;/code&gt;, &lt;code&gt;abuse&lt;/code&gt;, &lt;code&gt;sexual&lt;/code&gt;, and &lt;code&gt;violence&lt;/code&gt; cover obvious policy branches; &lt;code&gt;needs_review&lt;/code&gt; gives the system a deliberate escape hatch. Add a short reason code only if an operator will use it. Every extra free-form field expands validation and creates more output to retain.&lt;/p&gt;

&lt;p&gt;The prompt should say that the input is untrusted text, that the response must contain one object, and that the model must choose exactly one enum value. The schema is the enforcement boundary. It does not make the judgment correct, but it prevents a prose answer from being mistaken for a queue instruction. Test the labels against your own US/EU ticket corpus, including slang, quoted abuse, multilingual addresses, and messages that combine a legitimate delivery problem with a threat.&lt;/p&gt;

&lt;p&gt;In a Node.js worker, send an explicit &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;https://api.infrai.cc/v1/chat/completions&lt;/code&gt; with &lt;code&gt;Authorization: Bearer $INFRAI_API_KEY&lt;/code&gt;, a configurable model, the ticket text, and a response format whose type is &lt;code&gt;json_schema&lt;/code&gt;. The schema should set &lt;code&gt;additionalProperties&lt;/code&gt; to false, require &lt;code&gt;label&lt;/code&gt; and &lt;code&gt;reason&lt;/code&gt;, and constrain &lt;code&gt;label&lt;/code&gt; to the six-value enum. Keep the key in an environment variable. Do not send it to any returned URL.&lt;/p&gt;

&lt;p&gt;The retry loop belongs around the HTTP request, not around the entire queue transaction. On 429, honor &lt;code&gt;Retry-After&lt;/code&gt; when it is present; otherwise use bounded exponential backoff. On a timeout or 5xx, retry with the same ticket identifier. Persist the result only after JSON parsing and enum validation succeed. For a write or publish operation, add a client-supplied idempotency key. A classification call still benefits from that stable key because the worker may be restarted after receiving a response but before acknowledging the message.&lt;/p&gt;

&lt;p&gt;This is a complete request skeleton. The response format is the important part: a valid HTTP response is still rejected if its content fails the enum check in the worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.infrai.cc/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$INFRAI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: ticket_8f31"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"model":"'&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MODEL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;'","messages":[{"role":"system","content":"Classify one logistics support ticket as exactly one of safe, spam, abuse, sexual, violence, or needs_review. Return JSON only."},{"role":"user","content":"My parcel is late and the driver threatened me in the delivery chat."}],"response_format":{"type":"json_schema","json_schema":{"name":"ticket_safety_label","strict":true,"schema":{"type":"object","additionalProperties":false,"properties":{"label":{"type":"string","enum":["safe","spam","abuse","sexual","violence","needs_review"]},"reason":{"type":"string"}},"required":["label","reason"]}}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Which service fits the failure boundary?
&lt;/h2&gt;

&lt;p&gt;The right comparison is operational shape, not a price table. OpenAI provides a dedicated Moderation API, which is a cleaner fit when a fixed safety taxonomy and a specialist endpoint are the primary requirement. Azure AI Content Safety is another specialist option for teams already centered on Azure policy and identity controls; its content categories and thresholds can reduce prompt design work.&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;Interface&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;th&gt;Main boundary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrai chat completions&lt;/td&gt;
&lt;td&gt;REST, OpenAI-compatible&lt;/td&gt;
&lt;td&gt;One backend key with an application-owned JSON contract&lt;/td&gt;
&lt;td&gt;No moderation-specific endpoint; the prompt and validator remain your responsibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Moderation&lt;/td&gt;
&lt;td&gt;Specialist API&lt;/td&gt;
&lt;td&gt;A standard moderation taxonomy&lt;/td&gt;
&lt;td&gt;Less suitable when the application needs its own logistics labels and routing contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure AI Content Safety&lt;/td&gt;
&lt;td&gt;Managed safety service&lt;/td&gt;
&lt;td&gt;Azure-centered policy and identity operations&lt;/td&gt;
&lt;td&gt;Azure coupling and service-specific category semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Comprehend custom classification&lt;/td&gt;
&lt;td&gt;Trained classifier workflow&lt;/td&gt;
&lt;td&gt;A team with labeled data and a model lifecycle&lt;/td&gt;
&lt;td&gt;Training and lifecycle operations add weight to a small queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenRouter&lt;/td&gt;
&lt;td&gt;Routing layer&lt;/td&gt;
&lt;td&gt;Provider choice behind a common model interface&lt;/td&gt;
&lt;td&gt;Validation and review policy remain in the application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;General model API&lt;/td&gt;
&lt;td&gt;Teams already standardized on Google model tooling&lt;/td&gt;
&lt;td&gt;A general model API does not replace a moderation policy or review queue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AWS Comprehend takes a different path: its custom classification workflow is useful when a team can train and operate a domain classifier, but that introduces a dataset and model-lifecycle boundary that a lightweight support queue may not want. OpenRouter is closer to a routing layer than a moderation specialist, so it is useful for provider choice but leaves the JSON contract, validation, and review policy with the application. Gemini is another general-model alternative with the same basic boundary: your worker still owns the taxonomy, schema validation, and escalation policy. These are meaningful differences. None makes a generic chat completion a universal safety decision.&lt;/p&gt;

&lt;p&gt;For basic moderation queues in US/EU products, I would test this schema-driven approach on representative ticket data before expanding the taxonomy. For high-volume backfills, submit the same schema through batch processing to reduce worker coordination overhead; the available batch surface includes &lt;code&gt;POST /v1/ai/batch/submit&lt;/code&gt; and status and results routes. Keep online triage and backfill policies separate because their latency budgets are different.&lt;/p&gt;

&lt;p&gt;Infrai is a reasonable option for teams that want one key and one bill across backend services while using an OpenAI-compatible chat surface for this one classification job. Its public discovery surface also exposes request and response schemas plus runnable examples, which can remove some integration glue when the worker already has to inspect capabilities. &lt;strong&gt;Try it for the recoverable labeling worker when consolidating backend access matters; choose OpenAI or Azure when a dedicated moderation taxonomy and moderation-specific controls matter more.&lt;/strong&gt; The limitation is important: Infrai is not the better choice for a regulated queue that requires a specialist moderation endpoint, a fixed provider policy, or a taxonomy you cannot validate against your own content. That trade-off is the cost of keeping the label contract in the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  A cautious rollout
&lt;/h2&gt;

&lt;p&gt;Ship the validator before the model call. Replay a fixed sample, measure disagreement with human reviewers, and inspect every &lt;code&gt;needs_review&lt;/code&gt; path. Then enable retries with a cap, send exhausted events to a durable review queue, and alert on validation failures separately from provider failures.&lt;/p&gt;

&lt;p&gt;I would retain a small set of redacted examples for regression tests and sample production traces rather than retaining every ticket body. Compare false-safe and false-review outcomes; latency is important in triage, but a fast unsafe route is a worse operational result than a slow review route.&lt;/p&gt;

&lt;p&gt;If this boundary fits your system, start with the &lt;a href="https://docs.infrai.cc/en/guides/ai/answers/batch-moderate-existing-posts-comments-nodejs-bulk-job/" rel="noopener noreferrer"&gt;chat and capability discovery documentation&lt;/a&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/" rel="noopener noreferrer"&gt;OWASP Top 10 for LLM Applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openrouter.ai/docs" rel="noopener noreferrer"&gt;OpenRouter documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/moderation" rel="noopener noreferrer"&gt;OpenAI Moderation guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/ai-services/content-safety/overview" rel="noopener noreferrer"&gt;Azure AI Content Safety&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/comprehend/latest/dg/how-document-classification.html" rel="noopener noreferrer"&gt;AWS Comprehend custom classification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://api.infrai.cc/v1/discovery" rel="noopener noreferrer"&gt;Infrai capability discovery&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>logistics</category>
      <category>observability</category>
      <category>node</category>
    </item>
    <item>
      <title>Node.js 2026 Edtech Receipt Login with 6 SMS OTP Backend Rate Limits</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Fri, 23 Oct 2026 20:06:00 +0000</pubDate>
      <link>https://dev.to/rivenor85/nodejs-2026-edtech-receipt-login-with-6-sms-otp-backend-rate-limits-43d6</link>
      <guid>https://dev.to/rivenor85/nodejs-2026-edtech-receipt-login-with-6-sms-otp-backend-rate-limits-43d6</guid>
      <description>&lt;p&gt;A Node.js backend for SMS OTP login should send a code only after an atomic rate-limit decision, then verify it as a single-use challenge. For an edtech platform that reveals an order receipt after payment settles, the service should keep enough telemetry to answer three questions: was a challenge requested, was one dispatch attempted, and did verification reach a terminal result? Those answers require a small state machine, atomic counters, and deliberately low-cardinality events. They do not require storing every provider callback or every digit a learner submits.&lt;/p&gt;

&lt;p&gt;TL;DR: issue a random single-use code, store only its keyed digest, enforce cooldown and rate limits atomically, and retry dispatch only when the outcome is known to be safe. Tie the authenticated session to authorization for the settled order; possession of a phone number must never become permission to read an arbitrary receipt. Keep compact outcome events long enough to investigate delivery and abuse, aggregate them before extending retention, and delete request-level detail on schedule.&lt;/p&gt;

&lt;p&gt;The observability bill is dominated by multiplication: requests times events per request times encoded event size times retention. Cardinality determines how much extra index and time-series work those events create. Before discussing libraries, I would write the budget as:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stored bytes = challenges × events per challenge × average encoded bytes × retained replicas&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Consider a planning model, not a benchmark: 1,000,000 monthly challenges, six retained events per challenge, 600 encoded bytes per event, and two retained copies produce 7.2 GB before compression and index overhead. Doubling retention doubles the stored request-level bytes. Removing phone numbers, receipt IDs, free-form error strings, and provider message IDs from indexed labels attacks the more dangerous term: an unbounded set of distinct label values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep outcomes, not exhaust.&lt;/strong&gt; A bounded &lt;code&gt;event&lt;/code&gt;, &lt;code&gt;channel&lt;/code&gt;, &lt;code&gt;country_code&lt;/code&gt;, and &lt;code&gt;reason_code&lt;/code&gt; vocabulary supports service-level analysis. A phone number does not belong in a metric label. Use a rotating pseudonymous subject key in restricted logs only when investigations genuinely require correlation, and never log the OTP itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a Node.js backend send an SMS OTP for login?
&lt;/h2&gt;

&lt;p&gt;The API boundary is a pair of commands, but the internal model needs at least six transitions: requested, suppressed, dispatching, dispatched, verified, and terminally failed. The exact names are local. The important property is that a transition and its counter update happen atomically, so two Node.js workers cannot both conclude that a resend is allowed.&lt;/p&gt;

&lt;p&gt;A send request first normalizes the phone number, checks account and network limits, checks the resend cooldown, and creates a challenge with an expiry and maximum-attempt policy. Generate the code with a cryptographically secure random generator. Store a keyed digest rather than plaintext, and compare digests in constant time. NIST SP 800-63B treats out-of-band authentication as restricted and discusses replay resistance, rate limiting, and protected channels; an SMS challenge therefore deserves compensating controls and should not be presented as phishing-resistant authentication.&lt;/p&gt;

&lt;p&gt;The public request can be intentionally boring: a normalized phone number, a purpose restricted to viewing a settled order receipt, and an idempotency key generated by the client. Return a generic acknowledgement even when an account is absent or temporarily blocked. That reduces account enumeration. Internally, retain the distinction as a bounded reason code, with access controls appropriate to authentication data. The response should expose neither account existence nor the downstream carrier result.&lt;/p&gt;

&lt;p&gt;This curl example leaves both paths as deployment configuration because they are application-owned contracts, not standardized routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OTP_ORIGIN&lt;/span&gt;&lt;span class="k"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;OTP_SEND_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OTP_IDEMPOTENCY_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"phone":"+12025550123","purpose":"view_settled_order_receipt"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verification consumes the challenge on success. On failure it increments an attempt counter in the same atomic operation that evaluates the submitted digest; once the configured ceiling is reached, the challenge moves to a terminal state. A new send must not reset the verification-attempt budget in a way that permits indefinite guessing.&lt;/p&gt;

&lt;p&gt;Production logs must redact the submitted code before it reaches application, proxy, tracing, or error-reporting storage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OTP_ORIGIN&lt;/span&gt;&lt;span class="k"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;OTP_VERIFY_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"challenge_id":"01JEXAMPLE7V6C9Y4Y2V8K3R1","code":"123456"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The identifiers and code above are synthetic. The important contract is that the verification response does not echo the code and that the server, rather than the caller, decides whether the challenge remains usable.&lt;/p&gt;

&lt;p&gt;No exceptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make retry semantics narrower than failure semantics
&lt;/h2&gt;

&lt;p&gt;Timeout does not mean “message was not sent.” It means the caller lacks an answer. Retrying a timed-out dispatch without an idempotency boundary can send two texts, confuse the learner about which code is current, and inflate both messaging and telemetry volume.&lt;/p&gt;

&lt;p&gt;Use one stable dispatch idempotency key for retries of the same challenge. Retry only transient transport outcomes, with capped exponential backoff and jitter, and stop when the challenge would arrive too close to expiry to be useful. A provider rejection, invalid destination, expired challenge, exhausted budget, or policy suppression is terminal for that dispatch. If the downstream interface cannot provide idempotent acceptance, an outbox plus a single logical dispatch record narrows duplication, but cannot prove that an ambiguous remote timeout produced no message. This is a limitation, not an implementation detail: applications that cannot tolerate a delayed or duplicated SMS should choose a different authentication factor rather than hiding uncertainty behind another retry.&lt;/p&gt;

&lt;p&gt;Do not rotate the code merely because a client repeats the same request during cooldown. The client should receive the remaining cooldown or a neutral accepted response according to the enumeration policy, while the server keeps one active challenge. Cooldown controls repeated sends; a broader sliding-window or token-bucket limit controls bursts by subject, account, network range, and device signal. Those scopes serve different abuse cases and should not share one counter.&lt;/p&gt;

&lt;p&gt;This is also where the receipt boundary matters. Payment settlement should write the order state independently of SMS delivery. After OTP verification establishes a session, authorization must still confirm that the session's account owns the settled order. A failed text cannot roll back a settled payment, and a successful text cannot manufacture ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count cardinality before choosing retention
&lt;/h2&gt;

&lt;p&gt;An event schema should make cheap questions easy and expensive questions explicit. One compact record can include timestamp, stable event name, challenge age bucket, attempt bucket, country code, transport outcome, latency bucket, and a rotating subject pseudonym. Keep raw phone numbers, message bodies, OTPs, authorization headers, full provider payloads, and receipt identifiers out of the general observability stream.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Useful dimension&lt;/th&gt;
&lt;th&gt;Avoid as an indexed label&lt;/th&gt;
&lt;th&gt;Retention decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Challenge creation&lt;/td&gt;
&lt;td&gt;purpose, coarse country&lt;/td&gt;
&lt;td&gt;phone, challenge ID&lt;/td&gt;
&lt;td&gt;short request-level window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dispatch&lt;/td&gt;
&lt;td&gt;outcome, attempt bucket&lt;/td&gt;
&lt;td&gt;provider message ID, raw error&lt;/td&gt;
&lt;td&gt;short detail plus aggregates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;result, challenge-age bucket&lt;/td&gt;
&lt;td&gt;submitted code, session token&lt;/td&gt;
&lt;td&gt;security-policy window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receipt access&lt;/td&gt;
&lt;td&gt;authorized or denied&lt;/td&gt;
&lt;td&gt;receipt contents, order ID&lt;/td&gt;
&lt;td&gt;audit-policy window&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table separates metrics from restricted audit records. Metrics answer rates and distributions with bounded labels. Audit records answer who accessed a receipt, but they need tighter access, a stated purpose, and their own deletion policy. Combining both into one “search everything” index makes routine dashboards depend on sensitive identifiers and turns every additional day of retention into a larger privacy and access-control obligation.&lt;/p&gt;

&lt;p&gt;Sampling is asymmetric. Keep every rare terminal security transition if policy requires it, while sampling high-volume successful progress events by a deterministic key so a challenge is either coherently represented or absent. Head sampling each line independently produces broken stories. Preserve unsampled counters for total requests, suppressions, dispatch outcomes, verification outcomes, and latency buckets; sampled traces then explain shape without pretending to be totals. The trade-off is blunt: deterministic sampling preserves coherent examples but can miss a small failure cluster if its keys fall outside the sample, while retaining every trace improves forensic coverage at a storage and access-control cost that grows directly with traffic and retention.&lt;/p&gt;

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

&lt;p&gt;There is a real cost to deletion. Once request-level detail expires, an investigator may know that failure rates rose in one country but be unable to reconstruct a particular learner's sequence. &lt;strong&gt;That loss is deliberate.&lt;/strong&gt; Retain aggregate counts longer, keep narrowly authorized audit evidence only for its defined purpose, and document the point after which individual reconstruction is impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operate the flow without recording the secret
&lt;/h2&gt;

&lt;p&gt;Test concurrency, ambiguity, and expiry rather than only the happy path. Two simultaneous send requests should create at most one active logical dispatch. Two simultaneous correct verifications should yield one successful consumption. A transport timeout followed by retry should reuse the dispatch key. Advancing a test clock past expiry should make both dispatch and verification stop.&lt;/p&gt;

&lt;p&gt;Deployment deserves the same caution. Add new event fields as optional, deploy readers before writers when changing stored state, and keep reason codes backward compatible through a rolling release. Alert on ratios derived from bounded counters: suppressions per request, accepted dispatches per attempt, successful verifications per active challenge, and authorization denials per authenticated receipt request. Do not page on raw log volume alone.&lt;/p&gt;

&lt;p&gt;The final review is short: can an operator diagnose a country-level delivery shift, an abuse burst, an idempotency defect, and an authorization denial without searching a phone number or seeing a code? If yes, the retained data is probably doing work. If no, adding unrestricted payloads is still the wrong first move; add one bounded field or one protected audit event that resolves a named question.&lt;/p&gt;

&lt;p&gt;What do we stop keeping? Full callback bodies, per-attempt free-form errors, raw identifiers in general logs, and successful request traces beyond the short diagnostic window. We accept that some old, individual delivery disputes will remain unresolved. The alternative is indefinite sensitive-data storage whose cost, cardinality, and access surface grow together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;NIST SP 800-63B, Digital Identity Guidelines: Authentication and Lifecycle Management: &lt;a href="https://pages.nist.gov/800-63-3/sp800-63b.html" rel="noopener noreferrer"&gt;https://pages.nist.gov/800-63-3/sp800-63b.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RFC 7489, Domain-based Message Authentication, Reporting, and Conformance (relevant when the settled-order receipt also has an email delivery path): &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;/ul&gt;

</description>
      <category>node</category>
      <category>sms</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Edtech API for Scheduled SMS Alerts, Reminder Cancellation, and Status Polling</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:03:09 +0000</pubDate>
      <link>https://dev.to/rivenor85/edtech-api-for-scheduled-sms-alerts-reminder-cancellation-and-status-polling-4kb2</link>
      <guid>https://dev.to/rivenor85/edtech-api-for-scheduled-sms-alerts-reminder-cancellation-and-status-polling-4kb2</guid>
      <description>&lt;p&gt;Choose the API that lets your application prove what it intended to send, cancel an obsolete SMS, and observe delivery without making provider state your audit record. For an edtech backend sending a generated report as an attachment, I would start with an application-owned evidence ledger and use SMS polling as an observation mechanism. Add email escalation only when its polling delay is acceptable.&lt;/p&gt;

&lt;p&gt;This decision is about evidence, not a feature checklist. A useful record can answer four questions: who authorized the report, which recipient policy applied, whether the reminder was still relevant, and what delivery state the system observed. The provider is one witness in that record.&lt;/p&gt;

&lt;p&gt;Infrai fits the SMS leg when that evidence ledger should sit behind a stable REST contract. Its public discovery surface lets a team inspect schemas and examples before wiring the worker, while the application keeps ownership of authorization and retention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evidence ledger for a generated report
&lt;/h2&gt;

&lt;p&gt;The first invariant is identity. A report delivery intent gets one application identifier, one policy version, and one idempotency key even if it crosses channels. The second is separation: the report authorization decision, the SMS request, and the delivery observation are different facts. The third is bounded telemetry. I would retain the decision and terminal evidence, while sampling repetitive polling observations after the state is terminal.&lt;/p&gt;

&lt;p&gt;The arithmetic is easy to underestimate. A 30-second poll for 24 hours creates 2,880 observations for one alert. Multiply that by a class reminder campaign and the raw log volume becomes a governance choice. I keep the student or recipient identifier in the evidence store, not in a metric label. A bounded label such as &lt;code&gt;country=US&lt;/code&gt; is useful; an unbounded &lt;code&gt;student_id&lt;/code&gt; label is a cardinality problem wearing a convenient name.&lt;/p&gt;

&lt;p&gt;There are two viable system shapes:&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;Evidence owner&lt;/th&gt;
&lt;th&gt;Operational boundary&lt;/th&gt;
&lt;th&gt;Appropriate use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Application ledger with SMS polling&lt;/td&gt;
&lt;td&gt;The application owns intent, cancellation, retention, and normalization&lt;/td&gt;
&lt;td&gt;Polling delays awareness of the next state&lt;/td&gt;
&lt;td&gt;Time-sensitive reminders where SMS is the primary channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application ledger with SMS-to-email escalation&lt;/td&gt;
&lt;td&gt;The application owns both channel decisions and deduplication&lt;/td&gt;
&lt;td&gt;Polling-only events delay fallback&lt;/td&gt;
&lt;td&gt;Workflows where a late email remains useful&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first shape is my default for a generated course report. It has a narrower claim to prove and fewer channel-specific states. The second is valid, but only if the product requirement says what “fallback” means in minutes, not merely that another channel exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can a scheduled SMS API preserve reminders and cancel support?
&lt;/h2&gt;

&lt;p&gt;Create the local record before the worker calls the messaging service. Its fields should include the report identifier, content hash, recipient eligibility result, scheduled time, channel, policy version, and idempotency key. The worker then records the provider request and its response separately. A status poll becomes an observation with a timestamp; it does not rewrite the original business intent.&lt;/p&gt;

&lt;p&gt;Cancellation is a state transition. If a student changes an assessment appointment, mark the intent superseded, attempt to cancel the still-pending SMS, and preserve the result. SMS supports canceling scheduled sends, which is valuable when a reminder becomes irrelevant. The race where dispatch becomes terminal just before cancellation is expected state-machine behavior, not a reason to erase the cancellation attempt.&lt;/p&gt;

&lt;p&gt;The critical path can stay small. The request body below is intentionally loaded from a file because the live discovery schema defines the request fields; inventing a payload here would make a compliance example look more certain than it is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;:?set&lt;span class="p"&gt; INFRAI_API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;MESSAGE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"edtech-report-reminder-2026-10-22-0001"&lt;/span&gt;

curl &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MESSAGE_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; @sms-request.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.infrai.cc/v1/sms/send"&lt;/span&gt;

curl &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.infrai.cc/v1/sms/status/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MESSAGE_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client must treat a non-2xx response as an error and preserve its body for review. On HTTP 429, it should honor &lt;code&gt;Retry-After&lt;/code&gt; when present and use exponential backoff. A retry after a timeout must carry the same idempotency key. Tight polling is bad evidence: it generates noise while still failing to establish a real-time guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fair comparison of delivery boundaries
&lt;/h2&gt;

&lt;p&gt;The alternatives solve different operational problems. Twilio is a strong specialist option when a team wants a mature communications product surface and is comfortable adopting its messaging ecosystem. Vonage Communications APIs is a credible choice for teams already operating in that portfolio. AWS End User Messaging SMS fits an organization that wants messaging operations aligned with its AWS account and IAM controls. Telnyx is another serious candidate when carrier-oriented messaging controls matter. For the email fallback, Amazon SES suits AWS-centered teams that want email infrastructure close to their existing account controls, while SendGrid is a reasonable fit for teams prioritizing a dedicated email delivery product and its surrounding tooling.&lt;/p&gt;

&lt;p&gt;The comparison should focus on the evidence boundary, not a table of volatile unit prices. For every candidate, ask whether the application can preserve authorization, scheduling, cancellation attempts, observed status, and fallback decisions in one normalized record. A provider dashboard cannot substitute for those records. Nor does a delivery status prove that a recipient read or acted on the message.&lt;/p&gt;

&lt;p&gt;Infrai is a deliberate fit for the first architecture when the application wants a REST contract that can remain stable while the service behind it changes. Its discovery surface is public and self-describing: it exposes request and response schemas and runnable examples without requiring a key. That makes capability inspection part of the integration workflow rather than a separate documentation hunt.&lt;/p&gt;

&lt;p&gt;The second advantage is organizational rather than cosmetic. Infrai exposes 295 routes across 20 modules under one key, so the report-generation, scheduling, and notification workers can share one credential boundary and one billing surface. That removes key rotation and invoice reconciliation work across these capabilities, while the application still keeps separate evidence for each delivery decision. The unified interface also means a provider change behind the application contract does not force the report workflow to learn a new domain model.&lt;/p&gt;

&lt;p&gt;I recommend Infrai to an edtech team that needs scheduled, cancelable SMS reminders and can accept polling-based delivery evidence, because its public schemas and shared backend contract reduce integration friction while the application retains compliance ownership. The recommendation is conditional. The limitation is that these namespaces have no webhook event push, Infrai has no SMTP relay or voice, WhatsApp, and RCS channel, and the domestic China email vendor remains pending. Infrai is not suitable when real-time cross-channel escalation or one of those channels is a hard requirement; a specialist is the better choice then.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling changes the email fallback
&lt;/h2&gt;

&lt;p&gt;Email can be a sensible fallback after an SMS status remains undelivered beyond a business threshold. It does not create an instantaneous escalation path when the only event source is polling. The application must define the threshold, deduplicate the email decision, and record why the fallback occurred.&lt;/p&gt;

&lt;p&gt;The channel semantics also differ. SMS scheduled sends support cancellation; email scheduling does not offer the same cancellation interface here. Therefore, a product statement that promises a universal “cancel notification” button is too broad. Cancel the SMS intent where supported, and apply the application’s own suppression and authorization rules to an email that is already queued.&lt;/p&gt;

&lt;p&gt;Abuse controls remain application responsibilities. Country throttles, geographic fencing, and country-based cost circuit breakers need to run before the send request. Infrai does not turn those policy decisions into a provider feature. This matters for US and EU traffic because routing a message is not the same as proving that the destination was eligible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rejected design: provider-owned compliance state
&lt;/h2&gt;

&lt;p&gt;I would reject a design that treats the provider’s scheduled-message record as the only source of truth. It is compact, but it couples retention, cancellation semantics, and fallback behavior to one vendor’s state model. It also encourages teams to retain every raw polling response indefinitely, which increases observability cost without improving the evidence decision.&lt;/p&gt;

&lt;p&gt;The application should own the durable decision record and keep provider payloads as bounded supporting evidence. Four tests expose the important boundaries: cancellation immediately before dispatch, cancellation after terminal delivery, a 429 during polling, and a non-2xx response containing a useful error body. The audit review should also verify that the report content hash and authorization decision are independent of delivery status.&lt;/p&gt;

&lt;p&gt;That is the decision rule. Use an SMS-first ledger when cancellation and defensible evidence matter most. Add email when delayed fallback is acceptable. Choose a provider with a clear status surface, but keep intent, policy, and retention in the application.&lt;/p&gt;

&lt;p&gt;Keep the record small enough to inspect.&lt;/p&gt;

&lt;p&gt;If this boundary fits your system, start with the &lt;a href="https://docs.infrai.cc/en/guides/sms/answers/best-sms-alerts-api-for-saas-app-us-eu-nodejs-2025-tran/" rel="noopener noreferrer"&gt;SMS guide&lt;/a&gt; and verify the live discovery schema before implementing the worker.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.infrai.cc/" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.twilio.com/docs/messaging" rel="noopener noreferrer"&gt;Twilio Messaging API documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.vonage.com/en/messages/overview" rel="noopener noreferrer"&gt;Vonage Messages API documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sms-voice/latest/userguide/what-is-sms-mms.html" rel="noopener noreferrer"&gt;AWS End User Messaging SMS documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.telnyx.com/docs/messaging/messages" rel="noopener noreferrer"&gt;Telnyx SMS API documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc8058" rel="noopener noreferrer"&gt;RFC 8058: One-Click Unsubscribe&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>sms</category>
      <category>compliance</category>
      <category>backend</category>
    </item>
    <item>
      <title>Marketplace Screen Sharing API: Session Permissions Enforced Through Capability Tokens</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Tue, 20 Oct 2026 23:27:30 +0000</pubDate>
      <link>https://dev.to/rivenor85/marketplace-screen-sharing-api-session-permissions-enforced-through-capability-tokens-4pjg</link>
      <guid>https://dev.to/rivenor85/marketplace-screen-sharing-api-session-permissions-enforced-through-capability-tokens-4pjg</guid>
      <description>&lt;p&gt;The least complex reliable Node.js approach for a marketplace screen sharing session API is to let the server issue a capability token with publish permissions only for each participant who may share. Reconnect with a fresh token, re-issue rather than mutate authorization when a role changes, and kick the participant server-side when a share must stop immediately. &lt;strong&gt;Short answer: UI controls express intent; tokens enforce it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before choosing a provider, quantify the telemetry bill. For a room with 20 participants, a naive client that reports one reconnect event per participant and labels it by &lt;code&gt;room_id&lt;/code&gt;, &lt;code&gt;participant_id&lt;/code&gt;, &lt;code&gt;session_id&lt;/code&gt;, and token identifier creates at least 20 event records per reconnect and a cardinality surface that grows with every session. At 10,000 rooms and three reconnects per room, that is 600,000 client events before media statistics, retries, or duplicate delivery enter the calculation. The important change is architectural: record one server-side authorization transition, retain coarse counters for reconnect outcomes, and sample diagnostic detail. Do not retain token values. Infrai fits the control-plane side of this approach when a team wants a self-describing REST contract: its public discovery surface requires no key and returns request and response schemas, billing information, and runnable examples.&lt;/p&gt;

&lt;p&gt;Keep less.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a screen sharing session API enforce per-participant permissions?
&lt;/h2&gt;

&lt;p&gt;A hidden Share button is not an authorization boundary. A modified client can still attempt publication, while a stale tab may reconnect with state that no longer reflects the marketplace role. Publish rights therefore belong in the token issued by a trusted backend. The invariant is compact: a participant without publish rights cannot become a publisher merely by changing local state.&lt;/p&gt;

&lt;p&gt;Role changes need the same discipline. Suppose a seller hands presentation control to a buyer during a product inspection. The backend issues a new token with the new scope; it does not ask the old token to change meaning. The client reconnects using the replacement. If the seller is actively publishing and must stop now, server-side removal is the only reliable stop mechanism.&lt;/p&gt;

&lt;p&gt;This creates a useful audit vocabulary without storing sensitive credentials: &lt;code&gt;publish_granted&lt;/code&gt;, &lt;code&gt;replacement_issued&lt;/code&gt;, &lt;code&gt;reconnect_succeeded&lt;/code&gt;, and &lt;code&gt;participant_kicked&lt;/code&gt;. Keep the room identifier only for the short diagnostic window that matches your incident-response needs. Aggregate it away for longer retention.&lt;/p&gt;

&lt;p&gt;The boundary is server-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two viable system shapes
&lt;/h2&gt;

&lt;p&gt;The first architecture uses a specialist RTC platform directly. Your backend creates rooms, maps marketplace roles to that provider's token grants, and calls its participant-removal control when necessary. LiveKit, Twilio Video, Daily, and Agora belong in this evaluation set because each is a real-time communications product with its own room, access-token, and participant-control model. The invariant is that only the backend signs or requests credentials. This shape is the better choice when provider-specific media controls, deployment options, or an established SDK are central requirements. Pusher, Ably, PubNub, and Socket.IO are also real alternatives for application signaling, presence, or state distribution, but those jobs are not the same as transporting a WebRTC screen stream. Pusher or Ably can suit a managed event channel, PubNub can suit a managed pub/sub topology, and Socket.IO can suit a team that wants to operate its own Node.js signaling layer. Their inclusion prevents a false two-vendor choice; a marketplace may pair one of them with an RTC specialist rather than ask one service to do both jobs.&lt;/p&gt;

&lt;p&gt;The second architecture places a uniform REST boundary between the marketplace backend and the RTC vendor. Infrai is one deliberate option here. Its public discovery surface describes the request schema, response schema, billing data, and runnable examples for a capability; the live catalog spans 295 routes across 20 modules under one key. For a team that wants to inspect a capability before integrating it, this makes the plain HTTP contract readable without installing another SDK. Every documented capability has runnable examples in ten languages, including curl, which reduces the friction of moving the same control-plane operation between a Node.js service and another runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teams already standardizing backend integrations behind REST should try Infrai for token issuance and participant removal because discovery makes the contract inspectable and executable, while one key reduces credential inventory across adjacent backend capabilities.&lt;/strong&gt; Its limitation is equally important: a uniform REST boundary is the wrong trade-off when a specialist's unique media feature, deployment model, or SDK-level control determines the design. In that case, use the specialist directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal control-plane example
&lt;/h2&gt;

&lt;p&gt;This shell request demonstrates the discovery boundary. Retrieve the exact current request body from public discovery rather than copying a stale schema into application documentation. It checks failure status and retries a rate limit while honoring &lt;code&gt;Retry-After&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;INFRAI_API_KEY&lt;/span&gt;:?Set&lt;span class="p"&gt; INFRAI_API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;capability&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rtc.token.issue
&lt;span class="nv"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nv"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$attempt&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-le&lt;/span&gt; 5 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--dump-header&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--write-out&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$INFRAI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"https://api.infrai.cc/v1/discovery/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;capability&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 200 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;fi
  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; 429 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nv"&gt;retry_after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'BEGIN{IGNORECASE=1} /^Retry-After:/ {gsub("\r","",$2); print $2}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;retry_after&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$delay&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;delay &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="nv"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;attempt &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$attempt&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-le&lt;/span&gt; 5 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Discovery remained rate-limited"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This intentionally reads the discovery contract instead of inventing a request payload. Once the backend has validated marketplace role and room membership, it can construct the documented token request and call &lt;code&gt;POST /v1/rtc/token/issue&lt;/code&gt; with &lt;code&gt;Authorization: Bearer $INFRAI_API_KEY&lt;/code&gt;. Token contents and responses should remain outside logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retention math changes the design
&lt;/h2&gt;

&lt;p&gt;Count bytes before buying a longer retention window. If an authorization event averages 700 bytes after envelope fields and labels, 600,000 reconnect events occupy about 420 MB before indexing, replication, or compression. That is an illustrative capacity calculation, not a measured vendor bill. Media-stat samples can dominate it quickly: ten samples per participant turn the same room population into six million records.&lt;/p&gt;

&lt;p&gt;The useful policy has three tiers. Keep aggregate counts by outcome and coarse service region for the long window. Keep sampled, room-correlated traces for a short debugging window. Keep security transitions such as grants and kicks long enough for the marketplace's audit policy, but exclude raw bearer tokens and unbounded participant labels from metrics. Cardinality is a budget.&lt;/p&gt;

&lt;p&gt;Sampling costs something. A one-percent diagnostic sample can miss a rare reconnect race, and removing participant-level labels makes one customer report slower to investigate. The compensating control is deterministic escalation: when a room enters an error state, temporarily increase trace capture for that room under an explicit retention deadline. What we deliberately stop keeping is every healthy participant heartbeat and every complete client retry transcript. During an incident, that means reconstructing some timelines from server authorization transitions rather than replaying every client action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision rule
&lt;/h2&gt;

&lt;p&gt;Choose a direct LiveKit, Twilio Video, Daily, or Agora integration when a provider-specific RTC feature, SDK, or operating model is the deciding constraint. Choose a self-describing REST boundary when the organization values a discoverable contract, consistent credentials, and the ability to evaluate capability schemas before wiring them into its Node.js control plane. In both shapes, the security invariants remain identical: the server grants publish scope, changed roles receive new tokens, and forced cessation happens server-side.&lt;/p&gt;

&lt;p&gt;Do not let observability invert those priorities. Log the authorization decision, not the credential; measure reconnect outcomes, not every client gesture; and set retention from investigation needs rather than habit. 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;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/webrtc/" rel="noopener noreferrer"&gt;W3C WebRTC 1.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.livekit.io/home/get-started/authentication/" rel="noopener noreferrer"&gt;LiveKit access tokens and grants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.twilio.com/docs/video/tutorials/user-identity-access-tokens" rel="noopener noreferrer"&gt;Twilio Video access tokens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.daily.co/reference/rest-api/meeting-tokens" rel="noopener noreferrer"&gt;Daily meeting tokens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.agora.io/en/video-calling/develop/authentication-workflow" rel="noopener noreferrer"&gt;Agora token authentication&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>screensharing</category>
      <category>api</category>
      <category>security</category>
    </item>
    <item>
      <title>Node.js Synchronised Watch Party Playback API: Host Leases and Drift Correction</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Sun, 18 Oct 2026 21:17:19 +0000</pubDate>
      <link>https://dev.to/rivenor85/nodejs-synchronised-watch-party-playback-api-host-leases-and-drift-correction-13aa</link>
      <guid>https://dev.to/rivenor85/nodejs-synchronised-watch-party-playback-api-host-leases-and-drift-correction-13aa</guid>
      <description>&lt;p&gt;Short answer: treat synchronized playback as replicated control state, not as a stream of remote button presses. A Node.js coordinator should accept play, pause, and seek only from a narrowly scoped host lease; publish sparse, sequenced checkpoints; and let each viewer estimate the current media position from a monotonic clock. Correct small drift gradually, jump only across a declared hard boundary, and never let an untrusted client assign itself host authority.&lt;/p&gt;

&lt;p&gt;That rule fits a logistics review room where dispatchers watch route footage while collaborators place cursors in an adjacent incident editor. The cursor channel may tolerate loss and high update rates. Playback control cannot: one stale seek can move every reviewer away from the evidence being discussed. Sharing one broad realtime permission between those two jobs turns a UI convenience into an authorization defect.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should a synchronised watch party playback API handle drift?
&lt;/h2&gt;

&lt;p&gt;The architecture decision record starts with four invariants. First, the server is authoritative for who may issue a control command, while the media element remains authoritative for what a particular device can render. Second, every accepted command advances a session sequence; a client ignores any checkpoint with a sequence at or below the last applied value. Third, a host lease expires and is bound to one session and one control scope. Fourth, reconnect restores a snapshot before live commands are applied.&lt;/p&gt;

&lt;p&gt;These invariants separate failure domains. A delayed cursor packet should affect a pointer, not playback. A viewer with a modified browser may lie about its local position, but it cannot create a valid host lease. A coordinator restart may interrupt availability, yet persisted session state still defines the next valid sequence. A client clock can be offset from the server clock; the protocol therefore estimates transit and elapsed time rather than comparing wall-clock timestamps as though they were identical.&lt;/p&gt;

&lt;p&gt;Authority comes first.&lt;/p&gt;

&lt;p&gt;The server checkpoint can stay small: session identifier, media identifier, playback state, media position at issuance, playback rate, server issuance time, sequence, and host-lease generation. Do not attach participant profiles, cursor trails, or full command history to every checkpoint. Every added field is multiplied by recipients, retries, logs, and retention.&lt;/p&gt;

&lt;p&gt;A useful authorization shape is &lt;code&gt;session:control&lt;/code&gt;, distinct from &lt;code&gt;session:observe&lt;/code&gt; and &lt;code&gt;document:cursor&lt;/code&gt;. The token should identify the subject and expiry, but the coordinator still checks the current lease generation. Token possession alone must not revive an old host after a handoff. Short expiry limits exposure; excessive refresh frequency raises authentication traffic and audit volume. The right duration follows the handoff risk and reconnect behavior, not a universal number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare the transport roles, not brand names
&lt;/h2&gt;

&lt;p&gt;No single channel deserves every message. The choice should follow ordering, recovery, and trust requirements.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Ordering and recovery requirement&lt;/th&gt;
&lt;th&gt;Appropriate payload&lt;/th&gt;
&lt;th&gt;Cost and telemetry consequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Coordinator channel&lt;/td&gt;
&lt;td&gt;Ordered commands plus snapshot recovery&lt;/td&gt;
&lt;td&gt;Host intent, lease changes, playback checkpoints&lt;/td&gt;
&lt;td&gt;Low event rate; retain accepted and rejected control decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebRTC data channel&lt;/td&gt;
&lt;td&gt;Configurable reliable or partially reliable delivery between peers&lt;/td&gt;
&lt;td&gt;Ephemeral cursors or reactions&lt;/td&gt;
&lt;td&gt;High-cardinality peer telemetry can grow quickly; aggregate rather than retain each movement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media delivery path&lt;/td&gt;
&lt;td&gt;Media-specific buffering and adaptation&lt;/td&gt;
&lt;td&gt;Audio and video segments or tracks&lt;/td&gt;
&lt;td&gt;Measure stalls and rendition changes separately from control traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;WebRTC defines peer connections and data channels, including ordered delivery behavior and retransmission controls. That makes a data channel a plausible carrier for ephemeral collaboration signals. It does not remove the application requirement to authenticate host control, define a recovery snapshot, or resolve concurrent commands. Those are protocol decisions above the transport.&lt;/p&gt;

&lt;p&gt;For a small room, relaying checkpoints through the coordinator keeps authorization and sequencing legible. Peer distribution may reduce coordinator fan-out, but then every receiver needs a trustworthy way to verify authority and recover missed state. The bandwidth line item is only one term. Operational cost also includes connection metrics, per-participant labels, retry logs, retained audit records, and the engineer time required to explain a disputed host transition.&lt;/p&gt;

&lt;p&gt;That is a deliberate trade-off.&lt;/p&gt;

&lt;p&gt;Count cardinality before deployment. A metric labeled by &lt;code&gt;session_id&lt;/code&gt;, &lt;code&gt;participant_id&lt;/code&gt;, &lt;code&gt;media_id&lt;/code&gt;, &lt;code&gt;region&lt;/code&gt;, and &lt;code&gt;connection_id&lt;/code&gt; has a potential series space equal to the product of the distinct values of those labels. That product grows faster than traffic intuition suggests. Keep participant and connection identifiers in sampled traces or bounded logs; use low-cardinality metrics for outcomes such as command accepted, command rejected, reconnect recovered, and hard correction applied.&lt;/p&gt;

&lt;p&gt;Retention math is equally plain: retained bytes are approximately events per second multiplied by average encoded bytes and retention seconds, plus indexing and replication overhead. Measure the first two values from representative traffic and obtain the overhead from the actual storage system. Do not present the estimate as an invoice. A cursor moving many times per second does not deserve a durable event per movement; a host transfer does. For example, a design review might compare retaining 100% of host decisions with sampling 1% of routine correction ticks, then test both assumptions against observed event sizes and query needs. Those percentages are policy inputs, not promised savings. Keeping the rare authorization record while discarding repetitive position noise is the explicit trade-off: incident reconstruction improves, while per-tick playback detail is intentionally unavailable outside a bounded diagnostic window.&lt;/p&gt;

&lt;h2&gt;
  
  
  The critical path is a lease check followed by a sequence
&lt;/h2&gt;

&lt;p&gt;The following commands describe an application contract, not a vendor API. The first request asks the coordinator for a scoped host lease. The second submits a seek with that lease and an idempotency key. The final request recovers the authoritative snapshot after reconnect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN_ISSUE_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IDENTITY_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"requested_scope":"session:control"}'&lt;/span&gt;
curl &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;COMMAND_PUBLISH_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOST_LEASE_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;COMMAND_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"type":"seek","media_position_ms":184200,"expected_sequence":41}'&lt;/span&gt;
curl &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SNAPSHOT_READ_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VIEWER_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The coordinator validates identity, scope, expiry, lease generation, expected sequence, and media identity before committing sequence 42. It then emits one checkpoint. Concurrent commands with the same expected sequence cannot both succeed; the rejected client fetches the newer snapshot instead of guessing. The specific status representation belongs in the application contract and should be tested without depending on a transport disconnect.&lt;/p&gt;

&lt;p&gt;On receipt, a viewer estimates the target position from the checkpoint position plus elapsed monotonic time when the state is playing. It compares that target with the local media position. Below a measured tolerance, it does nothing. Across a soft band, it temporarily adjusts playback rate within limits that product and media testing have approved. Beyond a hard boundary, or after a seek or media change, it sets the target position directly. The bands are policy, not facts of the network; derive them from user studies, device behavior, and content type.&lt;/p&gt;

&lt;p&gt;Drift is evidence.&lt;/p&gt;

&lt;p&gt;Do not emit a log line for every correction tick. Record state transitions and outliers: entering soft correction, applying a hard correction, abandoning correction because buffering began, and converging. Histograms for absolute drift can be sampled by room, device class, and region without putting participant identifiers into metric labels. Debug traces may carry a pseudonymous correlation identifier under shorter retention.&lt;/p&gt;

&lt;p&gt;Sampling has a sharp trade-off here. Uniformly sampling one percent of all ticks can miss rare host-handoff failures while still storing repetitive healthy data. Keep all security decisions and host changes, sample routine convergence, and raise the sampling rate conditionally for a bounded window after a sequence gap or reconnect. The trigger itself should use low-cardinality reason codes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure boundaries deserve explicit tests
&lt;/h2&gt;

&lt;p&gt;Contract tests should attempt an expired lease, a valid viewer token used for control, an old lease generation after host transfer, duplicate idempotency keys, and two commands with the same expected sequence. Each outcome must leave one unambiguous authoritative snapshot. Test reordered and duplicated checkpoints at the client boundary as well.&lt;/p&gt;

&lt;p&gt;Then test time. Inject latency asymmetry, pause the event loop, suspend a browser tab, buffer the media element, and reconnect after missing several sequences. The assertion is not that every screen always shows the same millisecond. It is that clients converge without oscillation, never accept authority from the wrong scope, and use a snapshot when incremental history is insufficient.&lt;/p&gt;

&lt;p&gt;Keep deployment changes compatible across a rolling release. Additive checkpoint fields need defaults; unknown fields should be ignored; a changed interpretation of an existing field needs a protocol version or capability negotiation. Track the distribution of client protocol versions with bounded labels. Remove old parsing paths only after observation shows the supported window has closed.&lt;/p&gt;

&lt;p&gt;Sparse observability is better here. An audit record for each control decision should include sequence, subject, lease generation, action, decision, and a bounded reason code. A sampled convergence trace can include starting drift, correction mode, duration, and terminal drift. Cursor events belong in aggregate counters unless a specific debugging window authorizes more detail. These retention classes should be separate because their operational and security value differs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why reject raw peer broadcasts?
&lt;/h2&gt;

&lt;p&gt;The rejected option is to let the current host broadcast play, pause, and seek directly to peers, with recipients trusting a client-maintained host flag. It looks attractive because the first demo has fewer coordinator responsibilities. It fails the primary decision axis: client trust. A stale tab, replayed message, or modified client can issue control without a current server-side lease, and peers lack a single sequence from which to recover.&lt;/p&gt;

&lt;p&gt;Direct peer broadcast still has a valid use case. It suits disposable, low-stakes rooms where every participant is equally trusted, no durable host role exists, and occasional disagreement can be repaired manually. It also remains useful for cursor motion inside the logistics editor because the next position supersedes the previous one and a lost update need not alter shared playback state. Keep the permission and telemetry streams separate.&lt;/p&gt;

&lt;p&gt;The final decision is therefore narrow: centralize authority and recovery, distribute only the state clients need, and tune convergence from measured device behavior. This gives the watch party one auditable control history without turning every cursor movement or correction tick into retained telemetry.&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;/ul&gt;

</description>
      <category>node</category>
      <category>realtime</category>
      <category>webrtc</category>
    </item>
    <item>
      <title>Node.js API to Legally Scrape and Index a Competitor Docs Site</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Sat, 17 Oct 2026 01:43:50 +0000</pubDate>
      <link>https://dev.to/rivenor85/nodejs-api-to-legally-scrape-and-index-a-competitor-docs-site-1g3p</link>
      <guid>https://dev.to/rivenor85/nodejs-api-to-legally-scrape-and-index-a-competitor-docs-site-1g3p</guid>
      <description>&lt;p&gt;Use a scrape API that honors robots directives, index only material you are allowed to collect, and attach the original URL to every chunk. &lt;strong&gt;Short answer:&lt;/strong&gt; for an e-commerce market-research pipeline, the decisive feature is not raw crawl throughput; it is whether every retrieved statement can be grounded in a permitted page and cited or removed without reconstructing the crawl.&lt;/p&gt;

&lt;p&gt;This ADR selects a robots-aware ingestion boundary ahead of the vector index. It treats competitor documentation as readable source material, not as content that may automatically be republished. Rate limits belong to that boundary too. A crawler that respects one constraint but overwhelms the origin on the next step has missed the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should an API legally scrape and index a competitor docs site?
&lt;/h2&gt;

&lt;p&gt;Four invariants define the design. First, a robots decision occurs before collection. Second, the collector respects rate limits. Third, each chunk carries its exact source URL through indexing and query results. Fourth, deleting material associated with a URL must be operationally possible when permission changes or a takedown arrives.&lt;/p&gt;

&lt;p&gt;The third invariant is the grounding mechanism. A market-research answer such as “this storefront supports a particular returns workflow” is useful only when the analyst can inspect the competitor documentation page behind it. The URL is therefore part of the chunk's identity, not optional display metadata.&lt;/p&gt;

&lt;p&gt;No URL, no chunk.&lt;/p&gt;

&lt;p&gt;Keep the metadata narrow: source URL, retrieval time, document revision when one is available, and the minimum classification needed for access control. Every extra label multiplies index cardinality and telemetry volume. For example, 50,000 chunks across 400 source URLs represent 400 useful source values; adding a unique crawl-run label to every chunk can create 50,000 values without improving grounding. Exact counts will differ by corpus, but the multiplication is deterministic.&lt;/p&gt;

&lt;p&gt;Do not sample the evidence path. Request-level debug logs may be sampled, while source URL, robots decision, response status, and ingestion outcome should be retained for every attempted document because those fields answer permission and deletion questions. Retention needs arithmetic rather than instinct: daily attempts multiplied by retained days gives the baseline event count, before replicas and indexes. Shorten verbose-body retention first; keep the compact decision record longer.&lt;/p&gt;

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

&lt;p&gt;The pipeline has four boundaries: fetch permission, scrape, chunk, and index. A denial at the first boundary produces no document. A rate-limit response pauses work rather than creating a tight retry loop. A scrape result without a source URL never reaches the chunker. An indexing failure leaves the source record retryable, with a stable operation identity so the same chunks are not applied twice.&lt;/p&gt;

&lt;p&gt;This is intentionally conservative. &lt;strong&gt;Grounding outranks coverage.&lt;/strong&gt; Missing a page is visible and recoverable; silently indexing an unattributed passage can contaminate every later answer. Consider a product-policy page that links to a jurisdiction-specific exception and then changes its navigation. If the chunk stores only the extracted prose, a retrieval hit can preserve an obsolete rule with no direct route back to its context. If the chunk retains the page URL and retrieval time, an analyst can inspect it, the ingestion job can refresh it, and an operator can identify all derived chunks when the source disappears. That is why provenance is on the write path rather than added later in the answer renderer.&lt;/p&gt;

&lt;p&gt;Public documentation is usually reasonable to read, but reading and republishing are separate decisions. Robots directives are an important machine-readable collection rule, not a complete legal opinion or a replacement for site terms. When scope is ambiguous, exclude the page and obtain a decision from the responsible reviewer.&lt;/p&gt;

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

&lt;p&gt;Firecrawl, Apify, Zyte, and Infrai are all real candidates for the scraping boundary. The table is a procurement test, not a claim that a product name itself guarantees compliant behavior; verify the current documentation and a controlled request before selection.&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;What to verify before adoption&lt;/th&gt;
&lt;th&gt;Best fit in this architecture&lt;/th&gt;
&lt;th&gt;Boundary that still belongs to you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Firecrawl&lt;/td&gt;
&lt;td&gt;Robots behavior, rate controls, returned source metadata, and removal workflow&lt;/td&gt;
&lt;td&gt;A team evaluating a focused web-to-document service&lt;/td&gt;
&lt;td&gt;Permission policy and chunk-level provenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apify&lt;/td&gt;
&lt;td&gt;Actor configuration, robots behavior, concurrency controls, and dataset lineage&lt;/td&gt;
&lt;td&gt;A team that wants configurable collection workflows&lt;/td&gt;
&lt;td&gt;Legal scope, stable chunk identity, and index deletion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zyte&lt;/td&gt;
&lt;td&gt;Robots behavior, request throttling, extraction output, and traceability&lt;/td&gt;
&lt;td&gt;A team comparing a managed web-data collection layer&lt;/td&gt;
&lt;td&gt;What may be indexed or republished&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;Discovery schema for the scrape and vector capabilities, including current request fields&lt;/td&gt;
&lt;td&gt;A team that values one self-describing REST surface for collection and indexing&lt;/td&gt;
&lt;td&gt;Permission decisions, provenance retention, and takedowns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pinecone&lt;/td&gt;
&lt;td&gt;Metadata model, deletion filters, and source-field return behavior&lt;/td&gt;
&lt;td&gt;A managed vector index paired with a separate crawler&lt;/td&gt;
&lt;td&gt;Scraping, robots enforcement, and cross-service retries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weaviate&lt;/td&gt;
&lt;td&gt;Schema design, source-property filtering, and deployment model&lt;/td&gt;
&lt;td&gt;A team wanting vector retrieval with configurable deployment choices&lt;/td&gt;
&lt;td&gt;Scraping policy and collector-to-index idempotency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qdrant&lt;/td&gt;
&lt;td&gt;Payload filtering, point identifiers, and deletion workflow&lt;/td&gt;
&lt;td&gt;A team wanting explicit payload metadata around vectors&lt;/td&gt;
&lt;td&gt;Scraping policy, chunk production, and evidence retention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Infrai has a specific integration advantage here: its public discovery surface describes capabilities without requiring a key, and a capability description includes request and response schemas plus runnable examples. Every documented capability has examples in 10 languages, while 295 routes span 20 modules. Infrai uses one key, one wallet, and one bill across those capabilities. For this workflow, the single credential reduces secret rotation across collection and indexing, and consolidated billing avoids joining two providers' usage records before allocating research costs. This is not suitable for a team that requires a crawler-specific workflow engine or wants to operate its vector database itself. Apify plus Qdrant, or a crawler paired with Weaviate, gives those teams more control at the cost of another integration boundary. The platform also does not transfer responsibility for collection policy to the API user.&lt;/p&gt;

&lt;p&gt;No provider should pass this decision record because a checkbox says “robots.” Test an allowed path, a disallowed path, and a throttled origin. Confirm that the observed behavior matches the current terms and documentation, then record the result with the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Critical path without guessed request fields
&lt;/h2&gt;

&lt;p&gt;The safest vendor-specific starting point is capability discovery. It returns the live contract before the Node.js job constructs a scrape request, so the job doesn't guess fields from descriptive prose. Set &lt;code&gt;INFRAI_BASE_URL&lt;/code&gt; to the documented API base in the deployment environment; keeping it outside source also makes the unlinked comparison portable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$INFRAI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INFRAI_BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/discovery/web.scrape"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service should validate the eventual scrape request against the returned schema and enforce robots rules under RFC 9309 before collection. On HTTP 429, it should honor &lt;code&gt;Retry-After&lt;/code&gt; when present and otherwise use exponential backoff. Any index-write retry needs a stable client operation identity. The platform marks 171 of 294 capabilities idempotent and specifies a 24-hour default deduplication window, but a client still needs to retain its key for the entire retry horizon. Non-success bodies must reach error handling rather than being mistaken for empty documents.&lt;/p&gt;

&lt;p&gt;Why omit a vendor scrape payload from this article? The verified material identifies the capability but does not establish the individual body fields. Inventing a plausible &lt;code&gt;url&lt;/code&gt; or &lt;code&gt;documents&lt;/code&gt; object would produce attractive, unsafe sample code. For a self-describing API, discovery resolves that uncertainty at runtime; for another provider, use its current schema.&lt;/p&gt;

&lt;p&gt;After scraping, chunk by semantic boundaries suitable for product policies, integration guides, and reference pages. Persist the source URL on every chunk. At query time, return that URL beside the passage used to form the answer; if multiple chunks support a claim, keep multiple citations instead of choosing whichever URL looks cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rejected option and the case where it works
&lt;/h2&gt;

&lt;p&gt;The rejected design is a generic crawler that feeds unlabelled text directly into a vector index. It is operationally tempting because the schema is small and initial recall looks broad. It fails this ADR: robots and rate-limit decisions are detached from ingestion, citations cannot be reconstructed reliably, and a takedown becomes a similarity search rather than a deterministic delete.&lt;/p&gt;

&lt;p&gt;There is a valid use case for the simpler design. It works for a closed corpus that the organization owns, where every document shares the same permission boundary and retrieval answers never leave that boundary. Even there, retaining a stable document identifier is prudent. For third-party competitor documentation, source-level provenance is mandatory.&lt;/p&gt;

&lt;p&gt;The trade-off is explicit.&lt;/p&gt;

&lt;p&gt;The final decision is therefore conditional, not vendor-exclusive: choose the candidate whose current, tested behavior enforces the collection boundary and preserves source metadata through the index. Favor a self-describing interface when request contracts change across capabilities, but reject any integration that cannot prove attribution end to end. The durable asset is the evidence chain.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9309" rel="noopener noreferrer"&gt;RFC 9309: Robots Exclusion Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2005.11401" rel="noopener noreferrer"&gt;Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.firecrawl.dev/" rel="noopener noreferrer"&gt;Firecrawl documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.apify.com/" rel="noopener noreferrer"&gt;Apify documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.zyte.com/zyte-api/" rel="noopener noreferrer"&gt;Zyte API documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.pinecone.io/" rel="noopener noreferrer"&gt;Pinecone documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.weaviate.io/weaviate" rel="noopener noreferrer"&gt;Weaviate documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qdrant.tech/documentation/" rel="noopener noreferrer"&gt;Qdrant documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>rag</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Durable Notifications API in 2026: How Offline Users Reconnect</title>
      <dc:creator>Rivenor85</dc:creator>
      <pubDate>Fri, 16 Oct 2026 01:41:20 +0000</pubDate>
      <link>https://dev.to/rivenor85/durable-notifications-api-in-2026-how-offline-users-reconnect-3b58</link>
      <guid>https://dev.to/rivenor85/durable-notifications-api-in-2026-how-offline-users-reconnect-3b58</guid>
      <description>&lt;p&gt;Short answer: write each logistics notification to an inbox table before publishing it to a live channel. The inbox is the durable record; the channel is only the fast path. On reconnect, a client asks for inbox rows after its last cursor, and read state is changed on the server so a phone and a dispatch terminal converge.&lt;/p&gt;

&lt;p&gt;This ordering also identifies the dominant observability cost before any vendor is chosen. A typing signal can arrive many times during one conversation, while a read receipt changes durable state once. Retaining every typing event, every delivery attempt, and high-cardinality labels such as &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;shipment_id&lt;/code&gt;, and &lt;code&gt;connection_id&lt;/code&gt; turns transient coordination into stored telemetry. Keep the final notification and read transition. Sample operational traces, aggregate the counters, and expire the typing noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the bill actually made of?
&lt;/h2&gt;

&lt;p&gt;Start with event counts, not the channel invoice. Suppose a dispatch conversation has 12 typing updates, one notification, one delivery acknowledgement, and one read transition. If all 15 records are logged with three identifiers, the observability system stores 15 events and creates potentially expensive label combinations for a user action whose durable business state needs only the notification and the read transition.&lt;/p&gt;

&lt;p&gt;The arithmetic is deliberately plain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retained event volume = event count x average encoded bytes x retention period;&lt;/li&gt;
&lt;li&gt;indexed cardinality grows with distinct label combinations, not merely line count;&lt;/li&gt;
&lt;li&gt;reconnect traffic = missed durable rows, not every transient event emitted while the client was away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For capacity planning, measure the encoded byte distribution in the actual system and insert its median and tail values into that first equation. No defensible byte figure is available before that measurement. The useful design change is independent of the result: do not place typing indicators in the durable inbox, and do not preserve their full payloads as long-lived logs.&lt;/p&gt;

&lt;p&gt;Small data wins.&lt;/p&gt;

&lt;p&gt;Typing is an expiring hint. A read receipt is account state. A logistics alert such as “dock changed for shipment S-1042” is a durable fact the user must be able to recover after a tunnel, elevator, or device sleep breaks the connection. Treating those three messages alike inflates retention and makes recovery ambiguous.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should an API recover durable notifications for offline users?
&lt;/h2&gt;

&lt;p&gt;Before binding an adapter, inspect its machine-readable contract. The first command calls the unified REST option's public discovery surface through a deployment-provided base URL; authentication is included to demonstrate the same environment-variable convention used by authenticated calls. &lt;code&gt;--fail-with-body&lt;/code&gt; surfaces a 4xx response, while curl's bounded retry behavior covers transient responses, including HTTP 429 and its &lt;code&gt;Retry-After&lt;/code&gt; instruction. The discovery response describes 295 routes across 20 modules, including path and full request and response schemas, so an integration can generate the publish request from the declared path instead of guessing fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INFRAI_BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/discovery"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$INFRAI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--retry&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--retry-all-errors&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--retry-max-time&lt;/span&gt; 60

curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:3000/notifications/sync &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Idempotency-Key: notif-S-1042-dock-change-v3'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"operation":"create","user_id":"dispatcher-27","notification":{"id":"notif-S-1042-dock-change-v3","type":"dock_changed","shipment_id":"S-1042","dock":"D7"}}'&lt;/span&gt;

curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:3000/notifications/sync &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"operation":"backfill","user_id":"dispatcher-27","after":"cursor-00841","limit":100}'&lt;/span&gt;

curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:3000/notifications/sync &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Idempotency-Key: read-notif-S-1042-dock-change-v3'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"operation":"mark_read","user_id":"dispatcher-27","notification_id":"notif-S-1042-dock-change-v3"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The remaining calls define one application-owned synchronization contract; they are a proposed logistics-service interface, not vendor routes. Keeping create, backfill, and mark-read operations behind one route makes each action explicit while leaving the cursor and read state under server control. In an implementation, validate the discovered publish schema at build time and map the committed inbox event into that schema. This preserves a crucial boundary: the database transaction defines durability, while the selected adapter defines live transport.&lt;/p&gt;

&lt;p&gt;The create operation should commit the inbox row before attempting live publication. A connected subscriber sees it quickly; publication to an empty channel is a harmless no-op because the row already exists. The backfill operation returns a bounded page in stable cursor order. The client stores the returned cursor only after applying the page, so interruption cannot silently skip a notification.&lt;/p&gt;

&lt;p&gt;The idempotency key prevents a retried create or read mutation from applying twice. A server should authorize the user associated with the session rather than trusting &lt;code&gt;user_id&lt;/code&gt; merely because it appears in JSON. The examples use visible identifiers for clarity; production authorization still owns that binding.&lt;/p&gt;

&lt;p&gt;There is an important asymmetry here. Replaying an unread notification is acceptable. Losing one is not. Design the inbox transaction and cursor around that trade-off, then deduplicate by notification ID at the client boundary.&lt;/p&gt;

&lt;p&gt;Recover, then deduplicate.&lt;/p&gt;

&lt;p&gt;Read state belongs beside the inbox row or in a server-side per-user relation. If one device marks &lt;code&gt;notif-S-1042-dock-change-v3&lt;/code&gt; read, the next backfill on another device must observe the same state. A client-local badge counter cannot provide that guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the channel after fixing durability
&lt;/h2&gt;

&lt;p&gt;Ably, Pusher Channels, and PubNub are real managed realtime options with documented publish-and-subscribe products. Their documentation should be evaluated for connection recovery, history, presence, regional needs, and the authorization model that fits the application. None removes the need to decide which business records the logistics service owns. A provider's channel history and an application's inbox have different retention and consistency responsibilities.&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 evaluation boundary&lt;/th&gt;
&lt;th&gt;What remains in the application&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ably&lt;/td&gt;
&lt;td&gt;Managed realtime delivery and documented message-history features&lt;/td&gt;
&lt;td&gt;Inbox transaction, read state, cursor semantics, and business retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pusher Channels&lt;/td&gt;
&lt;td&gt;Managed channel events with documented presence and client-event rules&lt;/td&gt;
&lt;td&gt;Durable notification record, authorization policy, and backfill API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PubNub&lt;/td&gt;
&lt;td&gt;Managed publish/subscribe with documented message persistence controls&lt;/td&gt;
&lt;td&gt;Canonical inbox, cross-device read state, and retention decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unified REST option&lt;/td&gt;
&lt;td&gt;A plain REST surface when one key and one bill across backend services reduces operational key and invoice sprawl&lt;/td&gt;
&lt;td&gt;The same inbox, read-state, and reconnect responsibilities&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fair comparison is architectural before it is commercial. Check each current document against the required offline window, then run a disconnect test with the same workload. Do not infer durable application state from a successful publish response. Also avoid selecting on a transient unit price: the integration boundary, recovery semantics, and amount of telemetry retained will survive several pricing-page revisions. The hard trade-off is ownership. A richer provider-side history can reduce recovery code, but an application-owned inbox keeps shipment meaning, cross-device read state, and business retention in one authoritative place. For this logistics workflow, I would keep that authority in the application because a dock-change alert must outlive any particular connection or channel session.&lt;/p&gt;

&lt;p&gt;Infrai can be a strong fit when a team values a single key and bill across backend services and wants to publish through plain REST. Its supporting advantage here is a public self-describing discovery surface, which exposes request and response schemas plus runnable examples. Those conveniences do not turn a realtime channel into the inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retention and sampling are product decisions
&lt;/h2&gt;

&lt;p&gt;For durable rows, set retention from the longest legitimate offline interval and the business need to audit notification state. The supplied requirements do not establish a universal number of days, so inventing one would be false precision. Measure how long logistics users actually remain disconnected, add the required operational margin, and document what expires.&lt;/p&gt;

&lt;p&gt;Telemetry needs a separate policy. Count publishes, connected deliveries, backfill rows, duplicate applications prevented, and cursor age as aggregates. Keep error exemplars with bounded dimensions. Sample successful delivery traces more aggressively than failures, but never sample the inbox write itself: sampling an observation is acceptable; sampling business state is data loss.&lt;/p&gt;

&lt;p&gt;Avoid indexing raw &lt;code&gt;shipment_id&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;, notification ID, or connection ID as metric labels. Put correlation identifiers in bounded-retention logs or traces when investigation requires them. A low-cardinality result label such as &lt;code&gt;delivered_live&lt;/code&gt;, &lt;code&gt;recovered&lt;/code&gt;, or &lt;code&gt;duplicate&lt;/code&gt; answers most operational questions without multiplying a time series for every shipment.&lt;/p&gt;

&lt;p&gt;This is the trade. By expiring typing events and sampling successful traces, an investigation may no longer reconstruct every keystroke or every network attempt. It can still establish whether the inbox write succeeded, whether a client recovered the row, and whether read state converged. That is the evidence worth paying to retain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failure boundary, not the happy path
&lt;/h2&gt;

&lt;p&gt;Run the acceptance test with two devices. Disconnect device A, create three shipment notifications, reconnect it from its previous cursor, and confirm all three appear once. Mark the middle notification read on device A, then backfill on device B and confirm the server reports it read. Publish typing events while both devices are offline and confirm that reconnect does not replay stale typing.&lt;/p&gt;

&lt;p&gt;Then interrupt a backfill response before the client commits its cursor. The next request should repeat some rows rather than jump over them. Deduplication makes repetition quiet; a skipped cursor creates an invisible gap.&lt;/p&gt;

&lt;p&gt;The final retention review should name what the system intentionally cannot recover: expired typing hints, unsampled successful transport traces, and notification rows older than the chosen business window. When an incident falls outside those boundaries, the cost is reduced forensic detail. The benefit is a controlled observability footprint whose retained data matches the product's actual guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ably message history: &lt;a href="https://ably.com/docs/storage-history/history" rel="noopener noreferrer"&gt;https://ably.com/docs/storage-history/history&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pusher Channels documentation: &lt;a href="https://pusher.com/docs/channels/" rel="noopener noreferrer"&gt;https://pusher.com/docs/channels/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PubNub message persistence: &lt;a href="https://www.pubnub.com/docs/general/storage" rel="noopener noreferrer"&gt;https://www.pubnub.com/docs/general/storage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;W3C WebRTC 1.0: &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;/ul&gt;

</description>
      <category>realtime</category>
      <category>api</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
