<?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>Forem: Kamil Uhryn</title>
    <description>The latest articles on Forem by Kamil Uhryn (@kamil_uhryn).</description>
    <link>https://forem.com/kamil_uhryn</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%2F3516340%2F1e2a759e-a639-402b-99a5-8b5f59b17fda.jpg</url>
      <title>Forem: Kamil Uhryn</title>
      <link>https://forem.com/kamil_uhryn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kamil_uhryn"/>
    <language>en</language>
    <item>
      <title>Building TouchSlides: A Real-Time Presentation Remote on Cloudflare's Edge</title>
      <dc:creator>Kamil Uhryn</dc:creator>
      <pubDate>Mon, 10 Nov 2025 19:27:54 +0000</pubDate>
      <link>https://forem.com/kamil_uhryn/building-touchslides-a-real-time-presentation-remote-on-cloudflares-edge-1iee</link>
      <guid>https://forem.com/kamil_uhryn/building-touchslides-a-real-time-presentation-remote-on-cloudflares-edge-1iee</guid>
      <description>&lt;p&gt;&lt;strong&gt;How I built a globally-distributed, lightning-fast presentation remote control app using Cloudflare Workers, Durable Objects, and Next.js&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full disclosure:&lt;/strong&gt; This was my first time diving deep into Cloudflare's edge platform. I'd only used their free Web Analytics before and was genuinely curious - could the "edge" really be &lt;em&gt;that&lt;/em&gt; fast? What started as a weekend side project to scratch my own itch turned into a surprisingly polished piece of software that I now use for all my presentations. Spoiler alert: the performance claims are real. 🚀&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Problem That Started It All 🎯
&lt;/h2&gt;

&lt;p&gt;We've all been there. You're about to give an important presentation. Your laptop is connected to the projector, slides are ready, but you're stuck behind a podium, tethered to your computer by a clicker that barely works. You can't walk around, engage with your audience, or check your speaker notes without awkwardly turning away from them.&lt;/p&gt;

&lt;p&gt;I wanted something better: &lt;strong&gt;a presentation remote that works on any phone, from anywhere in the world, with zero lag&lt;/strong&gt;. No apps to install, no Bluetooth pairing, no proximity requirements. Just open a URL on your phone and control your presentation in real-time.&lt;/p&gt;

&lt;p&gt;That's how &lt;strong&gt;TouchSlides&lt;/strong&gt; was born.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vision: Edge-First, Real-Time, Global 🌍
&lt;/h2&gt;

&lt;p&gt;From day one, I had three non-negotiable requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sub-100ms latency&lt;/strong&gt; - Slide changes must feel instant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works anywhere&lt;/strong&gt; - No geographic limitations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero setup&lt;/strong&gt; - Share a PIN, scan a QR code, done&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Traditional architectures wouldn't cut it. A centralized server would add latency for global users. WebSockets from a single origin would struggle with scale. I needed something distributed by default, running as close to users as possible.&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;Cloudflare Workers&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cloudflare's Edge Network? 🚀
&lt;/h2&gt;

&lt;p&gt;Cloudflare Workers run your code in &lt;strong&gt;over 300+ data centers worldwide&lt;/strong&gt;. When someone in Tokyo uses TouchSlides, their requests hit Tokyo servers. When someone in London connects, they hit London servers. No cross-continental roundtrips. No single points of failure.&lt;/p&gt;

&lt;p&gt;But here's the really clever part: &lt;strong&gt;Durable Objects&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real-Time Sync Challenge 🔄
&lt;/h3&gt;

&lt;p&gt;WebSockets are stateful. You need a server that stays alive and remembers connections. But Workers are stateless by design - they spin up, handle a request, and disappear. How do you maintain persistent WebSocket connections?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durable Objects&lt;/strong&gt; solve this beautifully. They're stateful objects that live on the edge, with guaranteed single-instance consistency. Think of them as mini-servers that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintain WebSocket connections&lt;/li&gt;
&lt;li&gt;Store state in memory&lt;/li&gt;
&lt;li&gt;Live in the nearest data center to your users&lt;/li&gt;
&lt;li&gt;Automatically handle failover&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how my &lt;code&gt;PresentationSession&lt;/code&gt; Durable Object works:&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;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PresentationSession&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DurableObjectState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&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="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;remotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;WebSocket&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;currentSlide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DurableObjectState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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;url&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 'presenter' or 'remote'&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userId&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;pair&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;WebSocketPair&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="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="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pair&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;accept&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;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;presenter&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;presenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;presenter_connected&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remotes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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;server&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remote_connected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="p"&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;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&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;event&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;message&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleMessage&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;role&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;webSocket&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="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;handleMessage&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="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;senderRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch &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="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slide_change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSlide&lt;/span&gt; &lt;span class="o"&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;slideIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slide_change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
          &lt;span class="na"&gt;slideIndex&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;slideIndex&lt;/span&gt; 
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;senderRole&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next_slide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSlide&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slide_change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;slideIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSlide&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="c1"&gt;// ... more message types&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;broadcast&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="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;excludeRole&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;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;stringify&lt;/span&gt;&lt;span class="p"&gt;({&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="na"&gt;timestamp&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="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;excludeRole&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;presenter&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&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="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;excludeRole&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remote&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remotes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&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="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;What makes this special?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automatic proximity&lt;/strong&gt; - Cloudflare routes connections to the nearest Durable Object instance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero coordination&lt;/strong&gt; - Each presentation has exactly one Durable Object, guaranteed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant broadcast&lt;/strong&gt; - All connected remotes get updates simultaneously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault tolerance&lt;/strong&gt; - If a data center fails, Cloudflare migrates the object&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Architecture: Four Layers Working in Harmony 🏗️
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Layer 1: The Frontend (Next.js 14 + App Router) 🎨
&lt;/h3&gt;

&lt;p&gt;I chose Next.js 14 with the new App Router for its server component capabilities and edge runtime support. The UI is built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; for rapid, consistent styling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framer Motion&lt;/strong&gt; for smooth animations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lucide React&lt;/strong&gt; for beautiful icons&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/                    → Landing page
/dashboard           → List of presentations
/edit/[id]           → Upload slides, add notes
/present/[id]        → Full-screen presenter view
/remote/[id]         → Mobile remote controller
/p/[pin]             → Public share page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design philosophy: &lt;strong&gt;Dark gradients, glass morphism, rounded corners&lt;/strong&gt;. I wanted it to feel premium-like Apple's attention to detail combined with Headspace's calming aesthetics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example: The remote controller&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RemotePage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&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;presentation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPresentation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentSlide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCurrentSlide&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="mi"&gt;0&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;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setWs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;websocket&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;WebSocket&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;WORKER_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/ws/presentation/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&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="s2"&gt;?role=remote&amp;amp;userId=&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="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;message&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &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;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slide_change&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="nf"&gt;setCurrentSlide&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;slideIndex&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;setWs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;websocket&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;params&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;goToNextSlide&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="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next_slide&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mobile-remote"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;goToNextSlide&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Next →&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;presentation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentSlide&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 2: The Worker (Hono + TypeScript) ⚙️
&lt;/h3&gt;

&lt;p&gt;Cloudflare Workers are JavaScript/TypeScript functions that run on every request. I used &lt;strong&gt;Hono&lt;/strong&gt; - a blazingly fast web framework (think Express, but for the edge).&lt;/p&gt;

&lt;p&gt;My worker handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; (JWT tokens)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST API&lt;/strong&gt; (presentations, slides, users)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket upgrades&lt;/strong&gt; (routing to Durable Objects)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static file serving&lt;/strong&gt; (images from R2)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Main worker entry point&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;Hono&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;hono&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;cors&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;hono/cors&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;authRouter&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;./routes/auth&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;presentationsRouter&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;./routes/presentations&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;slidesRouter&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;./routes/slides&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;websocketRouter&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;./routes/websocket&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="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Hono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Bindings&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// CORS middleware&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;cors&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;origin&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;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Validated against ALLOWED_ORIGINS&lt;/span&gt;
  &lt;span class="na"&gt;credentials&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="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// API routes&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;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authRouter&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;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/presentations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;presentationsRouter&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;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/slides&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slidesRouter&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;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/ws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;websocketRouter&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;default&lt;/span&gt; &lt;span class="nx"&gt;app&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;Why Hono over vanilla Workers?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type-safe routing with TypeScript&lt;/li&gt;
&lt;li&gt;Middleware composition&lt;/li&gt;
&lt;li&gt;Request validation with Zod&lt;/li&gt;
&lt;li&gt;Cleaner code organization&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer 3: Data Storage (D1 + R2 + KV) 💾
&lt;/h3&gt;

&lt;p&gt;Cloudflare offers three storage primitives, each optimized for different use cases:&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;D1 (SQLite at the Edge)&lt;/strong&gt; 🗄️
&lt;/h4&gt;

&lt;p&gt;Relational database for structured data. My schema:&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;users&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;TEXT&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;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&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;password_hash&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;name&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;created_at&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&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;presentations&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;TEXT&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;user_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;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;pin&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&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;-- 6-digit PIN for public access&lt;/span&gt;
  &lt;span class="n"&gt;transition_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'fade'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;share_enabled&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&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;share_notes&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;created_at&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&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;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&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="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&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;slides&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;TEXT&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;presentation_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;slide_key&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;-- R2 object key&lt;/span&gt;
  &lt;span class="n"&gt;thumbnail_key&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;notes&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;order_index&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="k"&gt;FOREIGN&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;presentation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;presentations&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="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&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;Key design decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PIN system&lt;/strong&gt; - Every presentation gets a unique 6-digit PIN for easy sharing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cascading deletes&lt;/strong&gt; - Delete a presentation, slides go too&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order index&lt;/strong&gt; - Enables drag-and-drop reordering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share settings&lt;/strong&gt; - Control public access and add custom notes&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;R2 (Object Storage)&lt;/strong&gt; 📦
&lt;/h4&gt;

&lt;p&gt;S3-compatible storage for slides and thumbnails. &lt;strong&gt;Zero egress fees&lt;/strong&gt; (huge cost saver).&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;// Upload slide to R2&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StorageService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;uploadSlide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;key&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;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nx"&gt;contentType&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="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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRESENTATIONS_BUCKET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;httpMetadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getSlide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&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;object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRESENTATIONS_BUCKET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;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;Slide not found&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpMetadata&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;KV (Key-Value Store)&lt;/strong&gt; ⚡
&lt;/h4&gt;

&lt;p&gt;Ultra-fast, eventually-consistent storage for sessions and cache.&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;// Session management&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SessionService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createSession&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&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="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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SESSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`session:&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expirationTtl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// 30 days&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;Why this three-tier approach?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;D1&lt;/strong&gt; for queries and relations (user → presentations → slides)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;R2&lt;/strong&gt; for binary data (images, future: PDFs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KV&lt;/strong&gt; for hot data (sessions, active connections)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer 4: Real-Time Sync (WebSockets + Durable Objects) 🔌
&lt;/h3&gt;

&lt;p&gt;This is where the magic happens. When a presenter advances a slide:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Presenter's browser sends WebSocket message → Durable Object&lt;/li&gt;
&lt;li&gt;Durable Object updates state → broadcasts to all remotes&lt;/li&gt;
&lt;li&gt;Remote phones receive update → UI updates instantly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Latency breakdown:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser → Edge: ~10-30ms (local network)&lt;/li&gt;
&lt;li&gt;Edge → Durable Object: &amp;lt;5ms (same data center)&lt;/li&gt;
&lt;li&gt;Durable Object → Remotes: ~10-30ms (parallel broadcast)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total: 25-65ms&lt;/strong&gt; - faster than human reaction time!&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: Four Layers Working in Harmony 🏗️
&lt;/h2&gt;

&lt;p&gt;Compare this to traditional WebSocket servers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser → Server: 50-200ms (geographic distance)&lt;/li&gt;
&lt;li&gt;Server processing: 10-50ms&lt;/li&gt;
&lt;li&gt;Server → Clients: 50-200ms per client (sequential)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total: 110-450ms&lt;/strong&gt; - noticeable lag.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Layer 🔒
&lt;/h2&gt;

&lt;p&gt;Authentication is JWT-based with bcrypt password hashing:&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;// Signup&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;signup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&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;password&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;name&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;passwordHash&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;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateId&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&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 users (id, email, password_hash, name) VALUES (?, ?, ?, ?)&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;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;passwordHash&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="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;jose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SignJWT&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;email&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProtectedHeader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HS256&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="nf"&gt;setExpirationTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;30d&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="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&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;token&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;id&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;email&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="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Auth middleware&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&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="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &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="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;token&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&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;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;401&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="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;jose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jwtVerify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&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;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&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;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userId&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="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&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="k"&gt;return&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;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&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;Invalid token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;401&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;Security features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bcrypt hashing (10 rounds)&lt;/li&gt;
&lt;li&gt;JWT with 30-day expiry&lt;/li&gt;
&lt;li&gt;CORS validation&lt;/li&gt;
&lt;li&gt;SQL injection prevention (prepared statements)&lt;/li&gt;
&lt;li&gt;File type validation&lt;/li&gt;
&lt;li&gt;PIN uniqueness checks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Challenges I Overcame 💪
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: WebSocket State Management 🎯
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Durable Objects can migrate between requests. How do you maintain connection state?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Store connections in memory within the Durable Object. When messages arrive, route them through the object's state machine. Cloudflare handles persistence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: Image Upload Optimization 🖼️
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Large images would timeout or consume too much memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate file size on client (max 10MB)&lt;/li&gt;
&lt;li&gt;Use streaming uploads to R2&lt;/li&gt;
&lt;li&gt;Generate thumbnails on the edge (future: Workers AI)&lt;/li&gt;
&lt;li&gt;Return immediately, process async
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Streaming upload&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&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;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;File&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;arrayBuffer&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;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&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;key&lt;/span&gt; &lt;span class="o"&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;presentationId&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;generateId&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;.jpg`&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRESENTATIONS_BUCKET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;httpMetadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Challenge 3: Cross-Region Consistency 🌐
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; D1 replicates globally, but writes happen in one region. How do you ensure reads see latest data?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept eventual consistency for non-critical reads (presentation lists)&lt;/li&gt;
&lt;li&gt;Use KV for session data (fast, eventually consistent)&lt;/li&gt;
&lt;li&gt;Rely on Durable Objects for real-time state (strongly consistent)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Challenge 4: The Share Feature 🔗
&lt;/h3&gt;

&lt;p&gt;Recently, I added a share feature where presenters can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable public sharing with a toggle&lt;/li&gt;
&lt;li&gt;Add custom notes for viewers&lt;/li&gt;
&lt;li&gt;Upload additional files (PDFs, documents)&lt;/li&gt;
&lt;li&gt;Generate a short URL: &lt;code&gt;touchslides.com/p/{PIN}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The challenge:&lt;/strong&gt; How to update the database schema without breaking production?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The solution:&lt;/strong&gt; Cloudflare D1 supports &lt;code&gt;ALTER TABLE&lt;/code&gt; with zero downtime:&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;# Add columns to remote database&lt;/span&gt;
npx wrangler d1 execute touchslides-db-dev &lt;span class="nt"&gt;--env&lt;/span&gt; dev &lt;span class="nt"&gt;--remote&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE presentations ADD COLUMN share_enabled INTEGER DEFAULT 0;"&lt;/span&gt;

npx wrangler d1 execute touchslides-db-dev &lt;span class="nt"&gt;--env&lt;/span&gt; dev &lt;span class="nt"&gt;--remote&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE presentations ADD COLUMN share_notes TEXT;"&lt;/span&gt;

npx wrangler d1 execute touchslides-db-dev &lt;span class="nt"&gt;--env&lt;/span&gt; dev &lt;span class="nt"&gt;--remote&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE presentations ADD COLUMN share_files TEXT;"&lt;/span&gt;

&lt;span class="c"&gt;# Deploy updated code&lt;/span&gt;
npx wrangler deploy &lt;span class="nt"&gt;--env&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Zero downtime.&lt;/strong&gt; Existing rows got default values, new rows got the new schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Performance Numbers 📊
&lt;/h2&gt;

&lt;p&gt;After deploying to production, here are the metrics:&lt;/p&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;Value&lt;/th&gt;
&lt;th&gt;Comparison&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Slide change latency&lt;/td&gt;
&lt;td&gt;25-65ms&lt;/td&gt;
&lt;td&gt;Zoom: 150-300ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start time&lt;/td&gt;
&lt;td&gt;&amp;lt;5ms&lt;/td&gt;
&lt;td&gt;AWS Lambda: 100-500ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database query time&lt;/td&gt;
&lt;td&gt;10-30ms&lt;/td&gt;
&lt;td&gt;PostgreSQL: 50-200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image load time&lt;/td&gt;
&lt;td&gt;50-150ms&lt;/td&gt;
&lt;td&gt;CDN: 100-300ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost (1000 users)&lt;/td&gt;
&lt;td&gt;~$5&lt;/td&gt;
&lt;td&gt;Traditional: $50-200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why so fast?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workers start instantly (V8 isolates, not containers)&lt;/li&gt;
&lt;li&gt;Durable Objects live in-region&lt;/li&gt;
&lt;li&gt;R2 serves from global cache&lt;/li&gt;
&lt;li&gt;D1 replicates to 300+ locations&lt;/li&gt;
&lt;li&gt;No cold starts, no VPC, no NAT&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Developer Experience 👨‍💻
&lt;/h2&gt;

&lt;p&gt;One of my favorite parts about this stack is the &lt;strong&gt;local development experience&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1: Start the worker locally&lt;/span&gt;
npm run worker:dev
&lt;span class="c"&gt;# → http://localhost:8787&lt;/span&gt;

&lt;span class="c"&gt;# Terminal 2: Start Next.js&lt;/span&gt;
npm run dev
&lt;span class="c"&gt;# → http://localhost:3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Everything works offline:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;D1 uses local SQLite&lt;/li&gt;
&lt;li&gt;R2 uses local filesystem simulation&lt;/li&gt;
&lt;li&gt;KV uses local memory&lt;/li&gt;
&lt;li&gt;Durable Objects work in Miniflare&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No cloud account needed for development!&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Looking back, here's what I'd change:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with Durable Objects immediately&lt;/strong&gt; - I initially tried polling, then server-sent events. WebSockets via Durable Objects was always the right answer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add proper error boundaries earlier&lt;/strong&gt; - The frontend needed better error handling from day one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use Cloudflare Images&lt;/strong&gt; - I'm manually handling thumbnails. Cloudflare Images would've saved time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Implement rate limiting&lt;/strong&gt; - Easy to add with Workers, should've been there from the start.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better TypeScript types&lt;/strong&gt; - The Worker &amp;lt;-&amp;gt; Frontend contract could be more type-safe.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Future Roadmap 🗺️
&lt;/h2&gt;

&lt;p&gt;I'm actively working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDF upload &amp;amp; conversion&lt;/strong&gt; - Using Cloudflare Workers AI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slide annotations&lt;/strong&gt; - Draw on slides in real-time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presenter mode timer&lt;/strong&gt; - Built-in countdown&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QR code final slide&lt;/strong&gt; - Auto-generated share slide&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics dashboard&lt;/strong&gt; - Engagement tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team collaboration&lt;/strong&gt; - Multiple presenters&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned 📚
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Edge-first architecture is the future 🌟
&lt;/h3&gt;

&lt;p&gt;Traditional client-server architecture adds 100-500ms of latency. Edge computing brings that down to 10-50ms. For real-time apps, this is game-changing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Durable Objects are underrated 💎
&lt;/h3&gt;

&lt;p&gt;They solve the stateful-at-the-edge problem elegantly. No Redis, no coordination, no locks. Just pure, distributed state.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cloudflare's free tier is incredible 🆓
&lt;/h3&gt;

&lt;p&gt;For indie hackers and small projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;100,000 requests/day&lt;/strong&gt; on Workers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1GB storage&lt;/strong&gt; on R2 (zero egress!)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlimited reads&lt;/strong&gt; on KV&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D1 included&lt;/strong&gt; with 5GB storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This entire app runs on the &lt;strong&gt;free tier&lt;/strong&gt; for hobby projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. TypeScript is non-negotiable ✅
&lt;/h3&gt;

&lt;p&gt;The Worker codebase is 100% TypeScript. Catching errors at compile time saved me hours of debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Keep it simple first 🎯
&lt;/h3&gt;

&lt;p&gt;I started with basic features: upload slides, show them, control remotely. Only after that worked did I add animations, transitions, and advanced features.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 14 (App Router, React Server Components)&lt;/li&gt;
&lt;li&gt;Tailwind CSS (custom design system)&lt;/li&gt;
&lt;li&gt;Framer Motion (animations)&lt;/li&gt;
&lt;li&gt;Lucide React (icons)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare Workers (global compute)&lt;/li&gt;
&lt;li&gt;Hono 4.6 (routing framework)&lt;/li&gt;
&lt;li&gt;TypeScript (type safety)&lt;/li&gt;
&lt;li&gt;Zod (validation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Storage:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;D1 (SQLite database)&lt;/li&gt;
&lt;li&gt;R2 (object storage)&lt;/li&gt;
&lt;li&gt;KV (key-value store)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-Time:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Durable Objects (WebSocket state)&lt;/li&gt;
&lt;li&gt;WebSocket API (bidirectional communication)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bcrypt (password hashing)&lt;/li&gt;
&lt;li&gt;jose (JWT tokens)&lt;/li&gt;
&lt;li&gt;CORS middleware&lt;/li&gt;
&lt;li&gt;Prepared statements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Development:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrangler (Cloudflare CLI)&lt;/li&gt;
&lt;li&gt;Miniflare (local simulation)&lt;/li&gt;
&lt;li&gt;TypeScript compiler&lt;/li&gt;
&lt;li&gt;ESLint (linting)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Get Started 🚀
&lt;/h2&gt;

&lt;p&gt;Want to build something similar? Here's my advice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with Wrangler:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   npm create cloudflare@latest my-app
   &lt;span class="nb"&gt;cd &lt;/span&gt;my-app
   npm &lt;span class="nb"&gt;install &lt;/span&gt;hono
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add a Durable Object:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello from the edge!&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connect from the frontend:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ws&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;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wss://my-app.workers.dev/ws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deploy:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   npx wrangler deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Seriously, it's that simple.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Source &amp;amp; Links 🔗
&lt;/h2&gt;

&lt;p&gt;TouchSlides is open source! Check out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/vegetablecode/touchslides" rel="noopener noreferrer"&gt;github.com/vegetablecode/touchslides&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://touchslides.com" rel="noopener noreferrer"&gt;touchslides.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture Docs:&lt;/strong&gt; &lt;a href="https://github.com/vegetablecode/touchslides/blob/master/BACKEND_SETUP.md" rel="noopener noreferrer"&gt;BACKEND_SETUP.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love to hear your feedback, questions, or suggestions. Feel free to open issues or contribute!&lt;/p&gt;

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

&lt;p&gt;Building TouchSlides taught me that &lt;strong&gt;the edge is ready for production&lt;/strong&gt;. Cloudflare Workers aren't just for static sites or simple APIs - they can handle complex, stateful, real-time applications with ease.&lt;/p&gt;

&lt;p&gt;The combination of Workers + Durable Objects + D1 + R2 is incredibly powerful. You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global distribution by default&lt;/li&gt;
&lt;li&gt;Sub-100ms latency worldwide&lt;/li&gt;
&lt;li&gt;Infinite scale (auto-scaling)&lt;/li&gt;
&lt;li&gt;Minimal cost (pay-per-use)&lt;/li&gt;
&lt;li&gt;Great DX (local development)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building anything real-time - chat apps, collaborative editors, live dashboards, multiplayer games - &lt;strong&gt;consider the edge first&lt;/strong&gt;. Your users will thank you for the speed.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Questions? Comments?&lt;/strong&gt; Drop them below! I'm happy to dive deeper into any part of the architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want to try TouchSlides?&lt;/strong&gt; Visit &lt;a href="https://touchslides.com" rel="noopener noreferrer"&gt;touchslides.com&lt;/a&gt; and experience sub-100ms presentation control for yourself.&lt;/p&gt;

&lt;p&gt;Happy building! 🚀&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Kamil Uhryn&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Full-Stack Developer | Edge Computing Enthusiast&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://github.com/vegetablecode" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://x.com/kamiluhryn" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; | &lt;a href="https://touchslides.com" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>saas</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Waited 5 Years for AI to Be Good Enough to Build This: A Fully Automatic Logo Designer</title>
      <dc:creator>Kamil Uhryn</dc:creator>
      <pubDate>Sun, 02 Nov 2025 18:08:52 +0000</pubDate>
      <link>https://forem.com/kamil_uhryn/i-waited-5-years-for-ai-to-be-good-enough-to-build-this-a-fully-automatic-logo-designer-5dhd</link>
      <guid>https://forem.com/kamil_uhryn/i-waited-5-years-for-ai-to-be-good-enough-to-build-this-a-fully-automatic-logo-designer-5dhd</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Built &lt;a href="https://goodlogoai.com" rel="noopener noreferrer"&gt;GoodLogoAI&lt;/a&gt; - an AI logo maker that outputs production-ready vector SVG + transparent PNG files. $9.99 one-time payment. Had the vision in 2019, but had to wait for AI models to mature. Now it's finally viable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 5-Year Wait
&lt;/h2&gt;

&lt;p&gt;Back in 2019, I had a clear vision: &lt;strong&gt;a fully automatic logo designer that takes a brand name and outputs well-crafted logos in vector SVG and transparent PNG.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No templates. No manual design work. Just intelligent AI that understands design principles.&lt;/p&gt;

&lt;p&gt;The problem? In 2019, AI couldn't do this. Not even close.&lt;/p&gt;

&lt;p&gt;I tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPT-2 (couldn't understand visual design)&lt;/li&gt;
&lt;li&gt;Early GAN image generators (rasterized garbage)&lt;/li&gt;
&lt;li&gt;Vector generation models (didn't exist yet)&lt;/li&gt;
&lt;li&gt;Template-based systems (not "true" AI)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every single one failed the litmus test: &lt;strong&gt;"Could a real business use this logo?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Answer: No.&lt;/p&gt;

&lt;p&gt;So I waited. And watched. And tested every new model that dropped.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Catalyst
&lt;/h2&gt;

&lt;p&gt;In 2022, I was preparing for my biggest investor pitch. I needed a logo. Fast.&lt;/p&gt;

&lt;p&gt;Called 5 designers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"That'll be $500, I can have concepts in 2-3 weeks"&lt;/li&gt;
&lt;li&gt;"My minimum is $800, 3-week turnaround"&lt;/li&gt;
&lt;li&gt;"I'm booked for the next month"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I had 3 days.&lt;/p&gt;

&lt;p&gt;Tried to DIY it in Canva. Spent 8 hours. It looked like a 12-year-old designed it.&lt;/p&gt;

&lt;p&gt;That frustration crystalized the vision: &lt;strong&gt;This should be automated. This should be instant. This should cost $10, not $500.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But the AI still wasn't ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  2025: The Technology Finally Caught Up
&lt;/h2&gt;

&lt;p&gt;Three things converged in 2025 that made this finally possible:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Better Generative Models
&lt;/h3&gt;

&lt;p&gt;Modern diffusion models + GPT-4 class language models could finally understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Design principles (balance, hierarchy, negative space)&lt;/li&gt;
&lt;li&gt;Color theory and psychology&lt;/li&gt;
&lt;li&gt;Typography rules&lt;/li&gt;
&lt;li&gt;Brand positioning&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Vector Generation Breakthrough
&lt;/h3&gt;

&lt;p&gt;The ability to generate clean vector paths (not just rasterized images) became viable. This was the game-changer.&lt;/p&gt;

&lt;p&gt;SVG output isn't just "nice to have" - it's &lt;strong&gt;essential&lt;/strong&gt; for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infinite scalability&lt;/li&gt;
&lt;li&gt;Print production&lt;/li&gt;
&lt;li&gt;Professional use&lt;/li&gt;
&lt;li&gt;File size efficiency&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Natural Language Editing
&lt;/h3&gt;

&lt;p&gt;Being able to refine with prompts like "make it more modern" or "use blue tones" without regenerating from scratch = crucial UX improvement.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works (Technical Overview)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Input (brand name, industry, style)
    ↓
AI Analysis Layer
    ↓
Logo Concept Generation (30 seconds)
    ↓
Vector Path Construction
    ↓
Output: SVG + PNG (4096x4096)
    ↓
User Refinement (natural language)
    ↓
Iterative Improvement Loop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Technical Challenges Solved
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Challenge 1: Vector Generation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rasterized AI outputs don't scale&lt;/li&gt;
&lt;li&gt;Had to ensure clean, production-ready SVG paths&lt;/li&gt;
&lt;li&gt;Solution: recraft-vectorize&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Challenge 2: Consistency Across Edits&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users need to refine, not start over&lt;/li&gt;
&lt;li&gt;Had to maintain design identity through iterations&lt;/li&gt;
&lt;li&gt;Solution: Contextual editing that remembers the design system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Challenge 3: Design Quality&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI can generate "something" - but is it GOOD?&lt;/li&gt;
&lt;li&gt;Had to encode design principles into the system&lt;/li&gt;
&lt;li&gt;Solution: crystal-upscaler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Challenge 4: Speed&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users expect instant results&lt;/li&gt;
&lt;li&gt;Had to optimize inference time without sacrificing quality&lt;/li&gt;
&lt;li&gt;Solution: ~30 second generation time&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 14 (App Router)&lt;/li&gt;
&lt;li&gt;Tailwind CSS for styling&lt;/li&gt;
&lt;li&gt;Framer Motion for animations&lt;/li&gt;
&lt;li&gt;Zustand for state management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firebase&lt;/li&gt;
&lt;li&gt;Stripe for payments&lt;/li&gt;
&lt;li&gt;PostHog for tracking users&lt;/li&gt;
&lt;li&gt;Cloudflare Analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI/ML:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google nano-banana for initial concept and editing&lt;/li&gt;
&lt;li&gt;Vector generation pipeline&lt;/li&gt;
&lt;li&gt;Natural language processing for edits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vercel for frontend&lt;/li&gt;
&lt;li&gt;CDN for asset delivery&lt;/li&gt;
&lt;li&gt;Firebase Storage for logo designs&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lessons from Building This
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Sometimes You Have to Wait for the Technology
&lt;/h3&gt;

&lt;p&gt;I wanted to build this in 2019. But the AI models weren't ready. If I had forced it then, it would've been another mediocre tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Not every idea's time is NOW. Sometimes the best move is to watch the space and wait for the enabling technology.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pricing is a Product Decision
&lt;/h3&gt;

&lt;p&gt;I could've priced this at $29, $49, or $99. I went with &lt;strong&gt;$9.99&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why? Because when I got ghosted by that designer, I would've paid $10 without thinking twice. I would've hesitated at $50.&lt;/p&gt;

&lt;p&gt;The goal isn't to maximize per-sale revenue. It's to make this accessible to every founder who's getting quoted $500+ by designers.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. SEO from Day 1 (Learned This the Hard Way)
&lt;/h3&gt;

&lt;p&gt;My first app hit $40K revenue. But I didn't start SEO until month 4. Huge mistake.&lt;/p&gt;

&lt;p&gt;This time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proper meta tags from day 1&lt;/li&gt;
&lt;li&gt;Content strategy planned out&lt;/li&gt;
&lt;li&gt;Keywords researched before launch&lt;/li&gt;
&lt;li&gt;Internal linking structure designed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Google indexed me in 3 days instead of 3 weeks.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Don't Buy Backlinks on Fiverr
&lt;/h3&gt;

&lt;p&gt;Yeah, I made this mistake with app #1. Paid $200 for "high DA backlinks."&lt;/p&gt;

&lt;p&gt;Google penalized me in 2 weeks. Took 3 months to recover.&lt;/p&gt;

&lt;p&gt;Build links naturally or don't build them at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Track Everything from Launch
&lt;/h3&gt;

&lt;p&gt;I added PostHog on day 1 this time. With app #1, I waited 3 months.&lt;/p&gt;

&lt;p&gt;Being able to see exactly where users drop off in the funnel = invaluable.&lt;/p&gt;

&lt;p&gt;I found out 40% were leaving because the "Try Free" button wasn't clear enough. Changed it. Conversion jumped 2x.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool recommendation:&lt;/strong&gt; PostHog is fantastic for this. Open source, easy to set up, and the free tier is generous.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Production Hunt is Overrated for Traffic
&lt;/h3&gt;

&lt;p&gt;Everyone says "launch on Product Hunt!" I did. Got 50 users.&lt;/p&gt;

&lt;p&gt;Know what got me 5,000+ users? SEO.&lt;/p&gt;

&lt;p&gt;Don't get me wrong - PH is great for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial validation&lt;/li&gt;
&lt;li&gt;Getting a backlink (DA 90+)&lt;/li&gt;
&lt;li&gt;Social proof ("Featured on Product Hunt")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But don't expect it to solve distribution. It won't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current State
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Launched:&lt;/strong&gt; October 30, 2025&lt;br&gt;
&lt;strong&gt;Users:&lt;/strong&gt; 100+ logos created&lt;br&gt;
&lt;strong&gt;Revenue:&lt;/strong&gt; Starting from $0 (first app did $40K, but this is fresh)&lt;br&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; Fully automated, no manual intervention needed&lt;br&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; $9.99 one-time payment&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's working:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI quality is solid (98% satisfaction rate)&lt;/li&gt;
&lt;li&gt;Conversion rate is good (~5%)&lt;/li&gt;
&lt;li&gt;Word of mouth is picking up&lt;/li&gt;
&lt;li&gt;SEO is starting to compound&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What's not:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Traffic is still early stage&lt;/li&gt;
&lt;li&gt;Haven't cracked paid acquisition yet (testing)&lt;/li&gt;
&lt;li&gt;Need more content marketing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Hardest Part
&lt;/h2&gt;

&lt;p&gt;You know what the hardest part of building this was?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The waiting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sitting on a vision for 5 years while the technology slowly caught up. Watching other people launch "AI logo makers" with template systems and rasterized outputs. Wondering if my vision was even possible.&lt;/p&gt;

&lt;p&gt;But also knowing that if I launched too early, it would be just another mediocre tool.&lt;/p&gt;

&lt;p&gt;Sometimes the best thing you can do as a builder is &lt;strong&gt;wait for the enabling technology to be ready.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;If you're curious what 5 years of waiting and current AI tech can produce:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://goodlogoai.com" rel="noopener noreferrer"&gt;GoodLogoAI&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Try free (2 edits, no credit card)&lt;/li&gt;
&lt;li&gt;Download vector SVG + transparent PNG&lt;/li&gt;
&lt;li&gt;$9.99 one-time payment&lt;/li&gt;
&lt;li&gt;No subscription, you own it forever&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd genuinely love feedback from this community. Technical feedback especially welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Questions I'm Happy to Answer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Technical implementation details (within reason - some is proprietary)&lt;/li&gt;
&lt;li&gt;Challenges in building AI products&lt;/li&gt;
&lt;li&gt;Going from first app ($40K) to second app ($0)&lt;/li&gt;
&lt;li&gt;SEO strategy for SaaS&lt;/li&gt;
&lt;li&gt;Building in public experiences&lt;/li&gt;
&lt;li&gt;Anything about the 5-year journey&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop your questions below. 👇&lt;/p&gt;




&lt;h2&gt;
  
  
  Discussion Topics
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For the dev.to community:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Have you ever had to wait years for technology to catch up to your vision?&lt;/li&gt;
&lt;li&gt;What AI projects are you working on?&lt;/li&gt;
&lt;li&gt;Vector generation - anyone else tackling this problem?&lt;/li&gt;
&lt;li&gt;Thoughts on AI + design automation?&lt;/li&gt;
&lt;li&gt;Should I open-source parts of this? (Considering it)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's discuss! 💬&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>programming</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
