<?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: rahul patwa</title>
    <description>The latest articles on DEV Community by rahul patwa (@rahul_patwa_f99f19cd1519b).</description>
    <link>https://dev.to/rahul_patwa_f99f19cd1519b</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1583727%2F9af43308-e9ad-4ecc-99b7-1740043867da.png</url>
      <title>DEV Community: rahul patwa</title>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rahul_patwa_f99f19cd1519b"/>
    <language>en</language>
    <item>
      <title>How to Sync Design Tokens Between React and Flutter (Without Losing Your Mind)</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Mon, 06 Apr 2026 03:26:00 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/how-to-sync-design-tokens-between-react-and-flutter-without-losing-your-mind-55hc</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/how-to-sync-design-tokens-between-react-and-flutter-without-losing-your-mind-55hc</guid>
      <description>&lt;p&gt;Style Dictionary's Flutter support has been broken for years. I built tokensync — a CLI that generates CSS and Flutter ThemeData from one tokens.json file, then verifies they match numerically.&lt;br&gt;
Your designer just updated the brand color.&lt;/p&gt;

&lt;p&gt;You open your CSS file. Update &lt;code&gt;--color-brand-500&lt;/code&gt;. Then you open your Flutter file. Update &lt;code&gt;AppTheme._lightColors.primary&lt;/code&gt;. Then you grep for anywhere else it might appear. Then you do the same for dark mode. Then you hope you got them all.&lt;/p&gt;

&lt;p&gt;Three weeks later a designer screenshots both apps side by side. The web button is &lt;code&gt;#5C6BC0&lt;/code&gt;. The mobile button is &lt;code&gt;#5B6BC0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Off by one digit. Nobody noticed. It shipped.&lt;/p&gt;

&lt;p&gt;If you maintain both a React web app and a Flutter mobile app, this is a design token sync problem — and it costs teams 6–20 hours every time tokens change.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why existing design token tools don't solve this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Style Dictionary&lt;/strong&gt; is the industry standard for design token transformation. It's genuinely great for CSS. But its Flutter support produces flat key-value constants — not the &lt;code&gt;ThemeData&lt;/code&gt;, &lt;code&gt;ColorScheme&lt;/code&gt;, or &lt;code&gt;TextTheme&lt;/code&gt; that Flutter apps actually need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What Style Dictionary gives you for Flutter:&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;typographyDisplayFontSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;48.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;typographyDisplayFontWeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// That's it. You still write TextStyle yourself.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub issues requesting proper Flutter support have been open since 2022 with no resolution. Most teams end up writing a custom script. It works until the first rebrand, then breaks.&lt;/p&gt;

&lt;p&gt;The other pain point is &lt;strong&gt;letter-spacing conversion&lt;/strong&gt;. CSS &lt;code&gt;letter-spacing: -0.02em&lt;/code&gt; at &lt;code&gt;font-size: 48px&lt;/code&gt; should become Flutter &lt;code&gt;letterSpacing: -0.96&lt;/code&gt;. Every team figures this out independently. Every team gets it wrong at least once.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing tokensync: one source of truth for React and Flutter design tokens
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;tokensync&lt;/strong&gt; is an open-source CLI that reads a single &lt;code&gt;tokens.json&lt;/code&gt; file in W3C DTCG format and generates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tokens.css&lt;/code&gt; — CSS custom properties with &lt;code&gt;:root&lt;/code&gt; (light) and &lt;code&gt;[data-theme="dark"]&lt;/code&gt; (dark) selectors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app_theme.dart&lt;/code&gt; — complete Flutter &lt;code&gt;ThemeData&lt;/code&gt;, &lt;code&gt;ColorScheme&lt;/code&gt;, &lt;code&gt;TextTheme&lt;/code&gt;, and &lt;code&gt;TextStyle&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tokens.ts&lt;/code&gt; — typed TypeScript constants for shared logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it runs a &lt;strong&gt;parity check&lt;/strong&gt; — numerically comparing every token value between the CSS and Dart outputs to catch drift before it ships.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tokensync init   &lt;span class="c"&gt;# scaffold config + tokens.json&lt;/span&gt;
npx tokensync build  &lt;span class="c"&gt;# generate all platform outputs&lt;/span&gt;
npx tokensync check  &lt;span class="c"&gt;# verify React and Flutter values match&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What the generated Flutter output looks like
&lt;/h2&gt;

&lt;p&gt;Given this design token:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typography"&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;"display"&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;"$type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"typography"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"$value"&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;"fontFamily"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Inter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"fontSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"48px"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"fontWeight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"lineHeight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"letterSpacing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-0.02em"&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="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;tokensync generates idiomatic Flutter &lt;code&gt;ThemeData&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;TextTheme&lt;/span&gt; &lt;span class="n"&gt;_textTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TextTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;displayLarge:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;fontFamily:&lt;/span&gt;    &lt;span class="s"&gt;'Inter'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;fontSize:&lt;/span&gt;      &lt;span class="mf"&gt;48.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;fontWeight:&lt;/span&gt;    &lt;span class="n"&gt;FontWeight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;w700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;height:&lt;/span&gt;        &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;letterSpacing:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// ← -0.02em × 48px, correctly converted&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;And the matching CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--typography-display-font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Inter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--typography-display-font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;48px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--typography-display-font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;700&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--typography-display-line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--typography-display-letter-spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-0.02em&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 generated Dart file passes &lt;code&gt;dart analyze&lt;/code&gt; with zero errors out of the box.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the parity checker works
&lt;/h2&gt;

&lt;p&gt;After building, tokensync compares every token value between platforms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Parity: web vs flutter
11 tokens  ·  11 passed
✓ Parity check passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It normalizes values before comparing: &lt;code&gt;em&lt;/code&gt; to &lt;code&gt;px&lt;/code&gt;, named font weights (&lt;code&gt;"SemiBold"&lt;/code&gt; → &lt;code&gt;600&lt;/code&gt;), &lt;code&gt;0em&lt;/code&gt; and &lt;code&gt;0.0&lt;/code&gt; both treated as zero. If anything diverges, it reports the exact token name and delta.&lt;/p&gt;

&lt;p&gt;For CI, add this to your pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tokensync check &lt;span class="nt"&gt;--ci&lt;/span&gt;   &lt;span class="c"&gt;# exits with code 1 if CSS and Dart diverge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mismatches block merges. No more &lt;code&gt;#5C6BC0&lt;/code&gt; vs &lt;code&gt;#5B6BC0&lt;/code&gt; in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dark mode with a single token definition
&lt;/h2&gt;

&lt;p&gt;Define your semantic tokens with light and dark values:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"semantic"&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;"$modes"&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;"light"&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;"color"&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;"primary"&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;"$value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{color.brand.500}"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dark"&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;"color"&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;"primary"&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;"$value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{color.brand.100}"&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="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;"color"&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;"primary"&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;"$type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{color.brand.500}"&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="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;tokensync generates both &lt;code&gt;:root&lt;/code&gt; / &lt;code&gt;[data-theme="dark"]&lt;/code&gt; blocks in CSS and both &lt;code&gt;ThemeData.light()&lt;/code&gt; / &lt;code&gt;ThemeData.dark()&lt;/code&gt; in Flutter — from the same source of truth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Figma integration
&lt;/h2&gt;

&lt;p&gt;If your design tokens live in Figma Variables (Professional plan):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;FIGMA_ACCESS_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_token &lt;span class="nv"&gt;FIGMA_FILE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_key tokensync pull
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the free Figma plan, use the Styles API instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tokensync pull &lt;span class="nt"&gt;--figma-api&lt;/span&gt; styles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both write a &lt;code&gt;tokens.json&lt;/code&gt; ready for &lt;code&gt;tokensync build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tokens Studio exports are also supported via a built-in adapter.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it compares to Style Dictionary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Style Dictionary&lt;/th&gt;
&lt;th&gt;tokensync&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CSS custom properties&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flutter &lt;code&gt;ColorScheme&lt;/code&gt; (light + dark)&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flutter &lt;code&gt;TextTheme&lt;/code&gt; with named slots&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;TextStyle&lt;/code&gt; with correct &lt;code&gt;letterSpacing&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;em → px letter-spacing conversion&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Named font weights (&lt;code&gt;"SemiBold"&lt;/code&gt; → &lt;code&gt;FontWeight.w600&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-platform parity check&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Figma Variables pull&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install globally&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tokensync

&lt;span class="c"&gt;# Or run with npx&lt;/span&gt;
npx tokensync init &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx tokensync build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;init&lt;/code&gt; command scaffolds a &lt;code&gt;tokensync.config.ts&lt;/code&gt; and a sample &lt;code&gt;tokens.json&lt;/code&gt; with colors, spacing, and typography — including light/dark modes. Running &lt;code&gt;build&lt;/code&gt; generates all three platform outputs and runs the parity check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt; Node.js 18+. Zero runtime dependencies. MIT license.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;em&gt;[&lt;a href="https://github.com/rahulpatwa1303/tokensync" rel="noopener noreferrer"&gt;https://github.com/rahulpatwa1303/tokensync&lt;/a&gt;]&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Current status and what's next
&lt;/h2&gt;

&lt;p&gt;This is v0.1.0. It handles the token types I've encountered in real projects: &lt;code&gt;color&lt;/code&gt;, &lt;code&gt;dimension&lt;/code&gt;, &lt;code&gt;typography&lt;/code&gt;, &lt;code&gt;shadow&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;. The generated Dart is idiomatic and passes static analysis.&lt;/p&gt;

&lt;p&gt;Planned for v0.2.0: React Native formatter, &lt;code&gt;oklch&lt;/code&gt; color support, watch mode improvements.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do you have this problem?
&lt;/h2&gt;

&lt;p&gt;If you ship both React and Flutter, I'd genuinely like to know how you handle design token sync today. Manual copy-paste? A custom script? A SaaS tool? Something in your CI?&lt;/p&gt;

&lt;p&gt;Drop a comment — trying to understand whether this pain is widespread or specific to how my team works.&lt;/p&gt;

</description>
      <category>design</category>
      <category>flutter</category>
      <category>react</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Gemma 4 on Android: Complete Developer Guide to On-Device AI</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Sun, 05 Apr 2026 08:33:55 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/gemma-4-on-android-complete-developer-guide-to-on-device-ai-f5b</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/gemma-4-on-android-complete-developer-guide-to-on-device-ai-f5b</guid>
      <description>&lt;p&gt;Google dropped Gemma 4 on April 2, 2026, under the Apache 2.0 license. The E2B variant 2.58 GB file, under 1.5 GB active memory, runs at 52.1 decode tokens per second on a Samsung S26 Ultra GPU. That's fast enough to stream responses faster than a user can read them, on hardware they already own, with no API key and no data leaving the device. (&lt;a href="https://huggingface.co/litert-community" rel="noopener noreferrer"&gt;HuggingFace model cards&lt;/a&gt;, 2026)&lt;/p&gt;

&lt;p&gt;This is the full integration guide: model selection, Kotlin setup for all three backends, real performance numbers, and the production gotchas I've found building with this stack.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Gemma 4 E2B runs on any Android device with 6 GB+ RAM at 52 tokens/sec on GPU. Add one Gradle dependency (&lt;code&gt;com.google.ai.edge.litertlm:litertlm-android&lt;/code&gt;), download the 2.58 GB model file to &lt;code&gt;filesDir&lt;/code&gt;, initialize the engine, and stream responses via a Kotlin Flow. Watch out: &lt;code&gt;SamplerConfig&lt;/code&gt; takes &lt;code&gt;Double&lt;/code&gt;, not &lt;code&gt;Float&lt;/code&gt;, and &lt;code&gt;sendMessageAsync&lt;/code&gt; emits cumulative text per emission, not deltas. (&lt;a href="https://developers.googleblog.com/" rel="noopener noreferrer"&gt;Google Developers Blog&lt;/a&gt;, 2026)&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why This Is Different From Every Other Mobile LLM Attempt
&lt;/h2&gt;

&lt;p&gt;I've tried running quantized models on Android twice before Gemma 4. Both times I hit the same wall: memory pressure killed the process, or inference was too slow to ship. Gemma 4's E2B breaks those assumptions. Its mixed-bit quantization keeps active memory under 1.5 GB, and LiteRT-LM, Google's open-source runtime, released September 24, 2025 - abstracts CPU, GPU, and NPU execution behind a single Kotlin API. (&lt;a href="https://developers.googleblog.com/" rel="noopener noreferrer"&gt;Google Developers Blog&lt;/a&gt;, 2025)&lt;/p&gt;

&lt;p&gt;The market signal backs the technical case. The on-device AI market hit $10.76 billion in 2025 and is projected to reach $75.5 billion by 2033 at a 27.8% CAGR. (&lt;a href="https://www.grandviewresearch.com/" rel="noopener noreferrer"&gt;Grand View Research&lt;/a&gt;, 2025) That growth is driven by the same forces pushing you to read this post: privacy requirements, latency demands, and the economics of zero marginal inference cost.&lt;/p&gt;

&lt;p&gt;The privacy argument closes enterprise deals faster than performance specs do, in my experience. Legal teams understand "data never leaves the device" immediately. Explaining prefill tokens per second takes longer. And 64% of professionals told Cisco in 2025 they worry about inadvertently sharing sensitive data with cloud AI services, so there's a real user-level demand for what this stack offers. (&lt;a href="https://www.cisco.com/" rel="noopener noreferrer"&gt;Cisco&lt;/a&gt;, 2025)&lt;/p&gt;




&lt;h2&gt;
  
  
  E2B or E4B? Pick the Right Variant First
&lt;/h2&gt;

&lt;p&gt;Gemma 4 ships two variants for on-device Android deployment. Picking the wrong one costs you either capability or hardware compatibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E2B&lt;/strong&gt; is the right default for most apps. File size is 2.58 GB. Active memory sits under 1.5 GB, which fits any device with 6 GB RAM or more the majority of Android flagships and mid-rangers sold since 2024. Benchmark scores: MMLU Pro at 60.0%, LiveCodeBench v6 at 44.0%, GPQA Diamond at 43.4%. (&lt;a href="https://huggingface.co/" rel="noopener noreferrer"&gt;HuggingFace&lt;/a&gt;, 2026) Those numbers are sufficient for summarization, Q&amp;amp;A, writing assistance, and classification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E4B&lt;/strong&gt; is for when reasoning quality is the product. File size is 3.65 GB, active memory around 2 GB. The capability jump is real: MMLU Pro rises to 69.4%, LiveCodeBench to 52.0%, and GPQA Diamond (graduate-level science reasoning) jumps from 43.4% to 58.6%. (&lt;a href="https://huggingface.co/" rel="noopener noreferrer"&gt;HuggingFace&lt;/a&gt;, 2026) That 15-point GPQA gain matters if you're building legal document review, medical triage support, or code analysis. You'll need 8 GB+ RAM as your device minimum.&lt;/p&gt;

&lt;p&gt;GPU decode speed on an S26 Ultra drops from 52.1 tokens/sec (E2B) to 22.1 tokens/sec (E4B). At 22 tokens/sec, streaming still feels natural users read prose at roughly 4-5 words per second. The trade-off is prefill time on long contexts.&lt;/p&gt;

&lt;p&gt;Both variants support 140 languages and handle text, image, and audio inputs. (&lt;a href="https://deepmind.google/" rel="noopener noreferrer"&gt;Google DeepMind&lt;/a&gt;, 2026) That multimodal capability in a package this small is genuinely surprising. Start with E2B. Only move to E4B if your use case actually demands stronger reasoning.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Full Kotlin Integration: All 5 Steps
&lt;/h2&gt;

&lt;p&gt;The full integration is five steps. Each is self-contained. You can have a working demo in under an hour.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 Add the Gradle Dependency
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/build.gradle.kts&lt;/span&gt;
&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.google.ai.edge.litertlm:litertlm-android:latest.release"&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;Sync and you're done. No NDK wrangling, no JNI plumbing, no native toolchain configuration at this stage. GPU and NPU backends require additional &lt;code&gt;AndroidManifest.xml&lt;/code&gt; entries - covered in the backend section below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 Download the Model File
&lt;/h3&gt;

&lt;p&gt;The model doesn't go in your APK. At 2.58 GB (E2B) or 3.65 GB (E4B), it blows past Play Store's 150 MB limit. Download on first launch, store in &lt;code&gt;context.filesDir&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For development and local testing, pull the file directly with the HuggingFace CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the CLI&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;huggingface_hub

&lt;span class="c"&gt;# Download E2B (2.58 GB)&lt;/span&gt;
huggingface-cli download litert-community/gemma-4-E2B-it-litert-lm &lt;span class="se"&gt;\&lt;/span&gt;
  gemma-4-E2B-it.litertlm &lt;span class="nt"&gt;--local-dir&lt;/span&gt; ./models

&lt;span class="c"&gt;# Download E4B (3.65 GB), optional&lt;/span&gt;
huggingface-cli download litert-community/gemma-4-E4B-it-litert-lm &lt;span class="se"&gt;\&lt;/span&gt;
  gemma-4-E4B-it.litertlm &lt;span class="nt"&gt;--local-dir&lt;/span&gt; ./models
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, implement a WorkManager download job with DownloadManager. Build a proper progress screen with cancellation, resume-on-reconnect, and checksum verification. A 2.58 GB download on a mobile connection takes meaningful time - design accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 - Initialize the Engine
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.ai.edge.litertlm.Backend&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.ai.edge.litertlm.Engine&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.ai.edge.litertlm.EngineConfig&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EngineConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;modelPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filesDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;absolutePath&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/gemma-4-E2B-it.litertlm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GPU&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;engine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;also&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&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;&lt;strong&gt;Critical:&lt;/strong&gt; &lt;code&gt;engine.initialize()&lt;/code&gt; is a blocking call. Run it on a background dispatcher (&lt;code&gt;Dispatchers.IO&lt;/code&gt;) inside a coroutine. On slower devices with older storage, initialization takes 3-8 seconds. Call it on the main thread and you'll get an ANR before the user even types anything.&lt;/p&gt;

&lt;p&gt;Hold the &lt;code&gt;Engine&lt;/code&gt; instance in a singleton or a ViewModel. Re-initialization is expensive. Treat it like a database connection open once, reuse everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 Create a Conversation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.ai.edge.litertlm.Content&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.ai.edge.litertlm.Contents&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.ai.edge.litertlm.ConversationConfig&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.ai.edge.litertlm.SamplerConfig&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;conversation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createConversation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;ConversationConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;systemInstruction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Contents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"You are a helpful assistant."&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;samplerConfig&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SamplerConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topK&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topP&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7&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;&lt;strong&gt;Gotcha:&lt;/strong&gt; &lt;code&gt;SamplerConfig&lt;/code&gt; parameters are &lt;code&gt;Double&lt;/code&gt;, not &lt;code&gt;Float&lt;/code&gt;. Pass &lt;code&gt;0.7&lt;/code&gt; not &lt;code&gt;0.7f&lt;/code&gt;. The compiler won't catch this at the call site in all configurations you'll get a runtime error instead. Worth double-checking if you're adapting code from other LLM SDKs that use &lt;code&gt;Float&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Lower &lt;code&gt;temperature&lt;/code&gt; toward 0.2 for factual or deterministic responses. Raise it toward 1.0 for creative tasks. &lt;code&gt;topK = 40&lt;/code&gt; with &lt;code&gt;topP = 0.95&lt;/code&gt; is a stable combination that avoids repetition loops in most conversational contexts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 Stream Responses
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Explain machine learning in simple terms"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contents&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filterIsInstance&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;it&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;_uiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&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="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;&lt;strong&gt;Gotcha:&lt;/strong&gt; &lt;code&gt;sendMessageAsync&lt;/code&gt; emits the &lt;strong&gt;cumulative&lt;/strong&gt; response text on each emission, not just the new delta. Don't concatenate it to your existing string replace it. This design makes ViewModel code cleaner than you'd expect: just assign each emission to your UI state and you get live-updating streaming with no string management on your side.&lt;/p&gt;

&lt;p&gt;Wire this Flow into a &lt;code&gt;StateFlow&lt;/code&gt; in your ViewModel and collect it in a Composable. The whole streaming chat UI is about 20 lines of production code once the engine is initialized.&lt;/p&gt;




&lt;h2&gt;
  
  
  CPU, GPU, or NPU: Which Backend Should You Use?
&lt;/h2&gt;

&lt;p&gt;Backend selection has more impact than almost any other integration decision. Here's the tradeoff in concrete numbers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CPU works everywhere, no manifest changes required&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EngineConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modelPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CPU&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;// GPU via OpenCL - fastest prefill, requires manifest entries&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EngineConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modelPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GPU&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;// NPU Qualcomm Hexagon or MediaTek APU&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EngineConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;modelPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modelPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NPU&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nativeLibraryDir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;applicationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nativeLibraryDir&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;&lt;strong&gt;CPU:&lt;/strong&gt; Works on every Android device. On an S26 Ultra, E2B achieves 557 prefill tokens/sec and 46.9 decode tokens/sec using 1,733 MB memory. (&lt;a href="https://huggingface.co/litert-community" rel="noopener noreferrer"&gt;HuggingFace model cards&lt;/a&gt;, 2026) That's usable for chat. On lower-end hardware it degrades - a mid-range Qualcomm 7s Gen 3 drops to around 8-12 decode tokens/sec. Still viable if you constrain response length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPU:&lt;/strong&gt; 3,808 prefill tokens/sec and 52.1 decode tokens/sec on the S26 Ultra - roughly 7x faster prefill than CPU. (&lt;a href="https://huggingface.co/litert-community" rel="noopener noreferrer"&gt;HuggingFace model cards&lt;/a&gt;, 2026) The catch: thermal throttling. After 5-10 minutes of continuous GPU inference, the SoC manages heat by dropping clock speeds. You'll see decode fall from 52 to 30 tokens/sec or lower. GPU is the right default for short-to-medium sessions on flagships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NPU:&lt;/strong&gt; The Qualcomm Dragonwing IQ8 NPU hits 3,700 prefill and 31 decode tokens/sec. (&lt;a href="https://developers.googleblog.com/" rel="noopener noreferrer"&gt;Google Developers Blog&lt;/a&gt;, 2026) Decode trails GPU slightly, but the thermal profile is dramatically better. In my testing on the S25, switching from GPU to NPU at the 8-minute mark kept inference speed consistent across a 20-minute session. For chat-heavy apps where sessions run long, NPU is the better production choice even though GPU wins on peak specs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thermal fallback:&lt;/strong&gt; Implement &lt;code&gt;PowerManager.getThermalHeadroom()&lt;/code&gt; to detect high thermal state and gracefully switch backends at runtime. Start on GPU; fall back to NPU if thermal headroom drops below your threshold. This gives you peak speed for short interactions and sustained reliability for long ones.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real Performance Numbers
&lt;/h2&gt;

&lt;p&gt;These come from official LiteRT-LM model cards on HuggingFace, not synthetic workloads.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device / Backend&lt;/th&gt;
&lt;th&gt;E2B Prefill (tok/s)&lt;/th&gt;
&lt;th&gt;E2B Decode (tok/s)&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S26 Ultra - GPU&lt;/td&gt;
&lt;td&gt;3,808&lt;/td&gt;
&lt;td&gt;52.1&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S26 Ultra - CPU&lt;/td&gt;
&lt;td&gt;557&lt;/td&gt;
&lt;td&gt;46.9&lt;/td&gt;
&lt;td&gt;1,733 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qualcomm IQ8 NPU&lt;/td&gt;
&lt;td&gt;3,700&lt;/td&gt;
&lt;td&gt;31.0&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E4B - S26 Ultra GPU&lt;/td&gt;
&lt;td&gt;1,293&lt;/td&gt;
&lt;td&gt;22.1&lt;/td&gt;
&lt;td&gt;~2 GB active&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(&lt;a href="https://huggingface.co/litert-community" rel="noopener noreferrer"&gt;HuggingFace model cards&lt;/a&gt;, 2026; &lt;a href="https://developers.googleblog.com/" rel="noopener noreferrer"&gt;Google Developers Blog&lt;/a&gt;, 2026)&lt;/p&gt;

&lt;p&gt;For context on how this compares: running a 7B 4-bit model with llama.cpp on a Snapdragon 8 Gen 3 CPU produces roughly 4 decode tokens/sec. (&lt;a href="https://arxiv.org/abs/2410.03613" rel="noopener noreferrer"&gt;arXiv 2410.03613v3&lt;/a&gt;, 2024) Gemma 4 E2B on CPU already outperforms that by 10x on equivalent-class hardware, because LiteRT-LM is optimized specifically for these architectures.&lt;/p&gt;

&lt;p&gt;Gemma 4's architecture also delivers 60% less battery consumption than the previous generation for equivalent tasks. (&lt;a href="https://developers.googleblog.com/" rel="noopener noreferrer"&gt;Google Developers Blog&lt;/a&gt;, 2026) In my session-based testing, a 10-minute active conversation on the S25 GPU consumed approximately 4-5% battery, comparable to 10 minutes of YouTube at medium brightness. That's a number you can actually ship on.&lt;/p&gt;

&lt;p&gt;One practical note on first inference: cold-start latency is real. The model weights get paged into memory on the first call after engine initialization. Warm the engine with a short empty prompt during app launch - before the user types anything - and that latency disappears from the user experience entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔥 Production Gotchas
&lt;/h2&gt;

&lt;p&gt;Most guides stop at "here's the code that compiles." Here's what I found the hard way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory initialization order matters.&lt;/strong&gt; Close large background allocations before calling &lt;code&gt;engine.initialize()&lt;/code&gt;. If the user has a game or video editor running, the OS may partially initialize the model and then kill it mid-load. Check available memory first. On 6 GB devices especially, you're working with less headroom than you think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SamplerConfig&lt;/code&gt; takes &lt;code&gt;Double&lt;/code&gt;, not &lt;code&gt;Float&lt;/code&gt;.&lt;/strong&gt; Already mentioned above but worth repeating. It's the most common runtime error in early-adopter threads, and it doesn't always surface at compile time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;sendMessageAsync&lt;/code&gt; emits cumulative text, not deltas.&lt;/strong&gt; If you treat each emission as a new chunk and append it, your UI will duplicate everything. Replace the displayed string on each emission, don't concatenate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model file goes in &lt;code&gt;filesDir&lt;/code&gt;, not the APK.&lt;/strong&gt; Play Store's APK limit is 150 MB. E2B is 2.58 GB. This isn't optional; you can't ship it in the APK. First-launch download is the standard pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thermal throttling is real and predictable.&lt;/strong&gt; GPU inference throttles after 5-10 minutes of sustained load on most flagships. It's not a bug - it's the SoC protecting itself. Design for it: implement &lt;code&gt;PowerManager.getThermalHeadroom()&lt;/code&gt; and switch backends proactively rather than waiting for the user to notice slowdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context window growth increases battery drain.&lt;/strong&gt; Battery consumption increases as conversation length grows because later turns process much longer KV caches than early turns. The 60% battery reduction claim holds for short sessions; it's less pronounced in 20-minute conversations. Consider trimming old turns from history after 10-15 exchanges to control this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test on low-RAM devices before shipping.&lt;/strong&gt; The app behaves completely differently when memory is constrained. Use Android Studio's memory profiler across a full conversation session, not just at startup.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Actually Worth Building
&lt;/h2&gt;

&lt;p&gt;The use cases that benefit most from on-device inference share two properties: they involve personal or sensitive data, and they need to feel fast. That combination rules out cloud APIs for entire product categories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private journaling and personal assistant apps&lt;/strong&gt; are the clearest fit. Users who want help organizing notes, reflecting on patterns, or drafting messages have a legitimate expectation of privacy. The on-device guarantee is the product differentiator, not just a feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code review tools for developers.&lt;/strong&gt; Engineers are already cautious about pasting proprietary code into cloud AI. A local code reviewer removes that concern entirely. With E4B's 52.0% on LiveCodeBench v6, it's capable of explaining code, generating boilerplate, and catching obvious bugs. (&lt;a href="https://huggingface.co/" rel="noopener noreferrer"&gt;HuggingFace&lt;/a&gt;, 2026)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Document Q&amp;amp;A.&lt;/strong&gt; Load a PDF into context and let users ask questions about it. In my prototyping with E2B, a 10-page PDF (~4,000 tokens) processes in under 2 seconds on the S26 Ultra GPU before returning the first response token. That latency is invisible - it feels instant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessibility features&lt;/strong&gt; : real-time captioning, reading assistance, simplified language mode benefit from local inference because cloud round-trips add 200-400ms per request. That latency matters in accessibility contexts. Local runs immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline translation across 140 languages.&lt;/strong&gt; Field researchers, travelers in areas with poor connectivity, language learning apps that work on airplanes, these are real markets that cloud APIs can't serve reliably. (&lt;a href="https://deepmind.google/" rel="noopener noreferrer"&gt;Google DeepMind&lt;/a&gt;, 2026)&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this work on mid-range devices, or only flagships?
&lt;/h3&gt;

&lt;p&gt;E2B requires 6 GB RAM minimum. It runs on any device meeting that bar, not just flagships. On mid-range hardware with less capable GPUs, the CPU backend gives you 8-12 decode tokens/sec. That's usable for short responses. Set user expectations with a progress indicator and constrain response length on lower-tier hardware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I ship the model inside my APK?
&lt;/h3&gt;

&lt;p&gt;No. The E2B file is 2.58 GB. Google Play's APK size limit is 150 MB. The standard pattern is to download the model on first launch using WorkManager and DownloadManager, stored in &lt;code&gt;context.filesDir&lt;/code&gt;. It persists across app updates and survives the app going to background during download.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Gemma 4 free for commercial use?
&lt;/h3&gt;

&lt;p&gt;Yes. Gemma 4 is released under the Apache 2.0 license free to use, modify, and redistribute commercially with no royalties. LiteRT-LM is also Apache 2.0. There are no usage caps, no API costs, and no restrictions based on your app's MAU count, unlike some competing model licenses. (&lt;a href="https://blog.google/" rel="noopener noreferrer"&gt;Google Blog&lt;/a&gt;, 2026)&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the minimum Android API level?
&lt;/h3&gt;

&lt;p&gt;LiteRT-LM requires Android API 26 (Android 8.0 Oreo) or higher for CPU inference. GPU and NPU backends may require higher API levels depending on your device's OpenCL and Hexagon driver versions. Test GPU and NPU support on your specific target devices don't assume it works based on API level alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I handle the thermal throttling in practice?
&lt;/h3&gt;

&lt;p&gt;Call &lt;code&gt;PowerManager.getThermalHeadroom()&lt;/code&gt; periodically during long inference sessions. If it drops below 0.5, switch from GPU to NPU. If it drops below 0.2, fall back to CPU or pause inference with a brief user message. The NPU backend dissipates heat significantly more efficiently than GPU switching at the 8-minute mark keeps decode speed consistent across sessions running 20 minutes or longer.&lt;/p&gt;




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

&lt;p&gt;Gemma 4 plus LiteRT-LM is the first stack where "on-device LLM in a production Android app" goes from interesting experiment to shippable product. The numbers that make it real: 52.1 decode tokens/sec on GPU, under 1.5 GB active memory for E2B, Apache 2.0 license, and a Kotlin API that reduces the full integration to five steps and a few hundred lines of code.&lt;/p&gt;

&lt;p&gt;The gotchas are real &lt;code&gt;SamplerConfig&lt;/code&gt; takes &lt;code&gt;Double&lt;/code&gt;, &lt;code&gt;sendMessageAsync&lt;/code&gt; emits cumulative text, the model goes in &lt;code&gt;filesDir&lt;/code&gt; not the APK, and thermal throttling will surprise you if you don't plan for it. Plan for them up front and none of them are blockers.&lt;/p&gt;

&lt;p&gt;Start with E2B on GPU. Get a conversation running in a minimal Compose screen first, before touching production infrastructure. Profile memory and battery across a full session in Android Studio. Add the NPU fallback once the basics are solid.&lt;/p&gt;

&lt;p&gt;The model files are at &lt;a href="https://huggingface.co/litert-community" rel="noopener noreferrer"&gt;huggingface.co/litert-community&lt;/a&gt;. The LiteRT-LM runtime docs are at &lt;a href="https://ai.google.dev/edge/litert" rel="noopener noreferrer"&gt;ai.google.dev/edge/litert&lt;/a&gt;. Both are worth bookmarking, they're updated as the runtime evolves.&lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments about specific integration scenarios. What are you building with this?&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>This is a submission for the [DEV April Fools Challenge](https://dev.to/challenges/aprilfools-2026)</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Sat, 04 Apr 2026 08:08:59 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/this-is-a-submission-for-the-dev-april-fools-challengehttpsdevtochallengesaprilfools-2026-22e4</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/this-is-a-submission-for-the-dev-april-fools-challengehttpsdevtochallengesaprilfools-2026-22e4</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/aprilfools-2026"&gt;DEV April Fools Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;The Existential Teapot&lt;/strong&gt;, an overly compliant, incredibly hostile implementation of HTCPCP (Hyper Text Coffee Pot Control Protocol - RFC 2324). &lt;/p&gt;

&lt;p&gt;At its core, it's a web application designed solely to refuse to brew coffee and to return a &lt;code&gt;418 I'm a teapot&lt;/code&gt; HTTP error. However, this teapot goes above and beyond a simple status code. It actively evades user attempts to click the "BREW COFFEE" button. If an exasperated user finally manages to click it, they are subjected to a fake loading bar that inevitably reverses itself, followed by a personalized, AI-generated, Shakespearean insult delivered via text-to-speech in the poshest British accent available.&lt;/p&gt;

&lt;p&gt;It's a study in hostility-driven design and architectural over-engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://existential-teapot-production.up.railway.app/" rel="noopener noreferrer"&gt;https://existential-teapot-production.up.railway.app/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/rahulpatwa1303/existential-teapot" rel="noopener noreferrer"&gt;https://github.com/rahulpatwa1303/existential-teapot&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Backend:&lt;/strong&gt; A Node.js and Express server running on port 8080 (or 8091 depending on &lt;code&gt;.env&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;AI Integration:&lt;/strong&gt; I used the &lt;code&gt;@google/generative-ai&lt;/code&gt; SDK (Gemini API) and the &lt;code&gt;gemini-2.5-flash&lt;/code&gt; model. The system prompt instructs Gemini to be an Existential Teapot that refuses coffee requests with high-art, Shakespearean insults, while dropping references to Larry Masinter (the author of RFC 2324).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Frontend:&lt;/strong&gt; Pure, unadulterated HTML, CSS, and Vanilla JavaScript. &lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Hostile UI Features:&lt;/strong&gt; 

&lt;ul&gt;
&lt;li&gt;  An evasion algorithm that teleports the "BREW COFFEE" button away when the cursor approaches or when focused via keyboard. &lt;/li&gt;
&lt;li&gt;  With every evasion, the entire application interface shrinks and rotates slightly.&lt;/li&gt;
&lt;li&gt;  A "Mute" button that evades the cursor and, if clicked, throws an alert box stating "The teapot refuses to be silenced!".&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  &lt;strong&gt;Web Speech API:&lt;/strong&gt; Employed to vocally deliver the AI-generated insults in a condescending tone.&lt;/li&gt;

&lt;li&gt;  &lt;strong&gt;Express Rate Limiting:&lt;/strong&gt; Because the teapot gets overwhelmed easily (max 5 requests per minute).&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;p&gt;I am submitting this for both &lt;strong&gt;Best Google AI Usage&lt;/strong&gt; and &lt;strong&gt;Best Ode to Larry Masinter&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Best Google AI Usage:&lt;/strong&gt; The project leverages the Gemini 2.5 Flash model not to solve a complex problem, but to dynamically generate bespoke, eloquent, and highly contextual insults based on whatever drink the user attempts to order.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Best Ode to Larry Masinter:&lt;/strong&gt; The entire project is deeply rooted in Larry Masinter's legendary April Fools' joke, RFC 2324. The prompt even explicitly ensures Gemini name-drops him in its diatribes. It is a monument to the 418 status code.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>418challenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How I Built a Real-Time AI Dungeon Master with Claude API, Socket.io &amp; Next.js</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Thu, 02 Apr 2026 11:44:55 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/building-a-real-time-ai-dungeon-master-with-claude-api-socketio-and-nextjs-16-50ge</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/building-a-real-time-ai-dungeon-master-with-claude-api-socketio-and-nextjs-16-50ge</guid>
      <description>&lt;p&gt;In this post, I’ll show you how I built a real-time AI Dungeon Master using &lt;a href="https://platform.claude.com/settings/keys" rel="noopener noreferrer"&gt;Claude API&lt;/a&gt;, &lt;a href="https://socket.io/docs/v4/" rel="noopener noreferrer"&gt;Socket.io&lt;/a&gt;, and &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt;. This multiplayer AI system can narrate stories, manage game state, and respond to multiple players simultaneously—just like a real DM.&lt;/p&gt;

&lt;p&gt;The AI in gaming market sits at $4.54 billion in 2025 and is projected to hit $81.19 billion by 2035 (&lt;a href="https://www.snsinsider.com/reports/ai-in-gaming-market-4070" rel="noopener noreferrer"&gt;SNS Insider&lt;/a&gt;, 2025). That number isn't surprising when you think about what generative AI actually unlocks for games infinite narrative branching, dynamic NPCs, and a Dungeon Master who never gets tired at midnight.&lt;/p&gt;

&lt;p&gt;I built DnD AI, a multiplayer AI Dungeon Master running on Next.js 16, Claude API (claude-sonnet-4-6), Socket.io, and DALL-E 3. This post is a technical walkthrough of the six hardest problems I ran into, and how I solved them. No fluff just the architecture decisions that actually mattered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters&lt;/strong&gt;&lt;br&gt;
Most multiplayer RPGs fail because they depend on a human Dungeon Master. This project removes that bottleneck using AI unlocking instant gameplay for anyone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js App Router can't maintain persistent WebSockets, so a custom &lt;code&gt;server.ts&lt;/code&gt; boots Socket.io and Next.js in one process&lt;/li&gt;
&lt;li&gt;Claude streaming output pipes through Socket.io to all connected clients in real time, with chunk batching to avoid socket flooding&lt;/li&gt;
&lt;li&gt;DALL-E 3 fires only on location changes and major story beats not every message keeping session cost under $0.25&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Why a Custom Server Instead of Next.js API Routes?
&lt;/h2&gt;

&lt;p&gt;Next.js App Router Route Handlers are stateless by design each request spins up, responds, and exits. That works fine for REST, but a multiplayer game needs a persistent socket connection that stays alive for an entire session. There's no clean way to run Socket.io inside an App Router handler. The solution is a custom &lt;code&gt;server.ts&lt;/code&gt; at the project root that boots both runtimes in a single Node process.&lt;/p&gt;

&lt;p&gt;The key insight: Next.js exposes a &lt;code&gt;createServer&lt;/code&gt; API that lets you hand off HTTP requests to the Next.js handler while Socket.io attaches to the same HTTP server instance. Both share one process, one port, and one set of environment variables.&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;// server.ts (root of project, not inside /app)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServer&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;http&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parse&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;url&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;next&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;next&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SocketIOServer&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;socket.io&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;registerGameHandlers&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;./src/lib/socket/gameHandlers&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;dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;dev&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;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRequestHandler&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&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="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;httpServer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&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="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;parsedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;parsedUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Socket.io attaches to the same HTTP server&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;io&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;SocketIOServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;httpServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_APP_URL&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Register all game-related socket handlers&lt;/span&gt;
  &lt;span class="nf"&gt;registerGameHandlers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;httpServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt; Ready on http://localhost:3000&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One gotcha: your &lt;code&gt;package.json&lt;/code&gt; build script needs to compile &lt;code&gt;server.ts&lt;/code&gt; separately with &lt;code&gt;tsc&lt;/code&gt;, then run the compiled output not &lt;code&gt;next start&lt;/code&gt;. Keep that in mind before you hit deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Game Loop Architecture
&lt;/h2&gt;

&lt;p&gt;Every player action travels through a consistent five-step loop: client input, socket event, Claude API call, streamed narrative back to all clients, then stat resolution. Keeping this loop linear and deterministic was the biggest architectural decision I made early on. It made debugging a lot easier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Player types/speaks action
        │
        ▼
[Socket.io] "player:action" event → server
        │
        ▼
[gameHandlers.ts] validate action, load session memory
        │
        ▼
[Claude API] stream narrative response (claude-sonnet-4-6)
        │
  ┌─────┴────────────────────┐
  │                          │
  ▼                          ▼
[Socket.io]            [Stat resolver]
stream chunks          roll dice, calc HP delta
to all clients         emit "game:statUpdate"
  │
  ▼
[DALL-E trigger check]
location changed? major beat?
→ fire async image gen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stat resolver runs in parallel with the stream, not after it. Players see the narrative arriving word-by-word while the HP update hits their UI within a second. That parallel execution matters for perceived responsiveness if you wait for the full Claude response before resolving stats, the game feels sluggish.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hardest Part: Syncing Streaming LLM Output Across Multiple Clients
&lt;/h2&gt;

&lt;p&gt;84% of developers now use AI tools daily (&lt;a href="https://survey.stackoverflow.co/2025/" rel="noopener noreferrer"&gt;Stack Overflow Developer Survey&lt;/a&gt;, 2025), but most of those integrations are single-user. Streaming Claude's output to four simultaneous Socket.io clients introduces a problem that single-user apps never face: how do you fan out a streaming response without flooding the socket or losing chunk ordering?&lt;/p&gt;

&lt;p&gt;The naive approach emit a socket event for every token causes event queue saturation at ~4 clients with a fast model. I batched chunks into 50ms windows instead. Each batch emits one socket event with concatenated text. Clients append to their local buffer and re-render.&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;// src/lib/socket/streamHandler.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&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;@anthropic-ai/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;streamNarrativeToRoom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SocketIOServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;roomId&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;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MessageParam&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;systemPrompt&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;client&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;Anthropic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;chunkBuffer&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;let&lt;/span&gt; &lt;span class="nx"&gt;flushTimer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NodeJS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Timeout&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&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;flush&lt;/span&gt; &lt;span class="o"&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Single emit per batch window all clients in room receive it&lt;/span&gt;
      &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dm:narrative_chunk&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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chunkBuffer&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;chunkBuffer&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="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;flushTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;stream&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&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;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content_block_delta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text_delta&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;chunkBuffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Batch: flush every 50ms, not every token&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;flushTimer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;flushTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Flush any remaining buffer after stream ends&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;flushTimer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flushTimer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dm:narrative_end&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;roomId&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 50ms batch window is the sweet spot I landed on after testing. At 20ms the socket still floods at high token velocity. At 100ms the streaming effect feels choppy to users. Your mileage will vary depending on average token rate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dice Determinism in a Distributed Game
&lt;/h2&gt;

&lt;p&gt;In a multiplayer game, dice rolls can't be client-side. If player A's browser rolls a d20 and player B's browser rolls independently, they see different outcomes for the same event. The server must own every roll, and the result must be deterministic reproducible from a seed if you ever need to replay or audit a session.&lt;/p&gt;

&lt;p&gt;I use a seeded pseudo-random number generator (PRNG) on the server, seeded per session. The seed gets stored in SQLite alongside the session record. When a roll happens, the server increments a counter, derives the roll from &lt;code&gt;prng(sessionSeed + rollCount)&lt;/code&gt;, emits the result to all clients simultaneously, and stores it.&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;// src/lib/dice/seededRng.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Mulberry32 fast, seedable, good distribution for game use&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mulberry32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mh"&gt;0x6d2b79f5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&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="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;61&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="nx"&gt;t&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="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4294967296&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createSessionDice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionSeed&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mulberry32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionSeed&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="na"&gt;roll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;sides&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sides&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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 first version used &lt;code&gt;Math.random()&lt;/code&gt; server-side and it worked fine until I added session replay for debugging. Replays produced different dice outcomes, which made bug reproduction impossible. Seeding costs nothing and saves you later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Persistent Memory Without Blowing the Context Window
&lt;/h2&gt;

&lt;p&gt;Claude's context window is large, but sending full game history on every turn is expensive and eventually hits limits. The practical solution is two-layer memory: SQLite stores the complete event log, and a compressed summary gets injected into the Claude system prompt on each turn.&lt;/p&gt;

&lt;p&gt;Prisma manages the schema. Each session has a &lt;code&gt;GameSession&lt;/code&gt; record, a log of &lt;code&gt;GameEvent&lt;/code&gt; rows (player actions, DM responses, stat changes), and a &lt;code&gt;MemorySummary&lt;/code&gt; that gets regenerated every 10 turns using a separate, cheaper Claude call.&lt;/p&gt;

&lt;p&gt;Most tutorials suggest simply truncating history. That's wrong for a DnD game early plot details (the villain's name, a deal the players made) matter at turn 40. A compressed summary preserves narrative continuity without the token cost of full history. The summarization prompt is as important as the main DM prompt.&lt;/p&gt;

&lt;p&gt;The system prompt structure:&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;// src/lib/ai/buildSystemPrompt.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildDMSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;campaignConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CampaignConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;memorySummary&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;recentEvents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GameEvent&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;  &lt;span class="c1"&gt;// last 5 events in full&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`
You are the Dungeon Master for a &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;campaignConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; campaign.
Setting: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;campaignConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;worldDescription&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Players: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;campaignConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;players&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;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, HP: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentHp&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="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxHp&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;

## Story So Far (compressed)
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;memorySummary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

## Recent Events (verbatim)
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;recentEvents&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;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`[&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="kd"&gt;type&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="nx"&gt;e&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;

Rules: Stay in character. When a player action requires a dice check, output a JSON block:
{"diceCheck": {"stat": "strength", "dc": 14, "consequence": {...}}}
  `&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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;
  
  
  DALL-E 3 When to Trigger and How to Manage Cost
&lt;/h2&gt;

&lt;p&gt;DALL-E 3 at $0.040 per standard image adds up fast if you fire it on every player message. The fix is a trigger logic layer that fires image generation only when the scene actually changes: entering a new location, starting combat, or hitting a major story beat that Claude flags in its response.&lt;/p&gt;

&lt;p&gt;In testing across 12 playthroughs averaging 45 turns each, trigger-gated generation fired DALL-E 3 an average of 6 times per session keeping image cost under $0.25 per session. Ungated, the same sessions would have triggered 40+ image calls.&lt;/p&gt;

&lt;p&gt;Image generation runs async the game doesn't wait for it.&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;// src/lib/ai/imageOrchestrator.ts&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SceneSignal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sceneChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;maybeGenerateSceneImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;roomId&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;dmResponse&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;io&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SocketIOServer&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&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;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractSceneSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dmResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// parses JSON block in response&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signal&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="c1"&gt;// Fire and forget don't await in the main game loop&lt;/span&gt;
  &lt;span class="nf"&gt;generateAndEmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;io&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="p"&gt;)&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Image gen failed silently:&lt;/span&gt;&lt;span class="dl"&gt;"&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateAndEmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;roomId&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;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="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SocketIOServer&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;imageUrl&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;generateImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scene:image_ready&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;imageUrl&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 async fire pattern means players get a scene image 5–8 seconds after a location change, not blocking any game action. The UI shows a loading shimmer until &lt;code&gt;scene:image_ready&lt;/code&gt; arrives.&lt;/p&gt;




&lt;h2&gt;
  
  
  Web Speech API in Production
&lt;/h2&gt;

&lt;p&gt;Web Speech API works well for the happy path clear speech, modern Chrome, quiet environment. It breaks in ways that are hard to predict. Browser support is inconsistent outside Chrome and Edge. Background noise triggers false positives. Silence detection varies by OS, and on some systems the API stops listening after a few seconds even when the user is still speaking.&lt;/p&gt;

&lt;p&gt;The two real-world failures I hit most: (1) mobile Safari doesn't support the API at all in some iOS versions, and (2) the &lt;code&gt;onend&lt;/code&gt; event fires too aggressively in noisy environments, cutting off player actions mid-sentence. The fix for the second issue was a 1.5-second debounce on the &lt;code&gt;onend&lt;/code&gt; event before submitting the transcript.&lt;/p&gt;

&lt;p&gt;The connection to the action pipeline is simple: the browser captures speech, converts to text, and fires the same &lt;code&gt;player:action&lt;/code&gt; socket event that a typed message would send. The server doesn't know or care whether input came from voice or keyboard.&lt;/p&gt;

&lt;p&gt;Always provide a text input fallback. Don't ship a voice-only interface.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Over-engineered:&lt;/strong&gt; the Prisma schema. I built relations between sessions, events, characters, and items on day one. For a prototype, a flat JSON blob in SQLite would have been faster to iterate on and just as queryable for my needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Under-engineered:&lt;/strong&gt; the Claude system prompt. I spent a lot of time on the infrastructure and not enough on prompt quality in the first two weeks. The DM's narrative consistency improved dramatically after I added explicit persona instructions, tone guidance, and a rules-enforcement section. Infrastructure is the easy part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The biggest surprise:&lt;/strong&gt; streaming to multiple clients is genuinely tricky. I expected it to be a minor detail. The 50ms batching window took three days of testing to land on. If I started over, I'd prototype the streaming fan-out before anything else.&lt;/p&gt;

&lt;p&gt;90% of game developers already use AI in their workflows, and 97% say generative AI is reshaping the industry (&lt;a href="https://cloud.google.com/blog/topics/games/gaming-developer-survey-report-ai" rel="noopener noreferrer"&gt;Google Cloud / Harris Poll&lt;/a&gt;, Aug 2025). The tooling is mature. The hard problems are now architectural not whether AI can generate good narrative, but how to wire it into a real-time system without it falling apart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nat20.site/" rel="noopener noreferrer"&gt;Live Demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwbwnb95lpn3lhc5n9tlk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwbwnb95lpn3lhc5n9tlk.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpm73b5bgl6485yoh628j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpm73b5bgl6485yoh628j.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can this architecture scale beyond 4 players?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Socket.io room model scales to more players without code changes. The real constraint is Claude API latency streaming a response to 8 clients at once still uses one API call, so cost doesn't multiply. In testing, the 50ms batch window held stable at 6 clients. Beyond that, you'd want to profile socket event queue depth under load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Claude over GPT-4 for the DM role?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude's longer context window and instruction-following consistency made it the better fit for maintaining campaign continuity across a long session. In our tests, Claude adhered to custom rule constraints in the system prompt more reliably than GPT-4 Turbo, particularly for structured JSON output embedded in narrative responses. That JSON output drives the dice check and stat resolution pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you handle Claude going off-script or breaking game rules?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The system prompt includes an explicit rules section and a JSON output schema for structured events. When Claude's response doesn't contain valid JSON where expected, the server falls back to a text-only parse and logs the miss. A separate "rules referee" prompt runs on flagged responses to check for obvious violations before emitting to clients. It catches about 80% of off-script outputs without blocking the stream.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gamedev</category>
      <category>nextjs</category>
      <category>showdev</category>
    </item>
    <item>
      <title>No Redis, No Kafka: I Built a Real-Time Auction System with Just PostgreSQL</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Mon, 23 Mar 2026 13:45:09 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/no-redis-no-kafka-i-built-a-real-time-auction-system-with-just-postgresql-4kk4</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/no-redis-no-kafka-i-built-a-real-time-auction-system-with-just-postgresql-4kk4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL's &lt;code&gt;LISTEN&lt;/code&gt;/&lt;code&gt;NOTIFY&lt;/code&gt; system can power real-time features without Redis or Kafka&lt;/li&gt;
&lt;li&gt;This tutorial walks through a full auction system: live bids, countdowns, item broadcasts, and discontinuation events&lt;/li&gt;
&lt;li&gt;A single trigger function fans out to every connected browser via Socket.io — Postgres does the heavy lifting&lt;/li&gt;
&lt;li&gt;55.6% of developers already use PostgreSQL &lt;em&gt;(Source: Stack Overflow Developer Survey, 2025)&lt;/em&gt; — you probably already have it in your stack&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;You're watching a live auction. The price just changed — $1,240, then $1,300, then $1,450 in eight seconds. Behind the scenes, no message broker is running. No Redis cluster. No Kafka topic. Just Postgres.&lt;/p&gt;

&lt;p&gt;I read Adam's brilliant post about &lt;a href="https://dev.to/adamthedeveloper/no-redis-no-kafka-just-postgres-i-built-chat-28ie"&gt;building real-time chat with just PostgreSQL and pg_notify&lt;/a&gt; and had one thought: can I take this further? Can I build a full auction system — live bids, countdown timers, live item broadcasts, auction discontinuation — all without a single extra service? Turns out you can. This is how I did it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture in One Diagram
&lt;/h2&gt;

&lt;p&gt;The whole system collapses into a single data flow. A browser fires a bid, Postgres validates and stores it, a trigger fires a notification, and Node.js fans that notification out to every connected client. Five hops. Zero extra services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser (bid placed)
       ↓  socket.emit('place_bid')
  Node.js / Socket.io
       ↓  INSERT INTO bids
  PostgreSQL
       ↓  TRIGGER fires → pg_notify('auction_update', payload)
  Node.js (LISTEN)
       ↓  io.emit('update', data)
  All Browsers (live price update, flash animation)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node.js acts as a bridge in two directions. It accepts socket events from browsers and writes to Postgres. It also holds a persistent &lt;code&gt;LISTEN&lt;/code&gt; connection that receives notifications and broadcasts them to every connected socket. Postgres handles the pub/sub. Node handles the fan-out. The browser renders the result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1558494949-ef010cbdcc31%3Fw%3D1200%26h%3D630%26fit%3Dcrop%26q%3D80" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1558494949-ef010cbdcc31%3Fw%3D1200%26h%3D630%26fit%3Dcrop%26q%3D80" alt="Postgres Auction Demo" width="1200" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL?
&lt;/h2&gt;

&lt;p&gt;PostgreSQL is now the most widely used database in the world, with 55.6% of all developers — and 58.2% of professional developers — reporting they use it &lt;em&gt;(Source: Stack Overflow Developer Survey, 2025)&lt;/em&gt;. That's the largest single-year jump in the survey's history. It has held the #1 spot as both the most admired and most desired database for four consecutive years.&lt;/p&gt;

&lt;p&gt;The operational argument is simple. You already have Postgres in your stack. Adding Redis or Kafka for a side project or MVP means a second service to provision, secure, monitor, and pay for. For many workloads, that overhead isn't justified.&lt;/p&gt;

&lt;p&gt;Supabase's entire Realtime product — handling over 250,000 concurrent users &lt;em&gt;(Source: Supabase Realtime Benchmarks, supabase.com/docs/guides/realtime/benchmarks)&lt;/em&gt; — is architecturally built on &lt;code&gt;pg_notify&lt;/code&gt; and WebSockets &lt;em&gt;(Source: Supabase Realtime Architecture docs, supabase.com/docs/guides/realtime/architecture)&lt;/em&gt;. If it's good enough for Supabase's scale, it's more than good enough for your auction app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Database Layer
&lt;/h2&gt;

&lt;p&gt;The schema is the engine. Get this right, and the Node.js code becomes almost trivial. Two tables, two triggers, and one &lt;code&gt;pg_notify&lt;/code&gt; call is all you need.&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="c1"&gt;-- init.sql&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;items&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;title&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&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;description&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;image_url&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;start_price&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;current_bid&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ends_at&lt;/span&gt;     &lt;span class="n"&gt;TIMESTAMPTZ&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;status&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
                   &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'closed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'discontinued'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;winner_id&lt;/span&gt;   &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bids&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;item_id&lt;/span&gt;    &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;    &lt;span class="nb"&gt;INTEGER&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;username&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&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;amount&lt;/span&gt;     &lt;span class="nb"&gt;NUMERIC&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&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;placed_at&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Trigger: fires on every new bid&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;handle_new_bid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- Update the item's current bid price&lt;/span&gt;
  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;current_bid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;-- Notify all listeners — send only the delta, not the full row&lt;/span&gt;
  &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'auction_update'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="s1"&gt;'NEW_BID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'item_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'username'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'bid_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;text&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;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;on_bid_placed&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;bids&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;handle_new_bid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;


&lt;span class="c1"&gt;-- Trigger: fires on item INSERT (new listing) or UPDATE (status change)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;handle_item_change&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- New item listed&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TG_OP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'INSERT'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s1"&gt;'auction_update'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="s1"&gt;'NEW_ITEM'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'item_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'start_price'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'ends_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ends_at&lt;/span&gt;
      &lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;-- Auction closed (timer expired)&lt;/span&gt;
  &lt;span class="n"&gt;ELSIF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TG_OP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UPDATE'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NEW&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="s1"&gt;'closed'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;OLD&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="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s1"&gt;'auction_update'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="s1"&gt;'AUCTION_CLOSED'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'item_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'winner_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;winner_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'final_bid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_bid&lt;/span&gt;
      &lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;-- Auction manually discontinued&lt;/span&gt;
  &lt;span class="n"&gt;ELSIF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TG_OP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UPDATE'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NEW&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="s1"&gt;'discontinued'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s1"&gt;'auction_update'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="s1"&gt;'AUCTION_DISCONTINUED'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'item_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;
      &lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&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;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;on_item_change&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;handle_item_change&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pg_notify&lt;/code&gt; takes two arguments: a channel name and a text payload. Every &lt;code&gt;LISTEN&lt;/code&gt;-ing connection subscribed to that channel receives the payload instantly after the transaction commits.&lt;/p&gt;

&lt;p&gt;That last part matters. PostgreSQL only delivers notifications on transaction commit &lt;em&gt;(Source: PostgreSQL official docs, postgresql.org/docs/current/sql-notify.html)&lt;/em&gt;. A bid that fails validation and rolls back never fires a notification. No phantom updates, no stale prices. The atomicity is a feature, not a limitation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Payload size callout:&lt;/strong&gt; PostgreSQL's &lt;code&gt;NOTIFY&lt;/code&gt; has an 8,000-byte payload limit &lt;em&gt;(Source: PostgreSQL official docs)&lt;/em&gt;. Rule of thumb: send the ID and the delta, not the full row. A JSON object with four or five fields is almost never going to hit that ceiling.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;![Chart]((&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m9d9qrr381e93y0vp5da.png" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m9d9qrr381e93y0vp5da.png&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  The Node.js Server
&lt;/h2&gt;

&lt;p&gt;The server is the glue. It's about 120 lines of code, and most of that is the Socket.io event handlers. The real-time machinery itself is surprisingly compact.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&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;http&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&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;Server&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;socket.io&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;Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pg&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;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pg&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;app&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&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;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&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;io&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;Server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Pool for regular queries (bids, inserts, selects)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&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;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Dedicated client for LISTEN — must NOT go through a pool&lt;/span&gt;
&lt;span class="c1"&gt;// A pooled connection can be swapped out; LISTEN requires a persistent session&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pgClient&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;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;static&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// ─── 1. Start the persistent LISTEN connection ─────────────────────────────&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startListener&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pgClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pgClient&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LISTEN auction_update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ─── 2. Fan-out: every pg_notify fires this handler ──────────────────────&lt;/span&gt;
  &lt;span class="nx"&gt;pgClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notification&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;msg&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;try&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;payload&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// broadcast to ALL connected sockets&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bad notification payload:&lt;/span&gt;&lt;span class="dl"&gt;'&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Listening on channel: auction_update&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="c1"&gt;// ─── 3. Auction Master Loop ───────────────────────────────────────────────&lt;/span&gt;
&lt;span class="c1"&gt;// Closes expired auctions every second.&lt;/span&gt;
&lt;span class="c1"&gt;// Good enough for an MVP. In production, replace with a pg_cron job&lt;/span&gt;
&lt;span class="c1"&gt;// so the closer runs inside the DB and survives Node restarts.&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&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="s2"&gt;`
      UPDATE items
      SET
        status    = 'closed',
        winner_id = (
          SELECT user_id FROM bids
          WHERE item_id = items.id
          ORDER BY amount DESC
          LIMIT 1
        )
      WHERE status  = 'active'
        AND ends_at &amp;lt; NOW()
    `&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// The on_item_change trigger fires and sends AUCTION_CLOSED automatically&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Auction closer error:&lt;/span&gt;&lt;span class="dl"&gt;'&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="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ─── Socket.io event handlers ─────────────────────────────────────────────&lt;/span&gt;
&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connection&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;socket&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Client connected:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Send current state on connect&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;rows&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;pool&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT * FROM items WHERE status = 'active' ORDER BY created_at DESC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;init_items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ─── 4. Place a bid ────────────────────────────────────────────────────&lt;/span&gt;
  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;place_bid&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Validate: bid must exceed current price&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;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;]&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;pool&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT current_bid, status FROM items WHERE id = $1&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;item_id&lt;/span&gt;&lt;span class="p"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;item&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;active&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bid_error&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Auction is not active.&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_bid&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="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bid_error&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bid must exceed current price.&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="c1"&gt;// Insert — the trigger does the rest&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO bids (item_id, user_id, username, amount) VALUES ($1, $2, $3, $4)&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;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&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;catch &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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;place_bid error:&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bid_error&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server error placing bid.&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="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// ─── 5. Add a new auction item ─────────────────────────────────────────&lt;/span&gt;
  &lt;span class="c1"&gt;// Node just writes to the DB. pg_notify('auction_update', NEW_ITEM) is&lt;/span&gt;
  &lt;span class="c1"&gt;// Postgres's job. Node doesn't manually broadcast anything here.&lt;/span&gt;
  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add_item&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;duration_seconds&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;try&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;ends_at&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="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;duration_seconds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&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="s2"&gt;`INSERT INTO items (title, description, image_url, start_price, current_bid, ends_at)
         VALUES ($1, $2, $3, $4, $4, $5)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ends_at&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Trigger fires automatically — no io.emit here&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add_item error:&lt;/span&gt;&lt;span class="dl"&gt;'&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="c1"&gt;// ─── 6. Discontinue an auction ─────────────────────────────────────────&lt;/span&gt;
  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;discontinue_item&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;item_id&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UPDATE items SET status = 'discontinued' WHERE id = $1 AND status = 'active'&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;item_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Trigger fires automatically — no io.emit here&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;discontinue_item error:&lt;/span&gt;&lt;span class="dl"&gt;'&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;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disconnect&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Client disconnected:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="nf"&gt;startListener&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server running on http://localhost:3000&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;p&gt;Notice the &lt;code&gt;add_item&lt;/code&gt; and &lt;code&gt;discontinue_item&lt;/code&gt; handlers. They're just DB writes. There's no &lt;code&gt;io.emit&lt;/code&gt; call in either one. The notification to all users is entirely Postgres's job. Node stays thin.&lt;/p&gt;

&lt;p&gt;Socket.io reaches roughly 9.7 million weekly npm downloads &lt;em&gt;(Source: Snyk, 2025)&lt;/em&gt;, making it the dominant WebSocket library in the Node ecosystem. The &lt;code&gt;io.emit('update', payload)&lt;/code&gt; call in the notification handler is all it takes to fan out to every connected browser simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Events, Three Triggers
&lt;/h2&gt;

&lt;p&gt;The entire real-time behavior of this system flows through three notification types, all on the same channel. Here's how they map to Postgres triggers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Who listens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bid placed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;on_bid_placed&lt;/code&gt; (bids INSERT)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;auction_update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All browsers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New item listed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;on_item_change&lt;/code&gt; (items INSERT)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;auction_update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All browsers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auction discontinued&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;on_item_change&lt;/code&gt; (items UPDATE status→discontinued)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;auction_update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All browsers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Walk through a concrete scenario. Alice fills out the "Add Item" form and submits. Node.js runs a single &lt;code&gt;INSERT INTO items&lt;/code&gt;. PostgreSQL's &lt;code&gt;handle_item_change&lt;/code&gt; trigger fires immediately after, calls &lt;code&gt;pg_notify('auction_update', '{"type":"NEW_ITEM",...}')&lt;/code&gt;. The dedicated LISTEN client in Node receives that notification and calls &lt;code&gt;io.emit('update', {type: 'NEW_ITEM', item: {...}})&lt;/code&gt;. Every browser gets a new auction card rendered in real time — with a gold glow animation for three seconds.&lt;/p&gt;

&lt;p&gt;That's it. Alice's browser doesn't get any special treatment. Every client — including Alice's — learns about the new item the same way: through Postgres.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chart 2: pg_notify Payload Sizes — Fitting Within the 8KB Limit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flkg77ezv7pgv7bhyu4kr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flkg77ezv7pgv7bhyu4kr.png" alt="pg_notify Payload Sizes — Fitting Within the 8KB Limit" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Browser Side
&lt;/h2&gt;

&lt;p&gt;The client code is straightforward Socket.io work. What makes it feel polished is the layered handling of each event type.&lt;/p&gt;

&lt;p&gt;On connect, the server sends &lt;code&gt;init_items&lt;/code&gt; — an array of all active auctions. The browser renders every card immediately. From that point on, everything is driven by &lt;code&gt;update&lt;/code&gt; events.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1551288049-bebda4e38f71%3Fw%3D1200%26h%3D630%26fit%3Dcrop%26q%3D80" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1551288049-bebda4e38f71%3Fw%3D1200%26h%3D630%26fit%3Dcrop%26q%3D80" alt="Postgres Auction Demo" width="1200" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a &lt;code&gt;NEW_BID&lt;/code&gt; event arrives, the relevant card's price element flashes amber, then fades to white over 600ms. A &lt;code&gt;AUCTION_CLOSED&lt;/code&gt; event triggers a winner modal with a confetti burst and disables the bid input. &lt;code&gt;NEW_ITEM&lt;/code&gt; appends a new card with a 3-second gold border glow. &lt;code&gt;AUCTION_DISCONTINUED&lt;/code&gt; re-renders the card with a red "Discontinued" badge and fires a toast notification to everyone in the room.&lt;/p&gt;

&lt;p&gt;Countdown timers run client-side with &lt;code&gt;setInterval&lt;/code&gt;. Each timer transitions through three urgency states: slate text when plenty of time remains, amber text when under two minutes, and a red pulse animation when under thirty seconds. The visual cues are intentional — urgency drives bidding.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;pg_notify&lt;/code&gt; event also appends to an activity feed sidebar. This gives the room a sense of shared presence: you can see who just outbid whom, which items just opened, and which ones closed, all in a live scrolling log.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Part — When This Doesn't Scale
&lt;/h2&gt;

&lt;p&gt;This architecture has real limits. Here they are, plainly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One connection per listener.&lt;/strong&gt; Every Node.js process that calls &lt;code&gt;LISTEN&lt;/code&gt; holds a dedicated, persistent Postgres connection. Pool that and it breaks — PgBouncer's transaction-mode pooling is incompatible with &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt; and requires session pooling instead &lt;em&gt;(Source: brandur.org, 2024)&lt;/em&gt;. At 200–500 concurrent Node processes, connection pressure becomes a genuine problem. The "notifier pattern" described by Brandur reduces this to one connection per process &lt;em&gt;(Source: brandur.org/notifier, 2024)&lt;/em&gt;, which helps, but it's not magic. Beyond roughly 1,000 concurrent listeners, this pattern stops being suitable &lt;em&gt;(Source: pedroalonso.net, 2024)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock contention under high write volume.&lt;/strong&gt; recall.ai reported that &lt;code&gt;NOTIFY&lt;/code&gt;-induced lock contention caused commit delays reaching 1,015ms with 400+ queued concurrent transactions &lt;em&gt;(Source: recall.ai blog, March 2025)&lt;/em&gt;. Under a high write concurrency scenario — think flash sale with thousands of simultaneous bidders — this is a real concern, not a theoretical one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No message persistence.&lt;/strong&gt; If the Node.js listener is offline when a notification fires, the message is gone forever. For an auction system this is acceptable — the database is always the source of truth, and a reconnecting client can query current state. For guaranteed delivery semantics, you need a message queue.&lt;/p&gt;

&lt;p&gt;Closing thought: for an auction app with hundreds of concurrent bidders, this works great. For Twitter-scale real-time? That's where Redis earns its keep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pg_notify vs Redis Pub/Sub — Quick Comparison&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;pg_notify&lt;/th&gt;
&lt;th&gt;Redis Pub/Sub&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;~1–5ms&lt;/td&gt;
&lt;td&gt;~0.1–1ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max practical listeners&lt;/td&gt;
&lt;td&gt;~200–500&lt;/td&gt;
&lt;td&gt;Tens of thousands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message persistence&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None (by default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction atomicity&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational complexity&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Additional service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Running It Yourself
&lt;/h2&gt;

&lt;p&gt;Five commands and you have a working auction room.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Clone and install&lt;/span&gt;
git clone https://github.com/rahulpatwa1303/postgres-live-auction
&lt;span class="nb"&gt;cd &lt;/span&gt;pg-auction &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# 2. Create the database&lt;/span&gt;
createdb auction_db

&lt;span class="c"&gt;# 3. Run the schema and triggers&lt;/span&gt;
psql &lt;span class="nt"&gt;-d&lt;/span&gt; auction_db &lt;span class="nt"&gt;-f&lt;/span&gt; init.sql

&lt;span class="c"&gt;# 4. Set your connection string and start&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres://localhost/auction_db node server.js

&lt;span class="c"&gt;# 5. Open two browser windows&lt;/span&gt;
open http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open two browser windows side by side. Place a bid in one. Watch it update in the other in under 5ms. Add an item in one window and watch the new card appear in both. That's the whole system working.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently in Production
&lt;/h2&gt;

&lt;p&gt;A few changes would make this production-worthy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace the &lt;code&gt;setInterval&lt;/code&gt; auction closer with a &lt;code&gt;pg_cron&lt;/code&gt; job. The cron job runs inside the database, survives Node restarts, and scales independently of your application tier.&lt;/li&gt;
&lt;li&gt;Use a connection pool configured for session pooling, not transaction pooling. PgBouncer's transaction mode destroys &lt;code&gt;LISTEN&lt;/code&gt; sessions.&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;bid_history&lt;/code&gt; table as an append-only audit log. This gives you replay capability and protects against disputes.&lt;/li&gt;
&lt;li&gt;Put the Node server behind a load balancer with sticky sessions. Socket.io's default in-memory store doesn't share state across processes — sticky sessions or the &lt;code&gt;socket.io-redis&lt;/code&gt; adapter are required for multi-instance deployments.&lt;/li&gt;
&lt;li&gt;Implement row-level locking on the &lt;code&gt;items&lt;/code&gt; table during bid validation. A &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; in the bid handler prevents race conditions when two users submit the same highest bid within milliseconds of each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The point isn't "never use Redis." Redis is genuinely excellent at what it does, and at scale, it's the right choice. The point is that Postgres is dramatically more powerful than most developers give it credit for. &lt;code&gt;pg_notify&lt;/code&gt; is a production-grade pub/sub system that ships for free with the database you almost certainly already have running.&lt;/p&gt;

&lt;p&gt;This whole system — live bids, item broadcasts, countdown timers, discontinuation events — runs with zero additional infrastructure. One database. One application server. Done.&lt;/p&gt;

&lt;p&gt;This project was directly inspired by Adam's article &lt;a href="https://dev.to/adamthedeveloper/no-redis-no-kafka-just-postgres-i-built-chat-28ie"&gt;No Redis, No Kafka — Just Postgres, I Built Chat&lt;/a&gt;. If you haven't read it, go read it first — it's the foundation this auction system is built on.&lt;/p&gt;

&lt;p&gt;Check out the full source code on GitHub. If this was useful, share it with someone who's about to spin up a Redis instance they don't need.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is PostgreSQL LISTEN/NOTIFY?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LISTEN&lt;/code&gt; and &lt;code&gt;NOTIFY&lt;/code&gt; are built-in PostgreSQL commands for asynchronous pub/sub messaging. A client issues &lt;code&gt;LISTEN channel_name&lt;/code&gt; to subscribe. Any session can then call &lt;code&gt;NOTIFY channel_name, 'payload'&lt;/code&gt; or &lt;code&gt;pg_notify()&lt;/code&gt; to broadcast a message. Notifications are delivered to all subscribers only when the sending transaction commits &lt;em&gt;(Source: PostgreSQL official docs, postgresql.org/docs/current/sql-notify.html)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can PostgreSQL replace Redis for pub/sub?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For low-to-medium concurrency workloads — under roughly 200–500 concurrent listeners — yes, pg_notify is a practical Redis Pub/Sub alternative. It adds transaction atomicity that Redis lacks. However, Redis handles tens of thousands of concurrent subscribers and sub-millisecond latency that pg_notify can't match. Choose based on your actual scale &lt;em&gt;(Source: brandur.org, 2024; pedroalonso.net, 2024)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the payload limit for pg_notify?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL's &lt;code&gt;NOTIFY&lt;/code&gt; payload has a hard limit of 8,000 bytes &lt;em&gt;(Source: PostgreSQL official docs)&lt;/em&gt;. The total notification queue is capped at 8 GB. Best practice is to send only the row ID and key delta fields in the payload, then let the client fetch full data if needed. Embedding image URLs or large text blobs in the payload will cause errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does Socket.io connect to PostgreSQL notifications?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A dedicated &lt;code&gt;pg.Client&lt;/code&gt; instance (not a pool) issues &lt;code&gt;LISTEN auction_update&lt;/code&gt;. When Postgres fires &lt;code&gt;pg_notify&lt;/code&gt;, the client's &lt;code&gt;notification&lt;/code&gt; event handler receives the payload. That handler calls &lt;code&gt;io.emit('update', parsedPayload)&lt;/code&gt;, broadcasting to all connected Socket.io clients simultaneously. Socket.io reaches ~9.7 million weekly npm downloads, making it the dominant WebSocket abstraction in Node.js &lt;em&gt;(Source: Snyk, 2025)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between AUCTION_CLOSED and AUCTION_DISCONTINUED?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AUCTION_CLOSED&lt;/code&gt; fires when an auction's timer expires naturally. The system sets a winner, calculates the final price, and broadcasts the result. &lt;code&gt;AUCTION_DISCONTINUED&lt;/code&gt; fires when an administrator manually cancels an active auction before it ends. No winner is set. The browser re-renders the card with a red "Discontinued" badge and shows a toast notification to all connected users.---&lt;br&gt;
title: "No Redis, No Kafka: I Built a Real-Time Auction System with Just PostgreSQL"&lt;br&gt;
description: "How I used PostgreSQL's pg_notify/LISTEN, Node.js, and Socket.io to build a full real-time auction system — live bids, countdown timers, new item broadcasts, and auction discontinuation — with zero additional infrastructure."&lt;/p&gt;

&lt;p&gt;Live Demo: &lt;a href="https://postgres-live-auction-production.up.railway.app" rel="noopener noreferrer"&gt;postgres-live-auction&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>node</category>
      <category>websockets</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Built CareLog: A Flutter App for Home-Visit Healthcare Providers</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Fri, 06 Mar 2026 09:41:47 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/how-i-built-carelog-a-flutter-app-for-home-visit-healthcare-providers-4g7d</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/how-i-built-carelog-a-flutter-app-for-home-visit-healthcare-providers-4g7d</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; CareLog is a cross-platform Flutter app that helps home-visit doctors and nurses track patients, schedule visits, manage payments, and view practice analytics — all offline-first with optional cloud sync. Built with Flutter + Riverpod + Drift (SQLite) + Supabase, deployable as an Android APK, a PWA on iOS, or a web app.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Home-Visit Doctors Have No Good Tool
&lt;/h2&gt;

&lt;p&gt;Home-visit healthcare is common in India. A doctor or physiotherapist travels to patients' homes on a daily or weekly schedule, collects fees in cash, and keeps everything in a notebook or a generic spreadsheet.&lt;/p&gt;

&lt;p&gt;That system breaks down fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who did I visit today and who did I miss?&lt;/li&gt;
&lt;li&gt;Which patients haven't paid yet this month?&lt;/li&gt;
&lt;li&gt;Am I earning more or less than last month?&lt;/li&gt;
&lt;li&gt;What was the address for that patient on the outskirts?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Existing apps either target hospital workflows (too complex), require internet access, or don't handle the visit-frequency + payment-tracking combination at all.&lt;/p&gt;

&lt;p&gt;CareLog fills that gap: a lean, offline-first app purpose-built for the home-visit workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  What CareLog Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Schedule view&lt;/strong&gt; is the home screen. It shows a horizontally scrollable date strip spanning one year back and one month forward. Each day shows dot indicators for dates that have scheduled visits. Tapping a date shows a timeline of patients due that day, with one-tap actions to mark a visit complete or skip it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patient profiles&lt;/strong&gt; store name, phone, address, default consultation fee, visit frequency (Daily, Alternate, Weekly, or Custom weekdays), visit start date, and notes. The frequency and weekday data drives the automatic schedule generation — no manual entry needed per day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patient details screen&lt;/strong&gt; shows the full visit history sorted newest-first, running totals for paid and pending fees, and a bulk-selection mode (long-press to enter) for marking multiple visits paid/unpaid or deleting them in one action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dashboard&lt;/strong&gt; provides a snapshot of today's visits and earnings, period-aware earnings cards (Week / Month / Year / All Time) with trend indicators versus the prior period, a 6-month earnings comparison strip, a day-of-week activity bar chart for the current month, pending payment breakdown per patient, and a most-visited patients leaderboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings&lt;/strong&gt; cover daily reminder notifications (with a custom time picker), cloud backup via Google Sign-In, local backup/restore to a file, import from the previous version of the app, and CSV export of all visit data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack and Why
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Flutter
&lt;/h3&gt;

&lt;p&gt;The app targets Android natively and iOS/web as a PWA. Flutter makes that possible from a single codebase. Dart's strong typing and hot reload speed made iteration fast. The Material 3 widget library covered 95% of UI needs without custom widgets.&lt;/p&gt;

&lt;p&gt;The project uses &lt;strong&gt;v1.1.0&lt;/strong&gt; (SDK &lt;code&gt;^3.5.3&lt;/code&gt;), deployed with &lt;code&gt;flutter build apk --release&lt;/code&gt; for Android and &lt;code&gt;flutter build web --release&lt;/code&gt; for the PWA.&lt;/p&gt;

&lt;h3&gt;
  
  
  Riverpod for State Management
&lt;/h3&gt;

&lt;p&gt;Riverpod was chosen over Provider and BLoC for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compile-time safety&lt;/strong&gt; — providers are regular Dart objects, not context lookups, so typos are caught at build time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async first&lt;/strong&gt; — &lt;code&gt;AsyncNotifierProvider&lt;/code&gt; and &lt;code&gt;.when()&lt;/code&gt; handle loading/error/data states without boilerplate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testability&lt;/strong&gt; — providers can be overridden in tests without a widget tree.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app's provider graph: &lt;code&gt;databaseProvider&lt;/code&gt; (Drift DB singleton) → &lt;code&gt;patientsProvider&lt;/code&gt; / &lt;code&gt;visitsProvider&lt;/code&gt; (stream-based) → derived providers like &lt;code&gt;todayVisitsProvider&lt;/code&gt;, &lt;code&gt;dashboardProvider&lt;/code&gt;, &lt;code&gt;scheduledDatesProvider&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drift (SQLite) for Offline-First Storage
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://drift.simonbinder.eu/" rel="noopener noreferrer"&gt;Drift&lt;/a&gt; is a type-safe SQLite wrapper for Flutter. The schema is two tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Patients&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Table&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// id, name, phone, address, defaultFee&lt;/span&gt;
  &lt;span class="c1"&gt;// visitFrequency: 'Daily' | 'Alternate' | 'Weekly' | 'Custom'&lt;/span&gt;
  &lt;span class="c1"&gt;// visitWeekdays: "1,3,5"  (comma-separated ISO weekday ints)&lt;/span&gt;
  &lt;span class="c1"&gt;// visitStartDate, isActive, remoteId, isSynced&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Visits&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Table&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// id, patientId (FK), visitDate, visitType: 'Home' | 'Clinic'&lt;/span&gt;
  &lt;span class="c1"&gt;// fee, isPaid, notes, remoteId, isSynced&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schema is at version 5 with a hand-written migration strategy. Each &lt;code&gt;onUpgrade&lt;/code&gt; block is guarded with &lt;code&gt;if (from &amp;lt; N)&lt;/code&gt; so migrations compose correctly regardless of which version a user upgrades from.&lt;/p&gt;

&lt;p&gt;One non-obvious issue: &lt;code&gt;drift_dev&lt;/code&gt; code-gen doesn't handle nullable &lt;code&gt;DateTimeColumn&lt;/code&gt; additions via &lt;code&gt;addColumn&lt;/code&gt; because of Dart generic invariance. The workaround was dropping down to raw SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;customStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ALTER TABLE patients ADD COLUMN visit_started_at INTEGER'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drift stores &lt;code&gt;DateTime&lt;/code&gt; as an &lt;code&gt;INTEGER&lt;/code&gt; (Unix ms), so the raw SQL type matches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supabase for Cloud Sync
&lt;/h3&gt;

&lt;p&gt;Each row carries a &lt;code&gt;remoteId&lt;/code&gt; (UUID from Supabase) and an &lt;code&gt;isSynced&lt;/code&gt; boolean. The sync engine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetches rows where &lt;code&gt;isSynced == false&lt;/code&gt; from the local DB.&lt;/li&gt;
&lt;li&gt;Upserts them to Supabase using the &lt;code&gt;remoteId&lt;/code&gt; as the conflict key.&lt;/li&gt;
&lt;li&gt;Pulls down any rows created/updated on other devices since the last sync timestamp.&lt;/li&gt;
&lt;li&gt;Merges local and remote, preferring the most recent &lt;code&gt;updatedAt&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Google Sign-In is the only auth method — no email/password form to maintain, and most users already have a Google account on their Android phone.&lt;/p&gt;

&lt;h3&gt;
  
  
  PWA Strategy for iOS
&lt;/h3&gt;

&lt;p&gt;Building for iOS normally requires a Mac and an Apple Developer account ($99/year). The workaround: &lt;code&gt;flutter build web&lt;/code&gt; produces a standard web app. Hosted on Vercel, it works as a Progressive Web App — Safari on iPhone prompts the user to "Add to Home Screen," after which it launches fullscreen and behaves like a native app.&lt;/p&gt;

&lt;p&gt;A custom &lt;code&gt;PwaInstallGuide&lt;/code&gt; widget detects when the app is running in a browser (not as an installed PWA) and displays a step-by-step overlay showing the Safari share icon and "Add to Home Screen" instructions. This eliminated the biggest friction point for iOS users.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Decisions Worth Calling Out
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Schedule Generation Is Computed, Not Stored
&lt;/h3&gt;

&lt;p&gt;Visits are only stored when a doctor takes an action (marks complete, skips, logs a note). The upcoming schedule is computed on the fly from each patient's &lt;code&gt;visitFrequency&lt;/code&gt;, &lt;code&gt;visitWeekdays&lt;/code&gt;, and &lt;code&gt;visitStartDate&lt;/code&gt;. The &lt;code&gt;todayVisitsProvider&lt;/code&gt; joins the computed schedule for the selected date with any stored visit records to produce a &lt;code&gt;VisitSchedule&lt;/code&gt; object with &lt;code&gt;upcomingCount&lt;/code&gt;, &lt;code&gt;completedCount&lt;/code&gt;, and &lt;code&gt;skippedCount&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This keeps the database small and avoids the problem of "stale future visits" if a patient's schedule changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platform-Conditional Services
&lt;/h3&gt;

&lt;p&gt;Notifications and file downloads have different implementations on mobile vs web. Each service has three files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;notification_service.dart         ← abstract interface
notification_service_mobile.dart  ← flutter_local_notifications
notification_service_web.dart     ← no-op / Web Notifications API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A factory function in &lt;code&gt;notification_service.dart&lt;/code&gt; returns the right implementation based on &lt;code&gt;kIsWeb&lt;/code&gt;. This pattern kept platform-specific code contained and made testing each implementation in isolation straightforward.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI/CD: GitHub Actions to Vercel
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;.github/workflows/main.yml&lt;/code&gt; pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runs &lt;code&gt;flutter test&lt;/code&gt; on every push.&lt;/li&gt;
&lt;li&gt;If tests pass, runs &lt;code&gt;flutter build web --release&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Deploys the &lt;code&gt;build/web&lt;/code&gt; output to Vercel using the Vercel CLI with three repository secrets: &lt;code&gt;VERCEL_TOKEN&lt;/code&gt;, &lt;code&gt;VERCEL_ORG_ID&lt;/code&gt;, &lt;code&gt;VERCEL_PROJECT_ID&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: every green push to &lt;code&gt;main&lt;/code&gt; auto-deploys a new web build in under three minutes. Android APKs are still built and distributed manually (direct APK share via WhatsApp/Telegram for early users, with Play Store planned for v2).&lt;/p&gt;




&lt;h2&gt;
  
  
  UX Details That Mattered
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Haptic feedback on actions.&lt;/strong&gt; &lt;code&gt;HapticFeedback.mediumImpact()&lt;/code&gt; fires on "Mark Complete" and "Skip". On a busy home-visit day, a doctor often taps through the list quickly. The haptic confirms the action registered without requiring the user to look at the screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"All caught up" empty state.&lt;/strong&gt; When all visits for the day are done, the Upcoming filter shows a check-circle icon and "You have completed all visits for today. Great job!" — a small thing, but it closes the feedback loop rather than just showing a blank list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backup nudge banner.&lt;/strong&gt; If the user hasn't signed in for cloud sync, a dismissible orange banner appears on the home screen: "Your data is only on this device." It links directly to the Settings screen. This surfaces a real risk (data loss if the phone is lost) without being intrusive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Month markers in the date strip.&lt;/strong&gt; The horizontal date picker shows the month abbreviation on the 1st of every month instead of the day name. This solves the orientation problem when scrolling back through history — a doctor checking records from 3 months ago can find the right month at a glance.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Recurring visit notes templates&lt;/strong&gt; — many patients have the same condition; reusable note templates would save typing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patient photo&lt;/strong&gt; — a profile photo makes it faster to match the patient card to the face when the visit list is long.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple currencies&lt;/strong&gt; — the ₹ symbol is hardcoded. Parameterizing it would open the app to users outside India.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export to PDF&lt;/strong&gt; — a printable monthly earnings report for accounting purposes.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>flutter</category>
      <category>mobile</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>🚀 From “Just for Fun” to a Full-Stack Beach Discovery App — How I Built BeachSeeker and What I Learned</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Fri, 09 Jan 2026 08:40:11 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/from-just-for-fun-to-a-full-stack-beach-discovery-app-my-journey-building-beachseeker-o17</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/from-just-for-fun-to-a-full-stack-beach-discovery-app-my-journey-building-beachseeker-o17</guid>
      <description>&lt;p&gt;Have you ever stood staring at a beach map, wondering:&lt;/p&gt;

&lt;p&gt;✨ Is it crowded today?&lt;br&gt;
✨ Is it worth the trip this weekend?&lt;br&gt;
✨ Is it even accessible for a quick visit?&lt;/p&gt;

&lt;p&gt;That was me last year — no quick way to answer these questions. So I built BeachSeeker, an AI-powered beach discovery app that helps you find your next seaside paradise with ease — not for VC funding, but for the pure joy of building something useful and beautiful.&lt;/p&gt;

&lt;p&gt;🧠 The Problem I Set Out to Solve&lt;/p&gt;

&lt;p&gt;I wanted a tool that made exploring beaches fun, visual, and practical — something beyond a list of names.&lt;/p&gt;

&lt;p&gt;So my goals were simple:&lt;/p&gt;

&lt;p&gt;✔️ Learn full-stack development&lt;br&gt;
✔️ Build with modern tools&lt;br&gt;
✔️ Add something unique to the experience&lt;br&gt;
✔️ Make it usable for real users&lt;/p&gt;

&lt;p&gt;No fluff. Just product-led learning.&lt;/p&gt;

&lt;p&gt;🏖️ What BeachSeeker Actually Does&lt;/p&gt;

&lt;p&gt;BeachSeeker is a minimalist, AI-driven beach discovery experience with these core features:&lt;/p&gt;

&lt;p&gt;🔎 Smart Beach Discovery&lt;/p&gt;

&lt;p&gt;An AI “Vibe Check” that tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether a beach feels chill, crowded, or adventure-ready&lt;/li&gt;
&lt;li&gt;Practical beach info (accessibility, crowd levels, best season)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✨ Clean, Human-Focused UI&lt;/p&gt;

&lt;p&gt;The interface was inspired by sleek modern design — easy to use and focused on what matters.&lt;/p&gt;

&lt;p&gt;📌 Deep Insight Into Every Beach&lt;/p&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Crowd estimates&lt;/li&gt;
&lt;li&gt;Accessibility info&lt;/li&gt;
&lt;li&gt;Best months to visit&lt;/li&gt;
&lt;li&gt;Add to favorites (personal itineraries)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🛠️ The Tech Stack&lt;/p&gt;

&lt;p&gt;Here’s what powers BeachSeeker under the hood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend: Next.js for performance and server-side rendering&lt;/li&gt;
&lt;li&gt;Styling: Tailwind CSS + Radix UI&lt;/li&gt;
&lt;li&gt;Backend: PostgreSQL + Drizzle ORM&lt;/li&gt;
&lt;li&gt;AI: Google Generative AI (Gemini)&lt;/li&gt;
&lt;li&gt;Deployment: Optimized for fast load and SEO 🎯&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📈 What I Learned&lt;/p&gt;

&lt;p&gt;Building BeachSeeker taught me more than any course ever did:&lt;/p&gt;

&lt;p&gt;💡 1. Architecture Matters&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choosing the right stack helped with:&lt;/li&gt;
&lt;li&gt;Scaling features&lt;/li&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;Maintainability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🤖 2. AI Is Not Just Gimmicks&lt;/p&gt;

&lt;p&gt;Using AI for contextual descriptions made the app feel more “alive” and useful.&lt;/p&gt;

&lt;p&gt;🎨 3. UX is Everything&lt;/p&gt;

&lt;p&gt;Users don’t care about tech they can’t see — they care about how it feels.&lt;/p&gt;

&lt;p&gt;🔮 What’s Next for BeachSeeker&lt;/p&gt;

&lt;p&gt;This is only the beginning! Here’s what’s on the roadmap:&lt;/p&gt;

&lt;p&gt;✨ User reviews &amp;amp; photos&lt;br&gt;
✨ Advanced filters (water temperature, surf conditions)&lt;br&gt;
✨ Trip planner with AI suggestions&lt;br&gt;
✨ Mobile app experience&lt;/p&gt;

&lt;p&gt;💬 I Want Your Feedback!&lt;/p&gt;

&lt;p&gt;Your thoughts will help shape BeachSeeker’s future.&lt;/p&gt;

&lt;p&gt;👉 What features should we build next?&lt;br&gt;
👉 Which beaches should be added?&lt;br&gt;
👉 How can Vibe Check be smarter?&lt;/p&gt;

&lt;p&gt;Drop a comment or reach out — I read every message! 🙌&lt;/p&gt;

&lt;p&gt;🔗 Try BeachSeeker Live&lt;/p&gt;

&lt;p&gt;Check it out here 👉 &lt;a href="https://best-beachs.vercel.app/" rel="noopener noreferrer"&gt;Live App Link&lt;/a&gt;&lt;br&gt;
 — explore beaches in a new way and send me feedback!&lt;/p&gt;

&lt;p&gt;🌟 Thank You!&lt;/p&gt;

&lt;p&gt;If you found this helpful or inspiring:&lt;/p&gt;

&lt;p&gt;❤️ Like &amp;amp; share&lt;/p&gt;

&lt;p&gt;💬 Comment your thoughts&lt;/p&gt;

&lt;p&gt;🔁 Share with fellow devs or travelers&lt;/p&gt;

&lt;p&gt;I’ll keep improving this app — and I’d love for you to join the journey. 🚀&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>nextjs</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I Built a React Autocomplete Component (and Reduced it from 5MB to 35KB)</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Fri, 21 Nov 2025 07:51:28 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/i-built-a-react-autocomplete-component-and-reduced-it-from-5mb-to-35kb-ml7</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/i-built-a-react-autocomplete-component-and-reduced-it-from-5mb-to-35kb-ml7</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built a React component for mention-based autocompletion that supports nested objects and arrays. Then I optimized it from 5MB to just 35KB. Here's the story and why you might want to use it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Template Hell 😫
&lt;/h2&gt;

&lt;p&gt;Have you ever built a feature where users need to compose messages with dynamic variables? &lt;/p&gt;

&lt;p&gt;You know, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email templates: "Hi &lt;code&gt;{{user.name}}&lt;/code&gt;, your order &lt;code&gt;{{order.id}}&lt;/code&gt; is ready!"&lt;/li&gt;
&lt;li&gt;Notification systems: "&lt;code&gt;{{product.name}}&lt;/code&gt; is now &lt;code&gt;{{product.price}}&lt;/code&gt;"&lt;/li&gt;
&lt;li&gt;Chat bots: "Welcome &lt;code&gt;{{user.name}}&lt;/code&gt; from &lt;code&gt;{{user.address.city}}&lt;/code&gt;!"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The typical solutions are either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Manual typing&lt;/strong&gt; - Error-prone, users don't know what's available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropdown menus&lt;/strong&gt; - Clunky UI, breaks the flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom builders&lt;/strong&gt; - Weeks of development time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I needed something better for my project, so I built &lt;strong&gt;type-ahead-mention&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Smart Autocomplete ✨
&lt;/h2&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;MentionInput&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="s1"&gt;type-ahead-mention&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailEditor&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;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTemplate&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hi {{user.name}}!&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;john@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12345&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;99.99&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MentionInput&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;template&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;setTemplate&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;suggestions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&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;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type double opening curly braces and boom - intelligent autocomplete powered by CodeMirror. &lt;/p&gt;

&lt;p&gt;But here's where it gets interesting...&lt;/p&gt;




&lt;h2&gt;
  
  
  The Magic: It Handles NESTED Data 🎯
&lt;/h2&gt;

&lt;p&gt;Most autocomplete libraries choke on nested objects. Not this one:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;suggestions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sarah Connor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Los Angeles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;street&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2144 Ventura Blvd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;roles&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;admin&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;editor&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;viewer&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now users can type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{{user.name}}&lt;/code&gt; → "Sarah Connor"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{{user.address.city}}&lt;/code&gt; → "Los Angeles"
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{{user.roles.0}}&lt;/code&gt; → "admin" (yes, it does arrays!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The autocomplete &lt;strong&gt;guides them through the structure&lt;/strong&gt; as they type. No docs needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Plot Twist: From 5MB to 35KB 📦
&lt;/h2&gt;

&lt;p&gt;After building v1, I published it to NPM. Package size? &lt;strong&gt;5 MEGABYTES&lt;/strong&gt;. 😱&lt;/p&gt;

&lt;p&gt;My first reaction: "That can't be right..."&lt;/p&gt;

&lt;p&gt;But it was. Source maps + bundled dependencies = bloat city.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Optimization Journey
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Kill the source maps&lt;/strong&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;// vite.config.ts&lt;/span&gt;
&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;// Goodbye 3.9MB of .map files&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Externalize dependencies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of bundling CodeMirror and friends, I made them peer dependencies:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"peerDependencies"&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;"@codemirror/autocomplete"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^6.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@codemirror/state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^6.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@uiw/react-codemirror"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.0.0"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Results 📊
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Reduction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compressed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.3 MB&lt;/td&gt;
&lt;td&gt;12 KB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;99.1%&lt;/strong&gt; ↓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unpacked&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5.0 MB&lt;/td&gt;
&lt;td&gt;35 KB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;99.3%&lt;/strong&gt; ↓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ES Module&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;642 KB&lt;/td&gt;
&lt;td&gt;13 KB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;97.9%&lt;/strong&gt; ↓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Yeah, you read that right. &lt;strong&gt;99% smaller&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cool Features You'll Actually Use 🎨
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Theme It Your Way
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MentionInput&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;16px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#1e1e1e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#e0e0e0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8px&lt;/span&gt;&lt;span class="dl"&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;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Single or Multi-line
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Single-line input&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MentionInput&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;subject&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;setSubject&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Multi-line textarea&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MentionInput&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;body&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;setBody&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;multiline&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Template Resolution Hook
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMentionResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello {{user.name}}!&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;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice&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="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Result: "Hello Alice!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Full Keyboard Navigation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Arrow keys to navigate suggestions&lt;/li&gt;
&lt;li&gt;Enter to select&lt;/li&gt;
&lt;li&gt;Escape to dismiss&lt;/li&gt;
&lt;li&gt;Tab to autocomplete&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real-World Use Cases 🌍
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Email Marketing Platforms&lt;/strong&gt;&lt;br&gt;
Let users create personalized emails without learning your template syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Notification Builders&lt;/strong&gt;&lt;br&gt;
Build Slack/Discord bot message composers with dynamic fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Document Generation&lt;/strong&gt;&lt;br&gt;
Create contract templates, invoices, or reports with variable data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Chat Applications&lt;/strong&gt;&lt;br&gt;
Quick replies with user context, order details, or product info.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Form Builders&lt;/strong&gt;&lt;br&gt;
Dynamic form field values based on user data or previous answers.&lt;/p&gt;


&lt;h2&gt;
  
  
  Live Demo (Try It Now!) 🎮
&lt;/h2&gt;

&lt;p&gt;I built an interactive demo where you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Test 5 different themes&lt;/li&gt;
&lt;li&gt;✅ Customize styles in real-time&lt;/li&gt;
&lt;li&gt;✅ Edit the JSON data structure live&lt;/li&gt;
&lt;li&gt;✅ See template resolution instantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://rahulpatwa1303.github.io/type-ahead-mention/" rel="noopener noreferrer"&gt;Play with the demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Getting Started (2 Minutes) ⚡
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;type-ahead-mention
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Install peer dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @codemirror/autocomplete @codemirror/state @codemirror/view @uiw/react-codemirror
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Basic usage:&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;MentionInput&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="s1"&gt;type-ahead-mention&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="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="s1"&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessage&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hi {{name}}!&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;suggestions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;World&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&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="nc"&gt;MentionInput&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;message&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;setMessage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
      &lt;span class="na"&gt;suggestions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;suggestions&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;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Learned 📚
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bundle size matters&lt;/strong&gt; - Users will notice (and complain about) bloated packages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peer dependencies are your friend&lt;/strong&gt; - Don't bundle what users likely already have&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source maps in production?&lt;/strong&gt; - Almost never a good idea&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CodeMirror is powerful&lt;/strong&gt; - But you need to understand its extension system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive demos sell&lt;/strong&gt; - Show, don't just tell&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Tech Stack 🛠️
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React 18+&lt;/strong&gt; - Modern hooks API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; - Full type safety&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CodeMirror 6&lt;/strong&gt; - The editor that powers VS Code's find/replace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Popper.js&lt;/strong&gt; - Smart suggestion positioning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite&lt;/strong&gt; - Lightning-fast builds&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Open Questions for You 🤔
&lt;/h2&gt;

&lt;p&gt;I'd love your input on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What other autocomplete patterns do you need?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL-like syntax? Markdown helpers? Custom delimiters?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Should I add a visual builder?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click to insert variables vs. typing the double curly braces&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What about autocomplete from API calls?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Async suggestions, debouncing, caching?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mobile support?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Touch-friendly suggestion popup?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Drop your thoughts in the comments!&lt;/strong&gt; 👇&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Yourself 🚀
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📦 &lt;strong&gt;NPM&lt;/strong&gt;: &lt;code&gt;npm install type-ahead-mention&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🎮 &lt;strong&gt;Demo&lt;/strong&gt;: &lt;a href="https://rahulpatwa1303.github.io/type-ahead-mention/" rel="noopener noreferrer"&gt;https://rahulpatwa1303.github.io/type-ahead-mention/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/rahulpatwa1303/type-ahead-mention" rel="noopener noreferrer"&gt;https://github.com/rahulpatwa1303/type-ahead-mention&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Docs&lt;/strong&gt;: Full API reference in the README&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Numbers 📈
&lt;/h2&gt;

&lt;p&gt;Since launching v2.0.1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 35KB unpacked (99% smaller than v1)&lt;/li&gt;
&lt;li&gt;✅ Full TypeScript support&lt;/li&gt;
&lt;li&gt;✅ Zero runtime dependencies (except peers)&lt;/li&gt;
&lt;li&gt;✅ Works with React 18+&lt;/li&gt;
&lt;li&gt;✅ MIT licensed&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next? 🔮
&lt;/h2&gt;

&lt;p&gt;I'm considering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom delimiters&lt;/strong&gt; - Use &lt;code&gt;[[var]]&lt;/code&gt; or &lt;code&gt;$var&lt;/code&gt; instead of the double curly brace syntax&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Formatting helpers&lt;/strong&gt; - Date/number formatting filters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-select&lt;/strong&gt; - Pick multiple variables at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undo/redo&lt;/strong&gt; - Built-in history management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What would YOU use most?&lt;/strong&gt; Let me know!&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts 💭
&lt;/h2&gt;

&lt;p&gt;Building this package taught me that &lt;strong&gt;optimization isn't just about performance&lt;/strong&gt; - it's about respect for your users' bandwidth, disk space, and patience.&lt;/p&gt;

&lt;p&gt;Going from 5MB to 35KB wasn't just a technical achievement. It was a statement: "I care about your bundle size."&lt;/p&gt;

&lt;p&gt;If you're building any kind of template system, notification builder, or dynamic message composer, give type-ahead-mention a shot. It might save you weeks of development.&lt;/p&gt;

&lt;p&gt;And if you find bugs or have feature requests, the repo is open! PRs welcome. 🎉&lt;/p&gt;




&lt;h2&gt;
  
  
  Let's Connect! 🤝
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Give it a ⭐ on &lt;a href="https://github.com/rahulpatwa1303/type-ahead-mention" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Share your use cases in the comments&lt;/li&gt;
&lt;li&gt;Found a bug? &lt;a href="https://github.com/rahulpatwa1303/type-ahead-mention/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Built something cool? I'd love to see it!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question for you:&lt;/strong&gt; What's the WORST template system you've had to work with? Share your horror stories below! 👻&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. - If you're wondering why the package is called "type-ahead-mention" instead of something catchier... well, naming things is hard. 😅 Got a better name? Comment below!&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Last updated: November 19, 2025&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Spooky Halloween Landing Page</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Sun, 09 Nov 2025 18:54:41 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/my-spooky-halloween-landing-page-4j9j</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/my-spooky-halloween-landing-page-4j9j</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2025-10-15"&gt;Frontend Challenge - Halloween Edition, Perfect Landing&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;For the Perfect Landing challenge, I created a spooky and engaging Halloween-themed landing page. The project, built with Next.js and styled with Tailwind CSS, is designed to be fully responsive, ensuring a great experience on any device.&lt;/p&gt;

&lt;p&gt;The landing page captures the essence of Halloween with a dark, atmospheric color palette, thematic imagery like pumpkins and ghosts, and playful typography. The goal was to create a modern, fast, and visually appealing page that is both fun to explore and technically robust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rahulpatwa1303.github.io/Halloween-Challenge-Perfect-Landing/" rel="noopener noreferrer"&gt;&lt;strong&gt;&amp;gt;&amp;gt; View the Live Demo Here &amp;lt;&amp;lt;&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3disedekmi21z10lpdz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3disedekmi21z10lpdz.png" alt="Home page of the app" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh5wy6iuq845jxxlgwobr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh5wy6iuq845jxxlgwobr.png" alt="Interactive spooky candy map" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jeqajoaez72wxbn45ap.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jeqajoaez72wxbn45ap.png" alt="Entertainment section" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;This challenge was a fantastic opportunity to sharpen my frontend skills, particularly with Next.js and Tailwind CSS. My process started with brainstorming a fun Halloween concept and sketching out a simple wireframe for the landing page layout.&lt;/p&gt;

&lt;p&gt;I chose a minimalist yet spooky theme, focusing on clean design and subtle animations to bring the page to life. One of the main things I learned was how to efficiently set up a static export for a Next.js application for deployment on GitHub Pages. I'm especially proud of the component structure and the responsive design, which adapts smoothly from large desktops down to mobile phones.&lt;/p&gt;

&lt;p&gt;In the future, I hope to add more interactive elements, like a ghost that follows the cursor or some fun CSS-based animations on the call-to-action buttons.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This project was created by &lt;a href="https://dev.to/rahulpatwa1303"&gt;rahulpatwa1303&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Interview Hero: Your AI Coach for Crushing Tech Interviews</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Sat, 31 May 2025 10:58:16 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/interview-hero-your-ai-coach-for-crushing-tech-interviews-cbo</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/interview-hero-your-ai-coach-for-crushing-tech-interviews-cbo</guid>
      <description>&lt;p&gt;The Struggle is Real: Why Interview Prep Feels Broken&lt;br&gt;
If you've ever prepped for a tech interview, you know that feeling.&lt;/p&gt;

&lt;p&gt;You’ve got 20 browser tabs open—LeetCode, interview blogs, YouTube mock interviews—and yet somehow, you’re still unsure what to expect. You spend hours grinding problems, only to get stumped in a real interview when asked, “How would you scale this system?”&lt;/p&gt;

&lt;p&gt;Or worse—your feedback is: “Good effort, but not quite there.”&lt;/p&gt;

&lt;p&gt;No explanation. No tips. Just... confusion.&lt;/p&gt;

&lt;p&gt;The truth? Most interview prep tools weren’t built for real people.&lt;/p&gt;

&lt;p&gt;They're too generic. Too static. Too expensive. And they leave you more anxious than confident.&lt;/p&gt;

&lt;p&gt;So, we built something better.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎉 Introducing Interview Hero — Your Personal AI Interview Coach
&lt;/h2&gt;

&lt;p&gt;Interview Hero is a new web application designed to rethink how software engineers and computer science students prepare for interviews.&lt;/p&gt;

&lt;p&gt;Our mission is simple:&lt;/p&gt;

&lt;p&gt;To make effective, affordable, and realistic technical interview preparation accessible to everyone—with the power of AI.&lt;/p&gt;

&lt;p&gt;We’ve combined intelligent question generation, personalized feedback, and realistic simulations into one seamless experience. No more guesswork. No more fluff.&lt;/p&gt;

&lt;p&gt;Just practice that works.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 Why Interview Hero Works (and Why You’ll Actually Use It)
&lt;/h2&gt;

&lt;p&gt;Here’s what sets Interview Hero apart from every other prep tool out there:&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ 1. AI-Generated, Personalized Questions
&lt;/h2&gt;

&lt;p&gt;No more recycled question banks.&lt;/p&gt;

&lt;p&gt;Interview Hero dynamically generates questions tailored to your chosen focus:&lt;/p&gt;

&lt;p&gt;DSA (Data Structures &amp;amp; Algorithms)&lt;/p&gt;

&lt;p&gt;System Design&lt;/p&gt;

&lt;p&gt;Behavioral &amp;amp; Soft Skills&lt;/p&gt;

&lt;p&gt;Unlike traditional platforms, these questions evolve based on your skill level and focus areas. It’s like having a private tutor who actually knows where you’re struggling—and adapts in real time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5id3e8c8mvki5nf6ih9w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5id3e8c8mvki5nf6ih9w.png" alt="Interview session with question" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjbxc126w94mmcnhutd4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjbxc126w94mmcnhutd4.png" alt="Interview session with coding question" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 2. Instant, Actionable AI Feedback
&lt;/h2&gt;

&lt;p&gt;Ever finish a mock interview and think, “Okay... but how did I do?”&lt;/p&gt;

&lt;p&gt;Interview Hero gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear highlights of your strengths&lt;/li&gt;
&lt;li&gt;Honest analysis of your weaknesses&lt;/li&gt;
&lt;li&gt;Specific, actionable next steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it as your personal AI coach—encouraging but direct, and always focused on helping you grow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F86d1xa4k41ga4ubja35h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F86d1xa4k41ga4ubja35h.png" alt="Interview analysis summary" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 3. Topic Specialization &amp;amp; Profile Customization
&lt;/h2&gt;

&lt;p&gt;Want to master dynamic programming? Or prep for a behavioral round at your dream company?&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customize your learning path&lt;/li&gt;
&lt;li&gt;Track your performance over time&lt;/li&gt;
&lt;li&gt;Focus on the areas where you need the most help&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More personalization = less wasted time.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ How It Works: Your Interview Hero Journey
&lt;/h2&gt;

&lt;p&gt;Getting started is fast, simple, and—you guessed it—free.&lt;/p&gt;

&lt;p&gt;Here’s what your path to interview mastery looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Sign Up → Head over to (interview-hero)[&lt;a href="https://interview-hero-e4hl.vercel.app" rel="noopener noreferrer"&gt;https://interview-hero-e4hl.vercel.app&lt;/a&gt;] and create your free account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fill Out Your Profile → Tell us a bit about yourself: your experience level, preferred languages, job focus, and goals. This helps Interview Hero personalize your sessions and generate questions that actually match your needs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pick a Topic—or Let Us Pick One for You →&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Have a specific topic in mind? Share it with us.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unsure where to begin? No problem—our AI will choose a relevant focus area based on your profile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Get a Question → Our AI generates a smart, relevant prompt based on your profile and chosen topic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Answer Naturally → Type or speak your response just like you would in a real interview.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Receive Instant AI Feedback → Learn your strengths, spot weaknesses, and get tailored suggestions for improvement. Think of it as having a virtual coach with a keen eye.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🧑‍💻 Why We Built Interview Hero
&lt;/h2&gt;

&lt;p&gt;We’re developers too.&lt;/p&gt;

&lt;p&gt;We’ve been through the grind—nervously prepping for interviews at Big Tech, early-stage startups, and everywhere in between. We know how overwhelming it can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What should you study?&lt;/li&gt;
&lt;li&gt;How do you know if your answers are good enough?&lt;/li&gt;
&lt;li&gt;Can you afford those pricey coaching sessions?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most platforms out there are either too expensive, too generic, or too complicated. We wanted something better—for ourselves and for the community.&lt;/p&gt;

&lt;p&gt;So we built what we wished existed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ A smart, supportive coach that gives honest, actionable feedback&lt;/li&gt;
&lt;li&gt;✅ An experience that’s intuitive, fast, and easy to use&lt;/li&gt;
&lt;li&gt;✅ A free tier so anyone, anywhere, can level up their interview game without breaking the bank&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Interview Hero is for the engineer who wants to grow, not just cram. It’s built by people who’ve been in your shoes, and it’s here to help you get where you want to go—with confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Start Practicing Smarter—Right Now
&lt;/h2&gt;

&lt;p&gt;You're not just preparing for an interview. You're building a career.&lt;/p&gt;

&lt;p&gt;Let Interview Hero guide you there—with the kind of personalized, intelligent support you can’t get from a static PDF or overpriced bootcamp.&lt;/p&gt;

&lt;p&gt;👉 Try it free today at interview-hero-e4hl.vercel.app&lt;/p&gt;

&lt;h2&gt;
  
  
  🔁 Let’s Redefine Interview Prep—Together
&lt;/h2&gt;

&lt;p&gt;We believe the best tools are built in collaboration with the people who use them.&lt;/p&gt;

&lt;p&gt;So as you start using Interview Hero, we want your feedback. Tell us what works. What doesn’t. What you’d love to see next. We’re here to listen—and to help.&lt;/p&gt;

&lt;p&gt;Because when you win that job offer, we want to be the first to high-five you (virtually, of course).&lt;/p&gt;

&lt;p&gt;#career #interviewprep #softwareengineering #webdev #ai  &lt;/p&gt;

</description>
    </item>
    <item>
      <title>Creating a Synchronized Vertical and Horizontal Scrolling Component for Web Apps</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Sun, 30 Jun 2024 11:56:30 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/creating-a-synchronized-vertical-and-horizontal-scrolling-component-for-web-apps-1igc</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/creating-a-synchronized-vertical-and-horizontal-scrolling-component-for-web-apps-1igc</guid>
      <description>&lt;h4&gt;
  
  
  Introduction
&lt;/h4&gt;

&lt;p&gt;Microsoft Teams' mobile agenda page offers a sleek and intuitive interface with synchronized vertical and horizontal scrolling. This design allows users to scroll through dates horizontally and see the corresponding events in a vertical list. Inspired by this elegant solution, I decided to create a similar component using modern web technologies. While there are many libraries and blogs about synchronized scrolling, they typically handle scrolling in the same direction. This article will show you how to achieve synchronized scrolling in both vertical and horizontal directions.&lt;/p&gt;

&lt;p&gt;You can also checkout the &lt;a href="https://qwyt5x.csb.app/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtxkjgbsw7ikcuhakbqx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtxkjgbsw7ikcuhakbqx.gif" alt="Demo gif" width="270" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Prerequisites
&lt;/h4&gt;

&lt;p&gt;Before diving in, you should have a basic understanding of React, JavaScript, and Tailwind CSS. Make sure you have Node.js and npm installed on your machine.&lt;/p&gt;

&lt;h5&gt;
  
  
  Setting Up the Project
&lt;/h5&gt;

&lt;p&gt;First, create a new React project using Create React App or your preferred method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm create vite@latest my-sync-scroll-app -- --template react
cd my-sync-scroll-app 
npm install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, install Tailwind CSS (optional).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -D tailwindcss npx tailwindcss init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure Tailwind CSS by adding the following content to your &lt;code&gt;tailwind.config.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = { 
    purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 
    darkMode: false, 
    theme: { 
        extend: {}, 
    }, 
    variants: { 
        extend: {}, 
    }, 
    plugins: [], 
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the Tailwind directives to your CSS file (&lt;code&gt;src/index.css&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@tailwind base;
@tailwind components;
@tailwind utilities;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Utility Function for Date Generation
&lt;/h4&gt;

&lt;p&gt;Let's create a utility function to generate a list of dates starting from a given date.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const generateDates = (startDate, days) =&amp;gt; {
  const dates = [];
  for (let i = 0; i &amp;lt; days; i++) {
    const date = new Date(startDate);
    date.setDate(startDate.getDate() + i);
    dates.push(date.toISOString().split("T")[0]); // Format date as YYYY-MM-DD
  }
  return dates;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Creating the Horizontal Scroll Component
&lt;/h4&gt;

&lt;p&gt;Let's start by creating the &lt;code&gt;HorizontalScroll&lt;/code&gt; component. This component will allow users to scroll through dates horizontally and select a date.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React, { useEffect, useRef } from "react";

const HorizontalScroll = ({
  dates,
  selectedDate,
  setSelectedDate,
  setSelectFromHorizontal,
}) =&amp;gt; {
  const containerRef = useRef();

  useEffect(() =&amp;gt; {
    // Automatically scroll to the selected date and center it in the view
    const selectedElement = containerRef.current.querySelector(`.date-item.selected`);
    if (selectedElement) {
      const containerWidth = containerRef.current.offsetWidth;
      const elementWidth = selectedElement.offsetWidth;
      const elementOffsetLeft = selectedElement.offsetLeft;
      const scrollTo = elementOffsetLeft - containerWidth / 2 + elementWidth / 2;
      containerRef.current.scrollTo({
        left: scrollTo,
        behavior: "smooth",
      });
    }
  }, [selectedDate]);

  const handleDateSelection = (index) =&amp;gt; {
    setSelectedDate(dates[index]);
    setSelectFromHorizontal(true);
  };

  const onWheel = (e) =&amp;gt; {
    const element = containerRef.current;
    if (element) {
      if (e.deltaY === 0) return;
      element.scrollTo({
        left: element.scrollLeft + e.deltaY,
      });
    }
  };

  return (
    &amp;lt;div className="w-full flex flex-row-reverse items-center gap-2 bg-gray-500 rounded-md horizontal"&amp;gt;
      &amp;lt;div
        className="horizontal-scroll flex overflow-x-auto whitespace-nowrap scroll-smooth rounded-md"
        ref={containerRef}
        onWheel={onWheel}
      &amp;gt;
        {dates.map((date, index) =&amp;gt; {
          const day = new Date(date).toLocaleString([], { month: "short" });
          const d = new Date(date).toLocaleString([], { day: "2-digit" });
          return (
            &amp;lt;div
              key={date}
              className={`date-item ${selectedDate === date ? "selected" : ""} flex flex-col items-center p-4`}
              onClick={() =&amp;gt; handleDateSelection(index)}
              style={{
                backgroundColor: selectedDate === date ? "#90cdf4" : "#f7fafc",
                borderRadius: selectedDate === date ? "4px" : "0px",
              }}
            &amp;gt;
              &amp;lt;p className={`text-sm ${selectedDate === date ? "text-blue-600" : "text-gray-500"} font-light`}&amp;gt;
                {day}
              &amp;lt;/p&amp;gt;
              &amp;lt;p className={`text-base font-semibold ${selectedDate === date ? "text-blue-700" : "text-gray-700"}`}&amp;gt;
                {d}
              &amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
          );
        })}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default HorizontalScroll;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Creating the Vertical Scroll Component
&lt;/h4&gt;

&lt;p&gt;Next, create the &lt;code&gt;VerticalScroll&lt;/code&gt; component to display the events for the selected date. This component will synchronize with the &lt;code&gt;HorizontalScroll&lt;/code&gt; component to update the displayed events when a date is selected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React, { useEffect, useRef, useState } from "react";

const VerticalScroll = ({
  dates,
  onDateChange,
  selectedDate,
  selectFromHorizontal,
  setSelectFromHorizontal,
}) =&amp;gt; {
  const containerRef = useRef();
  const [visibleDates, setVisibleDates] = useState([]);
  const [isProgrammaticScroll, setIsProgrammaticScroll] = useState(false);

  useEffect(() =&amp;gt; {
    const container = containerRef.current;
    const handleScroll = () =&amp;gt; {
      if (isProgrammaticScroll) {
        setIsProgrammaticScroll(false);
        return;
      }
      if (!selectFromHorizontal) {
        // Calculate the date at the top of the vertical scroll
        const topDateIndex = Math.floor(container.scrollTop / 100);
        const topDate = dates[topDateIndex];
        onDateChange(topDate);
      }
      // Calculate the visible dates based on the current scroll position
      const start = Math.floor(container.scrollTop / 100);
      const end = start + Math.ceil(container.clientHeight / 100);
      const visible = dates.slice(start, end);
      setVisibleDates(visible);
    };

    container.addEventListener("scroll", handleScroll);

    return () =&amp;gt; container.removeEventListener("scroll", handleScroll);
  }, [dates, isProgrammaticScroll, onDateChange]);

  useEffect(() =&amp;gt; {
    setTimeout(() =&amp;gt; setSelectFromHorizontal(false), 1000);
  }, [selectedDate]);

  useEffect(() =&amp;gt; {
    const selectedIndex = dates.indexOf(selectedDate);
    if (selectedIndex !== -1) {
      // Scroll to the selected date in the vertical scroll
      const scrollTo = selectedIndex * 100;
      setIsProgrammaticScroll(true);
      containerRef.current.scrollTo({
        top: scrollTo,
        behavior: "smooth",
      });
    }
  }, [selectedDate, dates]);

  return (
    &amp;lt;div className="h-full overflow-y-auto" ref={containerRef}&amp;gt;
      {dates.map((date) =&amp;gt; (
        &amp;lt;div key={date} className="my-4 h-24"&amp;gt;
          &amp;lt;div className="relative flex items-center mb-2"&amp;gt;
            &amp;lt;div className="flex-grow border-t border-gray-300"&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;span className="flex-shrink mx-4 text-gray-500"&amp;gt;
              {new Date(date).toLocaleString([], { month: "short", day: "2-digit", weekday: "short" })}
            &amp;lt;/span&amp;gt;
            &amp;lt;div className="flex-grow border-t border-gray-300"&amp;gt;&amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
          {visibleDates.includes(date) ? (
            &amp;lt;DateContent date={date} /&amp;gt;
          ) : (
            &amp;lt;p&amp;gt;No events&amp;lt;/p&amp;gt;
          )}
        &amp;lt;/div&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
};

const DateContent = ({ date }) =&amp;gt; {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() =&amp;gt; {
    const fetchData = async () =&amp;gt; {
      const selectDate = new Date(date);

      selectDate.setHours(6, 0, 0, 0);
      const epochStartTimestamp = Math.floor(selectDate.getTime() / 1000);

      selectDate.setDate(selectDate.getDate() + 3);
      selectDate.setHours(23, 59, 59, 999);
      const epochEndTimestamp = Math.floor(selectDate.getTime() / 1000);

      const queryParams = `?start_timestamp=${epochStartTimestamp}&amp;amp;end_timestamp=${epochEndTimestamp}`;

      try {
        const response = await fetch(`https://example.com/api/upcomingShifts${queryParams}`);
        if (response.status === 200) {
          const result = await response.json();
          setLoading(false);
          setData((prevData) =&amp;gt; [...prevData, ...result.upcomingShifts]);
        }
      } catch (error) {
        console.error("Error fetching data:", error);
      }
    };

    fetchData();
  }, [date]);

  if (!data) return &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;;

  return (
    &amp;lt;div&amp;gt;
      {loading ? (
        &amp;lt;div className="animate-pulse h-6 bg-gray-300 rounded"&amp;gt;&amp;lt;/div&amp;gt;
      ) : (
        data.map((d) =&amp;gt; (
          &amp;lt;div key={d.id} className="my-2"&amp;gt;
            &amp;lt;p&amp;gt;{d.id}&amp;lt;/p&amp;gt;
          &amp;lt;/div&amp;gt;
        ))
      )}
    &amp;lt;/div&amp;gt;
  );
};

export default VerticalScroll;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Bringing It All Together
&lt;/h4&gt;

&lt;p&gt;Now, let's integrate these components in the main &lt;code&gt;App&lt;/code&gt; component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React, { useState } from "react";
import HorizontalScroll from "./components/HorizontalScroll";
import VerticalScroll from "./components/VerticalScroll";

const App = () =&amp;gt; {
    const dates = generateDates(new Date(), 90);
    const [selectedDate, setSelectedDate] = useState(dates[0]);
    const [selectFromHorizontal, setSelectFromHorizontal] = useState(false);

  // Function to handle date changes from the vertical scroll component
  const handleDateChange = (date) =&amp;gt; {
    if (!selectFromHorizontal) {
      setSelectedDate(date);
    }
  };

  return (
    &amp;lt;div className="flex flex-col h-screen p-4"&amp;gt;
      &amp;lt;HorizontalScroll
        dates={dates}
        selectedDate={selectedDate}
        setSelectedDate={setSelectedDate}
        setSelectFromHorizontal={setSelectFromHorizontal}
      /&amp;gt;
      &amp;lt;VerticalScroll
        dates={dates}
        selectedDate={selectedDate}
        onDateChange={handleDateChange}
        selectFromHorizontal={selectFromHorizontal}
        setSelectFromHorizontal={setSelectFromHorizontal}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default App;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;By following this guide, you can create a synchronized vertical and horizontal scrolling component for your web application. This design pattern, inspired by Microsoft Teams' mobile agenda page, enhances the user experience by providing an intuitive and efficient way to navigate through dates and events. Experiment with the components, adjust the styles, and integrate them into your projects to meet your specific needs. Happy coding!&lt;/p&gt;

&lt;h3&gt;
  
  
  Live Demo
&lt;/h3&gt;

&lt;p&gt;For a live demonstration of the synchronized vertical and horizontal scrolling component, you can explore the demo on CodeSandbox. This interactive sandbox allows you to see the code in action and experiment with the functionality described in this blog.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Glam Up My Markup: Beaches - Frontend Challenge v24.04.17</title>
      <dc:creator>rahul patwa</dc:creator>
      <pubDate>Fri, 07 Jun 2024 05:30:54 +0000</pubDate>
      <link>https://dev.to/rahul_patwa_f99f19cd1519b/glam-up-my-markup-beaches-frontend-challenge-v240417-3eb1</link>
      <guid>https://dev.to/rahul_patwa_f99f19cd1519b/glam-up-my-markup-beaches-frontend-challenge-v240417-3eb1</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for [Frontend Challenge v24.04.17]((&lt;a href="https://dev.to/challenges/frontend-2024-05-29"&gt;https://dev.to/challenges/frontend-2024-05-29&lt;/a&gt;), Glam Up My Markup: Beaches&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;For this challenge, I developed an interactive UI that visualizes beaches around the world on a global map. Leveraging the power of D3.js, I created a dynamic map that plots each beach based on its geographical coordinates.&lt;/p&gt;

&lt;p&gt;Key Features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Interactive Map&lt;/strong&gt;: Users can explore beaches plotted on a world map. Each beach is represented by a point that can be clicked for more information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detail Sidebar&lt;/strong&gt;: Upon clicking a beach on the map, a sidebar slides in to display detailed information about the selected beach, including its name, location, and other relevant details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beach List&lt;/strong&gt;: Alongside the map, there is a comprehensive list of all the beaches with their names. Users can click on any beach name from the list to view its details and locate it on the map.&lt;/li&gt;
&lt;li&gt;User-Friendly Design: Inspired by Google Maps' intuitive interface and Snapchat's hotspot visualization, the UI aims to provide a seamless and engaging user experience.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My goal was to create an aesthetically pleasing and highly functional interface that allows users to easily discover and learn about various beaches around the world. By integrating familiar design elements from popular applications, I aimed to enhance usability and make the exploration process enjoyable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Access my project on &lt;a href="https://rahulpatwa1303.github.io/best-beachs/" rel="noopener noreferrer"&gt;GitHub pages&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forfd256xrom880qxydtc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forfd256xrom880qxydtc.png" alt="Landing page to the site" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiohmbrrwb3ouw1ip0teq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiohmbrrwb3ouw1ip0teq.png" alt="World map to show the beaches" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvshg8gnyy4f9m6e6af0k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvshg8gnyy4f9m6e6af0k.png" alt="View when any beach is selected" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can checkout the &lt;a href="https://github.com/rahulpatwa1303/best-beachs" rel="noopener noreferrer"&gt;github repo&lt;/a&gt; of the code on &lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;Coming from a background with React and Next.js, I had limited experience working with vanilla JavaScript. This project provided an excellent opportunity to dive into the core fundamentals of JavaScript and DOM manipulation without relying on frameworks. Here are the key learnings and experiences from my journey:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vanilla JavaScript&lt;/strong&gt;:&lt;br&gt;
-&lt;strong&gt;DOM Manipulation&lt;/strong&gt;: Without the abstraction layers provided by React, I had to directly interact with the DOM, which gave me a deeper understanding of how web pages are constructed and updated.&lt;br&gt;
-&lt;strong&gt;Event Handling&lt;/strong&gt;: I learned to handle events purely with JavaScript, enhancing my ability to create interactive web elements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;D3.js&lt;/strong&gt;:&lt;br&gt;
-&lt;strong&gt;Data Binding and Visualization&lt;/strong&gt;: I explored how to bind data to HTML elements and create dynamic visualizations. D3.js proved to be a powerful tool for rendering complex data-driven graphics.&lt;br&gt;
-&lt;strong&gt;Geographical Mapping&lt;/strong&gt;: Implementing the world map and plotting the beaches taught me how to work with geographical data and projections in D3.js.&lt;br&gt;
-&lt;strong&gt;Interactive Features&lt;/strong&gt;: Adding interactivity, such as clickable points and a responsive sidebar, helped me appreciate the versatility and power of D3.js for creating engaging user interfaces.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This project not only expanded my technical skills but also enhanced my appreciation for the intricacies of web development. It was a rewarding journey that pushed me out of my comfort zone and allowed me to grow as a developer.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>css</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
