<?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>Any website or file, searchable in one call</title>
      <dc:creator>Aman Bhakar</dc:creator>
      <pubDate>Fri, 11 Sep 2026 06:45:08 +0000</pubDate>
      <link>https://dev.to/aman_bhakar_289b2bef4b7c5/any-website-or-file-searchable-in-one-call-59b1</link>
      <guid>https://dev.to/aman_bhakar_289b2bef4b7c5/any-website-or-file-searchable-in-one-call-59b1</guid>
      <description>&lt;p&gt;Your AI feature is only as good as what you feed it.&lt;/p&gt;

&lt;p&gt;And what you have to feed it is a docs site that renders in JavaScript, 400 scanned PDFs, and a help centre nobody has exported since 2023.&lt;/p&gt;

&lt;p&gt;So you write a scraper. Then a PDF parser. Then OCR. Then you're picking a chunking strategy, an embedding model, a vector store. Three weeks gone and the feature still isn't built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the part we deleted.&lt;/strong&gt; 👇&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://crawl.sarv.com/api/kb/&lt;span class="nv"&gt;$KB_ID&lt;/span&gt;/jobs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$SARVCRAWL_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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"type":"crawl","url":"https://docs.yoursite.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call in. Clean markdown out — indexed, chunked, embedded, searchable by keyword &lt;em&gt;and&lt;/em&gt; by meaning. No pipeline. No vector database to run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F260ork29a8uo32ib7hsx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F260ork29a8uo32ib7hsx.png" alt="SarvCrawl — point it at a site and watch the crawl fan out, every page coming back as markdown" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 &lt;a href="https://crawl.sarv.com" rel="noopener noreferrer"&gt;Try it free → crawl.sarv.com&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;1 billion tokens on sign-up. No card.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🧰 What it does
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🕷️&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Crawl&lt;/strong&gt; a whole site — JS-rendered pages included&lt;/td&gt;
&lt;td&gt;&lt;a href="https://crawl.sarv.com/docs#crawl" rel="noopener noreferrer"&gt;docs →&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📄&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Scrape&lt;/strong&gt; one page to clean markdown&lt;/td&gt;
&lt;td&gt;&lt;a href="https://crawl.sarv.com/docs#scrape" rel="noopener noreferrer"&gt;docs →&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🗺️&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Map&lt;/strong&gt; every URL before you commit to a crawl&lt;/td&gt;
&lt;td&gt;&lt;a href="https://crawl.sarv.com/docs#map" rel="noopener noreferrer"&gt;docs →&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📎&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Upload&lt;/strong&gt; PDF, DOCX, XLSX, PPTX, CSV, images — OCR included&lt;/td&gt;
&lt;td&gt;&lt;a href="https://crawl.sarv.com/docs#upload" rel="noopener noreferrer"&gt;docs →&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔄&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Monitor&lt;/strong&gt; a source and re-crawl only what changed&lt;/td&gt;
&lt;td&gt;&lt;a href="https://crawl.sarv.com/docs#monitor" rel="noopener noreferrer"&gt;docs →&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔍&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Search&lt;/strong&gt; by keyword, or by meaning&lt;/td&gt;
&lt;td&gt;&lt;a href="https://crawl.sarv.com/docs#embed-search" rel="noopener noreferrer"&gt;docs →&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  🔌 Where it plugs in
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🤖&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;MCP&lt;/strong&gt; — crawl and search from Claude Code, Cursor, VS Code&lt;/td&gt;
&lt;td&gt;&lt;a href="https://crawl.sarv.com/docs/mcp-server" rel="noopener noreferrer"&gt;docs →&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Node SDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://crawl.sarv.com/docs/node-sdk" rel="noopener noreferrer"&gt;docs →&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🐍&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Python SDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://crawl.sarv.com/docs/python-sdk" rel="noopener noreferrer"&gt;docs →&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⌨️&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://crawl.sarv.com/docs/cli" rel="noopener noreferrer"&gt;docs →&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔗&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;n8n nodes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://crawl.sarv.com/docs/n8n" rel="noopener noreferrer"&gt;docs →&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  🔎 Every answer traces back to a page you can open
&lt;/h2&gt;

&lt;p&gt;The console isn't a wrapper on the API. It's where you check the crawl, read the exact chunks your model will be handed, and find the bad conversion before your users do.&lt;/p&gt;

&lt;p&gt;Most tools give you a vector and a shrug. This one gives you a score for the conversion and a link to the source page.&lt;/p&gt;

&lt;p&gt;📖 &lt;a href="https://crawl.sarv.com/docs" rel="noopener noreferrer"&gt;Read the API docs&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🏠 Or self-host it
&lt;/h2&gt;

&lt;p&gt;Open source, Apache-2.0, one &lt;code&gt;.env&lt;/code&gt; and one &lt;code&gt;docker compose up -d&lt;/code&gt;. Cloud when you want it to be someone else's problem, self-hosted when it can't leave your network — same API either way.&lt;/p&gt;




&lt;h3&gt;
  
  
  🚀 &lt;a href="https://crawl.sarv.com" rel="noopener noreferrer"&gt;crawl.sarv.com&lt;/a&gt; — 1 billion tokens free
&lt;/h3&gt;

&lt;p&gt;What are you trying to make searchable? Tell me in the comments — I'm curious which sources break it. 👇&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>rag</category>
      <category>opensource</category>
    </item>
    <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>
