<?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: Aman Bhakar</title>
    <description>The latest articles on DEV Community by Aman Bhakar (@aman_bhakar_289b2bef4b7c5).</description>
    <link>https://dev.to/aman_bhakar_289b2bef4b7c5</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%2F2825284%2Fcb5efe6f-edf2-42e5-a00c-f9226b9fd929.jpg</url>
      <title>DEV Community: Aman Bhakar</title>
      <link>https://dev.to/aman_bhakar_289b2bef4b7c5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aman_bhakar_289b2bef4b7c5"/>
    <language>en</language>
    <item>
      <title>FlowHub: I got tired of rewriting SMS/OAuth/webhook code every time we switched providers, so I split it into 16 npm packages and one all-in-one package</title>
      <dc:creator>Aman Bhakar</dc:creator>
      <pubDate>Thu, 23 Jul 2026 07:30:00 +0000</pubDate>
      <link>https://dev.to/aman_bhakar_289b2bef4b7c5/flowhub-i-got-tired-of-rewriting-smsoauthwebhook-code-every-time-we-switched-providers-so-i-2kb3</link>
      <guid>https://dev.to/aman_bhakar_289b2bef4b7c5/flowhub-i-got-tired-of-rewriting-smsoauthwebhook-code-every-time-we-switched-providers-so-i-2kb3</guid>
      <description>&lt;p&gt;Every backend I've worked on ends up with the same mess: Twilio calls in one file, Stripe-style OAuth in another, S3 upload logic somewhere else — each with its own error shape, its own (usually missing) retry logic, and no consistent way to know "this thing happened" across the app. Swapping Twilio for MSG91 means hunting down every call site.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Flow Hub&lt;/strong&gt; — 16 small, independently-installable &lt;code&gt;@flowhub/*&lt;/code&gt; packages that put one typed interface in front of each concern (SMS, email, OAuth, storage, OTP, webhooks, queue, cache), backed by a shared event bus. Provider swaps become a &lt;code&gt;register()&lt;/code&gt; call, not a refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before / after
&lt;/h2&gt;

&lt;p&gt;Hand-rolled multi-provider SMS with retry + fallback — the code most teams actually ship:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;ts&lt;br&gt;
async function sendOtpSms(to: string, body: string) {&lt;br&gt;
  for (const attempt of [1, 2, 3]) {&lt;br&gt;
    try {&lt;br&gt;
      await twilioClient.messages.create({ to, body, from: TWILIO_FROM });&lt;br&gt;
      return;&lt;br&gt;
    } catch {&lt;br&gt;
      if (attempt === 3) {&lt;br&gt;
        await msg91Client.send({ to, body }); // different call shape&lt;br&gt;
        return;&lt;br&gt;
      }&lt;br&gt;
      await new Promise((r) =&amp;gt; setTimeout(r, 200 * attempt));&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;With Flow Hub:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`ts&lt;br&gt;
import { SmsRegistry } from "@flowhub/sms";&lt;br&gt;
import { retry } from "@flowhub/retry";&lt;/p&gt;

&lt;p&gt;const sms = new SmsRegistry(app.events);&lt;br&gt;
sms.register(twilioProvider);&lt;br&gt;
sms.register(msg91Provider);&lt;/p&gt;

&lt;p&gt;async function sendOtpSms(to: string, body: string) {&lt;br&gt;
  try {&lt;br&gt;
    await retry(() =&amp;gt; sms.send("twilio", to, body), { attempts: 3, delayMs: 200, backoff: "exponential" });&lt;br&gt;
  } catch {&lt;br&gt;
    await sms.send("msg91", to, body);&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Same behavior, and &lt;code&gt;app.events.on("sms.sent", ...)&lt;/code&gt; / &lt;code&gt;sms.failed&lt;/code&gt; fire regardless of which provider handled it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 60 seconds
&lt;/h2&gt;

&lt;p&gt;Real terminal output, against what's actually published:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`console&lt;br&gt;
$ npm add -g @flowhub/cli&lt;br&gt;
$ flowkit init my-app&lt;br&gt;
  create  my-app/package.json&lt;br&gt;
  create  my-app/index.js&lt;br&gt;
  create  my-app/.gitignore&lt;/p&gt;

&lt;p&gt;$ cd my-app &amp;amp;&amp;amp; npm install &amp;amp;&amp;amp; flowkit add twilio&lt;br&gt;
  update  package.json (+@flowhub/sms)&lt;/p&gt;

&lt;p&gt;$ npm install &amp;amp;&amp;amp; npm start&lt;br&gt;
FlowKit app ready&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;core&lt;/code&gt; — event bus, DI, plugin lifecycle&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sms&lt;/code&gt; / &lt;code&gt;email&lt;/code&gt; / &lt;code&gt;oauth&lt;/code&gt; / &lt;code&gt;storage&lt;/code&gt; / &lt;code&gt;webhook&lt;/code&gt; / &lt;code&gt;queue&lt;/code&gt; / &lt;code&gt;cache&lt;/code&gt; — provider registries, each with typed &lt;code&gt;register()&lt;/code&gt; + events&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;otp&lt;/code&gt; — generation/verification/expiry/cooldown&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;express&lt;/code&gt; / &lt;code&gt;cli&lt;/code&gt; — route helpers and the &lt;code&gt;flowkit&lt;/code&gt; scaffolding CLI&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flowhub&lt;/code&gt; — a meta-package that re-exports all of the above in one &lt;code&gt;npm install&lt;/code&gt; if you don't want to add them one at a time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All MIT-licensed, all on npm under &lt;a href="https://www.npmjs.com/org/flowhub" rel="noopener noreferrer"&gt;&lt;code&gt;@flowhub&lt;/code&gt;&lt;/a&gt;, source at &lt;a href="https://github.com/gitaman69/flowhub" rel="noopener noreferrer"&gt;github.com/gitaman69/flowhub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I want to be honest about
&lt;/h2&gt;

&lt;p&gt;Right after publishing, I went back and installed all 16 packages fresh from the registry (not the local workspace) and wrote a smoke test hitting every module's real behavior. Found a real bug: &lt;code&gt;@flowhub/otp&lt;/code&gt;'s resend-cooldown was tracked on the same object &lt;code&gt;verify()&lt;/code&gt; deletes on success — so a successful verify silently reset the cooldown, letting &lt;code&gt;generate()&lt;/code&gt; → &lt;code&gt;verify()&lt;/code&gt; → &lt;code&gt;generate()&lt;/code&gt; bypass the rate limit meant to throttle SMS/email sends. Fixed, tested, republished as &lt;code&gt;0.0.3&lt;/code&gt;. Change log's in the repo if you want the details.&lt;/p&gt;

&lt;p&gt;Feedback, bug reports, and provider implementations (got MSG91, real GCS, real Mailgun?) welcome — that's what the &lt;a href="https://github.com/gitaman69/flowhub/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING guide&lt;/a&gt; is for.&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
