<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Raghul]]></title><description><![CDATA[Raghul]]></description><link>https://raghular.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 27 Oct 2026 07:59:08 GMT</lastBuildDate><atom:link href="https://raghular.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What does a drug shortage actually break inside a hospital? I asked a graph.]]></title><description><![CDATA[I graduated this year. After college I worked as an SDE at a startup for about three months, and these days I'm mostly building things on my own and trying…]]></description><link>https://raghular.hashnode.dev/what-does-a-drug-shortage-actually-break-inside-a-hospital-i-asked-a-graph</link><guid isPermaLink="true">https://raghular.hashnode.dev/what-does-a-drug-shortage-actually-break-inside-a-hospital-i-asked-a-graph</guid><category><![CDATA[falkordb]]></category><category><![CDATA[graph database]]></category><category><![CDATA[hackathon]]></category><category><![CDATA[AI]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Raghul A R]]></dc:creator><pubDate>Sun, 18 Oct 2026 16:45:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/64606ea7b5d2306aba677f25/aa363412-3b6d-444d-a21e-e69fb09f0255.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I graduated this year. After college I worked as an SDE at a startup for about three months, and these days I'm mostly building things on my own and trying to get better at it. When WeMakeDevs announced Graph Hacks with FalkorDB, I signed up mainly because I had never used a graph database properly. I'd read about them, but I'd never had a problem where I actually needed one.</p>
<p>The hackathon brief had one line that stuck with me: <em>the graph has to shape what the agent does, not sit on the side.</em> That ruled out my first few ideas. They were basically dashboards with a graph bolted on, and I could have built every one of them with Postgres and nobody would have noticed.</p>
<p>So I went looking for a problem where the answer is a path through connected data. That's how I ended up reading about drug shortages at midnight.</p>
<p>Live demo: <a href="https://shortagebridge-falkordb.vercel.app">https://shortagebridge-falkordb.vercel.app</a></p>
<p>Code: <a href="https://github.com/raghul017/shortagebridge-falkordb">https://github.com/raghul017/shortagebridge-falkordb</a></p>
<p>Demo video: <a href="https://youtu.be/357BPOkj9cY">https://youtu.be/357BPOkj9cY</a></p>
<h2>The problem</h2>
<p>The US FDA keeps a public list of drug shortages, and you can pull it from the openFDA API. Each record tells you the drug, the exact package (the NDC code), the status, and when it was last updated.</p>
<p>I picked one record to work with: Lidocaine Hydrochloride Injection, package NDC 0409-4776-01. Lidocaine is a local anaesthetic, so it shows up in a lot of everyday hospital work.</p>
<p>What surprised me was how little the alert tells a hospital. It says the drug is short nationally. It doesn't tell a pharmacy lead whether they stock that package, how many days their stock will last, which wards or procedure rooms depend on it, or who is supposed to look at it. And that's the question they actually ask on the day: <em>what does this affect, here?</em></p>
<p>To answer it you have to go step by step: the alert points to a package, the package matches something on the hospital's formulary, that item has stock and a usage rate, it's used by certain services, those services follow a shortage protocol, and the protocol sends it to a specific role.</p>
<p>When I wrote it down like that, it was just a chain of relationships, and that's the kind of question a graph database is built for.</p>
<h2>What I built</h2>
<p>ShortageBridge takes that one public shortage record and walks it through a hospital's data in FalkorDB until it reaches a person who should review it. Then it records that the person did.</p>
<p>On the page it goes like this:</p>
<ol>
<li><p>You see the public FDA alert, with a link to the source and the time it was pulled.</p>
</li>
<li><p>You click <strong>Ask the graph</strong>. That runs one Cypher query in FalkorDB.</p>
</li>
<li><p>The evidence path comes back, and you click <strong>Next connection</strong> to reveal it one hop at a time, six hops in total.</p>
</li>
<li><p>You see the impact: two affected services, their protocol, the review role, and a stock runway of about 4 days.</p>
</li>
<li><p>You click <strong>Generate review brief</strong>. Gemini writes a short brief, but only from what the graph returned.</p>
</li>
<li><p>You click <strong>Mark review complete</strong>, and a new node is written back into the graph, linked to the evidence.</p>
</li>
</ol>
<p>I'd call it a small agent with a strict leash. It reads the graph, explains what it found, and the one action it takes is recording a human decision.</p>
<h2>I had no hospital data, and that was fine</h2>
<p>This was the first real decision I had to make. I obviously don't have a hospital's inventory, and even if someone had given it to me, I shouldn't put it in a public demo.</p>
<p>So the project uses two kinds of data and never mixes them up:</p>
<ul>
<li><p><strong>Public:</strong> the FDA shortage record and the drug package. Shown in blue.</p>
</li>
<li><p><strong>Synthetic:</strong> a made-up hospital called Riverglen Demonstration Hospital, with 24 units in stock, about 6 used per day, two services, a protocol and a review role. Shown in violet with dashed borders.</p>
</li>
</ul>
<p>Every node in the graph has a <code>data_origin</code> property set to either <code>openfda</code> or <code>synthetic_demo</code>, plus an <code>evidence_id</code> pointing to an <code>EvidenceSource</code> node that says where it came from. At first this felt like a lot of bookkeeping for a hackathon. By the end, half the features depended on it: the colours in the UI, the citations in the brief, and the links on the review record all come from those two properties.</p>
<p>There's one edge I spent extra time on: <code>MATCHED_TO</code>, the link between the FDA package and the hospital's own item. That's where public data meets private data, and matching the two is a judgement call that someone has to own. So the edge itself carries a <code>status</code> and a <code>rationale</code>. I liked that the graph could store <em>why</em> two things are connected, not only <em>that</em> they are.</p>
<h2>The graph</h2>
<pre><code class="language-text">EvidenceSource -SUPPORTS-&gt; ShortageRecord -REPORTS_ON-&gt; DrugPresentation
                                                            |
                                                       MATCHED_TO (reviewed)
                                                            v
Facility -HAS_ITEM-&gt; FacilityProduct -HAS_SNAPSHOT-&gt; InventorySnapshot
                          |
                       USED_BY
                          v
                     ServiceLine -FOLLOWS-&gt; Protocol -ESCALATE_TO-&gt; ResponseRole

ReviewAction -BASED_ON-&gt; ShortageRecord, FacilityProduct
ReviewAction -CITES----&gt; EvidenceSource
</code></pre>
<h2>The one query</h2>
<p>The whole "what does this affect" answer is one read-only Cypher query with two path patterns. Here's the core of it (the full version is in <code>lib/impact-query.ts</code>):</p>
<pre><code class="language-cypher">MATCH shortageToInventoryPath =
  (shortage:ShortageRecord {id: $caseId})-[:REPORTS_ON]-&gt;(presentation:DrugPresentation)
    -[match:MATCHED_TO]-&gt;(facilityProduct:FacilityProduct)-[:HAS_SNAPSHOT]-&gt;(snapshot:InventorySnapshot)
MATCH facilityToReviewPath =
  (facilityProduct)-[:USED_BY]-&gt;(service:ServiceLine)-[:FOLLOWS]-&gt;(protocol:Protocol)
    -[:ESCALATE_TO]-&gt;(role:ResponseRole)
RETURN
  shortageToInventoryPath, facilityToReviewPath,
  match.rationale, service.name, protocol.name, role.name,
  round(snapshot.on_hand_quantity / snapshot.average_daily_use * 10) / 10 AS estimated_runway_days
ORDER BY service.name
</code></pre>
<p>Reading it back, it's almost the same sentence I wrote down when I first understood the problem. That was the moment the graph database clicked for me.</p>
<p>A few choices here ended up mattering more than I expected.</p>
<p><strong>I draw the graph from the paths FalkorDB returns.</strong> My first version drew the evidence graph with hardcoded positions. It looked fine, but it was lying, because if the data changed the picture wouldn't. Now the frontend takes the path objects from the query result and builds the picture from those nodes and edges. With the seed data that's 8 nodes, 8 relationships and 6 hops.</p>
<p><strong>The runway is calculated in Cypher, not in JavaScript.</strong> This gave me my favourite test. Open the FalkorDB Browser and run:</p>
<pre><code class="language-cypher">MATCH (i:InventorySnapshot) SET i.on_hand_quantity = 12 RETURN i.on_hand_quantity
</code></pre>
<p>Then go back to the app and run the traversal again. The runway drops from 4 days to 2. Nothing was redeployed and no code was touched. That's when I was convinced the graph was doing the work and not just storing data for my app.</p>
<p><strong>The query is fixed and read-only.</strong> The server runs it with FalkorDB's <code>roQuery</code>, so this path can't modify anything. The only input is <code>$caseId</code>, and that's validated with Zod before the query runs. Nothing typed in the browser ever becomes part of a Cypher string.</p>
<p>A friend asked me if I could have done this in SQL. Yes, I could have, with a join for every hop and then code to stitch the rows back into connections so I could draw them. But in Cypher the query <em>is</em> the question, and it comes back already shaped as connected evidence. For this problem that made the code a lot simpler.</p>
<h2>Keeping Gemini on a short leash</h2>
<p>I wanted the reviewer to get a readable summary, not raw JSON. But I really didn't want an LLM inventing details about a hospital's drug supply. So I gave Gemini a very small job:</p>
<ul>
<li><p>It never connects to FalkorDB and never writes Cypher.</p>
</li>
<li><p>The server runs the graph query itself and passes Gemini only what came back.</p>
</li>
<li><p>Gemini has to answer in a fixed JSON schema.</p>
</li>
<li><p>After it answers, the server checks that every service and evidence ID it mentions actually came from the graph. If anything doesn't match, the answer is thrown away.</p>
</li>
</ul>
<p>When that happens, or when there's no API key or the free quota is used up, the app builds the brief directly from the graph facts with no AI at all. The output has the same structure, and the UI tells you which mode ran.</p>
<p>I ran out of Gemini quota more than once while testing, so this fallback got tested properly, just not on purpose. It's probably the part I'm proudest of, because the app keeps working without the model.</p>
<h2>The action: recording a human decision</h2>
<p>Track 01 is about agents that act on connected data, so reading the graph wasn't going to be enough. But in healthcare, I didn't want my app placing orders or suggesting substitute drugs. That needs a clinician and patient-level context I'll never have.</p>
<p>So the action is narrow on purpose. When a reviewer clicks <strong>Mark review complete</strong>, the server writes a <code>ReviewAction</code> node into the same graph:</p>
<pre><code class="language-cypher">MATCH (shortage:ShortageRecord {id: $caseId})
MATCH (facilityProduct:FacilityProduct {id: $facilityProductId})
MERGE (action:ReviewAction {id: $actionId})
SET action += { data_origin: 'synthetic_demo', status: $status,
                reviewed_at: $reviewedAt, evidence_ids: $evidenceIds }
MERGE (action)-[:BASED_ON]-&gt;(shortage)
MERGE (action)-[:BASED_ON]-&gt;(facilityProduct)
WITH action
MATCH (evidence:EvidenceSource) WHERE evidence.id IN $evidenceIds
MERGE (action)-[:CITES]-&gt;(evidence)
</code></pre>
<p>The browser only sends <code>{"confirmed": true}</code>. The server re-reads the graph and writes only IDs it got from FalkorDB. Afterwards the decision sits in the database right next to the evidence it was based on. An audit trail is a graph too, so it made sense to keep it in the same place.</p>
<h2>Getting it online</h2>
<p>Locally I ran FalkorDB in Docker, which was one command. For the live demo I moved to a hosted FalkorDB instance and deployed the Next.js app on Vercel. The code is the same either way, and only the connection environment variables change.</p>
<p>Seeding uses <code>MERGE</code>, so I could run the seed script again and again without creating duplicates. I did that many times.</p>
<p>One thing I didn't expect: after the app sat idle for a while, the first request was slow while the serverless function started up. Before recording my demo I now do a warm-up click-through, then clear the test review nodes with <code>MATCH (a:ReviewAction) DETACH DELETE a</code> so the graph is clean on camera.</p>
<h2>What I learned</h2>
<p><strong>Look for problems where the answer is a path.</strong> Once I stopped asking "where can I put a graph?" and started asking "which questions are really a chain of relationships?", the project almost designed itself.</p>
<p><strong>Put the logic in the query.</strong> Every time I moved logic from JavaScript into Cypher, the frontend got simpler and the demo got more honest.</p>
<p><strong>Record where every piece of data came from, from the start.</strong> I'd skip it next time if I were lazy, and I'd regret it.</p>
<p><strong>Don't trust the model's output.</strong> Grounding isn't just putting facts in the prompt. You also have to check what comes back and have a plan for when it's wrong.</p>
<p><strong>Keep the action small and the human in charge.</strong> It's tempting to make the agent do more. Here, doing less was the right call.</p>
<h2>What it doesn't do (yet)</h2>
<p>It handles one seeded shortage case. The FDA record is a snapshot, not a live feed. The hospital is fictional and the 4-day runway is an illustration, not a forecast. It isn't a clinical tool, and you can't plug a real hospital database into it.</p>
<p>If I keep working on it, I want to pull the full openFDA shortage feed automatically and flag new matches for a human to confirm. I'd also like to run graph algorithms across many shortages at once, to find the services exposed to the most of them, or protocols that turn into bottlenecks. A real version would have to run inside a hospital's own environment, with their data staying with them.</p>
<h2>Thanks</h2>
<p>Thanks to WeMakeDevs and FalkorDB for running this. I came in having never written a real Cypher query, and I'm leaving with a project where the database is producing the answer, not just storing data for the app.</p>
<p>If you work in a hospital pharmacy or supply chain and I've modelled something wrong, please tell me. I'd genuinely like to know.</p>
]]></content:encoded></item></channel></rss>