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.
So I built Flow Hub — 16 small, independently-installable @flowhub/* 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 register() call, not a refactor.
Before / after
Hand-rolled multi-provider SMS with retry + fallback — the code most teams actually ship:
\ts
async function sendOtpSms(to: string, body: string) {
for (const attempt of [1, 2, 3]) {
try {
await twilioClient.messages.create({ to, body, from: TWILIO_FROM });
return;
} catch {
if (attempt === 3) {
await msg91Client.send({ to, body }); // different call shape
return;
}
await new Promise((r) => setTimeout(r, 200 * attempt));
}
}
}
\\
With Flow Hub:
\`ts
import { SmsRegistry } from "@flowhub/sms";
import { retry } from "@flowhub/retry";
const sms = new SmsRegistry(app.events);
sms.register(twilioProvider);
sms.register(msg91Provider);
async function sendOtpSms(to: string, body: string) {
try {
await retry(() => sms.send("twilio", to, body), { attempts: 3, delayMs: 200, backoff: "exponential" });
} catch {
await sms.send("msg91", to, body);
}
}
`\
Same behavior, and app.events.on("sms.sent", ...) / sms.failed fire regardless of which provider handled it.
Try it in 60 seconds
Real terminal output, against what's actually published:
\`console
$ npm add -g @flowhub/cli
$ flowkit init my-app
create my-app/package.json
create my-app/index.js
create my-app/.gitignore
$ cd my-app && npm install && flowkit add twilio
update package.json (+@flowhub/sms)
$ npm install && npm start
FlowKit app ready
`\
What's in it
-
core— event bus, DI, plugin lifecycle -
sms/email/oauth/storage/webhook/queue/cache— provider registries, each with typedregister()+ events -
otp— generation/verification/expiry/cooldown -
express/cli— route helpers and theflowkitscaffolding CLI -
flowhub— a meta-package that re-exports all of the above in onenpm installif you don't want to add them one at a time
All MIT-licensed, all on npm under @flowhub, source at github.com/gitaman69/flowhub.
The part I want to be honest about
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: @flowhub/otp's resend-cooldown was tracked on the same object verify() deletes on success — so a successful verify silently reset the cooldown, letting generate() → verify() → generate() bypass the rate limit meant to throttle SMS/email sends. Fixed, tested, republished as 0.0.3. Change log's in the repo if you want the details.
Feedback, bug reports, and provider implementations (got MSG91, real GCS, real Mailgun?) welcome — that's what the CONTRIBUTING guide is for.
Top comments (1)
Publishing the OTP cooldown bug and explaining how it was fixed is exactly the kind of transparency that builds confidence in an infrastructure library.
As FlowHub grows across multiple packages and providers, developers may need a reliable history of security-sensitive fixes, API guarantees and migration decisions.
Have you considered keeping an append-only public release journal where every important disclosure and commitment remains permanently available, even after the repository documentation is reorganized or rewritten?