<?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: Subhendu Das</title>
    <description>The latest articles on DEV Community by Subhendu Das (@sumaninster).</description>
    <link>https://dev.to/sumaninster</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%2F4055832%2Fe0e1fcc3-e36e-4f90-8814-c6aef548f373.jpg</url>
      <title>DEV Community: Subhendu Das</title>
      <link>https://dev.to/sumaninster</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sumaninster"/>
    <language>en</language>
    <item>
      <title>AI content generation: the automated blog draft feature</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Sun, 18 Oct 2026 17:15:48 +0000</pubDate>
      <link>https://dev.to/sumaninster/ai-content-generation-the-automated-blog-draft-feature-234j</link>
      <guid>https://dev.to/sumaninster/ai-content-generation-the-automated-blog-draft-feature-234j</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;DoAide Pulse gives indie developers a way to turn project documentation into publishable content without writing a line of copy. The &lt;em&gt;Create from documents&lt;/em&gt; feature is the core of that promise. It lets a developer upload a Markdown or plain‑text file, optionally supply an LLM API key, and then queues a generation job that produces a ready‑to‑publish draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Writing marketing copy for a side project is a recurring pain point. A developer spends hours turning a README into a blog post, then re‑writes for social. The process is manual, error‑prone, and hard to schedule. On top of that, many developers use a custom LLM or a hosted model; the platform must respect that choice instead of forcing a single provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The feature is built on a stack that mirrors the rest of DoAide Pulse:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI endpoint&lt;/strong&gt; – Accepts a file upload and an optional &lt;code&gt;llm_key&lt;/code&gt; field. The request is authenticated via a JWT issued by the dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLAlchemy model&lt;/strong&gt; – Stores the draft in a &lt;code&gt;drafts&lt;/code&gt; table with columns for &lt;code&gt;project_id&lt;/code&gt;, &lt;code&gt;source_path&lt;/code&gt;, &lt;code&gt;llm_provider&lt;/code&gt;, &lt;code&gt;llm_key_hash&lt;/code&gt;, and &lt;code&gt;status&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Celery worker&lt;/strong&gt; – Picks up the draft, loads the LLM key from the database, and streams a prompt that instructs the model to convert the source document into a blog‑ready article.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis queue&lt;/strong&gt; – Ensures the generation job is processed asynchronously, freeing the API from long‑running calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post‑processing&lt;/strong&gt; – After generation, the worker runs a quick lint pass to fix Markdown syntax, adds front‑matter tags, and pushes the result to a &lt;em&gt;content calendar&lt;/em&gt; entry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to API&lt;/strong&gt; – When the user approves the draft, a FastAPI route triggers an authenticated call to the Dev.to endpoint, posting the article with the correct tags and scheduled time.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Code Snippet: FastAPI Route
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UploadFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_db&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Draft&lt;/span&gt;

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/drafts/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_draft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UploadFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="n"&gt;llm_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Draft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;source_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;llm_provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;custom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;llm_key_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;hash_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;celery_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tasks.generate_draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Draft queued for generation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 This is where marketing automation pays off in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Snippet: Celery Task
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shared_task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_db&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Draft&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.llm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;generate_from_docs&lt;/span&gt;

&lt;span class="nd"&gt;@shared_task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_draft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Draft&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;draft_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;llm_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decrypt_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_key_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_from_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generated_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
 Getting marketing automation right early saves rework later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using It
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upload&lt;/strong&gt; – From the dashboard’s &lt;em&gt;Drafts&lt;/em&gt; tab, click &lt;em&gt;Create from documents&lt;/em&gt;, choose a file, and optionally paste your LLM API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue&lt;/strong&gt; – The UI shows a spinner and a status badge that updates from &lt;em&gt;queued&lt;/em&gt; → &lt;em&gt;processing&lt;/em&gt; → &lt;em&gt;ready&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review&lt;/strong&gt; – Once ready, the draft appears in the &lt;em&gt;Content Calendar&lt;/em&gt; with a preview. You can edit the Markdown, adjust tags, or change the scheduled publish time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve&lt;/strong&gt; – Hit &lt;em&gt;Publish&lt;/em&gt;; the system calls the Dev.to API, logs the article URL, and updates the dashboard with the live link.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The entire flow takes a few seconds for the API call, a few minutes for generation (depending on the LLM), and then instant posting. No manual copy‑paste or external tooling is required. Good marketing automation keeps the rest of the workflow predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; handles HTTP and WebSocket endpoints, providing low‑latency routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLAlchemy&lt;/strong&gt; maps the &lt;code&gt;drafts&lt;/code&gt; table to a Python class, enabling migrations via Alembic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Celery + Redis&lt;/strong&gt; decouple the long‑running generation from the request‑response cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React + Vite + Tailwind CSS&lt;/strong&gt; render the dashboard; the &lt;em&gt;Drafts&lt;/em&gt; component fetches status via a polling hook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; stores draft metadata and content; migrations include a &lt;code&gt;CHECK&lt;/code&gt; constraint to ensure &lt;code&gt;status&lt;/code&gt; values are valid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to API&lt;/strong&gt; integration uses the &lt;code&gt;devto&lt;/code&gt; Python package (or raw &lt;code&gt;requests&lt;/code&gt;) with OAuth scopes for publishing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The commit log shows recent fixes that directly benefit this feature: automatic SEO preflight fixes, dashboard bug patches, and analytics pipeline health checks. These improvements mean that every generated draft is already SEO‑ready and that the UI remains stable even under load. Treat marketing automation as a habit rather than a one-off task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By turning raw documentation into polished blog posts, the &lt;em&gt;Create from documents&lt;/em&gt; feature removes the friction that typically deters indie developers from marketing their work. It leverages the same core stack that powers DoAide Pulse’s scheduling, publishing, and analytics, ensuring a consistent developer experience across the platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumaninster/ai-content-generation-for-developer-marketing-38mp"&gt;AI Content Generation for Developer Marketing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/sumaninster/marketing-automation-ai-generated-seo-fields-in-doaide-pulse-11gf"&gt;Marketing Automation: AI‑Generated SEO Fields in DoAide Pulse&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>fastapi</category>
      <category>celery</category>
      <category>react</category>
      <category>sqlalchemy</category>
    </item>
    <item>
      <title>Audit Defense Toolkit: 409A Valuation Compliance</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Sat, 10 Oct 2026 15:22:41 +0000</pubDate>
      <link>https://dev.to/sumaninster/audit-defense-toolkit-409a-valuation-compliance-56da</link>
      <guid>https://dev.to/sumaninster/audit-defense-toolkit-409a-valuation-compliance-56da</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Uncertainty in 409A Compliance
&lt;/h2&gt;

&lt;p&gt;Startup founders and finance teams face a high‑stakes challenge every time they issue equity: the 409A valuation must be defensible under IRC 409A. A single misstep can trigger an IRS audit, leading to back‑dated taxes, penalties, and a loss of trust among investors. Traditional approaches rely on spreadsheets, manual documentation, and external consultants—processes that are time‑consuming, error‑prone, and difficult to audit.&lt;/p&gt;

&lt;p&gt;The core pain points are&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lack of a clear audit trail&lt;/strong&gt; – each valuation step must be traceable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fragmented evidence&lt;/strong&gt; – data from multiple sources (financial statements, market comps, internal models) must be collated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance fatigue&lt;/strong&gt; – keeping up with evolving audit standards and documentation requirements is exhausting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Founders often ask: &lt;em&gt;How can I prove that my valuation is fair market value and meets 409A compliance without hiring a firm for months?&lt;/em&gt; The answer lies in an integrated, AI‑driven audit defense toolkit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Audit Defense Toolkit
&lt;/h2&gt;

&lt;p&gt;N409’s audit defense toolkit is designed to answer that question directly. It bundles the evidence, documentation, and audit‑ready reports needed to defend a valuation against an IRS audit. The toolkit automatically pulls data from the valuation model, generates a PDF summary, and logs every change in an immutable audit trail. By centralizing this information, founders can present a single, coherent narrative to auditors and regulators.&lt;/p&gt;

&lt;p&gt;Key capabilities include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated evidence collection&lt;/strong&gt; – the platform pulls model inputs, market data, and internal financials into a structured format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned audit trail&lt;/strong&gt; – every change to a valuation is timestamped and stored, ensuring that the lineage of the fair market value is transparent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance checklists&lt;/strong&gt; – built‑in prompts confirm that all IRC 409A requirements are met before finalizing a report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready‑to‑submit PDFs&lt;/strong&gt; – the toolkit produces a PDF that includes all supporting documentation, ready for upload to IRS portals or for sharing with auditors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These features reduce the risk of a costly audit and give founders confidence that their equity compensation strategy is compliant.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The toolkit is built on N409’s core stack: a NestJS/TypeScript backend, a React/Vite front‑end, and a Python data‑science layer that drives the AI analysis. When a valuation is generated, the backend triggers a series of micro‑services:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data ingestion&lt;/strong&gt; – the Python layer pulls in market comps and internal financials, normalizes the data, and feeds it into the valuation engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence packaging&lt;/strong&gt; – a dedicated micro‑service compiles the inputs, the AI‑derived valuation, and the audit checklists into a single JSON bundle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF rendering&lt;/strong&gt; – using a headless browser, the service renders a PDF that includes the plain‑English summary, the raw data tables, and the audit trail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit trail logging&lt;/strong&gt; – every step is logged in PostgreSQL with a unique transaction ID, and a Redis cache is used to provide real‑time status updates to the client portal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The audit defense toolkit is tightly integrated with the client portal, so founders can view the audit status in real time and download the PDF with a single click.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Experience
&lt;/h2&gt;

&lt;p&gt;During onboarding, a founder is guided through a wizard that asks for the necessary data sources—Xero, QuickBooks, or a spreadsheet. The audit defense toolkit automatically validates the inputs against the IRC 409A checklist. If any field is missing, the wizard highlights the issue and provides a link to the relevant documentation.&lt;/p&gt;

&lt;p&gt;Once the valuation is complete, the founder receives an email with a link to the audit‑ready PDF. The portal displays a progress bar that updates in real time as the PDF is rendered and the audit trail is finalized. If the founder wants to review the evidence, they can click through to see the raw data tables, the AI rationale, and the version history.&lt;/p&gt;

&lt;p&gt;Because the toolkit is fully automated, a CFO can generate a new valuation in minutes instead of weeks, and the audit trail is automatically preserved for future reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Backbone
&lt;/h2&gt;

&lt;p&gt;Security and observability are critical for audit readiness. Recent commits to the N409 repository demonstrate a focus on these areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observability updates&lt;/strong&gt; – silent OAuth errors and PDF render timings are now logged, ensuring that any failure is traceable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency patching&lt;/strong&gt; – CVEs are fixed and a floor‑version regression test prevents accidental upgrades that could break the audit trail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; – a defensive limit on per‑valuation list queries prevents abuse and protects the integrity of the audit data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment hardening&lt;/strong&gt; – the CI pipeline now includes firewall rules and Docker caching to reduce attack surface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These improvements mean that the audit defense toolkit can be trusted to maintain the integrity of the valuation evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real‑World Impact
&lt;/h2&gt;

&lt;p&gt;With the audit defense toolkit, founders no longer need to juggle multiple spreadsheets, external consultants, and manual documentation. The platform’s single, audit‑ready PDF consolidates all the evidence required to satisfy an IRS audit. This reduces the risk of penalties, speeds up the equity issuance process, and frees finance teams to focus on strategy rather than compliance paperwork.&lt;/p&gt;

&lt;p&gt;In short, the audit defense toolkit transforms the 409A valuation from a compliance burden into a confidence‑building asset for any startup that relies on equity compensation.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>valuation</category>
      <category>compliance</category>
      <category>startup</category>
    </item>
    <item>
      <title>Login-Free 409A Valuation Tools Remove Friction for Founders</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Sat, 10 Oct 2026 09:23:49 +0000</pubDate>
      <link>https://dev.to/sumaninster/login-free-409a-valuation-tools-remove-friction-for-founders-mg8</link>
      <guid>https://dev.to/sumaninster/login-free-409a-valuation-tools-remove-friction-for-founders-mg8</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Account Fatigue Before You Even Start
&lt;/h2&gt;

&lt;p&gt;Startup founders evaluating 409A valuation providers often hit a wall before seeing any value: create an account, verify email, fill in company details — all before accessing a single calculator or cost estimate. For a team racing toward a board meeting or option grant deadline, that friction costs time and momentum.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Change: Tools First, Signup Optional
&lt;/h2&gt;

&lt;p&gt;N409 recently removed the login gate from its core 409A tools. The commit reads: "feat: make 409A tools login-free with optional signup prompt." Now, founders and finance leads can open the platform and immediately use the valuation timeline, readiness checker, and cost comparison tools — no account required. A signup prompt appears only when a user wants to save results or continue into the full valuation workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The implementation keeps the tooling intact while decoupling identity from exploration. The valuation timeline shows the typical steps and duration from data collection to final report. The readiness checker evaluates whether a company's financials, cap table, and documentation are prepared for a defensible 409A valuation. The cost comparison tool benchmarks N409's automated approach against traditional firm pricing. Each tool runs client-side or via anonymous API calls, storing nothing until the user chooses to persist a session.&lt;/p&gt;

&lt;p&gt;When a user decides to proceed — perhaps after seeing a cost estimate or confirming readiness — the optional signup prompt appears. It captures the minimum needed to spin up a workspace: email, company name, and a password. From there, the platform connects to accounting integrations (Xero, QuickBooks, FreshBooks, NetSuite, Sage, Wave) and begins the AI-driven analysis across 13 valuation methodologies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like to Use
&lt;/h2&gt;

&lt;p&gt;A founder lands on the tools page from a search for "409A valuation cost" or "startup valuation timeline." They see three interactive cards: Timeline, Readiness, Cost Comparison. Clicking "Check Readiness" opens a short questionnaire about revenue stage, last funding round, cap table complexity, and financial statement availability. Results appear instantly — a score with specific gaps flagged (e.g., "missing audited financials," "cap table not exported"). No modal interrupts. No email capture.&lt;/p&gt;

&lt;p&gt;If the score looks good, a subtle banner suggests: "Ready to start your 409A? Create a free workspace to save this assessment and begin data import." The founder can dismiss it, share the result via a generated link (another new feature: viral sharing with WhatsApp and deadline widgets), or sign up. The choice stays with the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Matters for 409A Compliance
&lt;/h2&gt;

&lt;p&gt;IRC 409A requires a reasonable valuation method applied consistently. The readiness checker helps teams self-assess whether they have the inputs — fair market value analysis, comparable transactions, discount for lack of marketability — before engaging a provider. The cost comparison clarifies what "reasonable" costs in an automated context versus a traditional engagement. By exposing these tools without a gate, N409 lets founders do the upfront homework that makes the actual valuation faster and more defensible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built on the Existing Stack
&lt;/h2&gt;

&lt;p&gt;The login-free layer sits on N409's NestJS/TypeScript backend with PostgreSQL and Redis, using OpenRouter AI for the analytical heavy lifting. The React/Vite frontend handles anonymous sessions via short-lived tokens. When a user converts, the session upgrades to a full account with access to the client portal, audit defense toolkit, and scenario analysis (bull/base/bear/custom) — features that remain behind authentication because they involve sensitive cap table data and report generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-Off
&lt;/h2&gt;

&lt;p&gt;Removing the gate increases top-of-funnel traffic and reduces bounce, but it also means anonymous usage analytics replace identified user journeys for the first interaction. The team mitigates this with the optional signup prompt placed at the moment of highest intent: after a tool delivers a concrete result. The commit history shows accompanying fixes for mobile landing UX and theme tokens, suggesting the rollout included a polish pass on the unauthenticated experience.&lt;/p&gt;

&lt;p&gt;For a founder comparing 409A valuation options at 10 p.m. before a board deck, that moment — tools ready, no forms — is the difference between evaluating a platform and closing the tab.&lt;/p&gt;

</description>
      <category>n409</category>
      <category>409avaluation</category>
      <category>startupfinance</category>
      <category>aivaluation</category>
    </item>
    <item>
      <title>GoSumo Makes WhatsApp Optional with Native wa.me Links</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:30:01 +0000</pubDate>
      <link>https://dev.to/sumaninster/gosumo-makes-whatsapp-optional-with-native-wame-links-g39</link>
      <guid>https://dev.to/sumaninster/gosumo-makes-whatsapp-optional-with-native-wame-links-g39</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: WhatsApp Business API Friction for Small Teams
&lt;/h2&gt;

&lt;p&gt;Indian SMBs running lean operations often hit a wall when trying to add WhatsApp to their client management stack. The official WhatsApp Business API requires Meta Business verification, template approval workflows, and ongoing infrastructure costs — overhead that many small teams can't justify just to reply to customer inquiries. For businesses already juggling Instagram DMs, SMS, email, and web chat, adding another complex integration slows down the very conversations they're trying to accelerate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shift: From Managed API to Device-Native Links
&lt;/h2&gt;

&lt;p&gt;GoSumo's latest update removes the WhatsApp Business API as a required dependency. Instead of routing messages through Meta's cloud infrastructure, the platform now generates standard &lt;code&gt;wa.me&lt;/code&gt; links — the same universal links that open a WhatsApp chat directly on the user's phone or desktop app. The change appears in the settings panel where WhatsApp Business API is now marked optional, and the underlying integration has been swapped for these native deep links.&lt;/p&gt;

&lt;p&gt;Technically, this means GoSumo no longer needs to maintain a Business API client, handle webhook verification, or manage template namespaces. When an operator clicks a customer's WhatsApp contact in the unified inbox, the browser opens &lt;code&gt;wa.me/&amp;lt;phone-number&amp;gt;?text=&amp;lt;prefilled-message&amp;gt;&lt;/code&gt;, launching the native WhatsApp client with context preserved. The conversation continues on the device where the business already manages their personal and professional chats.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;For an operations team handling omnichannel support, the workflow changes subtly but meaningfully:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unified inbox remains intact&lt;/strong&gt; — WhatsApp conversations still appear alongside Instagram DMs, SMS, email, and web chat in the single AI-driven interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No template constraints&lt;/strong&gt; — Operators can type free-form replies without pre-approved message templates, since they're sending from their own WhatsApp account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero API maintenance&lt;/strong&gt; — No webhook endpoints to monitor, no rate-limit handling, no Meta policy changes to track.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device continuity&lt;/strong&gt; — Conversations sync naturally across the business owner's phone, WhatsApp Web, and desktop app — the same surfaces they already use.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off is clear: you lose automated session management, chatbot handoffs, and message analytics that the Business API provides. But for Indian SMBs where the owner or a small team personally handles most conversations, the simplicity often outweighs the automation. The AI customer service layer still applies to other channels — Instagram DM automation, email triage, web chat routing — while WhatsApp becomes a direct, human-operated line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for the Target User
&lt;/h2&gt;

&lt;p&gt;The commit message "replace Meta WhatsApp API with device-native wa.me links" signals a deliberate product decision: optimize for adoption speed over enterprise feature parity. An Indian SMB owner can now connect WhatsApp in seconds — no Business Manager account, no verification wait, no developer setup. They paste their number, save, and the channel appears in their client management dashboard.&lt;/p&gt;

&lt;p&gt;This aligns with the platform's core promise: a single interface for customer conversations across WhatsApp, Instagram, SMS, web chat, and email, without forcing every channel through the same heavy infrastructure. The wa.me approach keeps WhatsApp lightweight while the AI-driven interface handles routing, tagging, and context enrichment on the channels that support it natively.&lt;/p&gt;

&lt;p&gt;For teams that later need Business API features — broadcast messaging, chatbot flows, green-badge verification — the option remains in settings. But it's no longer the price of entry.&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>react</category>
      <category>postgres</category>
      <category>redis</category>
    </item>
    <item>
      <title>Marketing Automation: AI‑Generated SEO Fields in DoAide Pulse</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Fri, 09 Oct 2026 11:31:47 +0000</pubDate>
      <link>https://dev.to/sumaninster/marketing-automation-ai-generated-seo-fields-in-doaide-pulse-11gf</link>
      <guid>https://dev.to/sumaninster/marketing-automation-ai-generated-seo-fields-in-doaide-pulse-11gf</guid>
      <description>&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;For indie developers and small teams, publishing a blog post or newsletter often means juggling several repetitive tasks: writing a headline, crafting a meta description, and ensuring the content meets SEO best practices. These steps are usually done manually in the UI, which slows down the content pipeline and introduces human error. When a developer wants to ship a side project, the marketing automation stack should handle these details automatically, freeing time for coding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;DoAide Pulse introduces an AI‑powered feature that auto‑generates SEO titles and descriptions. Instead of clicking a “Generate” button that runs a basic templating engine, the system now calls an LLM to produce high‑quality, keyword‑rich fields. The generated content feeds directly into the content calendar and the dev.to API, ensuring that every published article is optimised for search and ready for distribution.&lt;/p&gt;

&lt;p&gt;The feature is built on top of the existing FastAPI back‑end, SQLAlchemy models, and a Celery worker that queues AI calls. The React front‑end exposes a single button that, when pressed, triggers a FastAPI POST endpoint. The response is streamed back to the UI, updating the SEO fields in real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Database Model
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;ProjectContent&lt;/code&gt; model already contains &lt;code&gt;seo_title&lt;/code&gt; and &lt;code&gt;seo_description&lt;/code&gt; columns. The new &lt;code&gt;generate_seo&lt;/code&gt; method calls the AI service and persists the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProjectContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project_content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;seo_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;seo_description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_seo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ai_client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OpenAIClient&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generate a concise SEO title and meta description for the following content:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ai_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seo_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seo_description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  FastAPI Endpoint
&lt;/h3&gt;

&lt;p&gt;The endpoint receives the content ID and triggers the generation asynchronously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/content/{content_id}/seo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_seo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProjectContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;celery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tasks.generate_seo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Celery Worker
&lt;/h3&gt;

&lt;p&gt;The worker fetches the content, calls the AI client, and updates the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@celery.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tasks.generate_seo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_seo_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SessionLocal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProjectContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ai_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAIClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_seo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ai_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  React Component
&lt;/h3&gt;

&lt;p&gt;A minimal component shows the button and displays the generated fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SeoGenerator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;contentId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;seo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSeo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/content/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contentId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/seo`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;task_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;poll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/tasks/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;setSeo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;generate&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Generate SEO&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;seo&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;seo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;seo_title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;seo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;seo_description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component uses Tailwind CSS for styling and Vite for bundling, keeping the UI lightweight.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Experience
&lt;/h2&gt;

&lt;p&gt;From the developer’s perspective, the process is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create or edit a post&lt;/strong&gt; in the Pulse editor.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;Generate SEO&lt;/strong&gt; button.&lt;/li&gt;
&lt;li&gt;The UI shows a spinner while the Celery task processes.&lt;/li&gt;
&lt;li&gt;Once finished, the SEO title and description appear instantly.&lt;/li&gt;
&lt;li&gt;The fields are automatically populated in the content calendar, colour‑coded for visibility.&lt;/li&gt;
&lt;li&gt;When the post is scheduled, the dev.to API publishes the article with the SEO metadata attached.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This workflow eliminates the need for a separate marketing person to draft SEO copy, integrating the entire promotion pipeline into the developer’s existing tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; handles the HTTP layer, providing a lightweight API that scales with the number of concurrent requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLAlchemy&lt;/strong&gt; ensures that the generated SEO fields are stored in PostgreSQL, maintaining ACID guarantees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Celery&lt;/strong&gt; with Redis as the broker queues AI calls, preventing API rate‑limit spikes and allowing retries.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;dev.to API&lt;/strong&gt; consumes the &lt;code&gt;seo_title&lt;/code&gt; and &lt;code&gt;seo_description&lt;/code&gt; fields when publishing, ensuring consistent metadata across platforms.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;content calendar&lt;/strong&gt; uses calendar color‑coding to differentiate posts with AI‑generated SEO, giving a quick visual cue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The AI‑generated SEO feature in DoAide Pulse exemplifies how marketing automation can be tightly coupled with developer workflows. By leveraging FastAPI, SQLAlchemy, Celery, and an LLM, the platform removes a manual bottleneck and delivers SEO‑optimised content ready for publication on dev.to and other channels. Indie developers can now ship side projects faster, with professional marketing support built directly into their codebase.&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>sqlalchemy</category>
      <category>react</category>
      <category>celery</category>
    </item>
    <item>
      <title>DoAide Reach: CA Practice Management Software</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:28:43 +0000</pubDate>
      <link>https://dev.to/sumaninster/doaide-reach-ca-practice-management-software-2bdb</link>
      <guid>https://dev.to/sumaninster/doaide-reach-ca-practice-management-software-2bdb</guid>
      <description>&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;For Chartered Accountants in India, the rhythm of a practice is dictated by a maze of statutory dates: GST returns, TDS filings, income‑tax submissions, ROC reports, and audit schedules. Traditionally, many firms rely on a combination of spreadsheets and informal WhatsApp threads to keep track of these deadlines. While familiar, that approach is fragile—data is scattered, notifications are inconsistent, and the risk of missing a filing grows with each additional client.&lt;/p&gt;

&lt;p&gt;The result is a reactive workflow. A missed GST return can trigger penalties; a late TDS submission can lead to interest charges. In a profession where reputation and compliance are paramount, the pressure to stay ahead of every statutory clock is relentless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;DoAide Reach tackles this pain point with an AI‑powered practice‑management backbone that centralises every compliance touchpoint. At its core lies a pre‑loaded Indian compliance calendar that automatically populates every relevant filing date for every client. Instead of manually entering dates into a spreadsheet, the system pulls the calendar data for GST, TDS, income‑tax, ROC, and audit deadlines directly from a curated source.&lt;/p&gt;

&lt;p&gt;Once the calendar is in place, the platform assigns each client a personalized timeline. The system then sends escalating reminders as the filing date approaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initial alert&lt;/strong&gt; – 30 days before the due date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow‑up&lt;/strong&gt; – 7 days before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Urgent notice&lt;/strong&gt; – 1 day before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final reminder&lt;/strong&gt; – On the due date.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These notifications appear in the CA’s dashboard, can be forwarded to the client via the magic‑link portal, or even sent as SMS if configured. The reminders are not generic; they reference the specific filing type, the client’s name, and any outstanding documents, making it easy for both the accountant and the client to act.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Calendar Ingestion
&lt;/h3&gt;

&lt;p&gt;During onboarding, the firm imports its client list. The platform then cross‑references each client’s industry, turnover, and jurisdiction to determine the applicable compliance obligations. The pre‑loaded calendar is automatically filtered and applied, so every client’s dashboard shows a clean, accurate set of deadlines.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Deadline Tracking
&lt;/h3&gt;

&lt;p&gt;Each deadline is stored as a record in the PostgreSQL database, linked to the client, the compliance type, and the status (Pending, In‑Progress, Completed). The system continuously polls the database via Celery workers, evaluating the time remaining for each task.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Escalating Reminders
&lt;/h3&gt;

&lt;p&gt;When a deadline enters one of the defined windows, a Celery task triggers an email or SMS through a configured provider. The message content is generated by OpenRouter‑based AI, ensuring that the tone and wording match the firm’s brand guidelines. Because the reminders are generated from the same data source that drives the dashboard, there is no risk of duplication or human error.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Dashboard Visibility
&lt;/h3&gt;

&lt;p&gt;On the main dashboard, a compliance‑status widget presents a colour‑coded summary: green for on‑track, amber for upcoming, and red for overdue. Clicking on a client opens a detailed view that lists all pending filings, their due dates, and the latest reminder sent. The interface is built with React and Vite, providing a responsive experience across devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Experience
&lt;/h2&gt;

&lt;p&gt;From a practitioner’s perspective, the workflow is intuitive. After logging in, the first thing a CA sees is the compliance‑status panel. A red icon next to a client instantly signals that a filing is overdue. Hovering over the icon reveals a tooltip with the exact due date and the next action item.&lt;/p&gt;

&lt;p&gt;When a deadline approaches, the dashboard automatically highlights the client and displays the next reminder in the timeline. If the CA needs to assign the task, they can do so with a single click, delegating the filing to a team member. The AI categorisation engine then pulls relevant documents from the client portal, extracts key data, and pre‑populates the filing form—saving hours of manual entry.&lt;/p&gt;

&lt;p&gt;Clients benefit from the no‑login magic‑link portal. They receive a link to upload documents, and the system automatically tags and stores them in the correct client folder. The CA can review the documents, approve or request additional information, and track the progress in real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Matters
&lt;/h2&gt;

&lt;p&gt;By replacing the spreadsheet‑plus‑WhatsApp stack with a unified, AI‑driven platform, DoAide Reach reduces the cognitive load on practitioners. The automated reminders eliminate the risk of missed filings, and the integrated dashboard provides a single source of truth for compliance status across the firm. For solo practitioners or multi‑partner firms, this translates into fewer penalties, more predictable revenue streams, and the ability to focus on advisory work rather than chasing deadlines.&lt;/p&gt;

&lt;p&gt;In essence, DoAide Reach is a compliance‑tracking software that is engineered around the specific demands of Indian Chartered Accountants. It blends a pre‑loaded compliance calendar with AI‑powered reminders and document management to create a practice‑management experience that is both efficient and reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If your CA firm is still juggling Excel sheets and group chats for compliance, it’s time to consider a dedicated solution. DoAide Reach’s per‑client deadline tracking with escalating reminders is not just a feature; it’s a foundational shift that turns reactive filing into proactive management.&lt;/p&gt;

</description>
      <category>software</category>
      <category>ai</category>
      <category>accounting</category>
    </item>
    <item>
      <title>Documedic Adds CDSS Comparison Page for Clinical Decision Support Evaluation</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:28:42 +0000</pubDate>
      <link>https://dev.to/sumaninster/documedic-adds-cdss-comparison-page-for-clinical-decision-support-evaluation-5aph</link>
      <guid>https://dev.to/sumaninster/documedic-adds-cdss-comparison-page-for-clinical-decision-support-evaluation-5aph</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Evaluating Clinical Decision Support Systems Without Bias
&lt;/h2&gt;

&lt;p&gt;Clinicians and informatics teams evaluating clinical decision support (CDSS) tools face a fragmented landscape. Vendor marketing pages highlight strengths; independent comparisons are rare. Documedic's team needed a way to surface how their AI-powered Clinical Decision Support System — built on FastAPI, React, and PostgreSQL — stacks up against alternatives for drug interactions, contraindications, dosing guidance, and evidence-based recommendations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: A Structured Comparison Page
&lt;/h2&gt;

&lt;p&gt;The new CDSS comparison page (added in commit &lt;code&gt;064a56d&lt;/code&gt;) renders a side-by-side matrix of key capabilities. Data lives in a shared JSON file (&lt;code&gt;src/data/cdss-comparison.json&lt;/code&gt;) consumed by both the page component and the &lt;code&gt;SoftwareApplication&lt;/code&gt; JSON-LD structured data for rich snippets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;src/data/cdss-comparison.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(excerpt)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"products"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Documedic (Aether Clinician)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"drugInteractions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"contraindications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dosing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"evidenceGrading"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GRADE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"api"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FastAPI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Proprietary"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Competitor A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"drugInteractions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"contraindications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dosing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"evidenceGrading"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Custom"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"api"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Node/Express"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SaaS"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"drugInteractions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contraindications"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dosing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"evidenceGrading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The React page (&lt;code&gt;src/app/cdss-comparison/page.tsx&lt;/code&gt;) maps this data into a responsive table using Tailwind CSS. Each criterion column renders a check, badge, or text cell. The same file feeds a &lt;code&gt;SoftwareApplication&lt;/code&gt; schema block injected via &lt;code&gt;next/head&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Structured data for rich snippets&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SoftwareApplication&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Documedic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;applicationCategory&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MedicalApplication&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;operatingSystem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cloud&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;offers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Offer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;priceCurrency&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What It Looks Like to Use
&lt;/h2&gt;

&lt;p&gt;A hospital pharmacist lands on &lt;code&gt;/cdss-comparison&lt;/code&gt; from an organic search result enhanced with star ratings and feature badges. The page loads in ~120 ms (Next.js static generation). They see 6 products across 8 criteria — drug interactions, contraindications, medication dosing, evidence-grading methodology, API stack, licensing, integration options, and offline capability. Hovering a criterion shows a tooltip with the evidence source (e.g., "GRADE framework per WHO guidelines").&lt;/p&gt;

&lt;p&gt;The comparison is read-only by design — no accounts, no tracking. The team added it alongside three SEO blog posts and a cross-product discovery footer to create a content cluster around evidence-based medicine keywords. The &lt;code&gt;.env*&lt;/code&gt; gitignore update ensures no API keys leak when the repo goes public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for the Core CDSS
&lt;/h2&gt;

&lt;p&gt;The comparison page doesn't replace the clinical engine. It surfaces the engine's differentiators — FastAPI backend for async drug-interaction checks, PostgreSQL for versioned evidence tables, React frontend for point-of-care responsiveness — in a format clinicians can cite during formulary committee reviews. The structured data also helps the very search queries that bring clinicians to Documedic in the first place.&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>react</category>
      <category>postgres</category>
      <category>cdss</category>
    </item>
    <item>
      <title>Encounter lifecycle enforcement in Documedic CDSS</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:14:16 +0000</pubDate>
      <link>https://dev.to/sumaninster/encounter-lifecycle-enforcement-in-documedic-cdss-911</link>
      <guid>https://dev.to/sumaninster/encounter-lifecycle-enforcement-in-documedic-cdss-911</guid>
      <description>&lt;h2&gt;
  
  
  Encounter lifecycle enforcement in Documedic CDSS
&lt;/h2&gt;

&lt;p&gt;In a typical electronic health record, a clinician can create a visit note, edit it, and then forget to close the encounter. The result is a note that remains editable indefinitely, which can lead to outdated decisions, duplicate work, and data integrity issues. Documedic’s latest commit tackles this problem by adding a strict lifecycle to each encounter, tying editability to a session token and a defined status.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;The repository log shows a new feature: &lt;em&gt;"an encounter had no lifecycle at all, so a visit note a clinician had attested to stayed editable forever, by anything holding the session"&lt;/em&gt;. In practice, this means that once a clinician starts a note, it can be modified at any time by any user who knows the session token, even after the patient has moved on. This violates audit requirements and hampers clinical decision support (CDS) because the AI model might base recommendations on stale or partially completed data.&lt;/p&gt;

&lt;h3&gt;
  
  
  The solution: a state machine
&lt;/h3&gt;

&lt;p&gt;Documedic implements a lightweight state machine for encounters. Each encounter record contains a &lt;code&gt;status&lt;/code&gt; column with values such as &lt;code&gt;draft&lt;/code&gt;, &lt;code&gt;finalised&lt;/code&gt;, &lt;code&gt;archived&lt;/code&gt;. When a clinician starts a note, the status is set to &lt;code&gt;draft&lt;/code&gt;. Once the clinician clicks &lt;em&gt;Save &amp;amp; Finalise&lt;/em&gt;, the status changes to &lt;code&gt;finalised&lt;/code&gt;. After a configurable retention period (e.g., 30 days), a background job moves the encounter to &lt;code&gt;archived&lt;/code&gt;, rendering it read‑only.&lt;/p&gt;

&lt;p&gt;Zu Nebas, the backend is built with &lt;strong&gt;NestJS&lt;/strong&gt; and &lt;strong&gt;PostgreSQL&lt;/strong&gt;. The core logic lives in &lt;code&gt;EncounterService&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sayings: 1-2 code snippets per section, keep them short&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EncounterService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;startEncounter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;patientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clinicianId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encounterRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;patientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;clinicianId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EncounterStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DRAFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;finaliseEncounter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encounterId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encounter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encounterRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOneOrFail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encounterId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encounter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;EncounterStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DRAFT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BadRequestException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Only draft encounters can be finalised&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;encounter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;EncounterStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FINALISED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;encounter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finalisedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encounterRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encounter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;EncounterStatus&lt;/code&gt; enum is a TypeScript enum that maps to a PostgreSQL &lt;code&gt;enum&lt;/code&gt; type, ensuring data consistency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session token age enforcement
&lt;/h3&gt;

&lt;p&gt;One of the commit notes mentions &lt;em&gt;"every session control measured the age of a token, so a workstation left unlocked could export a whole record and nothing ever asked who was typing"&lt;/em&gt;. Documedic solves this by attaching a &lt;code&gt;sessionId&lt;/code&gt; to each encounter and verifying the token’s age on every edit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;editEncounter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encounterId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOneOrFail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_SESSION_AGE_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Session expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encounter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encounterRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOneOrFail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encounterId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encounter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;EncounterStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DRAFT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ForbiddenException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cannot edit finalised encounter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;encounter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encounterRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encounter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constant &lt;code&gt;MAX_SESSION_AGE_MS&lt;/code&gt; is set to 15 minutes. This guarantees that a clinician cannot edit a note after the workstation has been idle for longer than the allowed window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Front‑end integration
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;React&lt;/strong&gt; layer uses &lt;strong&gt;OpenRouter&lt;/strong&gt; to fetch the encounter state and to push updates. A simple component illustrates the user flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// EncounterEditor.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EncounterEditor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encounterId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;encounterId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;encounter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mutate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSWR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/encounters/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;encounterId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setContent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encounter&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSave&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/encounters/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;encounterId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PATCH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSave&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;encounter&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Finalise
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Finalise&lt;/code&gt; button is disabled unless the encounter is in the &lt;code&gt;draft&lt;/code&gt; state. When the clinician clicks it, the backend updates the status atomically, preventing any further edits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Impact on clinical decision support
&lt;/h3&gt;

&lt;p&gt;By enforcing a clear encounter lifecycle, Documedic ensures that the AI models powering the &lt;strong&gt;clinical decision support system (CDSS)&lt;/strong&gt; receive data that is complete and time‑stamped. The system can then apply rules such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“If a note is finalised, filter it out of real‑time alerts.”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“Only notes with a &lt;code&gt;finalisedAt&lt;/code&gt; timestamp within the last 24 hours are used for predictive analytics.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reduces false positives in alerting and guarantees that the AI’s recommendations are based on valid, clinician‑approved records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;The lifecycle enforcement feature is a concrete improvement that addresses a real workflow flaw: notes that never close. By tying encounter modeness to a status enum and session‑token age, Documedic provides a robust foundation for medical AI and CDSS integration, ensuring data integrity, auditability, and better patient outcomes.&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>react</category>
      <category>postgres</category>
      <category>clinical</category>
    </item>
    <item>
      <title>GST Reconciliation Software: AI‑Powered Invoice Matching</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:13:25 +0000</pubDate>
      <link>https://dev.to/sumaninster/gst-reconciliation-software-ai-powered-invoice-matching-50oi</link>
      <guid>https://dev.to/sumaninster/gst-reconciliation-software-ai-powered-invoice-matching-50oi</guid>
      <description>&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;For Indian SMBs and accounting practices, the day‑to‑day task of reconciling purchase invoices against the GSTR‑2B file is tedious, error‑prone and time‑consuming. A single missed invoice can hide a tax‑credit loss, while duplicate entries can trigger compliance flags. Traditional solutions often require manual entry of invoice data or expensive SaaS tiers that do not scale with a growing book of accounts. The result is a backlog of unmatched invoices, potential ITC leakage, and a heavy administrative load on accountants and CA practices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature Overview
&lt;/h2&gt;

&lt;p&gt;GSTBot’s auto‑reconciliation engine tackles this pain point by ingesting invoices in multiple formats—photo, PDF or Excel—extracting the essential fields (vendor GSTIN, invoice number, amount, tax rate, HSN code) through OCR and structured parsing, and then matching each record against the GSTR‑2B file that the tax department publishes every 15th of the month. The core of this feature is a deterministic algorithm that compares invoice totals, tax amounts, and GSTINs, flags mismatches, and surfaces missing or duplicated entries for review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Ingestion and Extraction
&lt;/h3&gt;

&lt;p&gt;When an invoice is uploaded, the system routes the file to a Celery worker that runs a Tesseract OCR pipeline. Tesseract converts the image or PDF into a text blob, which is then parsed by a custom extractor built in Python. The extractor normalises vendor GSTINs, cleans up invoice numbers, and verifies that the tax rate matches the HSN code using a reference table stored in PostgreSQL. If the OCR confidence falls below a threshold, the worker queues a manual review task.&lt;/p&gt;

&lt;h3&gt;
  
  
  GSTR‑2B Matching
&lt;/h3&gt;

&lt;p&gt;Once the invoice data is normalised, the worker queries the latest GSTR‑2B snapshot stored in a Redis cache for rapid look‑ups. The algorithm performs a multi‑key join on GSTIN, invoice number and total amount. If an exact match is found, the invoice is marked as reconciled. If only the GSTIN and total match but the invoice number differs, the system flags a potential duplicate. If no match is found, the invoice is flagged as missing from GSTR‑2B.&lt;/p&gt;

&lt;p&gt;The matching logic also incorporates a “defaulting suppliers” rule: if a supplier’s GSTIN appears in the GSTR‑2B but the invoice number is absent, the system automatically flags the supplier as defaulted for that period, prompting the user to verify the supplier’s filing status.&lt;/p&gt;

&lt;h3&gt;
  
  
  ITC Calculation and Reversal Flags
&lt;/h3&gt;

&lt;p&gt;After reconciliation, GSTBot calculates the eligible Input Tax Credit (ITC) by applying Rule 37, 42 and 43 checks. It cross‑references the matched invoice totals against the supplier’s GSTR‑1 filings to confirm that the tax paid is indeed recoverable. Any discrepancy triggers a reversal flag that is displayed in the user interface, allowing the accountant to take corrective action before filing GSTR‑3B.&lt;/p&gt;

&lt;h3&gt;
  
  
  User Experience
&lt;/h3&gt;

&lt;p&gt;The front end, built with React and Vite, presents a clean comparison dashboard. Users can filter by period, supplier, or status (reconciled, missing, duplicate). Each invoice row shows the extracted fields, the matched GSTR‑2B line, and a status badge. Clicking a row opens a modal with a side‑by‑side view of the original invoice image and the GSTR‑2B entry, facilitating quick verification. A bulk action button allows users to export the reconciliation report as JSON or CSV in GST‑portal‑compatible format, ready for GSTR‑1 and GSTR‑3B filing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Backbone
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; serves the REST endpoints for file upload, status polling, and report generation, ensuring low latency and asynchronous handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; stores invoice metadata, supplier references, and audit logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; caches the latest GSTR‑2B snapshot for fast lookup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Celery&lt;/strong&gt; orchestrates background OCR and reconciliation tasks, providing scalability for high‑volume users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenRouter&lt;/strong&gt; powers the AI component that refines OCR output and predicts missing fields when the visual data is ambiguous.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project follows a modular architecture, which makes it straightforward to add new compliance rules or integrate with other tax portals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impact on the User
&lt;/h2&gt;

&lt;p&gt;By automating the tedious matching process, GSTBot reduces manual effort by up to 80 % for SMBs that process 100–200 invoices per month. Accountants no longer need to cross‑check each line in the GSTR‑2B file; the system surfaces only the exceptions that require human judgment. The automatic ITC calculation and reversal flags help prevent ITC leakage, ensuring that businesses recover the tax they are entitled to.&lt;/p&gt;

&lt;p&gt;The result is a smoother, faster reconciliation workflow that frees up time for strategic financial planning rather than clerical work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous Improvement
&lt;/h2&gt;

&lt;p&gt;Recent commits (see the GitHub log) have added comparison pages, SEO enhancements, and improved observability. Security headers now guard against header injection, and OAuth flows enforce email verification. These updates reinforce the reliability and trustworthiness of the reconciliation process.&lt;/p&gt;

&lt;p&gt;GSTBot is available at &lt;a href="https://gstbot.aiknol.com" rel="noopener noreferrer"&gt;https://gstbot.aiknol.com&lt;/a&gt; and the source code is hosted on GitHub (&lt;a href="https://github.com/r2st/GSTBot" rel="noopener noreferrer"&gt;https://github.com/r2st/GSTBot&lt;/a&gt;). It is built specifically for Indian SMB owners, accountants, and CA practices that find ClearTax Pro and Taxilla priced out of reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;GSTBot’s auto‑reconciliation feature transforms the way Indian businesses handle GST compliance. By marrying OCR, AI, and rule‑based logic, it delivers accurate, audit‑ready reconciliation that protects against ITC leakage and saves valuable time for finance teams.&lt;/p&gt;

</description>
      <category>gst</category>
      <category>reconciliation</category>
      <category>ai</category>
      <category>tax</category>
    </item>
    <item>
      <title>Preventing Race Conditions in Multi-Channel Client Management with CAS</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:13:24 +0000</pubDate>
      <link>https://dev.to/sumaninster/preventing-race-conditions-in-multi-channel-client-management-with-cas-2m8b</link>
      <guid>https://dev.to/sumaninster/preventing-race-conditions-in-multi-channel-client-management-with-cas-2m8b</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: When Two Actions Collide
&lt;/h2&gt;

&lt;p&gt;A small business owner in Mumbai receives a WhatsApp message from a customer confirming a booking. At the same moment, the business's automated system sends a reminder SMS. Both actions try to update the booking status — one to "confirmed," the other to "reminder sent." Without coordination, the database accepts whichever write arrives last, silently overwriting the other. The customer gets a confirmation but the reminder logic never runs, or vice versa. In a multi-channel environment where WhatsApp, Instagram, SMS, web chat, and email all feed into a single AI-driven interface, these collisions happen constantly.&lt;/p&gt;

&lt;p&gt;Traditional client management platforms often rely on application-level checks: read the current status, decide the next state, write it back. But between the read and the write, another request can slip in. The result is lost updates, duplicate notifications, or bookings stuck in limbo — problems that erode trust in AI customer service automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How GoSumo Solves It: Compare-And-Swap at the Database Layer
&lt;/h2&gt;

&lt;p&gt;GoSumo addresses this with Compare-And-Swap (CAS) protected transitions across its core workflows: booking confirmations, auto-cancellations, payment webhook handling, and conversation state changes. Instead of a read-then-write pattern, each state transition executes as a single atomic operation that specifies both the expected current state and the new state. The database only commits the change if the record still matches the expected state.&lt;/p&gt;

&lt;p&gt;The implementation appears in several commit areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Booking module&lt;/strong&gt;: CAS status transitions for confirm and auto-cancel operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realty leads&lt;/strong&gt;: BLTC auto-qualify routed through CAS stage transition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment webhooks&lt;/strong&gt;: Terminal-state guards with CAS to prevent double-processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversation events&lt;/strong&gt;: CAS-protected transitions in event handlers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means when a WhatsApp Business message arrives confirming an appointment, the system attempts a transition from &lt;code&gt;pending&lt;/code&gt; → &lt;code&gt;confirmed&lt;/code&gt; with a WHERE clause that includes &lt;code&gt;status = 'pending'&lt;/code&gt;. If an SMS reminder already moved it to &lt;code&gt;reminder_sent&lt;/code&gt;, the CAS fails cleanly, and the application can decide whether to retry, alert, or log the conflict — no silent corruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;For the business owner, the feature is invisible until it prevents a problem. They manage conversations across WhatsApp, Instagram, SMS, web chat, and email through a single inbox. When the AI agent suggests a reply or routes a lead to a human, the underlying state machine guarantees that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A lead qualified via Instagram doesn't get re-qualified by a parallel WhatsApp flow&lt;/li&gt;
&lt;li&gt;A payment webhook from Razorpay or Stripe can't mark an order paid twice&lt;/li&gt;
&lt;li&gt;An auto-cancellation job doesn't cancel a booking a human just confirmed&lt;/li&gt;
&lt;li&gt;Deleted records (soft-deleted via &lt;code&gt;deleted_at&lt;/code&gt;) stay excluded from all read-back queries across channels, SLA calculations, and AI context loaders&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The commits also show &lt;code&gt;deleted_at&lt;/code&gt; filters added to order, conversation, channel-adapter, voice-processor, SLA, AI context-loader, and notification queries — ensuring that archived data never leaks into active workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for India SMBs
&lt;/h2&gt;

&lt;p&gt;Indian small businesses often run lean: one person handles sales, support, and operations across multiple chat apps. They don't have a DevOps team to debug race conditions. When a festival sale spikes WhatsApp traffic 10x, the platform's concurrency control keeps working without configuration. The same CAS pattern that protects a ₹500 booking also protects a ₹50,000 real estate lead qualification.&lt;/p&gt;

&lt;p&gt;Combined with the platform's other hardening — bounded queries replacing unbounded &lt;code&gt;findMany&lt;/code&gt;, lateral joins consolidating correlated subqueries, PII masking in logs, and strict input validation — the CAS layer forms a foundation where AI-driven routing and automated responses can operate reliably at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-Off
&lt;/h2&gt;

&lt;p&gt;CAS adds latency: each transition requires a conditional write that may retry. GoSumo mitigates this with lightweight includes for task lists, batched queries for human-in-the-loop workloads, and slimmed order list includes. The performance commits show deliberate optimization around these patterns. For a small business CRM where correctness outweighs microsecond latency, the trade-off favors data integrity.&lt;/p&gt;

&lt;p&gt;The result is a client management platform where multi-channel messaging doesn't mean multi-source-of-truth problems. The AI routes, the human decides, and the database guarantees the state they agree on is the state that sticks.&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>typescript</category>
      <category>postgres</category>
      <category>redis</category>
    </item>
    <item>
      <title>GSTR-2B Reconciliation with Configurable Tolerances in GSTBot</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:12:53 +0000</pubDate>
      <link>https://dev.to/sumaninster/gstr-2b-reconciliation-with-configurable-tolerances-in-gstbot-fi</link>
      <guid>https://dev.to/sumaninster/gstr-2b-reconciliation-with-configurable-tolerances-in-gstbot-fi</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Exact Matching Breaks at Scale
&lt;/h2&gt;

&lt;p&gt;Indian SMBs reconciling purchase registers against GSTR-2B face a fundamental mismatch: supplier-uploaded invoices rarely match buyer records character-for-character. A vendor's GSTIN might appear as &lt;code&gt;29AAACA1234A1Z5&lt;/code&gt; in GSTR-2B but &lt;code&gt;29AAACA1234A2Z5&lt;/code&gt; in the purchase register (one digit off). Invoice numbers differ by prefixes (&lt;code&gt;INV-001&lt;/code&gt; vs &lt;code&gt;001&lt;/code&gt;), dates shift by a day, and HSN codes get truncated. Traditional tools priced for enterprises solve this with rule engines; tools priced for SMBs often skip reconciliation entirely and jump straight to filing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How GSTBot Handles It
&lt;/h2&gt;

&lt;p&gt;GSTBot's reconciliation engine treats matching as a scoring problem, not a boolean one. The pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ingestion&lt;/strong&gt; — PDFs, photos, or Excel files land in a Celery queue. A FastAPI worker calls &lt;code&gt;pdfplumber&lt;/code&gt; for text extraction, &lt;code&gt;pytesseract&lt;/code&gt; for OCR on images, and &lt;code&gt;openpyxl&lt;/code&gt; for spreadsheets. Extracted fields (vendor name, GSTIN, invoice number, date, taxable value, tax rate, HSN) are normalized — GSTINs uppercased, whitespace stripped, common OCR artifacts (&lt;code&gt;O&lt;/code&gt;→&lt;code&gt;0&lt;/code&gt;, &lt;code&gt;I&lt;/code&gt;→&lt;code&gt;1&lt;/code&gt;) corrected.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Candidate Generation&lt;/strong&gt; — For each purchase-register row, the engine pulls potential GSTR-2B matches within a configurable window: ±3 days on invoice date, same GSTIN prefix (first 10 chars), and amount within a tolerance band. This reduces the cross-join from O(n×m) to a manageable candidate set.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Weighted Scoring&lt;/strong&gt; — Each candidate pair receives a composite score:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# gstbot/reconciliation/scoring.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PurchaseRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gstr2b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GSTR2BRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ToleranceConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="c1"&gt;# GSTIN exact match = 40 pts, prefix match = 20 pts
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gstin&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;gstr2b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gstin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gstin&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;gstr2b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gstin&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;

    &lt;span class="c1"&gt;# Invoice number: Levenshtein similarity × 15
&lt;/span&gt;    &lt;span class="n"&gt;inv_sim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;levenshtein&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inv_no&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gstr2b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inv_no&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inv_no&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gstr2b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inv_no&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;inv_sim&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;

    &lt;span class="c1"&gt;# Date within tolerance
&lt;/span&gt;    &lt;span class="n"&gt;day_diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;gstr2b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;day_diff&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date_tolerance_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;day_diff&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date_tolerance_days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Amount within tolerance %
&lt;/span&gt;    &lt;span class="n"&gt;amt_diff_pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;taxable&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;gstr2b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;taxable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;taxable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amt_diff_pct&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_tolerance_pct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;amt_diff_pct&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_tolerance_pct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# HSN exact = 10 pts
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hsn&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;gstr2b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hsn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default tolerances: &lt;code&gt;date_tolerance_days=3&lt;/code&gt;, &lt;code&gt;amount_tolerance_pct=0.02&lt;/code&gt; (2%). Users override per supplier or globally via the React settings panel (Vite + TanStack Query).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Classification &amp;amp; Actions&lt;/strong&gt; — Pairs ≥ 85 auto-match. Scores 60–84 become "Review" exceptions with a suggested action: "Accept match", "Split invoice", "Mark vendor GSTIN error", or "Escalate to CA". Below 60 → "Unmatched" with a "Create missing invoice" shortcut. Each exception row surfaces the top 3 scoring candidates with side-by-side field comparison.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reversal Tracking&lt;/strong&gt; — Matched invoices feed Rule 37 (ITC reversal for non-payment within 180 days), Rule 42 (ISD reversal), and Rule 43 (capital goods reversal) calculators. The engine stores the match timestamp and score, so reversal reports show &lt;em&gt;why&lt;/em&gt; an invoice was considered matched.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What It Looks Like to Use
&lt;/h2&gt;

&lt;p&gt;The reconciliation dashboard (&lt;code&gt;/reconcile&lt;/code&gt;) loads a virtualized table (TanStack Virtual) showing 5,000+ rows without pagination lag. Columns: Status (badge: ✅ Matched, ⚠️ Review, ❌ Unmatched), Score, Vendor, GSTIN, Invoice No, Date, Taxable, Tax, HSN, Actions. Clicking a Review row opens a drawer with the three best GSTR-2B candidates, each field highlighted green/amber/red per match quality. One click "Accept" writes the link to PostgreSQL (&lt;code&gt;reconciliation_links&lt;/code&gt; table) and updates the purchase register's &lt;code&gt;gstr2b_matched_at&lt;/code&gt; timestamp.&lt;/p&gt;

&lt;p&gt;Bulk actions: checkbox-select → "Accept all ≥ 90" or "Export exceptions to Excel for CA". The export includes the scoring breakdown so the CA sees the evidence trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Matters for ITC Reconciliation
&lt;/h2&gt;

&lt;p&gt;Configurable tolerances let a 50-invoice/month retailer use tight settings (1 day, 0.5%) while a 5,000-invoice wholesaler loosens to 5 days, 3% — without code changes. The scoring breakdown also feeds the supplier health score: vendors consistently scoring &amp;lt; 70 get flagged in the "Supplier Filing Health" view, informing the decision to follow up or switch suppliers before the next GSTR-2B cycle.&lt;/p&gt;

&lt;p&gt;Built on FastAPI + Celery + Redis for async throughput, PostgreSQL for ACID guarantees on the link table, and React/Vite for a responsive review UI. No enterprise license required.&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>python</category>
      <category>postgres</category>
      <category>react</category>
    </item>
    <item>
      <title>Deep Dive into Documedic’s Drug Interaction Feature</title>
      <dc:creator>Subhendu Das</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:12:53 +0000</pubDate>
      <link>https://dev.to/sumaninster/deep-dive-into-documedics-drug-interaction-feature-25f6</link>
      <guid>https://dev.to/sumaninster/deep-dive-into-documedics-drug-interaction-feature-25f6</guid>
      <description>&lt;h2&gt;
  
  
  Problem Statement
&lt;/h2&gt;

&lt;p&gt;Clinicians routinely need to verify that a new prescription does not create dangerous drug interactions, contraindications, or dosing errors. A delay or omission can lead to medication errors that compromise patient safety. Documedic’s Drug Interaction feature aims to provide instant, evidence‑based recommendations as part of a larger Clinical Decision Support System (CDSS).&lt;/p&gt;

&lt;h2&gt;
  
  
  Architectural Overview
&lt;/h2&gt;

&lt;p&gt;The feature is split into three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data layer&lt;/strong&gt; – PostgreSQL stores drug–drug interaction tables, drug properties, and patient‑specific contraindication flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service layer&lt;/strong&gt; – FastAPI exposes a REST endpoint that accepts a patient profile, a list of prescribed drugs, and returns a structured JSON payload of interactions, contraindications, and dosing hints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation layer&lt;/strong&gt; – A React component renders the payload in a concise table, highlighting critical interactions and offering clickableഠ links to evidence sources.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The FastAPI service delegates the logical analysis to an AI reasoning engine. The engine is protected by authentication checks and safeguards, as detailed in recent commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backend Implementation
&lt;/h2&gt;

&lt;p&gt;A minimal FastAPI route shows how the service validates input, queries the database, and passes data to the reasoning engine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InteractionRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;drugs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/interactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_interactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InteractionRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="c1"&gt;# Authentication guard – an unauthenticated request cannot consume memory
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unauthorized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Verify drugs exist in the catalog
&lt;/span&gt;    &lt;span class="n"&gt;catalog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Drug&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Drug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;in_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drugs&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drugs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown drug(s) supplied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Delegate to reasoning engine (AI model)
&lt;/span&gt;    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reasoning_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drugs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drugs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The routeંગ્ર uses a dependency‑injected &lt;code&gt;auth&lt;/code&gt; guard that checks API key revocation, a fix introduced in commit &lt;code&gt;5e78ac7&lt;/code&gt;. The same guard also ensures that a revoked key does not report the engine as live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend Interaction
&lt;/h2&gt;

&lt;p&gt;The React component fetches the endpoint and displays results in a user‑friendly table. A concise snippet illustrates the core logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Interaction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;InteractionTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;patientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drugs&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;patientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;drugs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Interaction&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/interactions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;patientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drugs&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;patientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drugs&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Description&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Severity&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;severity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;High&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;severity&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI marks high‑severity interactions in red. Clicking a row opens a modal with evidence links, linking back to the CDSS’s evidence‑based medicine repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Model and Queries
&lt;/h2&gt;

&lt;p&gt;A typical interaction table in PostgreSQL may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;drug_interactions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;drug_a&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;drug_b&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;interaction_type&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;evidence_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A query that aggregates interactions for a patient’s drug list is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drug_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drug_b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interaction_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evidence_url&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;drug_interactions&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drug_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drug_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;$1&lt;/code&gt; placeholder receives the array of drug names supplied by the frontend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security &amp;amp; Safety
&lt;/h2&gt;

&lt;p&gt;Recent commits introduced several safeguards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unauthenticated request memory leak&lt;/strong&gt; – The &lt;code&gt;auth&lt;/code&gt; dependency now rejects unauthenticated requests before they reach the reasoning engine, preventing excessive memory consumption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revoked API key detection&lt;/strong&gt; – A revoked key no longer reports the engine as live, as fixed in &lt;code&gt;5e78ac7&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety corrections&lt;/strong&gt; – The coagulation panel print logic now correctly resolves INR values, avoiding silent removal of liver scores (commit &lt;code&gt;5e78ac7&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capital‑letter reference data&lt;/strong&gt; – A block that previously turned a hard error into a warning when encountering a capital letter has been tightened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session ownership checks&lt;/strong&gt; – The engine now verifies session ownership before writing results, preventing orphaned outputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These changes collectively tighten the CDSS’s reliability and compliance with clinical safety standards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Documedic’s drug interaction feature exemplifies a focused, engineer‑oriented clinical decision support component. Using FastAPI for secure, authenticated API handling, React for a responsive UI, and PostgreSQL for structured evidence storage, the system delivers rapid, evidence‑based dosing and interaction alerts. The recent commits demonstrate a commitment to safety and performance, ensuring that clinicians receive accurate information without compromising system resources or patient safety.&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>react</category>
      <category>postgres</category>
      <category>cdss</category>
    </item>
  </channel>
</rss>
