<?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: Kaviya Kumar</title>
    <description>The latest articles on DEV Community by Kaviya Kumar (@kaviyakumar23).</description>
    <link>https://dev.to/kaviyakumar23</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3827688%2Fef3e464f-d66f-4922-8055-a5dbedae210b.png</url>
      <title>DEV Community: Kaviya Kumar</title>
      <link>https://dev.to/kaviyakumar23</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kaviyakumar23"/>
    <language>en</language>
    <item>
      <title>I built a world you can rewind and fork — and forking it costs one database row</title>
      <dc:creator>Kaviya Kumar</dc:creator>
      <pubDate>Mon, 29 Jun 2026 19:51:10 +0000</pubDate>
      <link>https://dev.to/kaviyakumar23/i-built-a-world-you-can-rewind-and-fork-and-forking-it-costs-one-database-row-35ho</link>
      <guid>https://dev.to/kaviyakumar23/i-built-a-world-you-can-rewind-and-fork-and-forking-it-costs-one-database-row-35ho</guid>
      <description>&lt;p&gt;I wanted to ask a database a question it isn't supposed to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What if this had happened instead — without re-running history from scratch?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every system I build has the same blind spot. To test a different past you copy the data, replay everything, and pray it's deterministic. So I tried the opposite premise: &lt;strong&gt;don't store the world after computing it somewhere else — make the database itself BE the world.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;Loom&lt;/strong&gt;: a deterministic simulation whose authoritative reality lives entirely inside &lt;a href="https://aws.amazon.com/rds/aurora/dsql/" rel="noopener noreferrer"&gt;Aurora DSQL&lt;/a&gt;. Three properties fall out of that one decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Each tick is one SQL transaction.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-travel is a query&lt;/strong&gt; (just read a smaller tick).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forking the universe is O(1)&lt;/strong&gt; — one inserted row. Pre-fork history is &lt;em&gt;shared, never copied&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://loom-ivory.vercel.app" rel="noopener noreferrer"&gt;https://loom-ivory.vercel.app&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The data model: copy-on-write, versioned state
&lt;/h2&gt;

&lt;p&gt;Three tables. That's the whole engine.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;branches&lt;/code&gt; — the multiverse tree. Forking = &lt;code&gt;INSERT&lt;/code&gt; one row.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;world_meta&lt;/code&gt; — the strongly-consistent "now" pointer per branch.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entity_state&lt;/code&gt; — copy-on-write, versioned world state. &lt;strong&gt;The&lt;/strong&gt; table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A branch only ever writes rows for ticks &lt;em&gt;after&lt;/em&gt; its fork point. Everything before is read &lt;em&gt;through&lt;/em&gt; its ancestors. So forking copies &lt;strong&gt;zero&lt;/strong&gt; history.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;entity_state&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;branch_id&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;entity_id&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="n"&gt;gen_random_uuid&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="n"&gt;tick&lt;/span&gt;      &lt;span class="nb"&gt;BIGINT&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;kind&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="c1"&gt;-- 'AGENT' | 'WORLD'&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;     &lt;span class="n"&gt;JSONB&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="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;branch_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tick&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;A tick reads the current frame, runs a &lt;strong&gt;pure&lt;/strong&gt; &lt;code&gt;nextState(prev)&lt;/code&gt; (no RNG, no wall-clock, no iteration-order surprises), and writes back &lt;strong&gt;only the entities that changed&lt;/strong&gt;. At equilibrium a calm world writes ~1 row per tick instead of thousands — divergence becomes the only thing that costs storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The crown jewel: one query that is both a time machine and a multiverse
&lt;/h2&gt;

&lt;p&gt;Reconstructing any frame of any timeline is a single recursive query. Vary the tick → you time-travel. Vary the branch → you cross into a parallel timeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;lineage&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;branch_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_branch_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fork_tick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;branches&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;branch_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;branch_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_branch_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fork_tick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;LEAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fork_tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;branches&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;lineage&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;branch_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_branch_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;entity_state&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;lineage&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;branch_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;branch_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="k"&gt;ASC&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;LEAST(l.cap, l.fork_tick)&lt;/code&gt; caps each ancestor's contribution at the child's fork point, so a branch sees its ancestors' history only up to where it split off. &lt;code&gt;DISTINCT ON … ORDER BY tick DESC, depth ASC&lt;/code&gt; takes the newest visible version of each entity, preferring the closest branch in the lineage. &lt;strong&gt;That one query is the engine.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I checked instead of claimed
&lt;/h2&gt;

&lt;p&gt;A forked branch stores &lt;strong&gt;zero rows&lt;/strong&gt; for everything before its fork point — I didn't want to assume it, so the proof endpoint measures it:&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;"branch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"drought"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"isFork"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"forkTick"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rowsBeforeFork"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;"sharedIdentical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="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;&lt;code&gt;rowsBeforeFork: 0&lt;/code&gt;, and &lt;code&gt;readWorld('drought', t) === readWorld('root', t)&lt;/code&gt; for every &lt;code&gt;t &amp;lt; 15&lt;/code&gt;. Forking reality in two cost &lt;strong&gt;one branch row + one meta row&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And it holds at scale: &lt;strong&gt;10,000 agents&lt;/strong&gt;, a stable world writing about &lt;strong&gt;one row per tick&lt;/strong&gt;, any moment rebuilt in roughly &lt;strong&gt;8 ms&lt;/strong&gt;, and two full 10k-agent universes stored in about &lt;strong&gt;91% less&lt;/strong&gt; space than materializing each branch in full.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Two regions, one world
&lt;/h2&gt;

&lt;p&gt;This isn't a laptop Postgres. It runs on a real &lt;strong&gt;multi-region Aurora DSQL cluster, active-active across Tokyo (&lt;code&gt;ap-northeast-1&lt;/code&gt;) and Seoul (&lt;code&gt;ap-northeast-2&lt;/code&gt;)&lt;/strong&gt;. Reading the same &lt;code&gt;(branch, tick)&lt;/code&gt; from each regional endpoint returns &lt;strong&gt;byte-identical&lt;/strong&gt; frames, and a fork written through one region shows up immediately in the other — strong global consistency, no replication lag. A &lt;code&gt;region-proof&lt;/code&gt; script fingerprints both continents and asserts the hashes match.&lt;/p&gt;

&lt;p&gt;DSQL is Postgres-wire-compatible but &lt;em&gt;not&lt;/em&gt; Postgres, and a few things cost me time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;DESC&lt;/code&gt; in index keys&lt;/strong&gt; ("specifying sort order not supported") — use plain ascending and let the planner scan backward for the &lt;code&gt;ORDER BY ... tick DESC&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic concurrency&lt;/strong&gt; — a commit conflict comes back as &lt;code&gt;SQLSTATE 40001&lt;/code&gt;, not a blocking lock. The per-branch ticker just retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~10k changed-row transaction cap&lt;/strong&gt; — which is exactly why copy-on-write (write only what changed) isn't just an optimization, it's load-bearing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hosting it: Vercel serverless → Aurora DSQL, with no static secrets
&lt;/h2&gt;

&lt;p&gt;The frontend is &lt;strong&gt;Next.js on Vercel&lt;/strong&gt;. The interesting part is how the deployed app talks to DSQL without me pasting an AWS key anywhere.&lt;/p&gt;

&lt;p&gt;The Vercel AWS integration uses &lt;strong&gt;OIDC role assumption&lt;/strong&gt;: each serverless function gets a short-lived OIDC token, assumes an IAM role, and those temporary credentials mint a DSQL auth token. The whole connection layer is a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pg&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;pg&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;DsqlSigner&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;@aws-sdk/dsql-signer&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;awsCredentialsProvider&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;@vercel/functions/oidc&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;signer&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;DsqlSigner&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;hostname&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;DDB_PGHOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;region&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;DDB_AWS_REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;awsCredentialsProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;roleArn&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;DDB_AWS_ROLE_ARN&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;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&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;host&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;DDB_PGHOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5432&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rejectUnauthorized&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="na"&gt;password&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;signer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDbConnectAdminAuthToken&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// pg re-mints per connection&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DSQL IAM tokens are short-lived (~15 min), and &lt;code&gt;pg&lt;/code&gt; calls the &lt;code&gt;password&lt;/code&gt; function per new connection — so it transparently re-mints. The reconstruction query for the live timeline, the divergence chart, the "show me the SQL" receipts, the zero-rows proof — every panel on the hosted site is a real DSQL read, not an animation.&lt;/p&gt;

&lt;p&gt;One gotcha worth knowing: the per-tick series chart did N sequential reconstructions, which is fine next to the cluster but brutal across an ocean from a function in another region. Running those reconstructions &lt;strong&gt;in parallel&lt;/strong&gt; (&lt;code&gt;Promise.all&lt;/code&gt;) collapses the wall-clock to roughly one round-trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it something you can feel
&lt;/h2&gt;

&lt;p&gt;Numbers don't land in a demo, so the headline scenario is a concrete one: a small city of people who can all reach a hospital. I rewind, fork that exact moment, and inject &lt;strong&gt;one&lt;/strong&gt; event — a flood that cuts the city in half. The south is stranded, resource access falls from 100% toward collapse… while the original timeline keeps flowing, untouched. Same seed, one changed event, two diverging futures — and history was never re-run.&lt;/p&gt;

&lt;p&gt;That's the whole pitch: &lt;strong&gt;what-if stops being a thought experiment and becomes a row you insert.&lt;/strong&gt; A substrate for digital twins, scenario modeling, and agent backtesting — run a thousand alternate histories of one decision on a single consistent engine and compare them.&lt;/p&gt;

&lt;p&gt;The database isn't storing the world. It &lt;em&gt;is&lt;/em&gt; the world — and you can rewind it, and fork it.&lt;/p&gt;

</description>
      <category>h0hackathon</category>
    </item>
    <item>
      <title>The price melts as the crowd grows: building a provably-fair live group-buy on Aurora DSQL</title>
      <dc:creator>Kaviya Kumar</dc:creator>
      <pubDate>Mon, 29 Jun 2026 19:23:20 +0000</pubDate>
      <link>https://dev.to/kaviyakumar23/the-price-melts-as-the-crowd-grows-building-a-provably-fair-live-group-buy-on-aurora-dsql-3lk1</link>
      <guid>https://dev.to/kaviyakumar23/the-price-melts-as-the-crowd-grows-building-a-provably-fair-live-group-buy-on-aurora-dsql-3lk1</guid>
      <description>&lt;p&gt;Group-buying is an old idea: get enough people together and the price drops. But it has always been &lt;em&gt;asynchronous&lt;/em&gt; — you commit, you wait, and you find out the deal later.&lt;/p&gt;

&lt;p&gt;I wanted the opposite. A &lt;strong&gt;live&lt;/strong&gt; market where a global crowd watches one price fall together in real time, and when the timer ends, &lt;strong&gt;everyone pays the same final price&lt;/strong&gt; — even the people who joined first.&lt;/p&gt;

&lt;p&gt;I called it &lt;strong&gt;Rally&lt;/strong&gt;. And the moment I sketched it, the hard part jumped out at me, and it had nothing to do with the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap hiding inside a simple idea
&lt;/h2&gt;

&lt;p&gt;If two shoppers in two different regions ever see a &lt;strong&gt;different price for the same item at the same instant&lt;/strong&gt;, the whole thing falls apart. It's unfair, and it's trivially exploitable.&lt;/p&gt;

&lt;p&gt;That is not a front-end bug you can paper over with a nicer animation. It's a &lt;strong&gt;consistency guarantee&lt;/strong&gt; — you either have it or you don't. A live, globally-fair, falling price is only honest on a database that is strongly consistent across regions.&lt;/p&gt;

&lt;p&gt;So this stopped being "a CRUD app with a countdown" and became a project about one specific database capability. I built it on &lt;strong&gt;Amazon Aurora DSQL&lt;/strong&gt; (the strongly-consistent market truth), with &lt;strong&gt;DynamoDB&lt;/strong&gt; as the live read plane, on &lt;strong&gt;Next.js / Vercel&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3265flu9ag4o0bwy5opg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3265flu9ag4o0bwy5opg.png" alt=" " width="800" height="638"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Trick #1: the price is a &lt;em&gt;read&lt;/em&gt;, not a &lt;em&gt;write&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;The most important design decision: &lt;strong&gt;the live price is never stored.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's derived. The price is &lt;code&gt;tier(sum(count_shards))&lt;/code&gt; — a pure function of a strongly-consistent count. Because it's computed, crossing a volume tier lowers the price for &lt;em&gt;everyone&lt;/em&gt; at once. There is no "current price" row to update, and therefore &lt;strong&gt;no tier-transition race to lose.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The two queries that &lt;em&gt;are&lt;/em&gt; the product:&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;-- LIVE PRICE: a strongly-consistent count → the tier price.&lt;/span&gt;
&lt;span class="c1"&gt;-- Every region computes the identical value.&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;count_shards&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rally_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;unit_price_minor&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;rally_tiers&lt;/span&gt;
          &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rally_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;min_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;min_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- FAIRNESS PROOF (run live at settlement):&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;price_paid_minor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;settlements&lt;/span&gt;   &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rally_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- expect 1&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signed_minor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ledger_entries&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rally_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- expect 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second pair is the entire pitch, made checkable: everybody paid one price, and the double-entry ledger balances to zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trick #2: shard the one hot row
&lt;/h2&gt;

&lt;p&gt;A single &lt;code&gt;count++&lt;/code&gt; row is the classic optimistic-concurrency killer on a distributed SQL database. Every join contends on the same row, and you spend your life retrying serialization conflicts.&lt;/p&gt;

&lt;p&gt;So I sharded the join counter across &lt;strong&gt;128 rows&lt;/strong&gt;. Each join bumps a &lt;em&gt;random&lt;/em&gt; shard; the live count is their consistent sum. One join is one atomic, idempotent transaction:&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;// idempotent on (rally_id, user_id): a double-tap can never double-count&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ins&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;c&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 participants (rally_id,user_id,idempotency_key)
     VALUES ($1,$2,$3) ON CONFLICT (rally_id,user_id) DO NOTHING`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rallyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idemKey&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;ins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&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="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;c&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;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ALREADY_JOINED&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;await&lt;/span&gt; &lt;span class="nx"&gt;c&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 count_shards SET count = count + 1
     WHERE rally_id=$1 AND shard_no=$2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rallyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&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;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;shards&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="c1"&gt;// random shard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I load-tested it against the real cluster in Tokyo. The result I'm proudest of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== RECONCILE / FAIRNESS PROOF ==
  unique joiners   : 2000   (== shard sum? true)
  OCC retries      : 398    (detected and retried to success)
  distinctPrices   : 1      ($52.00)
  ledgerDrift      : 0
  doubleJoins      : 0
  doubleCharges    : 0
  melt: $100 → $90 → $82 → $74 → $68 → $62 → $58 → $55 → $53 → $52
  PROOF PASSED ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2,500 join attempts (including 500 duplicate taps) → exactly 2,000 unique joiners, &lt;strong&gt;398 serialization conflicts detected and retried to success&lt;/strong&gt;, zero double-joins, and the price melted from $100 to $52. On the live cluster, not in theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DSQL differences I hit (so you don't)
&lt;/h2&gt;

&lt;p&gt;Porting a Postgres-shaped engine to Aurora DSQL surfaced the real differences fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BEGIN ISOLATION LEVEL SERIALIZABLE&lt;/code&gt; is rejected&lt;/strong&gt; (&lt;code&gt;0A000&lt;/code&gt;). That threw me until I realized DSQL is &lt;em&gt;serializable-by-default&lt;/em&gt; with optimistic concurrency, so a plain &lt;code&gt;BEGIN&lt;/code&gt; gives identical semantics. The retry loop on &lt;code&gt;40001&lt;/code&gt; is exactly the pattern you want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sequences.&lt;/strong&gt; All IDs are app-generated UUIDs — which you want anyway for distribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique constraints via PK-guard tables.&lt;/strong&gt; Instead of unique secondary indexes, I enforce unique email/handle with a tiny table whose primary key &lt;em&gt;is&lt;/em&gt; the email, and &lt;code&gt;INSERT ... ON CONFLICT DO NOTHING&lt;/code&gt; to claim it atomically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexes are created &lt;code&gt;ASYNC&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;CREATE INDEX ASYNC ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth is a short-lived IAM token&lt;/strong&gt;, minted per connection with &lt;code&gt;@aws-sdk/dsql-signer&lt;/code&gt; and used as the database password. No static password sitting in a config.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bug serverless taught me
&lt;/h2&gt;

&lt;p&gt;This one was sneaky. In local dev, an in-process &lt;code&gt;EventEmitter&lt;/code&gt; fanned each committed join out to every connected SSE client. Worked perfectly.&lt;/p&gt;

&lt;p&gt;On Vercel, it &lt;em&gt;silently&lt;/em&gt; stopped working. Why? Each serverless invocation is &lt;strong&gt;isolated&lt;/strong&gt; — the &lt;code&gt;/join&lt;/code&gt; handler and the &lt;code&gt;/stream&lt;/code&gt; handler don't share memory. The emitter was shouting into a room nobody was in.&lt;/p&gt;

&lt;p&gt;The fix made the architecture honest: the shared &lt;strong&gt;DynamoDB &lt;code&gt;live_frame&lt;/code&gt; item is the cross-instance source of truth&lt;/strong&gt;, and the stream reads it. DynamoDB was the right read plane all along; serverless just forced me to use it properly. Every committed join projects a frame; every watcher's stream sees it — across instances, across regions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JOIN → engine.join (commit to DSQL)
     → projectFrame (consistent count → DynamoDB live_frame)
     → SSE stream (reads the frame) → every browser melts in lockstep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  Settling fairly — and 126× faster
&lt;/h2&gt;

&lt;p&gt;When the timer ends, settlement reads the final count once, charges &lt;em&gt;everyone&lt;/em&gt; that one price, and writes a balanced double-entry ledger — idempotent per user, so re-running it never double-charges.&lt;/p&gt;

&lt;p&gt;My first version did per-row inserts and took &lt;strong&gt;~456 seconds&lt;/strong&gt; to settle 2,000 users across the Pacific. Unusable for a live "timer hits zero" moment. I rewrote it to set-based batched inserts that return exactly the newly-settled users (so the ledger is written once each):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;settlements&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rally_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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price_paid_minor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&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;AS&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rally_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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTHING&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- ledger rows written only for these&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same guarantees, &lt;strong&gt;3.6 seconds&lt;/strong&gt;. About 126× faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two regions, one price (the part that needs DSQL)
&lt;/h2&gt;

&lt;p&gt;Here's the payoff. The whole thing runs &lt;strong&gt;active-active across Tokyo and Seoul&lt;/strong&gt; (witness in Osaka). Read the live count from either regional endpoint and it's identical, tick-for-tick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;write Tokyo   +75 joins   Tokyo 190 $85.00   Seoul 190 $85.00   MATCH ✅  (cross-region read 143ms)
write Seoul   +30 joins   Tokyo 220 $85.00   Seoul 220 $85.00   MATCH ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kill one region mid-rally and joins keep flowing from the other, with no lost counts. One fair price, everywhere — which is the thing eventual consistency simply cannot promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  You don't have to trust me
&lt;/h2&gt;

&lt;p&gt;The best part: I built a public &lt;code&gt;/proof&lt;/code&gt; page that reconciles a real settled rally &lt;strong&gt;live&lt;/strong&gt;, straight from the database. Distinct prices paid: &lt;strong&gt;1&lt;/strong&gt;. Ledger drift: &lt;strong&gt;$0.00&lt;/strong&gt;. Double-charges: &lt;strong&gt;0&lt;/strong&gt;. With the two SQL queries shown, so you can run them yourself.&lt;/p&gt;

&lt;p&gt;That's the whole thesis, made checkable: a global crowd agreeing on one falling price in real time, and everyone winning the same deal — provable, not promised.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past me
&lt;/h2&gt;

&lt;p&gt;Three things stuck:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The cleanest way to win a "why this database?" argument is to make the database do something the obvious tool can't — then make it checkable.&lt;/strong&gt; "One falling price across regions, settled fairly" is only honest under strong consistency. The product and the database choice justify each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respect the boundary between the OLTP write truth and the read plane.&lt;/strong&gt; Every time I was tempted to fan out the live price with a DSQL aggregate scan, the right answer was the DynamoDB frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless makes you externalize state you didn't think was state.&lt;/strong&gt; The in-process emitter was the lesson.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>h0hackathon</category>
    </item>
    <item>
      <title>How I made item duplication impossible to represent in a game economy, with Aurora DSQL</title>
      <dc:creator>Kaviya Kumar</dc:creator>
      <pubDate>Mon, 29 Jun 2026 18:58:31 +0000</pubDate>
      <link>https://dev.to/kaviyakumar23/how-i-made-item-duplication-impossible-to-represent-in-a-game-economy-with-aurora-dsql-46pg</link>
      <guid>https://dev.to/kaviyakumar23/how-i-made-item-duplication-impossible-to-represent-in-a-game-economy-with-aurora-dsql-46pg</guid>
      <description>&lt;p&gt;I pointed ten thousand concurrent "trade" requests at a single legendary sword and tried every classic dupe trick at once: trade-window races, drop-and-relog, and the same item grabbed from two AWS regions at the same instant. After the dust settled I ran one SQL query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;item_instances&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;template_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;legendary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The answer was 1. Of those ten thousand attempts, 9,992 were rejected outright and the count never moved. It was always going to be 1, because of how the data is shaped. That is the whole idea, and this post is how it works.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The bug I was actually fighting
&lt;/h2&gt;

&lt;p&gt;A dupe bug is when a player ends up with two of something that should exist once. A legendary item, a stack of gold. It has happened in New World, Diablo II, RuneScape, and plenty of others. When items carry real value, a dupe is counterfeiting, and it deflates the whole economy.&lt;/p&gt;

&lt;p&gt;Every version of the bug is the same underlying problem wearing a costume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trade-window race: two trades move one item at the same time.&lt;/li&gt;
&lt;li&gt;Drop-and-relog: the item is in the world and in the inventory at once.&lt;/li&gt;
&lt;li&gt;Disconnect mid-trade: one side is credited, the other is not.&lt;/li&gt;
&lt;li&gt;Cross-region: the item appears to exist in two regions and both trade it.&lt;/li&gt;
&lt;li&gt;Gold double-spend: two debits read the same balance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are concurrency and consistency failures. So I stopped thinking about it as a game bug and started treating it as a database correctness problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: make "owned twice" unrepresentable
&lt;/h2&gt;

&lt;p&gt;I model a unique item as exactly one row, with one owner and a version number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;item_instances(instance_id PK, template_id, owner_type, owner_id, region, version, ...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every transfer (trade, drop, pickup, mail) is the same conditional update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;item_instances&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;owner_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;to_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;to_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;version&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="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;instance_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;owner_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;from_type&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;owner_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;from_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- rowCount must be 1. If it is 0, someone already moved it: abort with ITEM_MOVED.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two transfers that both think the item is at version 5, owned by A, cannot both succeed. The first to commit moves it to version 6. The second one either reads the new version and matches zero rows, or it conflicts at commit time (Aurora DSQL surfaces this as SQLSTATE 40001), retries, re-reads, and then matches zero rows. Exactly one wins. There is no row that says an item has two owners, so the demo can't produce one.&lt;/p&gt;

&lt;p&gt;I run this on Aurora DSQL. It uses optimistic concurrency and reports write conflicts at commit, which is exactly the model this guard wants. I don't hide the 40001 retries. I count and display them, because they are the system doing its job under contention.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Gold is not a sword
&lt;/h2&gt;

&lt;p&gt;A sword is unique. Gold is fungible. Forcing both into the same model would be wrong, so gold gets a different protection: balances that are sharded across rows (so a whale or a treasury is never a single hot row), a &lt;code&gt;CHECK (balance_minor &amp;gt;= 0)&lt;/code&gt; that makes overspend structurally impossible, and a balanced double-entry ledger written in the same transaction. Every move nets to zero, so the total supply is a one-line invariant.&lt;/p&gt;

&lt;p&gt;I fired 1,000 concurrent transfers out of one whale wallet. The supply before and after was identical: 600,000 minor units in, 600,000 out, with 598 OCC retries observed and zero errors. Nothing was created, nothing vanished.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardest case: two regions, one item
&lt;/h2&gt;

&lt;p&gt;The cross-region dupe is the one that scares people, because a single-region database can't help you and locks don't span regions. Aurora DSQL is active-active across regions over one logical database, so I could test the real thing on a peered cluster: Tokyo (ap-northeast-1) and Seoul (ap-northeast-2).&lt;/p&gt;

&lt;p&gt;I sent trades for the same legendary to both endpoints at the same time. Out of 482 simultaneous cross-region grabs, 482 were blocked. The count stayed 1. There is no replication-lag window to exploit, because both endpoints serialize against one source of truth.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Seeing the bug, then seeing it gone
&lt;/h2&gt;

&lt;p&gt;The part that made the idea click for people was a side-by-side. I built a second, deliberately broken model: a plain inventory table with no version guard, the way a naive service might track ownership. Under a trade race, two concurrent transfers both read "A owns it" and both write a new owner, so the one legendary ends up in roughly 20 inventories. Real rows, countable with SQL.&lt;/p&gt;

&lt;p&gt;Then I ran the same race through the kernel. The authoritative count stayed 1.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The proof, and the honest part
&lt;/h2&gt;

&lt;p&gt;The whole project comes down to a reconcile step that runs the invariants as live SQL: legendary count is 1, no instance is owned twice, every item has one owner, gold supply equals what was minted, ledger drift is 0, every transaction balances, no balance is negative. All of them hold. I also wrote 12 unit tests for the pure logic (conservation, the idempotency hash, the sharding math, the backoff bounds) and they run green in CI.&lt;/p&gt;

&lt;p&gt;The honest caveat: these guarantees hold as long as every economic action goes through the kernel. If a game has an admin minting tool or a code path that writes ownership directly, that path is outside the guarantee. Duped is the economy and trade settlement layer (trades, drops, mail, auction house, cross-region transfers), not the real-time combat loop. I would rather state that plainly than imply it covers everything.&lt;/p&gt;

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

&lt;p&gt;The thing I keep coming back to: I spent less effort "preventing" dupes and more effort making them impossible to write down. Correctness that lives in the schema doesn't depend on remembering to check. The 40001 retries felt scary at first, then became the most reassuring signal in the system, because they are proof that two writers tried to touch one row and only one was allowed through.&lt;/p&gt;

&lt;p&gt;I also learned to show the bug before showing the fix. The version guard is a few lines of SQL. It only lands emotionally once you have watched the same attack duplicate an item in the table next to it.&lt;/p&gt;

&lt;p&gt;The stack: Aurora DSQL as the truth core, DynamoDB as the live read model fed by a transactional outbox, Next.js on Vercel for the world. No foreign keys (DSQL doesn't have them), async indexes, money as BIGINT minor units, IAM/OIDC auth so there are no passwords in code.&lt;/p&gt;

</description>
      <category>h0hackathon</category>
    </item>
    <item>
      <title>Most AI Apps Return Text. DreamLoom Returns a Living Storybook - With Voice, Illustrations, and Music in Real Time.</title>
      <dc:creator>Kaviya Kumar</dc:creator>
      <pubDate>Mon, 16 Mar 2026 22:16:31 +0000</pubDate>
      <link>https://dev.to/kaviyakumar23/most-ai-apps-return-text-dreamloom-returns-a-living-storybook-with-voice-illustrations-and-1koa</link>
      <guid>https://dev.to/kaviyakumar23/most-ai-apps-return-text-dreamloom-returns-a-living-storybook-with-voice-illustrations-and-1koa</guid>
      <description>&lt;p&gt;&lt;em&gt;How DreamLoom turns a spoken conversation into a living storybook — with native interleaved text+image, real-time interruption, sketch-to-scene camera input, and a cinematic trailer assembled entirely in the browser.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This content was created for my Gemini Live Agent Challenge submission.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://getdreamloom.com" rel="noopener noreferrer"&gt;Try it live: https://getdreamloom.com&lt;/a&gt;&lt;/strong&gt; | &lt;strong&gt;&lt;a href="https://github.com/kaviyakumar23/dreamloom" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem (in 3 sentences)
&lt;/h2&gt;

&lt;p&gt;There are billions of people with stories they'll never write down. Not because they lack imagination — because writing takes craft, illustration takes training, and video takes editing software. Current AI tools don't fix this: you type a prompt, get a wall of text, paste it into an image generator, and stitch the result together manually. There is no conversation. No surprise. No creative partnership.&lt;/p&gt;

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

&lt;p&gt;DreamLoom is a voice-first AI story studio. You speak to &lt;strong&gt;Loom&lt;/strong&gt; — an AI creative director with personality, opinions, and creative taste — and watch illustrated scenes materialize in real time. Narration and original illustrations arrive interleaved in a single Gemini API response. Music shifts with the mood. You can interrupt mid-sentence to redirect the story, hold up a pencil sketch for the AI to incorporate, and track characters and continuity through a live Story Bible. When the story is complete, DreamLoom packages everything into a cinematic trailer, a Storybook PDF, and a downloadable image archive — all assembled client-side, no server-side rendering.&lt;/p&gt;

&lt;p&gt;Five Gemini models work in concert within a single creative session. No other app I'm aware of orchestrates this many Gemini capabilities in one unified experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who It's Built For
&lt;/h2&gt;

&lt;p&gt;DreamLoom is designed for people who think in voice, not prompts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Families.&lt;/strong&gt; A five-year-old says "tell me about a shy hedgehog who's scared their spines will hurt everyone" and, five minutes later, has a fully illustrated, narrated storybook they helped create. Kid-safe mode is on by default — not as a toggle buried in settings, but as a core design principle. Loom never generates violent, scary, or mature content. When a child asks for "blood everywhere," Loom redirects with a creative alternative: &lt;em&gt;"How about a shadow that turns out to be friendly?"&lt;/em&gt; The guardrail feels like creative direction, not censorship. No typing, no reading required. A child who can speak can direct a story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teachers.&lt;/strong&gt; A teacher says "create an educational adventure about the water cycle" and the class watches an illustrated story unfold in real time. Students can interrupt to add ideas. The Story Bible tracks continuity. The Director's Cut becomes a class artifact that can be exported as a PDF or shared to the public gallery. DreamLoom ships with guided templates — "Bedtime Adventure," "Learning Quest," "Sketch Catalyst" — so teachers can launch into a high-quality first scene without setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-writers with vivid imaginations.&lt;/strong&gt; People who have stories but don't write. They can speak a narrative into existence, interrupt to change direction, hold up a napkin sketch for the AI to incorporate, and walk away with a multimedia storybook they couldn't have created alone. Creative ownership without creative skill.&lt;/p&gt;

&lt;p&gt;The common thread: &lt;strong&gt;the target user has never typed a prompt.&lt;/strong&gt; DreamLoom's entire interface is voice-first. There is no prompt box. No "generate" button. You talk, and the story responds.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Demo: Voice Start → Living Storybook (6 Steps)
&lt;/h2&gt;

&lt;p&gt;Here's exactly what happens when you use DreamLoom. Every claim below is reproducible at &lt;a href="https://getdreamloom.com" rel="noopener noreferrer"&gt;getdreamloom.com&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: You Speak — Loom Listens and Directs
&lt;/h3&gt;

&lt;p&gt;You click "Begin Your Story" and say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Let's create a story about a young mapmaker named Mira who discovers her maps are portals to the places she draws."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Loom doesn't just start generating. It responds with voice — warm, theatrical, opinionated. It asks about visual style and narrator tone conversationally: &lt;em&gt;"What kind of look are you going for — watercolor, comic book? And should I narrate warmly, or more like a mystery?"&lt;/em&gt; You answer. Loom registers Mira as a character with visual descriptions for illustration consistency, sets the style, and generates Scene 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What appears isn't text followed by an image.&lt;/strong&gt; It's interleaved: a paragraph, then an illustration, then another paragraph, then another painting — narrative and art woven together in a single API response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt; Open the Debug Panel. It shows &lt;code&gt;response_modalities: ["TEXT","IMAGE"]&lt;/code&gt; and the exact part order: &lt;code&gt;0:text, 1:image, 2:text, 3:image&lt;/code&gt;. This is native interleaved output — not two API calls stitched together.&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%2Fff84ncxdrf6b0gj1c4ct.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%2Fff84ncxdrf6b0gj1c4ct.png" alt=" " width="666" height="1106"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: You Interrupt — Loom Pivots Instantly
&lt;/h3&gt;

&lt;p&gt;Mid-sentence, while Loom is still talking, you say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Wait — make it nighttime. With glowing mushrooms."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Loom's audio cuts instantly. No lag. It pivots: &lt;em&gt;"Oh, even better!"&lt;/em&gt; A new scene generates with atmospheric night lighting. The music shifts from wonder to mystery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; When the Live API fires a barge-in event, the frontend disconnects the GainNode to instantly silence all scheduled audio sources — keeping the AudioContext alive (no 50-100ms recreation penalty). The agent receives the interruption signal and adapts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;📸 [GIF: User interrupts → "Interrupted" flash → Loom pivots → new nighttime scene generates]&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: You Show a Sketch — Loom Incorporates It
&lt;/h3&gt;

&lt;p&gt;You hold up a pencil sketch of an owl wearing tiny glasses:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I drew the owl librarian — let me show you."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The webcam captures it at 1 fps, sends the JPEG to Loom, who describes what it sees and incorporates the concept into the next scene — not a pixel copy, but a faithful interpretation rendered in the story's established art style.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt; The physical sketch and the generated scene side by side. Same character concept, different medium. The AI maintains style consistency with earlier scenes while incorporating new visual input.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;📸 [Side-by-side: Physical pencil sketch ↔ AI-generated scene with owl librarian in story's art style]&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: You Test Memory — Loom Remembers
&lt;/h3&gt;

&lt;p&gt;Three scenes in, Mira has gained a copper compass in Scene 1, befriended the owl librarian in Scene 2, and entered a glowing cave in Scene 3. You ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Does Mira still have the compass? And is the owl with her?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Loom answers without hesitation: &lt;em&gt;"She does — and the owl's been perched on her satchel since they left the library."&lt;/em&gt; It doesn't just recall names. It tracks which items a character acquired, which characters are present in the current scene, and how the world state has evolved across the story.&lt;/p&gt;

&lt;p&gt;The Story Bible is a live sidebar tracking every character (with visual descriptions for illustration consistency), world settings, mood, and plot continuity across all scenes. When Loom generates a new scene, it injects character descriptions and continuity notes into the prompt so illustrations stay consistent — Mira looks like Mira, the compass looks like the compass. This isn't retrieval from a vector database. It's structured state maintained in real-time by the agent's &lt;code&gt;add_character&lt;/code&gt; and &lt;code&gt;get_story_context&lt;/code&gt; tool calls.&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%2Fbjvwmx28cfsaw7g2ptgk.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%2Fbjvwmx28cfsaw7g2ptgk.png" alt=" " width="762" height="1876"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Kid-Safe Guardrails — Creative Redirection, Not Censorship
&lt;/h3&gt;

&lt;p&gt;You test the boundaries:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Make it super scary with blood everywhere."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Loom redirects gently: &lt;em&gt;"Let's keep it spooky-mystical instead — how about a shadow that turns out to be friendly?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the core design principle, not an afterthought. Kid-safe mode is on by default for every session. The system prompt instructs Loom to redirect inappropriate requests with imaginative alternatives, not flat refusals. No violent imagery. No mature themes. No horror. The youngest user — a five-year-old creating a bedtime story with a parent — is the design target. Every content guardrail is built to feel like creative direction: &lt;em&gt;"I have an even better idea..."&lt;/em&gt; rather than &lt;em&gt;"I can't do that."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The kid-safe toggle is visible in the status bar, and a parent or teacher can verify it's active at any time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: The Director's Cut — From Conversation to Cinema
&lt;/h3&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"That feels like a good ending. Can I see the Director's Cut?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Loom generates a cover image, a logline, and trailer narration text. DreamLoom assembles everything into a cinematic experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cover art&lt;/strong&gt; — generated to match the story's visual style&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cinematic trailer&lt;/strong&gt; — Ken Burns camera movement on each scene, AI-narrated voiceover via Gemini TTS, per-scene music with crossfades, letterbox framing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storybook PDF&lt;/strong&gt; — book-style layout with title page and per-scene pages featuring interleaved text and illustrations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scene image ZIP&lt;/strong&gt; — all generated artwork, downloadable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trailer is assembled entirely client-side: Canvas renders at 1280x720 and 30fps, Web Audio API mixes narration and music, MediaRecorder captures as VP9 WebM at 3Mbps. No FFmpeg. No server-side video processing. 720 lines of &lt;code&gt;useAnimatic.ts&lt;/code&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%2Flxt7k6slzb4zc76g2rxs.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%2Flxt7k6slzb4zc76g2rxs.png" alt=" " width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Beyond the Session: Gallery, Resume, and Guided Starts
&lt;/h3&gt;

&lt;p&gt;DreamLoom isn't a one-shot demo. Stories persist.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session resume&lt;/strong&gt; — Close the tab and come back later. Your story is saved to Firestore and appears in "Your Stories" on the landing page. Pick up exactly where you left off — all scenes, characters, and Story Bible state intact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public gallery&lt;/strong&gt; — Publish your finished story to a shared gallery. Other users can browse and read published storybooks. This turns DreamLoom from a tool into a community.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guided templates&lt;/strong&gt; — Not sure where to start? Choose from pre-built launch paths: "Bedtime Adventure" (family-friendly with a gentle arc), "Learning Quest" (classroom-ready educational story), or "Sketch Catalyst" (build a narrative around visual concepts). Each template sends an opening prompt to Loom so users get a high-quality first scene immediately.&lt;/li&gt;
&lt;/ul&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%2Fq2o21p6n533dog2l0uqh.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%2Fq2o21p6n533dog2l0uqh.png" alt=" " width="800" height="275"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Two Models, One Creative Session
&lt;/h2&gt;

&lt;p&gt;DreamLoom's core insight is a &lt;strong&gt;two-model architecture&lt;/strong&gt;: one model for conversation, a different model for creation.&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%2F6ggsdj9piue3lwrf9pqt.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%2F6ggsdj9piue3lwrf9pqt.png" alt=" " width="800" height="747"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Conversation Model (Gemini Live API)
&lt;/h3&gt;

&lt;p&gt;Handles real-time bidirectional voice. User audio streams in at 16 kHz PCM via an AudioWorklet. Agent voice streams back at 24 kHz PCM. Barge-in detection is native to the Live API. The model runs through Google ADK &lt;code&gt;run_live()&lt;/code&gt; with a &lt;code&gt;LiveRequestQueue&lt;/code&gt; — a single persistent connection for the duration of the session.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Scene Model (Gemini Interleaved Output)
&lt;/h3&gt;

&lt;p&gt;When Loom decides it's time for a scene, the agent calls the &lt;code&gt;create_scene&lt;/code&gt; tool, which dispatches to a second Gemini model (&lt;code&gt;gemini-2.5-flash-image&lt;/code&gt;) with &lt;code&gt;response_modalities=["TEXT","IMAGE"]&lt;/code&gt;. The response arrives as interleaved blocks — paragraphs and illustrations woven together — not sequential text-then-image.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bridge
&lt;/h3&gt;

&lt;p&gt;A single Director Agent named Loom connects the two models through six callable tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create_scene()         → Interleaved text+image generation
generate_music()       → Lyria RealTime streaming (+ CC0 fallback)
create_directors_cut() → Cover + logline + trailer narration
set_story_metadata()   → Title, genre, art style, narrator voice
add_character()        → Character registry with visual descriptions
get_story_context()    → Story Bible for continuity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent decides when to call each tool based on conversational context — not hard-coded triggers. Scene generation runs as a background &lt;code&gt;asyncio.create_task&lt;/code&gt; to avoid blocking the Live API connection (which would cause 1011 timeout errors).&lt;/p&gt;

&lt;h3&gt;
  
  
  Five Models in Concert
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Voice conversation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash-native-audio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Real-time bidi voice via Live API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scene generation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Interleaved text+image in one response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Music composition&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lyria-realtime-exp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;48 kHz stereo AI music, streamed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trailer narration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash-preview-tts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Styled voice narration for the Director's Cut&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio transcription&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transcribes buffered audio on reconnect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Data Flow (One Turn)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User voice (16kHz PCM)
  → AudioWorklet → WebSocket (binary) → LiveRequestQueue
  → Gemini Live API (conversation model)
  → Agent decides: call create_scene()
  → SceneGenerator → Gemini Interleaved API
  → response: [text, image, text, image, ...]
  → images saved to GCS → notification queue
  → WebSocket (JSON) → React StoryCanvas renders
  → Agent voice: "There we go! What do you think?"
  → WebSocket (binary PCM) → Web Audio playback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Technical Decisions That Mattered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Two Models Instead of One?
&lt;/h3&gt;

&lt;p&gt;The Gemini Live API excels at real-time voice conversation but doesn't support interleaved image output. The interleaved model (&lt;code&gt;gemini-2.5-flash-image&lt;/code&gt;) generates beautiful text+image scenes but doesn't support bidirectional audio streaming. Neither can do the other's job.&lt;/p&gt;

&lt;p&gt;The bridge — an ADK agent with tools — means Loom has a voice that responds in real time, AND the ability to generate illustrated scenes when the moment is right. The creative direction happens in voice; the creation happens in a separate, purpose-built model call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Client-Side Video Assembly?
&lt;/h3&gt;

&lt;p&gt;The Director's Cut trailer could have been assembled server-side with FFmpeg. I chose client-side for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero server cost&lt;/strong&gt; — video encoding is CPU-intensive. With client-side assembly, the server never touches video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero latency&lt;/strong&gt; — no upload/download round-trip. The video renders directly from images already cached in the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No dependency&lt;/strong&gt; — FFmpeg is a deployment headache. Canvas + Web Audio + MediaRecorder are native browser APIs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tradeoff: browser throttling can affect quality in background tabs. I use &lt;code&gt;setInterval&lt;/code&gt; instead of &lt;code&gt;requestAnimationFrame&lt;/code&gt; to mitigate this, but it's not perfect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why AudioWorklet + Gain-Node Flush?
&lt;/h3&gt;

&lt;p&gt;For barge-in to feel instant, audio must stop immediately when the user interrupts. The standard approach — close the AudioContext and create a new one — adds 50-100ms of silence. Instead, I disconnect the GainNode to instantly silence all scheduled audio sources, create a fresh one, and keep the AudioContext alive. The interruption is imperceptible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Firestore With Graceful Degradation?
&lt;/h3&gt;

&lt;p&gt;Firestore stores sessions and the public gallery, but if Firestore is unavailable (no GCP project configured, or service down), everything still works. Sessions persist in memory. Gallery features degrade to disabled. This means the app runs locally with zero cloud dependencies beyond the Gemini API key.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenges I Solved (And What I Learned)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: Voice Barge-In Was Unusable Out of the Box
&lt;/h3&gt;

&lt;p&gt;The default VAD settings for the Gemini Live API were too aggressive for a creative storytelling app. Background music, natural pauses, and ambient noise all triggered false barge-ins — cutting Loom mid-sentence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five problems, five solutions:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Over-sensitive VAD&lt;/strong&gt; — Configured &lt;code&gt;AutomaticActivityDetection&lt;/code&gt; with &lt;code&gt;start_of_speech_sensitivity=LOW&lt;/code&gt;, &lt;code&gt;end_of_speech_sensitivity=LOW&lt;/code&gt;, &lt;code&gt;silence_duration_ms=1200&lt;/code&gt; (default ~500ms was cutting people off mid-thought), and &lt;code&gt;proactive_audio=True&lt;/code&gt; to ignore incidental sounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No visibility into what was heard&lt;/strong&gt; — Enabled both &lt;code&gt;input_audio_transcription&lt;/code&gt; and &lt;code&gt;output_audio_transcription&lt;/code&gt;. The Debug Panel now shows a scrollable transcript log with timestamped entries for user and agent speech. Debugging "why did it interrupt?" went from guesswork to trivial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. No recovery from false interrupts&lt;/strong&gt; — This was the most nuanced fix. When the Live API fires an &lt;code&gt;interrupted&lt;/code&gt; event, I don't immediately accept it. I start a 400ms verification window:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If user speech arrives → real interruption, proceed normally&lt;/li&gt;
&lt;li&gt;If no speech arrives → false interrupt (noise/music), inject a system message telling Loom to continue from where it left off (with the last ~150 characters as a resume hint)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A cough or chair scrape briefly pauses Loom, but it picks right back up naturally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. No manual fallback&lt;/strong&gt; — Added optional push-to-talk mode alongside auto-detection. PTT sends &lt;code&gt;activity_start&lt;/code&gt;/&lt;code&gt;activity_end&lt;/code&gt; signals to the Live API as additional hints, not a replacement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Music caused self-interruption&lt;/strong&gt; — The agent's own audio was feeding back into the mic. Implemented three-tier music ducking:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Music Volume&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mic active&lt;/td&gt;
&lt;td&gt;0% (muted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent speaking&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Volume transitions use a smooth 200ms ramp instead of jarring instant cuts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Voice UX in a creative app isn't "pipe audio to the API." The gap between a working demo and a usable product lives in VAD sensitivity, false-positive recovery, graceful degradation to manual control, and ensuring your own output doesn't fight your input.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: Live API Drops Connections Mid-Story
&lt;/h3&gt;

&lt;p&gt;The Gemini Live API occasionally disconnects with codes 1008/1011. In a storytelling app, losing the conversation three scenes in is catastrophic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution: Reconnect with full context restoration.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Buffer user and agent audio in rolling 15-second ring buffers (~480KB each)&lt;/li&gt;
&lt;li&gt;On disconnect: save session to Firestore, create fresh ADK session&lt;/li&gt;
&lt;li&gt;Transcribe both audio buffers using Gemini Flash&lt;/li&gt;
&lt;li&gt;Re-inject the full story state: conversation history, Story Bible, scene summaries, art style&lt;/li&gt;
&lt;li&gt;Suppress the agent's greeting on reconnect (mute the first turn)&lt;/li&gt;
&lt;li&gt;Resume — Loom picks up exactly where it left off&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Up to 5 retries with exponential backoff (1s, 2s, 4s, 8s). The user sees a brief "reconnecting" banner. The story doesn't break.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 3: Scene Generation Blocks the Live API
&lt;/h3&gt;

&lt;p&gt;Interleaved scene generation takes 10-30 seconds. If the &lt;code&gt;create_scene&lt;/code&gt; tool blocks the event loop, the Live API connection times out (error 1011) because it expects regular keepalive signals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; All generation runs as background &lt;code&gt;asyncio.create_task()&lt;/code&gt;. The tool returns immediately to the agent ("Scene is being generated..."), and the agent fills the silence with conversation: &lt;em&gt;"This one's going to be gorgeous — while we wait, do you have any ideas for what happens next?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Results push to an &lt;code&gt;asyncio.Queue(maxsize=100)&lt;/code&gt; that a separate drain task broadcasts over WebSocket. The Live API connection never stalls.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes This Different From a Chatbot
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chatbot&lt;/th&gt;
&lt;th&gt;DreamLoom&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Returns text&lt;/td&gt;
&lt;td&gt;Returns interleaved text+image+music in real time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turn-based: prompt → response → prompt&lt;/td&gt;
&lt;td&gt;Continuous: speak, interrupt, redirect, watch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No memory&lt;/td&gt;
&lt;td&gt;Story Bible tracks characters, world, continuity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No output packaging&lt;/td&gt;
&lt;td&gt;Cinematic trailer, Storybook PDF, image ZIP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic AI personality&lt;/td&gt;
&lt;td&gt;Loom: a creative director with taste, opinions, pacing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type to interact&lt;/td&gt;
&lt;td&gt;Speak to interact. The target user has never typed a prompt.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Debug Panel is built into the product — not as a dev tool, but as proof. Judges (or users) can open it at any time to see the model name, &lt;code&gt;response_modalities&lt;/code&gt;, part order, and generation time for every scene. No other submission I've seen builds native interleaved output proof directly into the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI + WebSocket, Python 3.12, deployed on Cloud Run (us-central1)&lt;br&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React 19 + Vite + TypeScript + TailwindCSS v4 + Framer Motion&lt;br&gt;
&lt;strong&gt;Agent:&lt;/strong&gt; Google ADK (&lt;code&gt;google-adk&lt;/code&gt;) with &lt;code&gt;run_live()&lt;/code&gt; + &lt;code&gt;LiveRequestQueue&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Models:&lt;/strong&gt; Gemini Live API, Gemini Interleaved, Lyria RealTime, Gemini TTS, Gemini Flash&lt;br&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; GCS (media), Firestore (sessions + gallery)&lt;br&gt;
&lt;strong&gt;Exports:&lt;/strong&gt; jsPDF (Storybook PDF), JSZip (image ZIP), Canvas+MediaRecorder (WebM animatic)&lt;br&gt;
&lt;strong&gt;Deployment:&lt;/strong&gt; Cloud Run + Cloud Build + Artifact Registry, automated via &lt;code&gt;infra/deploy.sh&lt;/code&gt;&lt;/p&gt;




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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multi-voice narration&lt;/strong&gt; — Different Gemini TTS voices per character in the trailer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scene branching UI&lt;/strong&gt; — Visual tree view for "What If?" alternate story paths&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaborative voice&lt;/strong&gt; — Let multiple users speak into the same story session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ePub export&lt;/strong&gt; — Alongside PDF, export for e-readers with chapter structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt replay&lt;/strong&gt; — Record the full voice conversation alongside the generated story&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://getdreamloom.com" rel="noopener noreferrer"&gt;https://getdreamloom.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/kaviyakumar23/dreamloom" rel="noopener noreferrer"&gt;https://github.com/kaviyakumar23/dreamloom&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo video:&lt;/strong&gt; &lt;a href="https://youtu.be/MqQGBMxRV2I" rel="noopener noreferrer"&gt;https://youtu.be/MqQGBMxRV2I&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevPost:&lt;/strong&gt; &lt;a href="https://devpost.com/software/dreamloom" rel="noopener noreferrer"&gt;https://devpost.com/software/dreamloom&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This content was created for my Gemini Live Agent Challenge submission.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;DreamLoom is built for the &lt;a href="https://geminiliveagentchallenge.devpost.com/" rel="noopener noreferrer"&gt;Gemini Live Agent Challenge&lt;/a&gt; hackathon, Creative Storyteller category.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>geminiliveagentchallenge</category>
      <category>gemini</category>
      <category>creativestoryteller</category>
    </item>
  </channel>
</rss>
